1
16
17 package metadata
18
19 import (
20 "context"
21 "encoding/json"
22 "io"
23 "net/http"
24 "net/http/httptest"
25 "reflect"
26 "strings"
27 "testing"
28
29 "github.com/google/go-cmp/cmp"
30 corev1 "k8s.io/api/core/v1"
31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32 "k8s.io/apimachinery/pkg/runtime"
33 "k8s.io/apimachinery/pkg/runtime/schema"
34 "k8s.io/client-go/rest"
35 "k8s.io/klog/v2/ktesting"
36 )
37
38 func TestClient(t *testing.T) {
39 gvr := schema.GroupVersionResource{Group: "group", Version: "v1", Resource: "resource"}
40 statusOK := &metav1.Status{
41 Status: metav1.StatusSuccess,
42 Code: http.StatusOK,
43 }
44
45 writeJSON := func(t *testing.T, w http.ResponseWriter, obj runtime.Object) {
46 data, err := json.Marshal(obj)
47 if err != nil {
48 t.Fatal(err)
49 }
50 w.Header().Set("Content-Type", "application/json")
51 if _, err := w.Write(data); err != nil {
52 t.Fatal(err)
53 }
54 }
55
56 testCases := []struct {
57 name string
58 handler func(t *testing.T, w http.ResponseWriter, req *http.Request)
59 want func(ctx context.Context, t *testing.T, client *Client)
60 }{
61 {
62 name: "GET is able to convert a JSON object to PartialObjectMetadata",
63 handler: func(t *testing.T, w http.ResponseWriter, req *http.Request) {
64 if req.Header.Get("Accept") != "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json" {
65 t.Fatal(req.Header.Get("Accept"))
66 }
67 if req.Method != "GET" && req.URL.String() != "/apis/group/v1/namespaces/ns/resource/name" {
68 t.Fatal(req.URL.String())
69 }
70 writeJSON(t, w, &corev1.Pod{
71 TypeMeta: metav1.TypeMeta{
72 Kind: "Pod",
73 APIVersion: "v1",
74 },
75 ObjectMeta: metav1.ObjectMeta{
76 Name: "name",
77 Namespace: "ns",
78 },
79 })
80 },
81 want: func(ctx context.Context, t *testing.T, client *Client) {
82 obj, err := client.Resource(gvr).Namespace("ns").Get(ctx, "name", metav1.GetOptions{})
83 if err != nil {
84 t.Fatal(err)
85 }
86 expect := &metav1.PartialObjectMetadata{
87 ObjectMeta: metav1.ObjectMeta{
88 Name: "name",
89 Namespace: "ns",
90 },
91 }
92 if !reflect.DeepEqual(expect, obj) {
93 t.Fatal(cmp.Diff(expect, obj))
94 }
95 },
96 },
97
98 {
99 name: "LIST is able to convert a JSON object to PartialObjectMetadata",
100 handler: func(t *testing.T, w http.ResponseWriter, req *http.Request) {
101 if req.Header.Get("Accept") != "application/vnd.kubernetes.protobuf;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1,application/json" {
102 t.Fatal(req.Header.Get("Accept"))
103 }
104 if req.Method != "GET" && req.URL.String() != "/apis/group/v1/namespaces/ns/resource" {
105 t.Fatal(req.URL.String())
106 }
107 writeJSON(t, w, &corev1.PodList{
108 TypeMeta: metav1.TypeMeta{
109 Kind: "PodList",
110 APIVersion: "v1",
111 },
112 ListMeta: metav1.ListMeta{
113 ResourceVersion: "253",
114 },
115 Items: []corev1.Pod{
116 {
117 TypeMeta: metav1.TypeMeta{
118 Kind: "Pod",
119 APIVersion: "v1",
120 },
121 ObjectMeta: metav1.ObjectMeta{
122 Name: "name",
123 Namespace: "ns",
124 },
125 },
126 },
127 })
128 },
129 want: func(ctx context.Context, t *testing.T, client *Client) {
130 objs, err := client.Resource(gvr).Namespace("ns").List(ctx, metav1.ListOptions{})
131 if err != nil {
132 t.Fatal(err)
133 }
134 if objs.GetResourceVersion() != "253" {
135 t.Fatal(objs)
136 }
137 expect := []metav1.PartialObjectMetadata{
138 {
139 TypeMeta: metav1.TypeMeta{
140 Kind: "Pod",
141 APIVersion: "v1",
142 },
143 ObjectMeta: metav1.ObjectMeta{
144 Name: "name",
145 Namespace: "ns",
146 },
147 },
148 }
149 if !reflect.DeepEqual(expect, objs.Items) {
150 t.Fatal(cmp.Diff(expect, objs.Items))
151 }
152 },
153 },
154
155 {
156 name: "GET fails if the object is JSON and has no kind",
157 handler: func(t *testing.T, w http.ResponseWriter, req *http.Request) {
158 if req.Header.Get("Accept") != "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json" {
159 t.Fatal(req.Header.Get("Accept"))
160 }
161 if req.Method != "GET" && req.URL.String() != "/apis/group/v1/namespaces/ns/resource/name" {
162 t.Fatal(req.URL.String())
163 }
164 writeJSON(t, w, &corev1.Pod{
165 TypeMeta: metav1.TypeMeta{},
166 ObjectMeta: metav1.ObjectMeta{
167 UID: "123",
168 },
169 })
170 },
171 want: func(ctx context.Context, t *testing.T, client *Client) {
172 obj, err := client.Resource(gvr).Namespace("ns").Get(ctx, "name", metav1.GetOptions{})
173 if err == nil || !runtime.IsMissingKind(err) {
174 t.Fatal(err)
175 }
176 if obj != nil {
177 t.Fatal(obj)
178 }
179 },
180 },
181
182 {
183 name: "GET fails if the object is JSON and has no apiVersion",
184 handler: func(t *testing.T, w http.ResponseWriter, req *http.Request) {
185 if req.Header.Get("Accept") != "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json" {
186 t.Fatal(req.Header.Get("Accept"))
187 }
188 if req.Method != "GET" && req.URL.String() != "/apis/group/v1/namespaces/ns/resource/name" {
189 t.Fatal(req.URL.String())
190 }
191 writeJSON(t, w, &corev1.Pod{
192 TypeMeta: metav1.TypeMeta{
193 Kind: "Pod",
194 },
195 ObjectMeta: metav1.ObjectMeta{
196 UID: "123",
197 },
198 })
199 },
200 want: func(ctx context.Context, t *testing.T, client *Client) {
201 obj, err := client.Resource(gvr).Namespace("ns").Get(ctx, "name", metav1.GetOptions{})
202 if err == nil || !runtime.IsMissingVersion(err) {
203 t.Fatal(err)
204 }
205 if obj != nil {
206 t.Fatal(obj)
207 }
208 },
209 },
210
211 {
212 name: "GET fails if the object is JSON and not clearly metadata",
213 handler: func(t *testing.T, w http.ResponseWriter, req *http.Request) {
214 if req.Header.Get("Accept") != "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json" {
215 t.Fatal(req.Header.Get("Accept"))
216 }
217 if req.Method != "GET" && req.URL.String() != "/apis/group/v1/namespaces/ns/resource/name" {
218 t.Fatal(req.URL.String())
219 }
220 writeJSON(t, w, &corev1.Pod{
221 TypeMeta: metav1.TypeMeta{
222 Kind: "Pod",
223 APIVersion: "v1",
224 },
225 ObjectMeta: metav1.ObjectMeta{},
226 })
227 },
228 want: func(ctx context.Context, t *testing.T, client *Client) {
229 obj, err := client.Resource(gvr).Namespace("ns").Get(ctx, "name", metav1.GetOptions{})
230 if err == nil || !strings.Contains(err.Error(), "object does not appear to match the ObjectMeta schema") {
231 t.Fatal(err)
232 }
233 if obj != nil {
234 t.Fatal(obj)
235 }
236 },
237 },
238
239 {
240 name: "Delete fails if DeleteOptions cannot be serialized to JSON",
241 handler: func(t *testing.T, w http.ResponseWriter, req *http.Request) {
242 if req.Header.Get("Content-Type") != runtime.ContentTypeJSON {
243 t.Fatal(req.Header.Get("Content-Type"))
244 }
245 if req.Method != "DELETE" && req.URL.String() != "/apis/group/v1/namespaces/ns/resource/name" {
246 t.Fatal(req.URL.String())
247 }
248 defer req.Body.Close()
249 buf, err := io.ReadAll(req.Body)
250 if err != nil {
251 t.Fatal(err)
252 }
253 if !json.Valid(buf) {
254 t.Fatalf("request body is not a valid JSON: %s", buf)
255 }
256 writeJSON(t, w, statusOK)
257 },
258 want: func(ctx context.Context, t *testing.T, client *Client) {
259 err := client.Resource(gvr).Namespace("ns").Delete(ctx, "name", metav1.DeleteOptions{})
260 if err != nil {
261 t.Fatal(err)
262 }
263 },
264 },
265
266 {
267 name: "DeleteCollection fails if DeleteOptions cannot be serialized to JSON",
268 handler: func(t *testing.T, w http.ResponseWriter, req *http.Request) {
269 if req.Header.Get("Content-Type") != runtime.ContentTypeJSON {
270 t.Fatal(req.Header.Get("Content-Type"))
271 }
272 if req.Method != "DELETE" && req.URL.String() != "/apis/group/v1/namespaces/ns/resource/name" {
273 t.Fatal(req.URL.String())
274 }
275 defer req.Body.Close()
276 buf, err := io.ReadAll(req.Body)
277 if err != nil {
278 t.Fatal(err)
279 }
280 if !json.Valid(buf) {
281 t.Fatalf("request body is not a valid JSON: %s", buf)
282 }
283
284 writeJSON(t, w, statusOK)
285 },
286 want: func(ctx context.Context, t *testing.T, client *Client) {
287 err := client.Resource(gvr).Namespace("ns").DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{})
288 if err != nil {
289 t.Fatal(err)
290 }
291 },
292 },
293 }
294
295 for _, tt := range testCases {
296 t.Run(tt.name, func(t *testing.T) {
297 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { tt.handler(t, w, req) }))
298 defer s.Close()
299
300 _, ctx := ktesting.NewTestContext(t)
301 cfg := ConfigFor(&rest.Config{Host: s.URL})
302 client := NewForConfigOrDie(cfg).(*Client)
303 tt.want(ctx, t, client)
304 })
305 }
306 }
307
View as plain text