1/* 2Copyright 2017 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package jsonmergepatch 18 19import ( 20 "fmt" 21 "reflect" 22 "testing" 23 24 "github.com/davecgh/go-spew/spew" 25 "github.com/evanphx/json-patch" 26 "k8s.io/apimachinery/pkg/util/json" 27 "sigs.k8s.io/yaml" 28) 29 30type FilterNullTestCases struct { 31 TestCases []FilterNullTestCase 32} 33 34type FilterNullTestCase struct { 35 Description string 36 OriginalObj map[string]interface{} 37 ExpectedWithNull map[string]interface{} 38 ExpectedWithoutNull map[string]interface{} 39} 40 41var filterNullTestCaseData = []byte(` 42testCases: 43 - description: nil original 44 originalObj: {} 45 expectedWithNull: {} 46 expectedWithoutNull: {} 47 - description: simple map 48 originalObj: 49 nilKey: null 50 nonNilKey: foo 51 expectedWithNull: 52 nilKey: null 53 expectedWithoutNull: 54 nonNilKey: foo 55 - description: simple map with all nil values 56 originalObj: 57 nilKey1: null 58 nilKey2: null 59 expectedWithNull: 60 nilKey1: null 61 nilKey2: null 62 expectedWithoutNull: {} 63 - description: simple map with all non-nil values 64 originalObj: 65 nonNilKey1: foo 66 nonNilKey2: bar 67 expectedWithNull: {} 68 expectedWithoutNull: 69 nonNilKey1: foo 70 nonNilKey2: bar 71 - description: nested map 72 originalObj: 73 mapKey: 74 nilKey: null 75 nonNilKey: foo 76 expectedWithNull: 77 mapKey: 78 nilKey: null 79 expectedWithoutNull: 80 mapKey: 81 nonNilKey: foo 82 - description: nested map that all subkeys are nil 83 originalObj: 84 mapKey: 85 nilKey1: null 86 nilKey2: null 87 expectedWithNull: 88 mapKey: 89 nilKey1: null 90 nilKey2: null 91 expectedWithoutNull: {} 92 - description: nested map that all subkeys are non-nil 93 originalObj: 94 mapKey: 95 nonNilKey1: foo 96 nonNilKey2: bar 97 expectedWithNull: {} 98 expectedWithoutNull: 99 mapKey: 100 nonNilKey1: foo 101 nonNilKey2: bar 102 - description: explicitly empty map as value 103 originalObj: 104 mapKey: {} 105 expectedWithNull: {} 106 expectedWithoutNull: 107 mapKey: {} 108 - description: explicitly empty nested map 109 originalObj: 110 mapKey: 111 nonNilKey: {} 112 expectedWithNull: {} 113 expectedWithoutNull: 114 mapKey: 115 nonNilKey: {} 116 - description: multiple expliclty empty nested maps 117 originalObj: 118 mapKey: 119 nonNilKey1: {} 120 nonNilKey2: {} 121 expectedWithNull: {} 122 expectedWithoutNull: 123 mapKey: 124 nonNilKey1: {} 125 nonNilKey2: {} 126 - description: nested map with non-null value as empty map 127 originalObj: 128 mapKey: 129 nonNilKey: {} 130 nilKey: null 131 expectedWithNull: 132 mapKey: 133 nilKey: null 134 expectedWithoutNull: 135 mapKey: 136 nonNilKey: {} 137 - description: empty list 138 originalObj: 139 listKey: [] 140 expectedWithNull: {} 141 expectedWithoutNull: 142 listKey: [] 143 - description: list of primitives 144 originalObj: 145 listKey: 146 - 1 147 - 2 148 expectedWithNull: {} 149 expectedWithoutNull: 150 listKey: 151 - 1 152 - 2 153 - description: list of maps 154 originalObj: 155 listKey: 156 - k1: v1 157 - k2: null 158 - k3: v3 159 k4: null 160 expectedWithNull: {} 161 expectedWithoutNull: 162 listKey: 163 - k1: v1 164 - k2: null 165 - k3: v3 166 k4: null 167 - description: list of different types 168 originalObj: 169 listKey: 170 - k1: v1 171 - k2: null 172 - v3 173 expectedWithNull: {} 174 expectedWithoutNull: 175 listKey: 176 - k1: v1 177 - k2: null 178 - v3 179`) 180 181func TestKeepOrDeleteNullInObj(t *testing.T) { 182 tc := FilterNullTestCases{} 183 err := yaml.Unmarshal(filterNullTestCaseData, &tc) 184 if err != nil { 185 t.Fatalf("can't unmarshal test cases: %s\n", err) 186 } 187 188 for _, test := range tc.TestCases { 189 resultWithNull, err := keepOrDeleteNullInObj(test.OriginalObj, true) 190 if err != nil { 191 t.Errorf("Failed in test case %q when trying to keep null values: %s", test.Description, err) 192 } 193 if !reflect.DeepEqual(test.ExpectedWithNull, resultWithNull) { 194 t.Errorf("Failed in test case %q when trying to keep null values:\nexpected expectedWithNull:\n%+v\nbut got:\n%+v\n", test.Description, test.ExpectedWithNull, resultWithNull) 195 } 196 197 resultWithoutNull, err := keepOrDeleteNullInObj(test.OriginalObj, false) 198 if err != nil { 199 t.Errorf("Failed in test case %q when trying to keep non-null values: %s", test.Description, err) 200 } 201 if !reflect.DeepEqual(test.ExpectedWithoutNull, resultWithoutNull) { 202 t.Errorf("Failed in test case %q when trying to keep non-null values:\n expected expectedWithoutNull:\n%+v\nbut got:\n%+v\n", test.Description, test.ExpectedWithoutNull, resultWithoutNull) 203 } 204 } 205} 206 207type JSONMergePatchTestCases struct { 208 TestCases []JSONMergePatchTestCase 209} 210 211type JSONMergePatchTestCase struct { 212 Description string 213 JSONMergePatchTestCaseData 214} 215 216type JSONMergePatchTestCaseData struct { 217 // Original is the original object (last-applied config in annotation) 218 Original map[string]interface{} 219 // Modified is the modified object (new config we want) 220 Modified map[string]interface{} 221 // Current is the current object (live config in the server) 222 Current map[string]interface{} 223 // ThreeWay is the expected three-way merge patch 224 ThreeWay map[string]interface{} 225 // Result is the expected object after applying the three-way patch on current object. 226 Result map[string]interface{} 227} 228 229var createJSONMergePatchTestCaseData = []byte(` 230testCases: 231 - description: nil original 232 modified: 233 name: 1 234 value: 1 235 current: 236 name: 1 237 other: a 238 threeWay: 239 value: 1 240 result: 241 name: 1 242 value: 1 243 other: a 244 - description: nil patch 245 original: 246 name: 1 247 modified: 248 name: 1 249 current: 250 name: 1 251 threeWay: 252 {} 253 result: 254 name: 1 255 - description: add field to map 256 original: 257 name: 1 258 modified: 259 name: 1 260 value: 1 261 current: 262 name: 1 263 other: a 264 threeWay: 265 value: 1 266 result: 267 name: 1 268 value: 1 269 other: a 270 - description: add field to map with conflict 271 original: 272 name: 1 273 modified: 274 name: 1 275 value: 1 276 current: 277 name: a 278 other: a 279 threeWay: 280 name: 1 281 value: 1 282 result: 283 name: 1 284 value: 1 285 other: a 286 - description: add field and delete field from map 287 original: 288 name: 1 289 modified: 290 value: 1 291 current: 292 name: 1 293 other: a 294 threeWay: 295 name: null 296 value: 1 297 result: 298 value: 1 299 other: a 300 - description: add field and delete field from map with conflict 301 original: 302 name: 1 303 modified: 304 value: 1 305 current: 306 name: a 307 other: a 308 threeWay: 309 name: null 310 value: 1 311 result: 312 value: 1 313 other: a 314 - description: delete field from nested map 315 original: 316 simpleMap: 317 key1: 1 318 key2: 1 319 modified: 320 simpleMap: 321 key1: 1 322 current: 323 simpleMap: 324 key1: 1 325 key2: 1 326 other: a 327 threeWay: 328 simpleMap: 329 key2: null 330 result: 331 simpleMap: 332 key1: 1 333 other: a 334 - description: delete field from nested map with conflict 335 original: 336 simpleMap: 337 key1: 1 338 key2: 1 339 modified: 340 simpleMap: 341 key1: 1 342 current: 343 simpleMap: 344 key1: a 345 key2: 1 346 other: a 347 threeWay: 348 simpleMap: 349 key1: 1 350 key2: null 351 result: 352 simpleMap: 353 key1: 1 354 other: a 355 - description: delete all fields from map 356 original: 357 name: 1 358 value: 1 359 modified: {} 360 current: 361 name: 1 362 value: 1 363 other: a 364 threeWay: 365 name: null 366 value: null 367 result: 368 other: a 369 - description: delete all fields from map with conflict 370 original: 371 name: 1 372 value: 1 373 modified: {} 374 current: 375 name: 1 376 value: a 377 other: a 378 threeWay: 379 name: null 380 value: null 381 result: 382 other: a 383 - description: add field and delete all fields from map 384 original: 385 name: 1 386 value: 1 387 modified: 388 other: a 389 current: 390 name: 1 391 value: 1 392 other: a 393 threeWay: 394 name: null 395 value: null 396 result: 397 other: a 398 - description: add field and delete all fields from map with conflict 399 original: 400 name: 1 401 value: 1 402 modified: 403 other: a 404 current: 405 name: 1 406 value: 1 407 other: b 408 threeWay: 409 name: null 410 value: null 411 other: a 412 result: 413 other: a 414 - description: replace list of scalars 415 original: 416 intList: 417 - 1 418 - 2 419 modified: 420 intList: 421 - 2 422 - 3 423 current: 424 intList: 425 - 1 426 - 2 427 threeWay: 428 intList: 429 - 2 430 - 3 431 result: 432 intList: 433 - 2 434 - 3 435 - description: replace list of scalars with conflict 436 original: 437 intList: 438 - 1 439 - 2 440 modified: 441 intList: 442 - 2 443 - 3 444 current: 445 intList: 446 - 1 447 - 4 448 threeWay: 449 intList: 450 - 2 451 - 3 452 result: 453 intList: 454 - 2 455 - 3 456 - description: patch with different scalar type 457 original: 458 foo: 1 459 modified: 460 foo: true 461 current: 462 foo: 1 463 bar: 2 464 threeWay: 465 foo: true 466 result: 467 foo: true 468 bar: 2 469 - description: patch from scalar to list 470 original: 471 foo: 0 472 modified: 473 foo: 474 - 1 475 - 2 476 current: 477 foo: 0 478 bar: 2 479 threeWay: 480 foo: 481 - 1 482 - 2 483 result: 484 foo: 485 - 1 486 - 2 487 bar: 2 488 - description: patch from list to scalar 489 original: 490 foo: 491 - 1 492 - 2 493 modified: 494 foo: 0 495 current: 496 foo: 497 - 1 498 - 2 499 bar: 2 500 threeWay: 501 foo: 0 502 result: 503 foo: 0 504 bar: 2 505 - description: patch from scalar to map 506 original: 507 foo: 0 508 modified: 509 foo: 510 baz: 1 511 current: 512 foo: 0 513 bar: 2 514 threeWay: 515 foo: 516 baz: 1 517 result: 518 foo: 519 baz: 1 520 bar: 2 521 - description: patch from map to scalar 522 original: 523 foo: 524 baz: 1 525 modified: 526 foo: 0 527 current: 528 foo: 529 baz: 1 530 bar: 2 531 threeWay: 532 foo: 0 533 result: 534 foo: 0 535 bar: 2 536 - description: patch from map to list 537 original: 538 foo: 539 baz: 1 540 modified: 541 foo: 542 - 1 543 - 2 544 current: 545 foo: 546 baz: 1 547 bar: 2 548 threeWay: 549 foo: 550 - 1 551 - 2 552 result: 553 foo: 554 - 1 555 - 2 556 bar: 2 557 - description: patch from list to map 558 original: 559 foo: 560 - 1 561 - 2 562 modified: 563 foo: 564 baz: 0 565 current: 566 foo: 567 - 1 568 - 2 569 bar: 2 570 threeWay: 571 foo: 572 baz: 0 573 result: 574 foo: 575 baz: 0 576 bar: 2 577 - description: patch with different nested types 578 original: 579 foo: 580 - a: true 581 - 2 582 - false 583 modified: 584 foo: 585 - 1 586 - false 587 - b: 1 588 current: 589 foo: 590 - a: true 591 - 2 592 - false 593 bar: 0 594 threeWay: 595 foo: 596 - 1 597 - false 598 - b: 1 599 result: 600 foo: 601 - 1 602 - false 603 - b: 1 604 bar: 0 605`) 606 607func TestCreateThreeWayJSONMergePatch(t *testing.T) { 608 tc := JSONMergePatchTestCases{} 609 err := yaml.Unmarshal(createJSONMergePatchTestCaseData, &tc) 610 if err != nil { 611 t.Errorf("can't unmarshal test cases: %s\n", err) 612 return 613 } 614 615 for _, c := range tc.TestCases { 616 testThreeWayPatch(t, c) 617 } 618} 619 620func testThreeWayPatch(t *testing.T, c JSONMergePatchTestCase) { 621 original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c) 622 actual, err := CreateThreeWayJSONMergePatch(original, modified, current) 623 if err != nil { 624 t.Fatalf("error: %s", err) 625 } 626 testPatchCreation(t, expected, actual, c.Description) 627 testPatchApplication(t, current, actual, result, c.Description) 628} 629 630func testPatchCreation(t *testing.T, expected, actual []byte, description string) { 631 if !reflect.DeepEqual(actual, expected) { 632 t.Errorf("error in test case: %s\nexpected patch:\n%s\ngot:\n%s\n", 633 description, jsonToYAMLOrError(expected), jsonToYAMLOrError(actual)) 634 return 635 } 636} 637 638func testPatchApplication(t *testing.T, original, patch, expected []byte, description string) { 639 result, err := jsonpatch.MergePatch(original, patch) 640 if err != nil { 641 t.Errorf("error: %s\nin test case: %s\ncannot apply patch:\n%s\nto original:\n%s\n", 642 err, description, jsonToYAMLOrError(patch), jsonToYAMLOrError(original)) 643 return 644 } 645 646 if !reflect.DeepEqual(result, expected) { 647 format := "error in test case: %s\npatch application failed:\noriginal:\n%s\npatch:\n%s\nexpected:\n%s\ngot:\n%s\n" 648 t.Errorf(format, description, 649 jsonToYAMLOrError(original), jsonToYAMLOrError(patch), 650 jsonToYAMLOrError(expected), jsonToYAMLOrError(result)) 651 return 652 } 653} 654 655func threeWayTestCaseToJSONOrFail(t *testing.T, c JSONMergePatchTestCase) ([]byte, []byte, []byte, []byte, []byte) { 656 return testObjectToJSONOrFail(t, c.Original), 657 testObjectToJSONOrFail(t, c.Modified), 658 testObjectToJSONOrFail(t, c.Current), 659 testObjectToJSONOrFail(t, c.ThreeWay), 660 testObjectToJSONOrFail(t, c.Result) 661} 662 663func testObjectToJSONOrFail(t *testing.T, o map[string]interface{}) []byte { 664 if o == nil { 665 return nil 666 } 667 j, err := toJSON(o) 668 if err != nil { 669 t.Error(err) 670 } 671 return j 672} 673 674func jsonToYAMLOrError(j []byte) string { 675 y, err := jsonToYAML(j) 676 if err != nil { 677 return err.Error() 678 } 679 return string(y) 680} 681 682func toJSON(v interface{}) ([]byte, error) { 683 j, err := json.Marshal(v) 684 if err != nil { 685 return nil, fmt.Errorf("json marshal failed: %v\n%v\n", err, spew.Sdump(v)) 686 } 687 return j, nil 688} 689 690func jsonToYAML(j []byte) ([]byte, error) { 691 y, err := yaml.JSONToYAML(j) 692 if err != nil { 693 return nil, fmt.Errorf("json to yaml failed: %v\n%v\n", err, j) 694 } 695 return y, nil 696} 697