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