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