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