blob: 254352065704f8f90d915a00671f5646786ca251 [file] [log] [blame]
/*
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
}