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