1/* 2Copyright 2018 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 fake 18 19import ( 20 "fmt" 21 "testing" 22 23 "k8s.io/apimachinery/pkg/api/equality" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 26 "k8s.io/apimachinery/pkg/runtime" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 "k8s.io/apimachinery/pkg/types" 29 "k8s.io/apimachinery/pkg/util/diff" 30) 31 32const ( 33 testGroup = "testgroup" 34 testVersion = "testversion" 35 testResource = "testkinds" 36 testNamespace = "testns" 37 testName = "testname" 38 testKind = "TestKind" 39 testAPIVersion = "testgroup/testversion" 40) 41 42func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured { 43 return &unstructured.Unstructured{ 44 Object: map[string]interface{}{ 45 "apiVersion": apiVersion, 46 "kind": kind, 47 "metadata": map[string]interface{}{ 48 "namespace": namespace, 49 "name": name, 50 }, 51 }, 52 } 53} 54 55func newUnstructuredWithSpec(spec map[string]interface{}) *unstructured.Unstructured { 56 u := newUnstructured(testAPIVersion, testKind, testNamespace, testName) 57 u.Object["spec"] = spec 58 return u 59} 60 61func TestList(t *testing.T) { 62 scheme := runtime.NewScheme() 63 64 client := NewSimpleDynamicClient(scheme, 65 newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), 66 newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"), 67 newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), 68 newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"), 69 newUnstructured("group2/version", "TheKind", "ns-foo", "name2-baz"), 70 ) 71 listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(metav1.ListOptions{}) 72 if err != nil { 73 t.Fatal(err) 74 } 75 76 expected := []unstructured.Unstructured{ 77 *newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), 78 *newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), 79 *newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"), 80 } 81 if !equality.Semantic.DeepEqual(listFirst.Items, expected) { 82 t.Fatal(diff.ObjectGoPrintDiff(expected, listFirst.Items)) 83 } 84} 85 86type patchTestCase struct { 87 name string 88 object runtime.Object 89 patchType types.PatchType 90 patchBytes []byte 91 wantErrMsg string 92 expectedPatchedObject runtime.Object 93} 94 95func (tc *patchTestCase) runner(t *testing.T) { 96 client := NewSimpleDynamicClient(runtime.NewScheme(), tc.object) 97 resourceInterface := client.Resource(schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}).Namespace(testNamespace) 98 99 got, recErr := resourceInterface.Patch(testName, tc.patchType, tc.patchBytes, metav1.PatchOptions{}) 100 101 if err := tc.verifyErr(recErr); err != nil { 102 t.Error(err) 103 } 104 105 if err := tc.verifyResult(got); err != nil { 106 t.Error(err) 107 } 108 109} 110 111// verifyErr verifies that the given error returned from Patch is the error 112// expected by the test case. 113func (tc *patchTestCase) verifyErr(err error) error { 114 if tc.wantErrMsg != "" && err == nil { 115 return fmt.Errorf("want error, got nil") 116 } 117 118 if tc.wantErrMsg == "" && err != nil { 119 return fmt.Errorf("want no error, got %v", err) 120 } 121 122 if err != nil { 123 if want, got := tc.wantErrMsg, err.Error(); want != got { 124 return fmt.Errorf("incorrect error: want: %q got: %q", want, got) 125 } 126 } 127 return nil 128} 129 130func (tc *patchTestCase) verifyResult(result *unstructured.Unstructured) error { 131 if tc.expectedPatchedObject == nil && result == nil { 132 return nil 133 } 134 if !equality.Semantic.DeepEqual(result, tc.expectedPatchedObject) { 135 return fmt.Errorf("unexpected diff in received object: %s", diff.ObjectGoPrintDiff(tc.expectedPatchedObject, result)) 136 } 137 return nil 138} 139 140func TestPatch(t *testing.T) { 141 testCases := []patchTestCase{ 142 { 143 name: "jsonpatch fails with merge type", 144 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 145 patchType: types.StrategicMergePatchType, 146 patchBytes: []byte(`[]`), 147 wantErrMsg: "invalid JSON document", 148 }, { 149 name: "jsonpatch works with empty patch", 150 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 151 patchType: types.JSONPatchType, 152 // No-op 153 patchBytes: []byte(`[]`), 154 expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 155 }, { 156 name: "jsonpatch works with simple change patch", 157 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 158 patchType: types.JSONPatchType, 159 // change spec.foo from bar to foobar 160 patchBytes: []byte(`[{"op": "replace", "path": "/spec/foo", "value": "foobar"}]`), 161 expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "foobar"}), 162 }, { 163 name: "jsonpatch works with simple addition", 164 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 165 patchType: types.JSONPatchType, 166 // add spec.newvalue = dummy 167 patchBytes: []byte(`[{"op": "add", "path": "/spec/newvalue", "value": "dummy"}]`), 168 expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar", "newvalue": "dummy"}), 169 }, { 170 name: "jsonpatch works with simple deletion", 171 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar", "toremove": "shouldnotbehere"}), 172 patchType: types.JSONPatchType, 173 // remove spec.newvalue = dummy 174 patchBytes: []byte(`[{"op": "remove", "path": "/spec/toremove"}]`), 175 expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 176 }, { 177 name: "strategic merge patch fails with JSONPatch", 178 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 179 patchType: types.StrategicMergePatchType, 180 // add spec.newvalue = dummy 181 patchBytes: []byte(`[{"op": "add", "path": "/spec/newvalue", "value": "dummy"}]`), 182 wantErrMsg: "invalid JSON document", 183 }, { 184 name: "merge patch works with simple replacement", 185 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 186 patchType: types.MergePatchType, 187 patchBytes: []byte(`{ "spec": { "foo": "baz" } }`), 188 expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "baz"}), 189 }, 190 // TODO: Add tests for strategic merge using v1.Pod for example to ensure the test cases 191 // demonstrate expected use cases. 192 } 193 194 for _, tc := range testCases { 195 t.Run(tc.name, tc.runner) 196 } 197} 198