@@ -5,7 +5,7 @@ package watcher
55
66import (
77 "context"
8- "net/http "
8+ "fmt "
99 "testing"
1010 "time"
1111
@@ -54,13 +54,7 @@ func TestResourceNotFoundError(t *testing.T) {
5454 // dynamicClient converts Status objects from the apiserver into errors.
5555 // So we can just return the right error here to simulate an error from
5656 // the apiserver.
57- name := "" // unused by LIST requests
58- // The apisevrer confusingly does not return apierrors.NewNotFound,
59- // which has a nice constant for its error message.
60- // err = apierrors.NewNotFound(exampleGR, name)
61- // Instead it uses apierrors.NewGenericServerResponse, which uses
62- // a hard-coded error message.
63- err = apierrors.NewGenericServerResponse(http.StatusNotFound, "list", exampleGR, name, "unused", -1, false)
57+ err = newGenericServerResponse(action, newNotFoundResourceStatusError(action))
6458 return true, nil, err
6559 })
6660 },
@@ -88,13 +82,7 @@ func TestResourceNotFoundError(t *testing.T) {
8882 // dynamicClient converts Status objects from the apiserver into errors.
8983 // So we can just return the right error here to simulate an error from
9084 // the apiserver.
91- name := "" // unused by LIST requests
92- // The apisevrer confusingly does not return apierrors.NewNotFound,
93- // which has a nice constant for its error message.
94- // err = apierrors.NewNotFound(exampleGR, name)
95- // Instead it uses apierrors.NewGenericServerResponse, which uses
96- // a hard-coded error message.
97- err = apierrors.NewGenericServerResponse(http.StatusNotFound, "list", exampleGR, name, "unused", -1, false)
85+ err = newGenericServerResponse(action, newNotFoundResourceStatusError(action))
9886 return true, nil, err
9987 })
10088 },
@@ -110,7 +98,67 @@ func TestResourceNotFoundError(t *testing.T) {
11098 t.Errorf("Expected typed NotFound error, but got untyped NotFound error: %v", err)
11199 default:
112100 // If we got this error, the test is probably broken.
113- t.Errorf("Expected untyped NotFound error, but got a different error: %v", err)
101+ t.Errorf("Expected typed NotFound error, but got a different error: %v", err)
102+ }
103+ },
104+ },
105+ {
106+ name: "List resource forbidden error",
107+ setup: func(fakeClient *dynamicfake.FakeDynamicClient) {
108+ fakeClient.PrependReactor("list", exampleGR.Resource, func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
109+ listAction := action.(clienttesting.ListAction)
110+ if listAction.GetNamespace() != namespace {
111+ assert.Fail(t, "Received unexpected LIST namespace: %s", listAction.GetNamespace())
112+ return false, nil, nil
113+ }
114+ // dynamicClient converts Status objects from the apiserver into errors.
115+ // So we can just return the right error here to simulate an error from
116+ // the apiserver.
117+ err = newGenericServerResponse(action, newForbiddenResourceStatusError(action))
118+ return true, nil, err
119+ })
120+ },
121+ errorHandler: func(t *testing.T, err error) {
122+ switch {
123+ case apierrors.IsForbidden(err):
124+ // If we got this error, something changed in the apiserver or
125+ // client. If the client changed, it might be safe to stop parsing
126+ // the error string.
127+ t.Errorf("Expected untyped Forbidden error, but got typed Forbidden error: %v", err)
128+ case containsForbiddenMessage(err):
129+ // This is the expected hack, because the Informer/Reflector
130+ // doesn't wrap the error with "%w".
131+ t.Logf("Received expected untyped Forbidden error: %v", err)
132+ default:
133+ // If we got this error, the test is probably broken.
134+ t.Errorf("Expected untyped Forbidden error, but got a different error: %v", err)
135+ }
136+ },
137+ },
138+ {
139+ name: "Watch resource forbidden error",
140+ setup: func(fakeClient *dynamicfake.FakeDynamicClient) {
141+ fakeClient.PrependWatchReactor(exampleGR.Resource, func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
142+ // dynamicClient converts Status objects from the apiserver into errors.
143+ // So we can just return the right error here to simulate an error from
144+ // the apiserver.
145+ err = newGenericServerResponse(action, newForbiddenResourceStatusError(action))
146+ return true, nil, err
147+ })
148+ },
149+ errorHandler: func(t *testing.T, err error) {
150+ switch {
151+ case apierrors.IsForbidden(err):
152+ // This is the expected behavior, because the
153+ // Informer/Reflector DOES wrap watch errors
154+ t.Logf("Received expected untyped Forbidden error: %v", err)
155+ case containsForbiddenMessage(err):
156+ // If this happens, there was a regression.
157+ // Watch errors are expected to be wrapped with "%w"
158+ t.Errorf("Expected typed Forbidden error, but got untyped Forbidden error: %v", err)
159+ default:
160+ // If we got this error, the test is probably broken.
161+ t.Errorf("Expected typed Forbidden error, but got a different error: %v", err)
114162 }
115163 },
116164 },
@@ -164,3 +212,43 @@ func TestResourceNotFoundError(t *testing.T) {
164212 })
165213 }
166214}
215+
216+ // newForbiddenResourceStatusError emulates a Forbidden error from the apiserver
217+ // for a namespace-scoped resource.
218+ // https://github.com/kubernetes/apiserver/blob/master/pkg/endpoints/handlers/responsewriters/errors.go#L36
219+ func newForbiddenResourceStatusError(action clienttesting.Action) *apierrors.StatusError {
220+ username := "unused"
221+ verb := action.GetVerb()
222+ resource := action.GetResource().Resource
223+ if subresource := action.GetSubresource(); len(subresource) > 0 {
224+ resource = resource + "/" + subresource
225+ }
226+ apiGroup := action.GetResource().Group
227+ namespace := action.GetNamespace()
228+
229+ // https://github.com/kubernetes/apiserver/blob/master/pkg/endpoints/handlers/responsewriters/errors.go#L51
230+ err := fmt.Errorf("User %q cannot %s resource %q in API group %q in the namespace %q",
231+ username, verb, resource, apiGroup, namespace)
232+
233+ qualifiedResource := action.GetResource().GroupResource()
234+ name := "" // unused by ListAndWatch
235+ return apierrors.NewForbidden(qualifiedResource, name, err)
236+ }
237+
238+ // newNotFoundResourceStatusError emulates a NotFOund error from the apiserver
239+ // for a resource (not an object).
240+ func newNotFoundResourceStatusError(action clienttesting.Action) *apierrors.StatusError {
241+ qualifiedResource := action.GetResource().GroupResource()
242+ name := "" // unused by ListAndWatch
243+ return apierrors.NewNotFound(qualifiedResource, name)
244+ }
245+
246+ // newGenericServerResponse emulates a StatusError from the apiserver.
247+ func newGenericServerResponse(action clienttesting.Action, statusError *apierrors.StatusError) *apierrors.StatusError {
248+ errorCode := int(statusError.ErrStatus.Code)
249+ verb := action.GetVerb()
250+ qualifiedResource := action.GetResource().GroupResource()
251+ name := statusError.ErrStatus.Details.Name
252+ // https://github.com/kubernetes/apimachinery/blob/v0.24.0/pkg/api/errors/errors.go#L435
253+ return apierrors.NewGenericServerResponse(errorCode, verb, qualifiedResource, name, statusError.Error(), -1, false)
254+ }
0 commit comments