/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package jsonmergepatch import ( "fmt" "reflect" "testing" "github.com/davecgh/go-spew/spew" "github.com/evanphx/json-patch" "k8s.io/apimachinery/pkg/util/json" "sigs.k8s.io/yaml" ) type FilterNullTestCases struct { TestCases []FilterNullTestCase } type FilterNullTestCase struct { Description string OriginalObj map[string]interface{} ExpectedWithNull map[string]interface{} ExpectedWithoutNull map[string]interface{} } var filterNullTestCaseData = []byte(` testCases: - description: nil original originalObj: {} expectedWithNull: {} expectedWithoutNull: {} - description: simple map originalObj: nilKey: null nonNilKey: foo expectedWithNull: nilKey: null expectedWithoutNull: nonNilKey: foo - description: simple map with all nil values originalObj: nilKey1: null nilKey2: null expectedWithNull: nilKey1: null nilKey2: null expectedWithoutNull: {} - description: simple map with all non-nil values originalObj: nonNilKey1: foo nonNilKey2: bar expectedWithNull: {} expectedWithoutNull: nonNilKey1: foo nonNilKey2: bar - description: nested map originalObj: mapKey: nilKey: null nonNilKey: foo expectedWithNull: mapKey: nilKey: null expectedWithoutNull: mapKey: nonNilKey: foo - description: nested map that all subkeys are nil originalObj: mapKey: nilKey1: null nilKey2: null expectedWithNull: mapKey: nilKey1: null nilKey2: null expectedWithoutNull: {} - description: nested map that all subkeys are non-nil originalObj: mapKey: nonNilKey1: foo nonNilKey2: bar expectedWithNull: {} expectedWithoutNull: mapKey: nonNilKey1: foo nonNilKey2: bar - description: explicitly empty map as value originalObj: mapKey: {} expectedWithNull: {} expectedWithoutNull: mapKey: {} - description: explicitly empty nested map originalObj: mapKey: nonNilKey: {} expectedWithNull: {} expectedWithoutNull: mapKey: nonNilKey: {} - description: multiple expliclty empty nested maps originalObj: mapKey: nonNilKey1: {} nonNilKey2: {} expectedWithNull: {} expectedWithoutNull: mapKey: nonNilKey1: {} nonNilKey2: {} - description: nested map with non-null value as empty map originalObj: mapKey: nonNilKey: {} nilKey: null expectedWithNull: mapKey: nilKey: null expectedWithoutNull: mapKey: nonNilKey: {} - description: empty list originalObj: listKey: [] expectedWithNull: {} expectedWithoutNull: listKey: [] - description: list of primitives originalObj: listKey: - 1 - 2 expectedWithNull: {} expectedWithoutNull: listKey: - 1 - 2 - description: list of maps originalObj: listKey: - k1: v1 - k2: null - k3: v3 k4: null expectedWithNull: {} expectedWithoutNull: listKey: - k1: v1 - k2: null - k3: v3 k4: null - description: list of different types originalObj: listKey: - k1: v1 - k2: null - v3 expectedWithNull: {} expectedWithoutNull: listKey: - k1: v1 - k2: null - v3 `) func TestKeepOrDeleteNullInObj(t *testing.T) { tc := FilterNullTestCases{} err := yaml.Unmarshal(filterNullTestCaseData, &tc) if err != nil { t.Fatalf("can't unmarshal test cases: %s\n", err) } for _, test := range tc.TestCases { resultWithNull, err := keepOrDeleteNullInObj(test.OriginalObj, true) if err != nil { t.Errorf("Failed in test case %q when trying to keep null values: %s", test.Description, err) } if !reflect.DeepEqual(test.ExpectedWithNull, resultWithNull) { 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) } resultWithoutNull, err := keepOrDeleteNullInObj(test.OriginalObj, false) if err != nil { t.Errorf("Failed in test case %q when trying to keep non-null values: %s", test.Description, err) } if !reflect.DeepEqual(test.ExpectedWithoutNull, resultWithoutNull) { 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) } } } type JSONMergePatchTestCases struct { TestCases []JSONMergePatchTestCase } type JSONMergePatchTestCase struct { Description string JSONMergePatchTestCaseData } type JSONMergePatchTestCaseData struct { // Original is the original object (last-applied config in annotation) Original map[string]interface{} // Modified is the modified object (new config we want) Modified map[string]interface{} // Current is the current object (live config in the server) Current map[string]interface{} // ThreeWay is the expected three-way merge patch ThreeWay map[string]interface{} // Result is the expected object after applying the three-way patch on current object. Result map[string]interface{} } var createJSONMergePatchTestCaseData = []byte(` testCases: - description: nil original modified: name: 1 value: 1 current: name: 1 other: a threeWay: value: 1 result: name: 1 value: 1 other: a - description: nil patch original: name: 1 modified: name: 1 current: name: 1 threeWay: {} result: name: 1 - description: add field to map original: name: 1 modified: name: 1 value: 1 current: name: 1 other: a threeWay: value: 1 result: name: 1 value: 1 other: a - description: add field to map with conflict original: name: 1 modified: name: 1 value: 1 current: name: a other: a threeWay: name: 1 value: 1 result: name: 1 value: 1 other: a - description: add field and delete field from map original: name: 1 modified: value: 1 current: name: 1 other: a threeWay: name: null value: 1 result: value: 1 other: a - description: add field and delete field from map with conflict original: name: 1 modified: value: 1 current: name: a other: a threeWay: name: null value: 1 result: value: 1 other: a - description: delete field from nested map original: simpleMap: key1: 1 key2: 1 modified: simpleMap: key1: 1 current: simpleMap: key1: 1 key2: 1 other: a threeWay: simpleMap: key2: null result: simpleMap: key1: 1 other: a - description: delete field from nested map with conflict original: simpleMap: key1: 1 key2: 1 modified: simpleMap: key1: 1 current: simpleMap: key1: a key2: 1 other: a threeWay: simpleMap: key1: 1 key2: null result: simpleMap: key1: 1 other: a - description: delete all fields from map original: name: 1 value: 1 modified: {} current: name: 1 value: 1 other: a threeWay: name: null value: null result: other: a - description: delete all fields from map with conflict original: name: 1 value: 1 modified: {} current: name: 1 value: a other: a threeWay: name: null value: null result: other: a - description: add field and delete all fields from map original: name: 1 value: 1 modified: other: a current: name: 1 value: 1 other: a threeWay: name: null value: null result: other: a - description: add field and delete all fields from map with conflict original: name: 1 value: 1 modified: other: a current: name: 1 value: 1 other: b threeWay: name: null value: null other: a result: other: a - description: replace list of scalars original: intList: - 1 - 2 modified: intList: - 2 - 3 current: intList: - 1 - 2 threeWay: intList: - 2 - 3 result: intList: - 2 - 3 - description: replace list of scalars with conflict original: intList: - 1 - 2 modified: intList: - 2 - 3 current: intList: - 1 - 4 threeWay: intList: - 2 - 3 result: intList: - 2 - 3 - description: patch with different scalar type original: foo: 1 modified: foo: true current: foo: 1 bar: 2 threeWay: foo: true result: foo: true bar: 2 - description: patch from scalar to list original: foo: 0 modified: foo: - 1 - 2 current: foo: 0 bar: 2 threeWay: foo: - 1 - 2 result: foo: - 1 - 2 bar: 2 - description: patch from list to scalar original: foo: - 1 - 2 modified: foo: 0 current: foo: - 1 - 2 bar: 2 threeWay: foo: 0 result: foo: 0 bar: 2 - description: patch from scalar to map original: foo: 0 modified: foo: baz: 1 current: foo: 0 bar: 2 threeWay: foo: baz: 1 result: foo: baz: 1 bar: 2 - description: patch from map to scalar original: foo: baz: 1 modified: foo: 0 current: foo: baz: 1 bar: 2 threeWay: foo: 0 result: foo: 0 bar: 2 - description: patch from map to list original: foo: baz: 1 modified: foo: - 1 - 2 current: foo: baz: 1 bar: 2 threeWay: foo: - 1 - 2 result: foo: - 1 - 2 bar: 2 - description: patch from list to map original: foo: - 1 - 2 modified: foo: baz: 0 current: foo: - 1 - 2 bar: 2 threeWay: foo: baz: 0 result: foo: baz: 0 bar: 2 - description: patch with different nested types original: foo: - a: true - 2 - false modified: foo: - 1 - false - b: 1 current: foo: - a: true - 2 - false bar: 0 threeWay: foo: - 1 - false - b: 1 result: foo: - 1 - false - b: 1 bar: 0 - description: patch array with nil original: foo: - a: true - null - false bar: [] drop: - 1 modified: foo: - 1 - false - b: 1 bar: - c - null - null - a drop: - null current: foo: - a: true - 2 - false bar: - c - null - null - a drop: threeWay: foo: - 1 - false - b: 1 drop: - null result: foo: - 1 - false - b: 1 drop: - null bar: - c - null - null - a `) func TestCreateThreeWayJSONMergePatch(t *testing.T) { tc := JSONMergePatchTestCases{} err := yaml.Unmarshal(createJSONMergePatchTestCaseData, &tc) if err != nil { t.Errorf("can't unmarshal test cases: %s\n", err) return } for _, c := range tc.TestCases { testThreeWayPatch(t, c) } } func testThreeWayPatch(t *testing.T, c JSONMergePatchTestCase) { original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c) actual, err := CreateThreeWayJSONMergePatch(original, modified, current) if err != nil { t.Fatalf("error: %s", err) } testPatchCreation(t, expected, actual, c.Description) testPatchApplication(t, current, actual, result, c.Description) } func testPatchCreation(t *testing.T, expected, actual []byte, description string) { if !reflect.DeepEqual(actual, expected) { t.Errorf("error in test case: %s\nexpected patch:\n%s\ngot:\n%s\n", description, jsonToYAMLOrError(expected), jsonToYAMLOrError(actual)) return } } func testPatchApplication(t *testing.T, original, patch, expected []byte, description string) { result, err := jsonpatch.MergePatch(original, patch) if err != nil { t.Errorf("error: %s\nin test case: %s\ncannot apply patch:\n%s\nto original:\n%s\n", err, description, jsonToYAMLOrError(patch), jsonToYAMLOrError(original)) return } if !reflect.DeepEqual(result, expected) { format := "error in test case: %s\npatch application failed:\noriginal:\n%s\npatch:\n%s\nexpected:\n%s\ngot:\n%s\n" t.Errorf(format, description, jsonToYAMLOrError(original), jsonToYAMLOrError(patch), jsonToYAMLOrError(expected), jsonToYAMLOrError(result)) return } } func threeWayTestCaseToJSONOrFail(t *testing.T, c JSONMergePatchTestCase) ([]byte, []byte, []byte, []byte, []byte) { return testObjectToJSONOrFail(t, c.Original), testObjectToJSONOrFail(t, c.Modified), testObjectToJSONOrFail(t, c.Current), testObjectToJSONOrFail(t, c.ThreeWay), testObjectToJSONOrFail(t, c.Result) } func testObjectToJSONOrFail(t *testing.T, o map[string]interface{}) []byte { if o == nil { return nil } j, err := toJSON(o) if err != nil { t.Error(err) } return j } func jsonToYAMLOrError(j []byte) string { y, err := jsonToYAML(j) if err != nil { return err.Error() } return string(y) } func toJSON(v interface{}) ([]byte, error) { j, err := json.Marshal(v) if err != nil { return nil, fmt.Errorf("json marshal failed: %v\n%v\n", err, spew.Sdump(v)) } return j, nil } func jsonToYAML(j []byte) ([]byte, error) { y, err := yaml.JSONToYAML(j) if err != nil { return nil, fmt.Errorf("json to yaml failed: %v\n%v\n", err, j) } return y, nil }