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/apis/meta/v1/unstructured"
27	"k8s.io/apimachinery/pkg/runtime"
28	"k8s.io/apimachinery/pkg/runtime/schema"
29	"k8s.io/apimachinery/pkg/types"
30	"k8s.io/apimachinery/pkg/util/diff"
31)
32
33const (
34	testGroup      = "testgroup"
35	testVersion    = "testversion"
36	testResource   = "testkinds"
37	testNamespace  = "testns"
38	testName       = "testname"
39	testKind       = "TestKind"
40	testAPIVersion = "testgroup/testversion"
41)
42
43func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
44	return &unstructured.Unstructured{
45		Object: map[string]interface{}{
46			"apiVersion": apiVersion,
47			"kind":       kind,
48			"metadata": map[string]interface{}{
49				"namespace": namespace,
50				"name":      name,
51			},
52		},
53	}
54}
55
56func newUnstructuredWithSpec(spec map[string]interface{}) *unstructured.Unstructured {
57	u := newUnstructured(testAPIVersion, testKind, testNamespace, testName)
58	u.Object["spec"] = spec
59	return u
60}
61
62func TestGet(t *testing.T) {
63	scheme := runtime.NewScheme()
64
65	client := NewSimpleDynamicClient(scheme, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"))
66	get, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).Namespace("ns-foo").Get(context.TODO(), "name-foo", metav1.GetOptions{})
67	if err != nil {
68		t.Fatal(err)
69	}
70
71	expected := &unstructured.Unstructured{
72		Object: map[string]interface{}{
73			"apiVersion": "group/version",
74			"kind":       "TheKind",
75			"metadata": map[string]interface{}{
76				"name":      "name-foo",
77				"namespace": "ns-foo",
78			},
79		},
80	}
81	if !equality.Semantic.DeepEqual(get, expected) {
82		t.Fatal(diff.ObjectGoPrintDiff(expected, get))
83	}
84}
85
86func TestListDecoding(t *testing.T) {
87	// this the duplication of logic from the real List API.  This will prove that our dynamic client actually returns the gvk
88	uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKindList", "items":[]}`))
89	if err != nil {
90		t.Fatal(err)
91	}
92	list := uncastObj.(*unstructured.UnstructuredList)
93	expectedList := &unstructured.UnstructuredList{
94		Object: map[string]interface{}{
95			"apiVersion": "group/version",
96			"kind":       "TheKindList",
97		},
98		Items: []unstructured.Unstructured{},
99	}
100	if !equality.Semantic.DeepEqual(list, expectedList) {
101		t.Fatal(diff.ObjectGoPrintDiff(expectedList, list))
102	}
103}
104
105func TestGetDecoding(t *testing.T) {
106	// this the duplication of logic from the real Get API.  This will prove that our dynamic client actually returns the gvk
107	uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKind"}`))
108	if err != nil {
109		t.Fatal(err)
110	}
111	get := uncastObj.(*unstructured.Unstructured)
112	expectedObj := &unstructured.Unstructured{
113		Object: map[string]interface{}{
114			"apiVersion": "group/version",
115			"kind":       "TheKind",
116		},
117	}
118	if !equality.Semantic.DeepEqual(get, expectedObj) {
119		t.Fatal(diff.ObjectGoPrintDiff(expectedObj, get))
120	}
121}
122
123func TestList(t *testing.T) {
124	scheme := runtime.NewScheme()
125
126	client := NewSimpleDynamicClientWithCustomListKinds(scheme,
127		map[schema.GroupVersionResource]string{
128			{Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList",
129		},
130		newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
131		newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"),
132		newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
133		newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
134		newUnstructured("group2/version", "TheKind", "ns-foo", "name2-baz"),
135	)
136	listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{})
137	if err != nil {
138		t.Fatal(err)
139	}
140
141	expected := []unstructured.Unstructured{
142		*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
143		*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
144		*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
145	}
146	if !equality.Semantic.DeepEqual(listFirst.Items, expected) {
147		t.Fatal(diff.ObjectGoPrintDiff(expected, listFirst.Items))
148	}
149}
150
151func Test_ListKind(t *testing.T) {
152	scheme := runtime.NewScheme()
153
154	client := NewSimpleDynamicClientWithCustomListKinds(scheme,
155		map[schema.GroupVersionResource]string{
156			{Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList",
157		},
158		&unstructured.UnstructuredList{
159			Object: map[string]interface{}{
160				"apiVersion": "group/version",
161				"kind":       "TheKindList",
162			},
163			Items: []unstructured.Unstructured{
164				*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
165				*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
166				*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
167			},
168		},
169	)
170	listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{})
171	if err != nil {
172		t.Fatal(err)
173	}
174
175	expectedList := &unstructured.UnstructuredList{
176		Object: map[string]interface{}{
177			"apiVersion": "group/version",
178			"kind":       "TheKindList",
179			"metadata": map[string]interface{}{
180				"resourceVersion": "",
181			},
182		},
183		Items: []unstructured.Unstructured{
184			*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
185			*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
186			*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
187		},
188	}
189	if !equality.Semantic.DeepEqual(listFirst, expectedList) {
190		t.Fatal(diff.ObjectGoPrintDiff(expectedList, listFirst))
191	}
192}
193
194type patchTestCase struct {
195	name                  string
196	object                runtime.Object
197	patchType             types.PatchType
198	patchBytes            []byte
199	wantErrMsg            string
200	expectedPatchedObject runtime.Object
201}
202
203func (tc *patchTestCase) runner(t *testing.T) {
204	client := NewSimpleDynamicClient(runtime.NewScheme(), tc.object)
205	resourceInterface := client.Resource(schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}).Namespace(testNamespace)
206
207	got, recErr := resourceInterface.Patch(context.TODO(), testName, tc.patchType, tc.patchBytes, metav1.PatchOptions{})
208
209	if err := tc.verifyErr(recErr); err != nil {
210		t.Error(err)
211	}
212
213	if err := tc.verifyResult(got); err != nil {
214		t.Error(err)
215	}
216
217}
218
219// verifyErr verifies that the given error returned from Patch is the error
220// expected by the test case.
221func (tc *patchTestCase) verifyErr(err error) error {
222	if tc.wantErrMsg != "" && err == nil {
223		return fmt.Errorf("want error, got nil")
224	}
225
226	if tc.wantErrMsg == "" && err != nil {
227		return fmt.Errorf("want no error, got %v", err)
228	}
229
230	if err != nil {
231		if want, got := tc.wantErrMsg, err.Error(); want != got {
232			return fmt.Errorf("incorrect error: want: %q got: %q", want, got)
233		}
234	}
235	return nil
236}
237
238func (tc *patchTestCase) verifyResult(result *unstructured.Unstructured) error {
239	if tc.expectedPatchedObject == nil && result == nil {
240		return nil
241	}
242	if !equality.Semantic.DeepEqual(result, tc.expectedPatchedObject) {
243		return fmt.Errorf("unexpected diff in received object: %s", diff.ObjectGoPrintDiff(tc.expectedPatchedObject, result))
244	}
245	return nil
246}
247
248func TestPatch(t *testing.T) {
249	testCases := []patchTestCase{
250		{
251			name:       "jsonpatch fails with merge type",
252			object:     newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
253			patchType:  types.StrategicMergePatchType,
254			patchBytes: []byte(`[]`),
255			wantErrMsg: "invalid JSON document",
256		}, {
257			name:      "jsonpatch works with empty patch",
258			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
259			patchType: types.JSONPatchType,
260			// No-op
261			patchBytes:            []byte(`[]`),
262			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
263		}, {
264			name:      "jsonpatch works with simple change patch",
265			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
266			patchType: types.JSONPatchType,
267			// change spec.foo from bar to foobar
268			patchBytes:            []byte(`[{"op": "replace", "path": "/spec/foo", "value": "foobar"}]`),
269			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "foobar"}),
270		}, {
271			name:      "jsonpatch works with simple addition",
272			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
273			patchType: types.JSONPatchType,
274			// add spec.newvalue = dummy
275			patchBytes:            []byte(`[{"op": "add", "path": "/spec/newvalue", "value": "dummy"}]`),
276			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar", "newvalue": "dummy"}),
277		}, {
278			name:      "jsonpatch works with simple deletion",
279			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar", "toremove": "shouldnotbehere"}),
280			patchType: types.JSONPatchType,
281			// remove spec.newvalue = dummy
282			patchBytes:            []byte(`[{"op": "remove", "path": "/spec/toremove"}]`),
283			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
284		}, {
285			name:      "strategic merge patch fails with JSONPatch",
286			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
287			patchType: types.StrategicMergePatchType,
288			// add spec.newvalue = dummy
289			patchBytes: []byte(`[{"op": "add", "path": "/spec/newvalue", "value": "dummy"}]`),
290			wantErrMsg: "invalid JSON document",
291		}, {
292			name:                  "merge patch works with simple replacement",
293			object:                newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
294			patchType:             types.MergePatchType,
295			patchBytes:            []byte(`{ "spec": { "foo": "baz" } }`),
296			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "baz"}),
297		},
298		// TODO: Add tests for strategic merge using v1.Pod for example to ensure the test cases
299		// demonstrate expected use cases.
300	}
301
302	for _, tc := range testCases {
303		t.Run(tc.name, tc.runner)
304	}
305}
306