| /* |
| 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 |
| `) |
| |
| 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 |
| } |