@@ -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