k8s.io/client-go@v0.31.1/dynamic/client_test.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package dynamic
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"net/http/httptest"
    26  	"reflect"
    27  	"strings"
    28  	"testing"
    29  
    30  	"github.com/google/go-cmp/cmp"
    31  
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/apimachinery/pkg/runtime/serializer/streaming"
    37  	"k8s.io/apimachinery/pkg/types"
    38  	"k8s.io/apimachinery/pkg/watch"
    39  	clientfeatures "k8s.io/client-go/features"
    40  	clientfeaturestesting "k8s.io/client-go/features/testing"
    41  	restclient "k8s.io/client-go/rest"
    42  	restclientwatch "k8s.io/client-go/rest/watch"
    43  )
    44  
    45  func getJSON(version, kind, name string) []byte {
    46  	return []byte(fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "metadata": {"name": %q}}`, version, kind, name))
    47  }
    48  
    49  func getListJSON(version, kind string, items ...[]byte) []byte {
    50  	json := fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "items": [%s]}`,
    51  		version, kind, bytes.Join(items, []byte(",")))
    52  	return []byte(json)
    53  }
    54  
    55  func getObject(version, kind, name string) *unstructured.Unstructured {
    56  	return &unstructured.Unstructured{
    57  		Object: map[string]interface{}{
    58  			"apiVersion": version,
    59  			"kind":       kind,
    60  			"metadata": map[string]interface{}{
    61  				"name": name,
    62  			},
    63  		},
    64  	}
    65  }
    66  
    67  func getClientServer(h func(http.ResponseWriter, *http.Request)) (Interface, *httptest.Server, error) {
    68  	srv := httptest.NewServer(http.HandlerFunc(h))
    69  	cl, err := NewForConfig(&restclient.Config{
    70  		Host: srv.URL,
    71  	})
    72  	if err != nil {
    73  		srv.Close()
    74  		return nil, nil, err
    75  	}
    76  	return cl, srv, nil
    77  }
    78  
    79  func TestList(t *testing.T) {
    80  	tcs := []struct {
    81  		name      string
    82  		namespace string
    83  		path      string
    84  		resp      []byte
    85  		want      *unstructured.UnstructuredList
    86  	}{
    87  		{
    88  			name: "normal_list",
    89  			path: "/apis/gtest/vtest/rtest",
    90  			resp: getListJSON("vTest", "rTestList",
    91  				getJSON("vTest", "rTest", "item1"),
    92  				getJSON("vTest", "rTest", "item2")),
    93  			want: &unstructured.UnstructuredList{
    94  				Object: map[string]interface{}{
    95  					"apiVersion": "vTest",
    96  					"kind":       "rTestList",
    97  				},
    98  				Items: []unstructured.Unstructured{
    99  					*getObject("vTest", "rTest", "item1"),
   100  					*getObject("vTest", "rTest", "item2"),
   101  				},
   102  			},
   103  		},
   104  		{
   105  			name:      "namespaced_list",
   106  			namespace: "nstest",
   107  			path:      "/apis/gtest/vtest/namespaces/nstest/rtest",
   108  			resp: getListJSON("vTest", "rTestList",
   109  				getJSON("vTest", "rTest", "item1"),
   110  				getJSON("vTest", "rTest", "item2")),
   111  			want: &unstructured.UnstructuredList{
   112  				Object: map[string]interface{}{
   113  					"apiVersion": "vTest",
   114  					"kind":       "rTestList",
   115  				},
   116  				Items: []unstructured.Unstructured{
   117  					*getObject("vTest", "rTest", "item1"),
   118  					*getObject("vTest", "rTest", "item2"),
   119  				},
   120  			},
   121  		},
   122  	}
   123  	for _, tc := range tcs {
   124  		resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
   125  		cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
   126  			if r.Method != "GET" {
   127  				t.Errorf("List(%q) got HTTP method %s. wanted GET", tc.name, r.Method)
   128  			}
   129  
   130  			if r.URL.Path != tc.path {
   131  				t.Errorf("List(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
   132  			}
   133  
   134  			w.Header().Set("Content-Type", runtime.ContentTypeJSON)
   135  			w.Write(tc.resp)
   136  		})
   137  		if err != nil {
   138  			t.Errorf("unexpected error when creating client: %v", err)
   139  			continue
   140  		}
   141  		defer srv.Close()
   142  
   143  		got, err := cl.Resource(resource).Namespace(tc.namespace).List(context.TODO(), metav1.ListOptions{})
   144  		if err != nil {
   145  			t.Errorf("unexpected error when listing %q: %v", tc.name, err)
   146  			continue
   147  		}
   148  
   149  		if !reflect.DeepEqual(got, tc.want) {
   150  			t.Errorf("List(%q) want: %v\ngot: %v", tc.name, tc.want, got)
   151  		}
   152  	}
   153  }
   154  
   155  func TestWatchList(t *testing.T) {
   156  	clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, true)
   157  
   158  	type requestParam struct {
   159  		Path  string
   160  		Query string
   161  	}
   162  
   163  	scenarios := []struct {
   164  		name          string
   165  		namespace     string
   166  		watchResponse []watch.Event
   167  		listResponse  []byte
   168  
   169  		expectedRequestParams []requestParam
   170  		expectedList          *unstructured.UnstructuredList
   171  	}{
   172  		{
   173  			name: "watch-list request for cluster wide resource",
   174  			watchResponse: []watch.Event{
   175  				{Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "item1")},
   176  				{Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "item2")},
   177  				{Type: watch.Bookmark, Object: func() runtime.Object {
   178  					obj := getObject("gtest/vTest", "rTest", "item2")
   179  					obj.SetResourceVersion("10")
   180  					obj.SetAnnotations(map[string]string{metav1.InitialEventsAnnotationKey: "true"})
   181  					return obj
   182  				}()},
   183  			},
   184  			expectedRequestParams: []requestParam{
   185  				{
   186  					Path:  "/apis/gtest/vtest/rtest",
   187  					Query: "allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&watch=true",
   188  				},
   189  			},
   190  			expectedList: &unstructured.UnstructuredList{
   191  				Object: map[string]interface{}{
   192  					"apiVersion": "",
   193  					"kind":       "UnstructuredList",
   194  					"metadata": map[string]interface{}{
   195  						"resourceVersion": "10",
   196  					},
   197  				},
   198  				Items: []unstructured.Unstructured{
   199  					*getObject("gtest/vTest", "rTest", "item1"),
   200  					*getObject("gtest/vTest", "rTest", "item2"),
   201  				},
   202  			},
   203  		},
   204  		{
   205  			name:      "watch-list request for namespaced watch resource",
   206  			namespace: "nstest",
   207  			watchResponse: []watch.Event{
   208  				{Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "item1")},
   209  				{Type: watch.Bookmark, Object: func() runtime.Object {
   210  					obj := getObject("gtest/vTest", "rTest", "item2")
   211  					obj.SetResourceVersion("39")
   212  					obj.SetAnnotations(map[string]string{metav1.InitialEventsAnnotationKey: "true"})
   213  					return obj
   214  				}()},
   215  			},
   216  			expectedRequestParams: []requestParam{
   217  				{
   218  					Path:  "/apis/gtest/vtest/namespaces/nstest/rtest",
   219  					Query: "allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&watch=true",
   220  				},
   221  			},
   222  			expectedList: &unstructured.UnstructuredList{
   223  				Object: map[string]interface{}{
   224  					"apiVersion": "",
   225  					"kind":       "UnstructuredList",
   226  					"metadata": map[string]interface{}{
   227  						"resourceVersion": "39",
   228  					},
   229  				},
   230  				Items: []unstructured.Unstructured{
   231  					*getObject("gtest/vTest", "rTest", "item1"),
   232  				},
   233  			},
   234  		},
   235  		{
   236  			name:      "watch-list request falls back to standard list on any error",
   237  			namespace: "nstest",
   238  			// watchList method in client-go expect only watch.Add and watch.Bookmark events
   239  			// receiving watch.Error will cause this method to report an error which will
   240  			// trigger the fallback logic
   241  			watchResponse: []watch.Event{
   242  				{Type: watch.Error, Object: getObject("gtest/vTest", "rTest", "item1")},
   243  			},
   244  			listResponse: getListJSON("vTest", "UnstructuredList",
   245  				getJSON("gtest/vTest", "rTest", "item1"),
   246  				getJSON("gtest/vTest", "rTest", "item2")),
   247  			expectedRequestParams: []requestParam{
   248  				// a watch-list request first
   249  				{
   250  					Path:  "/apis/gtest/vtest/namespaces/nstest/rtest",
   251  					Query: "allowWatchBookmarks=true&resourceVersionMatch=NotOlderThan&sendInitialEvents=true&watch=true",
   252  				},
   253  				// a standard list request second
   254  				{
   255  					Path: "/apis/gtest/vtest/namespaces/nstest/rtest",
   256  				},
   257  			},
   258  			expectedList: &unstructured.UnstructuredList{
   259  				Object: map[string]interface{}{
   260  					"apiVersion": "vTest",
   261  					"kind":       "UnstructuredList",
   262  				},
   263  				Items: []unstructured.Unstructured{
   264  					*getObject("gtest/vTest", "rTest", "item1"),
   265  					*getObject("gtest/vTest", "rTest", "item2"),
   266  				},
   267  			},
   268  		},
   269  	}
   270  	for _, scenario := range scenarios {
   271  		t.Run(scenario.name, func(t *testing.T) {
   272  			var actualRequestParams []requestParam
   273  			resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
   274  			cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
   275  				if r.Method != "GET" {
   276  					t.Errorf("unexpected HTTP method %s. expected GET", r.Method)
   277  				}
   278  				actualRequestParams = append(actualRequestParams, requestParam{
   279  					Path:  r.URL.Path,
   280  					Query: r.URL.RawQuery,
   281  				})
   282  
   283  				w.Header().Set("Content-Type", runtime.ContentTypeJSON)
   284  				// handle LIST response
   285  				if len(scenario.listResponse) > 0 {
   286  					if _, err := w.Write(scenario.listResponse); err != nil {
   287  						t.Fatal(err)
   288  					}
   289  					return
   290  				}
   291  
   292  				// handle WATCH response
   293  				enc := restclientwatch.NewEncoder(streaming.NewEncoder(w, unstructured.UnstructuredJSONScheme), unstructured.UnstructuredJSONScheme)
   294  				for _, e := range scenario.watchResponse {
   295  					if err := enc.Encode(&e); err != nil {
   296  						t.Fatal(err)
   297  					}
   298  				}
   299  			})
   300  			if err != nil {
   301  				t.Fatalf("unexpected error when creating test client and server: %v", err)
   302  			}
   303  			defer srv.Close()
   304  
   305  			actualList, err := cl.Resource(resource).Namespace(scenario.namespace).List(context.TODO(), metav1.ListOptions{})
   306  			if err != nil {
   307  				t.Fatalf("unexpected error: %v", err)
   308  			}
   309  
   310  			if !cmp.Equal(scenario.expectedRequestParams, actualRequestParams) {
   311  				t.Fatalf("unexpected request params: %v", cmp.Diff(scenario.expectedRequestParams, actualRequestParams))
   312  			}
   313  			if !cmp.Equal(scenario.expectedList, actualList) {
   314  				t.Errorf("received expected list, diff: %s", cmp.Diff(scenario.expectedList, actualList))
   315  			}
   316  		})
   317  	}
   318  }
   319  
   320  func TestGet(t *testing.T) {
   321  	tcs := []struct {
   322  		resource    string
   323  		subresource []string
   324  		namespace   string
   325  		name        string
   326  		path        string
   327  		resp        []byte
   328  		want        *unstructured.Unstructured
   329  	}{
   330  		{
   331  			resource: "rtest",
   332  			name:     "normal_get",
   333  			path:     "/apis/gtest/vtest/rtest/normal_get",
   334  			resp:     getJSON("vTest", "rTest", "normal_get"),
   335  			want:     getObject("vTest", "rTest", "normal_get"),
   336  		},
   337  		{
   338  			resource:  "rtest",
   339  			namespace: "nstest",
   340  			name:      "namespaced_get",
   341  			path:      "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_get",
   342  			resp:      getJSON("vTest", "rTest", "namespaced_get"),
   343  			want:      getObject("vTest", "rTest", "namespaced_get"),
   344  		},
   345  		{
   346  			resource:    "rtest",
   347  			subresource: []string{"srtest"},
   348  			name:        "normal_subresource_get",
   349  			path:        "/apis/gtest/vtest/rtest/normal_subresource_get/srtest",
   350  			resp:        getJSON("vTest", "srTest", "normal_subresource_get"),
   351  			want:        getObject("vTest", "srTest", "normal_subresource_get"),
   352  		},
   353  		{
   354  			resource:    "rtest",
   355  			subresource: []string{"srtest"},
   356  			namespace:   "nstest",
   357  			name:        "namespaced_subresource_get",
   358  			path:        "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_get/srtest",
   359  			resp:        getJSON("vTest", "srTest", "namespaced_subresource_get"),
   360  			want:        getObject("vTest", "srTest", "namespaced_subresource_get"),
   361  		},
   362  	}
   363  	for _, tc := range tcs {
   364  		resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource}
   365  		cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
   366  			if r.Method != "GET" {
   367  				t.Errorf("Get(%q) got HTTP method %s. wanted GET", tc.name, r.Method)
   368  			}
   369  
   370  			if r.URL.Path != tc.path {
   371  				t.Errorf("Get(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
   372  			}
   373  
   374  			w.Header().Set("Content-Type", runtime.ContentTypeJSON)
   375  			w.Write(tc.resp)
   376  		})
   377  		if err != nil {
   378  			t.Errorf("unexpected error when creating client: %v", err)
   379  			continue
   380  		}
   381  		defer srv.Close()
   382  
   383  		got, err := cl.Resource(resource).Namespace(tc.namespace).Get(context.TODO(), tc.name, metav1.GetOptions{}, tc.subresource...)
   384  		if err != nil {
   385  			t.Errorf("unexpected error when getting %q: %v", tc.name, err)
   386  			continue
   387  		}
   388  
   389  		if !reflect.DeepEqual(got, tc.want) {
   390  			t.Errorf("Get(%q) want: %v\ngot: %v", tc.name, tc.want, got)
   391  		}
   392  	}
   393  }
   394  
   395  func TestDelete(t *testing.T) {
   396  	background := metav1.DeletePropagationBackground
   397  	uid := types.UID("uid")
   398  
   399  	statusOK := &metav1.Status{
   400  		TypeMeta: metav1.TypeMeta{Kind: "Status"},
   401  		Status:   metav1.StatusSuccess,
   402  	}
   403  	tcs := []struct {
   404  		subresource   []string
   405  		namespace     string
   406  		name          string
   407  		path          string
   408  		deleteOptions metav1.DeleteOptions
   409  	}{
   410  		{
   411  			name: "normal_delete",
   412  			path: "/apis/gtest/vtest/rtest/normal_delete",
   413  		},
   414  		{
   415  			namespace: "nstest",
   416  			name:      "namespaced_delete",
   417  			path:      "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete",
   418  		},
   419  		{
   420  			subresource: []string{"srtest"},
   421  			name:        "normal_delete",
   422  			path:        "/apis/gtest/vtest/rtest/normal_delete/srtest",
   423  		},
   424  		{
   425  			subresource: []string{"srtest"},
   426  			namespace:   "nstest",
   427  			name:        "namespaced_delete",
   428  			path:        "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete/srtest",
   429  		},
   430  		{
   431  			namespace:     "nstest",
   432  			name:          "namespaced_delete_with_options",
   433  			path:          "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete_with_options",
   434  			deleteOptions: metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &uid}, PropagationPolicy: &background},
   435  		},
   436  	}
   437  	for _, tc := range tcs {
   438  		resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
   439  		cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
   440  			if r.Method != "DELETE" {
   441  				t.Errorf("Delete(%q) got HTTP method %s. wanted DELETE", tc.name, r.Method)
   442  			}
   443  
   444  			if r.URL.Path != tc.path {
   445  				t.Errorf("Delete(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
   446  			}
   447  
   448  			content := r.Header.Get("Content-Type")
   449  			if content != runtime.ContentTypeJSON {
   450  				t.Errorf("Delete(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON)
   451  			}
   452  
   453  			w.Header().Set("Content-Type", runtime.ContentTypeJSON)
   454  			unstructured.UnstructuredJSONScheme.Encode(statusOK, w)
   455  		})
   456  		if err != nil {
   457  			t.Errorf("unexpected error when creating client: %v", err)
   458  			continue
   459  		}
   460  		defer srv.Close()
   461  
   462  		err = cl.Resource(resource).Namespace(tc.namespace).Delete(context.TODO(), tc.name, tc.deleteOptions, tc.subresource...)
   463  		if err != nil {
   464  			t.Errorf("unexpected error when deleting %q: %v", tc.name, err)
   465  			continue
   466  		}
   467  	}
   468  }
   469  
   470  func TestDeleteCollection(t *testing.T) {
   471  	statusOK := &metav1.Status{
   472  		TypeMeta: metav1.TypeMeta{Kind: "Status"},
   473  		Status:   metav1.StatusSuccess,
   474  	}
   475  	tcs := []struct {
   476  		namespace string
   477  		name      string
   478  		path      string
   479  	}{
   480  		{
   481  			name: "normal_delete_collection",
   482  			path: "/apis/gtest/vtest/rtest",
   483  		},
   484  		{
   485  			namespace: "nstest",
   486  			name:      "namespaced_delete_collection",
   487  			path:      "/apis/gtest/vtest/namespaces/nstest/rtest",
   488  		},
   489  	}
   490  	for _, tc := range tcs {
   491  		resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
   492  		cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
   493  			if r.Method != "DELETE" {
   494  				t.Errorf("DeleteCollection(%q) got HTTP method %s. wanted DELETE", tc.name, r.Method)
   495  			}
   496  
   497  			if r.URL.Path != tc.path {
   498  				t.Errorf("DeleteCollection(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
   499  			}
   500  
   501  			content := r.Header.Get("Content-Type")
   502  			if content != runtime.ContentTypeJSON {
   503  				t.Errorf("DeleteCollection(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON)
   504  			}
   505  
   506  			w.Header().Set("Content-Type", runtime.ContentTypeJSON)
   507  			unstructured.UnstructuredJSONScheme.Encode(statusOK, w)
   508  		})
   509  		if err != nil {
   510  			t.Errorf("unexpected error when creating client: %v", err)
   511  			continue
   512  		}
   513  		defer srv.Close()
   514  
   515  		err = cl.Resource(resource).Namespace(tc.namespace).DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{})
   516  		if err != nil {
   517  			t.Errorf("unexpected error when deleting collection %q: %v", tc.name, err)
   518  			continue
   519  		}
   520  	}
   521  }
   522  
   523  func TestCreate(t *testing.T) {
   524  	tcs := []struct {
   525  		resource    string
   526  		subresource []string
   527  		name        string
   528  		namespace   string
   529  		obj         *unstructured.Unstructured
   530  		path        string
   531  	}{
   532  		{
   533  			resource: "rtest",
   534  			name:     "normal_create",
   535  			path:     "/apis/gtest/vtest/rtest",
   536  			obj:      getObject("gtest/vTest", "rTest", "normal_create"),
   537  		},
   538  		{
   539  			resource:  "rtest",
   540  			name:      "namespaced_create",
   541  			namespace: "nstest",
   542  			path:      "/apis/gtest/vtest/namespaces/nstest/rtest",
   543  			obj:       getObject("gtest/vTest", "rTest", "namespaced_create"),
   544  		},
   545  		{
   546  			resource:    "rtest",
   547  			subresource: []string{"srtest"},
   548  			name:        "normal_subresource_create",
   549  			path:        "/apis/gtest/vtest/rtest/normal_subresource_create/srtest",
   550  			obj:         getObject("vTest", "srTest", "normal_subresource_create"),
   551  		},
   552  		{
   553  			resource:    "rtest/",
   554  			subresource: []string{"srtest"},
   555  			name:        "namespaced_subresource_create",
   556  			namespace:   "nstest",
   557  			path:        "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_create/srtest",
   558  			obj:         getObject("vTest", "srTest", "namespaced_subresource_create"),
   559  		},
   560  	}
   561  	for _, tc := range tcs {
   562  		resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource}
   563  		cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
   564  			if r.Method != "POST" {
   565  				t.Errorf("Create(%q) got HTTP method %s. wanted POST", tc.name, r.Method)
   566  			}
   567  
   568  			if r.URL.Path != tc.path {
   569  				t.Errorf("Create(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
   570  			}
   571  
   572  			content := r.Header.Get("Content-Type")
   573  			if content != runtime.ContentTypeJSON {
   574  				t.Errorf("Create(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON)
   575  			}
   576  
   577  			w.Header().Set("Content-Type", runtime.ContentTypeJSON)
   578  			data, err := io.ReadAll(r.Body)
   579  			if err != nil {
   580  				t.Errorf("Create(%q) unexpected error reading body: %v", tc.name, err)
   581  				w.WriteHeader(http.StatusInternalServerError)
   582  				return
   583  			}
   584  
   585  			w.Write(data)
   586  		})
   587  		if err != nil {
   588  			t.Errorf("unexpected error when creating client: %v", err)
   589  			continue
   590  		}
   591  		defer srv.Close()
   592  
   593  		got, err := cl.Resource(resource).Namespace(tc.namespace).Create(context.TODO(), tc.obj, metav1.CreateOptions{}, tc.subresource...)
   594  		if err != nil {
   595  			t.Errorf("unexpected error when creating %q: %v", tc.name, err)
   596  			continue
   597  		}
   598  
   599  		if !reflect.DeepEqual(got, tc.obj) {
   600  			t.Errorf("Create(%q) want: %v\ngot: %v", tc.name, tc.obj, got)
   601  		}
   602  	}
   603  }
   604  
   605  func TestUpdate(t *testing.T) {
   606  	tcs := []struct {
   607  		resource    string
   608  		subresource []string
   609  		name        string
   610  		namespace   string
   611  		obj         *unstructured.Unstructured
   612  		path        string
   613  	}{
   614  		{
   615  			resource: "rtest",
   616  			name:     "normal_update",
   617  			path:     "/apis/gtest/vtest/rtest/normal_update",
   618  			obj:      getObject("gtest/vTest", "rTest", "normal_update"),
   619  		},
   620  		{
   621  			resource:  "rtest",
   622  			name:      "namespaced_update",
   623  			namespace: "nstest",
   624  			path:      "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_update",
   625  			obj:       getObject("gtest/vTest", "rTest", "namespaced_update"),
   626  		},
   627  		{
   628  			resource:    "rtest",
   629  			subresource: []string{"srtest"},
   630  			name:        "normal_subresource_update",
   631  			path:        "/apis/gtest/vtest/rtest/normal_update/srtest",
   632  			obj:         getObject("gtest/vTest", "srTest", "normal_update"),
   633  		},
   634  		{
   635  			resource:    "rtest",
   636  			subresource: []string{"srtest"},
   637  			name:        "namespaced_subresource_update",
   638  			namespace:   "nstest",
   639  			path:        "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_update/srtest",
   640  			obj:         getObject("gtest/vTest", "srTest", "namespaced_update"),
   641  		},
   642  	}
   643  	for _, tc := range tcs {
   644  		resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource}
   645  		cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
   646  			if r.Method != "PUT" {
   647  				t.Errorf("Update(%q) got HTTP method %s. wanted PUT", tc.name, r.Method)
   648  			}
   649  
   650  			if r.URL.Path != tc.path {
   651  				t.Errorf("Update(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
   652  			}
   653  
   654  			content := r.Header.Get("Content-Type")
   655  			if content != runtime.ContentTypeJSON {
   656  				t.Errorf("Uppdate(%q) got Content-Type %s. wanted %s", tc.name, content, runtime.ContentTypeJSON)
   657  			}
   658  
   659  			w.Header().Set("Content-Type", runtime.ContentTypeJSON)
   660  			data, err := io.ReadAll(r.Body)
   661  			if err != nil {
   662  				t.Errorf("Update(%q) unexpected error reading body: %v", tc.name, err)
   663  				w.WriteHeader(http.StatusInternalServerError)
   664  				return
   665  			}
   666  
   667  			w.Write(data)
   668  		})
   669  		if err != nil {
   670  			t.Errorf("unexpected error when creating client: %v", err)
   671  			continue
   672  		}
   673  		defer srv.Close()
   674  
   675  		got, err := cl.Resource(resource).Namespace(tc.namespace).Update(context.TODO(), tc.obj, metav1.UpdateOptions{}, tc.subresource...)
   676  		if err != nil {
   677  			t.Errorf("unexpected error when updating %q: %v", tc.name, err)
   678  			continue
   679  		}
   680  
   681  		if !reflect.DeepEqual(got, tc.obj) {
   682  			t.Errorf("Update(%q) want: %v\ngot: %v", tc.name, tc.obj, got)
   683  		}
   684  	}
   685  }
   686  
   687  func TestWatch(t *testing.T) {
   688  	tcs := []struct {
   689  		name      string
   690  		namespace string
   691  		events    []watch.Event
   692  		path      string
   693  		query     string
   694  	}{
   695  		{
   696  			name:  "normal_watch",
   697  			path:  "/apis/gtest/vtest/rtest",
   698  			query: "watch=true",
   699  			events: []watch.Event{
   700  				{Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "normal_watch")},
   701  				{Type: watch.Modified, Object: getObject("gtest/vTest", "rTest", "normal_watch")},
   702  				{Type: watch.Deleted, Object: getObject("gtest/vTest", "rTest", "normal_watch")},
   703  			},
   704  		},
   705  		{
   706  			name:      "namespaced_watch",
   707  			namespace: "nstest",
   708  			path:      "/apis/gtest/vtest/namespaces/nstest/rtest",
   709  			query:     "watch=true",
   710  			events: []watch.Event{
   711  				{Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")},
   712  				{Type: watch.Modified, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")},
   713  				{Type: watch.Deleted, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")},
   714  			},
   715  		},
   716  	}
   717  	for _, tc := range tcs {
   718  		resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
   719  		cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
   720  			if r.Method != "GET" {
   721  				t.Errorf("Watch(%q) got HTTP method %s. wanted GET", tc.name, r.Method)
   722  			}
   723  
   724  			if r.URL.Path != tc.path {
   725  				t.Errorf("Watch(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
   726  			}
   727  			if r.URL.RawQuery != tc.query {
   728  				t.Errorf("Watch(%q) got query %s. wanted %s", tc.name, r.URL.RawQuery, tc.query)
   729  			}
   730  
   731  			w.Header().Set("Content-Type", "application/json")
   732  
   733  			enc := restclientwatch.NewEncoder(streaming.NewEncoder(w, unstructured.UnstructuredJSONScheme), unstructured.UnstructuredJSONScheme)
   734  			for _, e := range tc.events {
   735  				enc.Encode(&e)
   736  			}
   737  		})
   738  		if err != nil {
   739  			t.Errorf("unexpected error when creating client: %v", err)
   740  			continue
   741  		}
   742  		defer srv.Close()
   743  
   744  		watcher, err := cl.Resource(resource).Namespace(tc.namespace).Watch(context.TODO(), metav1.ListOptions{})
   745  		if err != nil {
   746  			t.Errorf("unexpected error when watching %q: %v", tc.name, err)
   747  			continue
   748  		}
   749  
   750  		for _, want := range tc.events {
   751  			got := <-watcher.ResultChan()
   752  			if !reflect.DeepEqual(got, want) {
   753  				t.Errorf("Watch(%q) want: %v\ngot: %v", tc.name, want, got)
   754  			}
   755  		}
   756  	}
   757  }
   758  
   759  func TestPatch(t *testing.T) {
   760  	tcs := []struct {
   761  		resource    string
   762  		subresource []string
   763  		name        string
   764  		namespace   string
   765  		patch       []byte
   766  		want        *unstructured.Unstructured
   767  		path        string
   768  	}{
   769  		{
   770  			resource: "rtest",
   771  			name:     "normal_patch",
   772  			path:     "/apis/gtest/vtest/rtest/normal_patch",
   773  			patch:    getJSON("gtest/vTest", "rTest", "normal_patch"),
   774  			want:     getObject("gtest/vTest", "rTest", "normal_patch"),
   775  		},
   776  		{
   777  			resource:  "rtest",
   778  			name:      "namespaced_patch",
   779  			namespace: "nstest",
   780  			path:      "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_patch",
   781  			patch:     getJSON("gtest/vTest", "rTest", "namespaced_patch"),
   782  			want:      getObject("gtest/vTest", "rTest", "namespaced_patch"),
   783  		},
   784  		{
   785  			resource:    "rtest",
   786  			subresource: []string{"srtest"},
   787  			name:        "normal_subresource_patch",
   788  			path:        "/apis/gtest/vtest/rtest/normal_subresource_patch/srtest",
   789  			patch:       getJSON("gtest/vTest", "srTest", "normal_subresource_patch"),
   790  			want:        getObject("gtest/vTest", "srTest", "normal_subresource_patch"),
   791  		},
   792  		{
   793  			resource:    "rtest",
   794  			subresource: []string{"srtest"},
   795  			name:        "namespaced_subresource_patch",
   796  			namespace:   "nstest",
   797  			path:        "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_patch/srtest",
   798  			patch:       getJSON("gtest/vTest", "srTest", "namespaced_subresource_patch"),
   799  			want:        getObject("gtest/vTest", "srTest", "namespaced_subresource_patch"),
   800  		},
   801  	}
   802  	for _, tc := range tcs {
   803  		resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource}
   804  		cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) {
   805  			if r.Method != "PATCH" {
   806  				t.Errorf("Patch(%q) got HTTP method %s. wanted PATCH", tc.name, r.Method)
   807  			}
   808  
   809  			if r.URL.Path != tc.path {
   810  				t.Errorf("Patch(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path)
   811  			}
   812  
   813  			content := r.Header.Get("Content-Type")
   814  			if content != string(types.StrategicMergePatchType) {
   815  				t.Errorf("Patch(%q) got Content-Type %s. wanted %s", tc.name, content, types.StrategicMergePatchType)
   816  			}
   817  
   818  			data, err := io.ReadAll(r.Body)
   819  			if err != nil {
   820  				t.Errorf("Patch(%q) unexpected error reading body: %v", tc.name, err)
   821  				w.WriteHeader(http.StatusInternalServerError)
   822  				return
   823  			}
   824  
   825  			w.Header().Set("Content-Type", "application/json")
   826  			w.Write(data)
   827  		})
   828  		if err != nil {
   829  			t.Errorf("unexpected error when creating client: %v", err)
   830  			continue
   831  		}
   832  		defer srv.Close()
   833  
   834  		got, err := cl.Resource(resource).Namespace(tc.namespace).Patch(context.TODO(), tc.name, types.StrategicMergePatchType, tc.patch, metav1.PatchOptions{}, tc.subresource...)
   835  		if err != nil {
   836  			t.Errorf("unexpected error when patching %q: %v", tc.name, err)
   837  			continue
   838  		}
   839  
   840  		if !reflect.DeepEqual(got, tc.want) {
   841  			t.Errorf("Patch(%q) want: %v\ngot: %v", tc.name, tc.want, got)
   842  		}
   843  	}
   844  }
   845  
   846  func TestInvalidSegments(t *testing.T) {
   847  	name := "bad/name"
   848  	namespace := "bad/namespace"
   849  	resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"}
   850  	obj := &unstructured.Unstructured{
   851  		Object: map[string]interface{}{
   852  			"apiVersion": "vtest",
   853  			"kind":       "vkind",
   854  			"metadata": map[string]interface{}{
   855  				"name": name,
   856  			},
   857  		},
   858  	}
   859  	cl, err := NewForConfig(&restclient.Config{
   860  		Host: "127.0.0.1",
   861  	})
   862  	if err != nil {
   863  		t.Fatalf("Failed to create config: %v", err)
   864  	}
   865  
   866  	_, err = cl.Resource(resource).Namespace(namespace).Create(context.TODO(), obj, metav1.CreateOptions{})
   867  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   868  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   869  	}
   870  
   871  	_, err = cl.Resource(resource).Update(context.TODO(), obj, metav1.UpdateOptions{})
   872  	if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
   873  		t.Fatalf("Expected `invalid resource name` error, got: %v", err)
   874  	}
   875  	_, err = cl.Resource(resource).Namespace(namespace).Update(context.TODO(), obj, metav1.UpdateOptions{})
   876  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   877  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   878  	}
   879  
   880  	_, err = cl.Resource(resource).UpdateStatus(context.TODO(), obj, metav1.UpdateOptions{})
   881  	if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
   882  		t.Fatalf("Expected `invalid resource name` error, got: %v", err)
   883  	}
   884  	_, err = cl.Resource(resource).Namespace(namespace).UpdateStatus(context.TODO(), obj, metav1.UpdateOptions{})
   885  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   886  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   887  	}
   888  
   889  	err = cl.Resource(resource).Delete(context.TODO(), name, metav1.DeleteOptions{})
   890  	if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
   891  		t.Fatalf("Expected `invalid resource name` error, got: %v", err)
   892  	}
   893  	err = cl.Resource(resource).Namespace(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
   894  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   895  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   896  	}
   897  
   898  	err = cl.Resource(resource).Namespace(namespace).DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{})
   899  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   900  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   901  	}
   902  
   903  	_, err = cl.Resource(resource).Get(context.TODO(), name, metav1.GetOptions{})
   904  	if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
   905  		t.Fatalf("Expected `invalid resource name` error, got: %v", err)
   906  	}
   907  	_, err = cl.Resource(resource).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
   908  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   909  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   910  	}
   911  
   912  	_, err = cl.Resource(resource).Namespace(namespace).List(context.TODO(), metav1.ListOptions{})
   913  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   914  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   915  	}
   916  
   917  	_, err = cl.Resource(resource).Namespace(namespace).Watch(context.TODO(), metav1.ListOptions{})
   918  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   919  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   920  	}
   921  
   922  	_, err = cl.Resource(resource).Patch(context.TODO(), name, types.StrategicMergePatchType, []byte("{}"), metav1.PatchOptions{})
   923  	if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
   924  		t.Fatalf("Expected `invalid resource name` error, got: %v", err)
   925  	}
   926  	_, err = cl.Resource(resource).Namespace(namespace).Patch(context.TODO(), name, types.StrategicMergePatchType, []byte("{}"), metav1.PatchOptions{})
   927  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   928  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   929  	}
   930  
   931  	_, err = cl.Resource(resource).Apply(context.TODO(), name, obj, metav1.ApplyOptions{})
   932  	if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
   933  		t.Fatalf("Expected `invalid resource name` error, got: %v", err)
   934  	}
   935  	_, err = cl.Resource(resource).Namespace(namespace).Apply(context.TODO(), name, obj, metav1.ApplyOptions{})
   936  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   937  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   938  	}
   939  
   940  	_, err = cl.Resource(resource).ApplyStatus(context.TODO(), name, obj, metav1.ApplyOptions{})
   941  	if err == nil || !strings.Contains(err.Error(), "invalid resource name") {
   942  		t.Fatalf("Expected `invalid resource name` error, got: %v", err)
   943  	}
   944  	_, err = cl.Resource(resource).Namespace(namespace).ApplyStatus(context.TODO(), name, obj, metav1.ApplyOptions{})
   945  	if err == nil || !strings.Contains(err.Error(), "invalid namespace") {
   946  		t.Fatalf("Expected `invalid namespace` error, got: %v", err)
   947  	}
   948  }