1
16
17 package jsonmergepatch
18
19 import (
20 "fmt"
21 "reflect"
22 "testing"
23
24 jsonpatch "github.com/evanphx/json-patch"
25 "k8s.io/apimachinery/pkg/util/dump"
26 "k8s.io/apimachinery/pkg/util/json"
27 "sigs.k8s.io/yaml"
28 )
29
30 type FilterNullTestCases struct {
31 TestCases []FilterNullTestCase
32 }
33
34 type FilterNullTestCase struct {
35 Description string
36 OriginalObj map[string]interface{}
37 ExpectedWithNull map[string]interface{}
38 ExpectedWithoutNull map[string]interface{}
39 }
40
41 var filterNullTestCaseData = []byte(`
42 testCases:
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
181 func 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
207 type JSONMergePatchTestCases struct {
208 TestCases []JSONMergePatchTestCase
209 }
210
211 type JSONMergePatchTestCase struct {
212 Description string
213 JSONMergePatchTestCaseData
214 }
215
216 type JSONMergePatchTestCaseData struct {
217
218 Original map[string]interface{}
219
220 Modified map[string]interface{}
221
222 Current map[string]interface{}
223
224 ThreeWay map[string]interface{}
225
226 Result map[string]interface{}
227 }
228
229 var createJSONMergePatchTestCaseData = []byte(`
230 testCases:
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 - description: patch array with nil
606 original:
607 foo:
608 - a: true
609 - null
610 - false
611 bar: []
612 drop:
613 - 1
614 modified:
615 foo:
616 - 1
617 - false
618 - b: 1
619 bar:
620 - c
621 - null
622 - null
623 - a
624 drop:
625 - null
626 current:
627 foo:
628 - a: true
629 - 2
630 - false
631 bar:
632 - c
633 - null
634 - null
635 - a
636 drop:
637 threeWay:
638 foo:
639 - 1
640 - false
641 - b: 1
642 drop:
643 - null
644 result:
645 foo:
646 - 1
647 - false
648 - b: 1
649 drop:
650 - null
651 bar:
652 - c
653 - null
654 - null
655 - a
656 `)
657
658 func TestCreateThreeWayJSONMergePatch(t *testing.T) {
659 tc := JSONMergePatchTestCases{}
660 err := yaml.Unmarshal(createJSONMergePatchTestCaseData, &tc)
661 if err != nil {
662 t.Errorf("can't unmarshal test cases: %s\n", err)
663 return
664 }
665
666 for _, c := range tc.TestCases {
667 testThreeWayPatch(t, c)
668 }
669 }
670
671 func testThreeWayPatch(t *testing.T, c JSONMergePatchTestCase) {
672 original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c)
673 actual, err := CreateThreeWayJSONMergePatch(original, modified, current)
674 if err != nil {
675 t.Fatalf("error: %s", err)
676 }
677 testPatchCreation(t, expected, actual, c.Description)
678 testPatchApplication(t, current, actual, result, c.Description)
679 }
680
681 func testPatchCreation(t *testing.T, expected, actual []byte, description string) {
682 if !reflect.DeepEqual(actual, expected) {
683 t.Errorf("error in test case: %s\nexpected patch:\n%s\ngot:\n%s\n",
684 description, jsonToYAMLOrError(expected), jsonToYAMLOrError(actual))
685 return
686 }
687 }
688
689 func testPatchApplication(t *testing.T, original, patch, expected []byte, description string) {
690 result, err := jsonpatch.MergePatch(original, patch)
691 if err != nil {
692 t.Errorf("error: %s\nin test case: %s\ncannot apply patch:\n%s\nto original:\n%s\n",
693 err, description, jsonToYAMLOrError(patch), jsonToYAMLOrError(original))
694 return
695 }
696
697 if !reflect.DeepEqual(result, expected) {
698 format := "error in test case: %s\npatch application failed:\noriginal:\n%s\npatch:\n%s\nexpected:\n%s\ngot:\n%s\n"
699 t.Errorf(format, description,
700 jsonToYAMLOrError(original), jsonToYAMLOrError(patch),
701 jsonToYAMLOrError(expected), jsonToYAMLOrError(result))
702 return
703 }
704 }
705
706 func threeWayTestCaseToJSONOrFail(t *testing.T, c JSONMergePatchTestCase) ([]byte, []byte, []byte, []byte, []byte) {
707 return testObjectToJSONOrFail(t, c.Original),
708 testObjectToJSONOrFail(t, c.Modified),
709 testObjectToJSONOrFail(t, c.Current),
710 testObjectToJSONOrFail(t, c.ThreeWay),
711 testObjectToJSONOrFail(t, c.Result)
712 }
713
714 func testObjectToJSONOrFail(t *testing.T, o map[string]interface{}) []byte {
715 if o == nil {
716 return nil
717 }
718 j, err := toJSON(o)
719 if err != nil {
720 t.Error(err)
721 }
722 return j
723 }
724
725 func jsonToYAMLOrError(j []byte) string {
726 y, err := jsonToYAML(j)
727 if err != nil {
728 return err.Error()
729 }
730 return string(y)
731 }
732
733 func toJSON(v interface{}) ([]byte, error) {
734 j, err := json.Marshal(v)
735 if err != nil {
736 return nil, fmt.Errorf("json marshal failed: %v\n%v\n", err, dump.Pretty(v))
737 }
738 return j, nil
739 }
740
741 func jsonToYAML(j []byte) ([]byte, error) {
742 y, err := yaml.JSONToYAML(j)
743 if err != nil {
744 return nil, fmt.Errorf("json to yaml failed: %v\n%v\n", err, j)
745 }
746 return y, nil
747 }
748
View as plain text