1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 package apierror
31
32 import (
33 "context"
34 "errors"
35 "flag"
36 "io/ioutil"
37 "path/filepath"
38 "testing"
39
40 "github.com/google/go-cmp/cmp"
41 "github.com/google/go-cmp/cmp/cmpopts"
42 jsonerror "github.com/googleapis/gax-go/v2/apierror/internal/proto"
43 "google.golang.org/api/googleapi"
44 "google.golang.org/genproto/googleapis/rpc/errdetails"
45 "google.golang.org/grpc/codes"
46 "google.golang.org/grpc/status"
47 "google.golang.org/protobuf/encoding/protojson"
48 "google.golang.org/protobuf/proto"
49 "google.golang.org/protobuf/testing/protocmp"
50 "google.golang.org/protobuf/types/descriptorpb"
51 "google.golang.org/protobuf/types/known/anypb"
52 "google.golang.org/protobuf/types/known/durationpb"
53 )
54
55 var update = flag.Bool("update", false, "update golden files")
56
57 func TestDetails(t *testing.T) {
58 qf := &errdetails.QuotaFailure{
59 Violations: []*errdetails.QuotaFailure_Violation{{Subject: "Foo", Description: "Bar"}},
60 }
61 qS, _ := status.New(codes.ResourceExhausted, "test").WithDetails(qf)
62 apierr := &APIError{
63 err: qS.Err(),
64 status: qS,
65 details: ErrDetails{QuotaFailure: qf},
66 }
67 got := apierr.Details()
68 want := ErrDetails{QuotaFailure: qf}
69 if diff := cmp.Diff(got, want, cmp.Comparer(proto.Equal)); diff != "" {
70 t.Errorf("got(-), want(+):\n%s", diff)
71 }
72 }
73
74 func TestDetails_ExtractProtoMessage(t *testing.T) {
75
76 customError := &jsonerror.CustomError{
77 Code: jsonerror.CustomError_UNIVERSE_WAS_DESTROYED,
78 Entity: "some entity",
79 ErrorMessage: "custom error message",
80 }
81
82 testCases := []struct {
83 description string
84 src *status.Status
85 extract proto.Message
86 want interface{}
87 wantErr error
88 }{
89 {
90 description: "no details",
91 src: status.New(codes.Unimplemented, "unimp"),
92 extract: &jsonerror.CustomError{},
93 wantErr: ErrMessageNotFound,
94 },
95 {
96 description: "nil argument",
97 src: func() *status.Status {
98 s, _ := status.New(codes.Unauthenticated, "who are you").WithDetails(
99 &descriptorpb.DescriptorProto{},
100 )
101 return s
102 }(),
103 wantErr: ErrMessageNotFound,
104 },
105 {
106 description: "custom error success",
107 src: func() *status.Status {
108 s, _ := status.New(codes.Unknown, "unknown error").WithDetails(
109 customError,
110 )
111 return s
112 }(),
113 extract: &jsonerror.CustomError{},
114 want: customError,
115 },
116 }
117 for _, tc := range testCases {
118
119 apiErr, ok := FromError(tc.src.Err())
120 if !ok {
121 t.Errorf("%s: FromError failure", tc.description)
122 }
123 val := tc.extract
124 gotErr := apiErr.Details().ExtractProtoMessage(val)
125 if tc.wantErr != nil {
126 if !errors.Is(gotErr, tc.wantErr) {
127 t.Errorf("%s: got error %v, wanted error %v", tc.description, gotErr, tc.wantErr)
128 }
129 } else {
130 if gotErr != nil {
131 t.Errorf("%s: got error %v", tc.description, gotErr)
132 }
133 if diff := cmp.Diff(val, tc.want, protocmp.Transform()); diff != "" {
134 t.Errorf("%s: got(-), want(+):\n%s", tc.description, diff)
135 }
136 }
137 }
138 }
139 func TestUnwrap(t *testing.T) {
140 pf := &errdetails.PreconditionFailure{
141 Violations: []*errdetails.PreconditionFailure_Violation{{Type: "Foo", Subject: "Bar", Description: "desc"}},
142 }
143 pS, _ := status.New(codes.FailedPrecondition, "test").WithDetails(pf)
144 apierr := &APIError{
145 err: pS.Err(),
146 status: pS,
147 details: ErrDetails{PreconditionFailure: pf},
148 }
149 got := apierr.Unwrap()
150 want := pS.Err()
151 if diff := cmp.Diff(got, want, cmpopts.EquateErrors()); diff != "" {
152 t.Errorf("got(-), want(+):\n%s", diff)
153 }
154 }
155 func TestError(t *testing.T) {
156 ei := &errdetails.ErrorInfo{
157 Reason: "Foo",
158 Domain: "Bar",
159 Metadata: map[string]string{"type": "test"},
160 }
161 eS, _ := status.New(codes.Unauthenticated, "ei").WithDetails(ei)
162
163 br := &errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{{
164 Field: "Foo",
165 Description: "Bar",
166 }},
167 }
168 bS, _ := status.New(codes.InvalidArgument, "br").WithDetails(br)
169
170 qf := &errdetails.QuotaFailure{
171 Violations: []*errdetails.QuotaFailure_Violation{{Subject: "Foo", Description: "Bar"}},
172 }
173 pf := &errdetails.PreconditionFailure{
174 Violations: []*errdetails.PreconditionFailure_Violation{{Type: "Foo", Subject: "Bar", Description: "desc"}},
175 }
176
177 ri := &errdetails.RetryInfo{
178 RetryDelay: &durationpb.Duration{Seconds: 10, Nanos: 10},
179 }
180 rq := &errdetails.RequestInfo{
181 RequestId: "Foo",
182 ServingData: "Bar",
183 }
184 rqS, _ := status.New(codes.Canceled, "Request cancelled by client").WithDetails(rq, ri, pf, br, qf)
185
186 rs := &errdetails.ResourceInfo{
187 ResourceType: "Foo",
188 ResourceName: "Bar",
189 Owner: "Client",
190 Description: "Directory not Found",
191 }
192 rS, _ := status.New(codes.NotFound, "rs").WithDetails(rs)
193
194 deb := &errdetails.DebugInfo{
195 StackEntries: []string{"Foo", "Bar"},
196 Detail: "Stack",
197 }
198 dS, _ := status.New(codes.DataLoss, "Here is the debug info").WithDetails(deb)
199
200 hp := &errdetails.Help{
201 Links: []*errdetails.Help_Link{{Description: "Foo", Url: "Bar"}},
202 }
203 hS, _ := status.New(codes.Unimplemented, "Help Info").WithDetails(hp)
204
205 lo := &errdetails.LocalizedMessage{
206 Locale: "Foo",
207 Message: "Bar",
208 }
209 lS, _ := status.New(codes.Unknown, "Localized Message").WithDetails(lo)
210
211 var uu []interface{}
212 uu = append(uu, "unknown detail 1")
213 uS := status.New(codes.Unknown, "Unknown")
214
215 httpErrInfo := &errdetails.ErrorInfo{Reason: "just because", Domain: "tests"}
216 any, err := anypb.New(httpErrInfo)
217 if err != nil {
218 t.Fatal(err)
219 }
220 e := &jsonerror.Error{Error: &jsonerror.Error_Status{Details: []*anypb.Any{any}}}
221 data, err := protojson.Marshal(e)
222 if err != nil {
223 t.Fatal(err)
224 }
225 hae := &googleapi.Error{
226 Message: "just because",
227 Body: string(data),
228 }
229 haeS := status.New(codes.Unknown, "just because")
230
231 tests := []struct {
232 apierr *APIError
233 name string
234 }{
235 {&APIError{err: eS.Err(), status: eS, details: ErrDetails{ErrorInfo: ei}}, "error_info"},
236 {&APIError{err: bS.Err(), status: bS, details: ErrDetails{BadRequest: br}}, "bad_request"},
237 {&APIError{err: rqS.Err(), status: rqS, details: ErrDetails{RequestInfo: rq, RetryInfo: ri,
238 PreconditionFailure: pf, QuotaFailure: qf, BadRequest: br}}, "multiple_info"},
239 {&APIError{err: bS.Err(), status: rS, details: ErrDetails{ResourceInfo: rs}}, "resource_info"},
240 {&APIError{err: bS.Err(), status: dS, details: ErrDetails{DebugInfo: deb}}, "debug_info"},
241 {&APIError{err: bS.Err(), status: hS, details: ErrDetails{Help: hp}}, "help"},
242 {&APIError{err: bS.Err(), status: lS, details: ErrDetails{LocalizedMessage: lo}}, "localized_message"},
243 {&APIError{err: bS.Err(), status: uS, details: ErrDetails{Unknown: uu}}, "unknown"},
244 {&APIError{err: bS.Err(), status: bS, details: ErrDetails{}}, "empty"},
245 {&APIError{err: hae, httpErr: hae, status: haeS, details: ErrDetails{ErrorInfo: httpErrInfo}}, "http_err"},
246 }
247 for _, tc := range tests {
248 t.Helper()
249 got := tc.apierr.Error()
250 want, err := golden(tc.name, got)
251 if err != nil {
252 t.Fatal(err)
253 }
254 if diff := cmp.Diff(got, want); diff != "" {
255 t.Errorf("got(-), want(+),: \n%s", diff)
256 }
257 }
258
259 }
260
261 func TestGRPCStatus(t *testing.T) {
262 qf := &errdetails.QuotaFailure{
263 Violations: []*errdetails.QuotaFailure_Violation{{Subject: "Foo", Description: "Bar"}},
264 }
265 want, _ := status.New(codes.ResourceExhausted, "test").WithDetails(qf)
266 apierr := &APIError{
267 err: want.Err(),
268 status: want,
269 details: ErrDetails{QuotaFailure: qf},
270 }
271 got := apierr.GRPCStatus()
272 if diff := cmp.Diff(got, want, cmp.Comparer(proto.Equal), cmp.AllowUnexported(status.Status{})); diff != "" {
273 t.Errorf("got(-), want(+),: \n%s", diff)
274 }
275 }
276
277 func TestReason(t *testing.T) {
278 tests := []struct {
279 ei *errdetails.ErrorInfo
280 }{
281 {&errdetails.ErrorInfo{Reason: "Foo"}},
282 {&errdetails.ErrorInfo{}},
283 }
284 for _, tc := range tests {
285 apierr := toAPIError(tc.ei)
286 if diff := cmp.Diff(apierr.Reason(), tc.ei.GetReason()); diff != "" {
287 t.Errorf("got(-), want(+),: \n%s", diff)
288 }
289 }
290 }
291 func TestDomain(t *testing.T) {
292 tests := []struct {
293 ei *errdetails.ErrorInfo
294 }{
295 {&errdetails.ErrorInfo{Domain: "Bar"}},
296 {&errdetails.ErrorInfo{}},
297 }
298 for _, tc := range tests {
299 apierr := toAPIError(tc.ei)
300 if diff := cmp.Diff(apierr.Domain(), tc.ei.GetDomain()); diff != "" {
301 t.Errorf("got(-), want(+),: \n%s", diff)
302 }
303 }
304 }
305 func TestMetadata(t *testing.T) {
306 tests := []struct {
307 ei *errdetails.ErrorInfo
308 }{
309 {&errdetails.ErrorInfo{Metadata: map[string]string{"type": "test"}}},
310 {&errdetails.ErrorInfo{}},
311 }
312 for _, tc := range tests {
313 apierr := toAPIError(tc.ei)
314 if diff := cmp.Diff(apierr.Metadata(), tc.ei.GetMetadata()); diff != "" {
315 t.Errorf("got(-), want(+),: \n%s", diff)
316 }
317 }
318 }
319
320 func TestFromError(t *testing.T) {
321 ei := &errdetails.ErrorInfo{
322 Reason: "Foo",
323 Domain: "Bar",
324 Metadata: map[string]string{"type": "test"},
325 }
326 eS, _ := status.New(codes.Unauthenticated, "ei").WithDetails(ei)
327
328 br := &errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{{
329 Field: "Foo",
330 Description: "Bar",
331 }},
332 }
333 bS, _ := status.New(codes.InvalidArgument, "br").WithDetails(br)
334
335 qf := &errdetails.QuotaFailure{
336 Violations: []*errdetails.QuotaFailure_Violation{{Subject: "Foo", Description: "Bar"}},
337 }
338 qS, _ := status.New(codes.ResourceExhausted, "qf").WithDetails(qf, br)
339
340 pf := &errdetails.PreconditionFailure{
341 Violations: []*errdetails.PreconditionFailure_Violation{{Type: "Foo", Subject: "Bar", Description: "desc"}},
342 }
343 pS, _ := status.New(codes.FailedPrecondition, "pf").WithDetails(pf)
344
345 ri := &errdetails.RetryInfo{
346 RetryDelay: &durationpb.Duration{Seconds: 10, Nanos: 10},
347 }
348 riS, _ := status.New(codes.Unavailable, "foo").WithDetails(ri)
349
350 rs := &errdetails.ResourceInfo{
351 ResourceType: "Foo",
352 ResourceName: "Bar",
353 Owner: "Client",
354 Description: "Directory not Found",
355 }
356 rS, _ := status.New(codes.NotFound, "rs").WithDetails(rs)
357
358 rq := &errdetails.RequestInfo{
359 RequestId: "Foo",
360 ServingData: "Bar",
361 }
362 rqS, _ := status.New(codes.Canceled, "Request cancelled by client").WithDetails(rq)
363
364 deb := &errdetails.DebugInfo{
365 StackEntries: []string{"Foo", "Bar"},
366 Detail: "Stack",
367 }
368 dS, _ := status.New(codes.DataLoss, "Here is the debug info").WithDetails(deb)
369
370 hp := &errdetails.Help{
371 Links: []*errdetails.Help_Link{{Description: "Foo", Url: "Bar"}},
372 }
373 hS, _ := status.New(codes.Unimplemented, "Help Info").WithDetails(hp)
374
375 lo := &errdetails.LocalizedMessage{
376 Locale: "Foo",
377 Message: "Bar",
378 }
379 lS, _ := status.New(codes.Unknown, "Localized Message").WithDetails(lo)
380
381 msg := &descriptorpb.DescriptorProto{
382 Name: proto.String("Foo"),
383 }
384 u := []interface{}{msg}
385 uS, _ := status.New(codes.Unknown, "test").WithDetails(msg)
386
387 httpErrInfo := &errdetails.ErrorInfo{Reason: "just because", Domain: "tests"}
388 any, err := anypb.New(httpErrInfo)
389 if err != nil {
390 t.Fatal(err)
391 }
392 e := &jsonerror.Error{Error: &jsonerror.Error_Status{Details: []*anypb.Any{any}}}
393 data, err := protojson.Marshal(e)
394 if err != nil {
395 t.Fatal(err)
396 }
397 hae := &googleapi.Error{
398 Message: "just because",
399 Body: string(data),
400 }
401 haeS := status.New(codes.Unknown, "just because")
402
403 tests := []struct {
404 apierr *APIError
405 b bool
406 }{
407 {&APIError{err: eS.Err(), status: eS, details: ErrDetails{ErrorInfo: ei}}, true},
408 {&APIError{err: bS.Err(), status: bS, details: ErrDetails{BadRequest: br}}, true},
409 {&APIError{err: qS.Err(), status: qS, details: ErrDetails{QuotaFailure: qf, BadRequest: br}}, true},
410 {&APIError{err: pS.Err(), status: pS, details: ErrDetails{PreconditionFailure: pf}}, true},
411 {&APIError{err: riS.Err(), status: riS, details: ErrDetails{RetryInfo: ri}}, true},
412 {&APIError{err: rS.Err(), status: rS, details: ErrDetails{ResourceInfo: rs}}, true},
413 {&APIError{err: rqS.Err(), status: rqS, details: ErrDetails{RequestInfo: rq}}, true},
414 {&APIError{err: dS.Err(), status: dS, details: ErrDetails{DebugInfo: deb}}, true},
415 {&APIError{err: hS.Err(), status: hS, details: ErrDetails{Help: hp}}, true},
416 {&APIError{err: lS.Err(), status: lS, details: ErrDetails{LocalizedMessage: lo}}, true},
417 {&APIError{err: uS.Err(), status: uS, details: ErrDetails{Unknown: u}}, true},
418 {&APIError{err: hae, httpErr: hae, status: haeS, details: ErrDetails{ErrorInfo: httpErrInfo}}, true},
419 {&APIError{err: errors.New("standard error")}, false},
420 }
421
422 for _, tc := range tests {
423 got, apiB := FromError(tc.apierr.err)
424 if tc.b != apiB {
425 t.Errorf("FromError(%s): got %v, want %v", tc.apierr.err, apiB, tc.b)
426 }
427 if tc.b {
428 if diff := cmp.Diff(got.details, tc.apierr.details, cmp.Comparer(proto.Equal)); diff != "" {
429 t.Errorf("FromError(%s): got(-), want(+),: \n%s", tc.apierr.err, diff)
430 }
431 if diff := cmp.Diff(got.status, tc.apierr.status, cmp.Comparer(proto.Equal), cmp.AllowUnexported(status.Status{})); diff != "" {
432 t.Errorf("FromError(%s): got(-), want(+),: \n%s", tc.apierr.err, diff)
433 }
434 if diff := cmp.Diff(got.err, tc.apierr.err, cmpopts.EquateErrors()); diff != "" {
435 t.Errorf("FromError(%s): got(-), want(+),: \n%s", tc.apierr.err, diff)
436 }
437 }
438 }
439 if err, _ := FromError(nil); err != nil {
440 t.Errorf("got %s, want nil", err)
441 }
442
443 if c, _ := FromError(context.DeadlineExceeded); c != nil {
444 t.Errorf("got %s, want nil", c)
445 }
446 }
447
448 func TestParseError(t *testing.T) {
449 httpErrInfo := &errdetails.ErrorInfo{Reason: "just because", Domain: "tests"}
450 any, err := anypb.New(httpErrInfo)
451 if err != nil {
452 t.Fatal(err)
453 }
454 e := &jsonerror.Error{Error: &jsonerror.Error_Status{Details: []*anypb.Any{any}}}
455 data, err := protojson.Marshal(e)
456 if err != nil {
457 t.Fatal(err)
458 }
459 hae := &googleapi.Error{
460 Message: "just because",
461 Body: string(data),
462 }
463 haeS := status.New(codes.Unknown, "just because")
464
465 se := errors.New("standard error")
466
467 tests := []struct {
468 source error
469 apierr *APIError
470 b bool
471 }{
472 {hae, &APIError{httpErr: hae, status: haeS, details: ErrDetails{ErrorInfo: httpErrInfo}}, true},
473 {se, &APIError{err: se}, false},
474 }
475
476 for _, tc := range tests {
477
478 got, apiB := ParseError(tc.source, false)
479 if tc.b != apiB {
480 t.Errorf("ParseError(%s, false): got %v, want %v", tc.apierr, apiB, tc.b)
481 }
482 if tc.b {
483 if diff := cmp.Diff(got.details, tc.apierr.details, cmp.Comparer(proto.Equal)); diff != "" {
484 t.Errorf("got(-), want(+),: \n%s", diff)
485 }
486 if diff := cmp.Diff(got.status, tc.apierr.status, cmp.Comparer(proto.Equal), cmp.AllowUnexported(status.Status{})); diff != "" {
487 t.Errorf("got(-), want(+),: \n%s", diff)
488 }
489 if got.err != nil {
490 t.Errorf("got %s, want nil", got.err)
491 }
492 }
493 }
494 if err, _ := ParseError(nil, false); err != nil {
495 t.Errorf("got %s, want nil", err)
496 }
497
498 if c, _ := ParseError(context.DeadlineExceeded, false); c != nil {
499 t.Errorf("got %s, want nil", c)
500 }
501 }
502
503 func golden(name, got string) (string, error) {
504 g := filepath.Join("testdata", name+".golden")
505 if *update {
506 if err := ioutil.WriteFile(g, []byte(got), 0644); err != nil {
507 return "", err
508 }
509 }
510 want, err := ioutil.ReadFile(g)
511 return string(want), err
512 }
513
514 func toAPIError(e *errdetails.ErrorInfo) *APIError {
515 st, _ := status.New(codes.Unavailable, "test").WithDetails(e)
516 return &APIError{
517 err: st.Err(),
518 status: st,
519 details: ErrDetails{ErrorInfo: e},
520 }
521 }
522
523 func TestHTTPCode(t *testing.T) {
524 tests := []struct {
525 name string
526 apierr *APIError
527 want int
528 }{
529 {
530 name: "basic http error",
531 apierr: &APIError{httpErr: &googleapi.Error{Code: 418}},
532 want: 418,
533 },
534 {
535 name: "http error, with unknown status",
536 apierr: &APIError{httpErr: &googleapi.Error{Code: 418}, status: status.New(codes.Unknown, "???")},
537 want: 418,
538 },
539 {
540 name: "gRPC error",
541 apierr: &APIError{status: status.New(codes.DataLoss, "where did it go?")},
542 want: -1,
543 },
544 }
545
546 for _, tt := range tests {
547 t.Run(tt.name, func(t *testing.T) {
548 if got := tt.apierr.HTTPCode(); got != tt.want {
549 t.Errorf("HTTPCode() = %v, want %v", got, tt.want)
550 }
551 })
552 }
553 }
554
View as plain text