diff --git a/auth/auth.go b/auth/auth.go index a7028698..475d3aa7 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -332,7 +332,7 @@ func (c *baseClient) verifyIDToken(ctx context.Context, idToken string, checkRev if c.tenantID != "" && c.tenantID != decoded.Firebase.Tenant { return nil, &internal.FirebaseError{ ErrorCode: internal.InvalidArgument, - String: fmt.Sprintf("invalid tenant id: %q", decoded.Firebase.Tenant), + Message: fmt.Sprintf("invalid tenant id: %q", decoded.Firebase.Tenant), Ext: map[string]interface{}{ authErrorCode: tenantIDMismatch, }, @@ -428,7 +428,7 @@ func (c *baseClient) checkRevokedOrDisabled(ctx context.Context, token *Token, e if user.Disabled { return &internal.FirebaseError{ ErrorCode: internal.InvalidArgument, - String: "user has been disabled", + Message: "user has been disabled", Ext: map[string]interface{}{ authErrorCode: userDisabled, }, @@ -438,7 +438,7 @@ func (c *baseClient) checkRevokedOrDisabled(ctx context.Context, token *Token, e if token.IssuedAt*1000 < user.TokensValidAfterMillis { return &internal.FirebaseError{ ErrorCode: internal.InvalidArgument, - String: errMessage, + Message: errMessage, Ext: map[string]interface{}{ authErrorCode: errCode, }, diff --git a/auth/token_verifier.go b/auth/token_verifier.go index 25e7bdc0..3d895715 100644 --- a/auth/token_verifier.go +++ b/auth/token_verifier.go @@ -187,7 +187,7 @@ func (tv *tokenVerifier) verifyContent(token string, isEmulator bool) (*Token, e if token == "" { return nil, &internal.FirebaseError{ ErrorCode: internal.InvalidArgument, - String: fmt.Sprintf("%s must be a non-empty string", tv.shortName), + Message: fmt.Sprintf("%s must be a non-empty string", tv.shortName), Ext: map[string]interface{}{authErrorCode: tv.invalidTokenCode}, } } @@ -196,7 +196,7 @@ func (tv *tokenVerifier) verifyContent(token string, isEmulator bool) (*Token, e if err != nil { return nil, &internal.FirebaseError{ ErrorCode: internal.InvalidArgument, - String: fmt.Sprintf( + Message: fmt.Sprintf( "%s; see %s for details on how to retrieve a valid %s", err.Error(), tv.docURL, tv.shortName), Ext: map[string]interface{}{authErrorCode: tv.invalidTokenCode}, @@ -210,7 +210,7 @@ func (tv *tokenVerifier) verifyTimestamps(payload *Token) error { if (payload.IssuedAt - clockSkewSeconds) > tv.clock.Now().Unix() { return &internal.FirebaseError{ ErrorCode: internal.InvalidArgument, - String: fmt.Sprintf("%s issued at future timestamp: %d", tv.shortName, payload.IssuedAt), + Message: fmt.Sprintf("%s issued at future timestamp: %d", tv.shortName, payload.IssuedAt), Ext: map[string]interface{}{authErrorCode: tv.invalidTokenCode}, } } @@ -218,7 +218,7 @@ func (tv *tokenVerifier) verifyTimestamps(payload *Token) error { if (payload.Expires + clockSkewSeconds) < tv.clock.Now().Unix() { return &internal.FirebaseError{ ErrorCode: internal.InvalidArgument, - String: fmt.Sprintf("%s has expired at: %d", tv.shortName, payload.Expires), + Message: fmt.Sprintf("%s has expired at: %d", tv.shortName, payload.Expires), Ext: map[string]interface{}{authErrorCode: tv.expiredTokenCode}, } } @@ -231,7 +231,7 @@ func (tv *tokenVerifier) verifySignature(ctx context.Context, token string) erro if err != nil { return &internal.FirebaseError{ ErrorCode: internal.Unknown, - String: err.Error(), + Message: err.Error(), Ext: map[string]interface{}{authErrorCode: certificateFetchFailed}, } } @@ -239,7 +239,7 @@ func (tv *tokenVerifier) verifySignature(ctx context.Context, token string) erro if !tv.verifySignatureWithKeys(ctx, token, keys) { return &internal.FirebaseError{ ErrorCode: internal.InvalidArgument, - String: "failed to verify token signature", + Message: "failed to verify token signature", Ext: map[string]interface{}{authErrorCode: tv.invalidTokenCode}, } } diff --git a/auth/user_mgt.go b/auth/user_mgt.go index 63a5c381..016e69d3 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -803,7 +803,7 @@ func (c *baseClient) GetUserByProviderUID(ctx context.Context, providerID string if len(getUsersResult.Users) == 0 { return nil, &internal.FirebaseError{ ErrorCode: internal.NotFound, - String: fmt.Sprintf("cannot find user from providerID: { %s, %s }", providerID, providerUID), + Message: fmt.Sprintf("cannot find user from providerID: { %s, %s }", providerID, providerUID), Response: nil, Ext: map[string]interface{}{ authErrorCode: userNotFound, @@ -848,7 +848,7 @@ func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord if len(parsed.Users) == 0 { return nil, &internal.FirebaseError{ ErrorCode: internal.NotFound, - String: fmt.Sprintf("no user exists with the %s", query.description()), + Message: fmt.Sprintf("no user exists with the %s", query.description()), Response: resp.LowLevelResponse(), Ext: map[string]interface{}{ authErrorCode: userNotFound, @@ -1487,9 +1487,9 @@ func handleHTTPError(resp *internal.Response) error { err.ErrorCode = authErr.code err.Ext[authErrorCode] = authErr.authCode if detail != "" { - err.String = fmt.Sprintf("%s: %s", authErr.message, detail) + err.Message = fmt.Sprintf("%s: %s", authErr.message, detail) } else { - err.String = authErr.message + err.Message = authErr.message } } diff --git a/db/db.go b/db/db.go index f53a197c..ae748069 100644 --- a/db/db.go +++ b/db/db.go @@ -150,7 +150,7 @@ func handleRTDBError(resp *internal.Response) error { } json.Unmarshal(resp.Body, &p) if p.Error != "" { - err.String = fmt.Sprintf("http error status: %d; reason: %s", resp.Status, p.Error) + err.Message = fmt.Sprintf("http error status: %d; reason: %s", resp.Status, p.Error) } return err diff --git a/errorutils/errorutils.go b/errorutils/errorutils.go index fe81b756..2d4d37b0 100644 --- a/errorutils/errorutils.go +++ b/errorutils/errorutils.go @@ -17,47 +17,138 @@ package errorutils import ( "net/http" +) + +// ErrorCode represents the platform-wide error codes that can be raised by +// Admin SDK APIs. +type ErrorCode string + +const ( + // InvalidArgument is a OnePlatform error code. + InvalidArgument ErrorCode = "INVALID_ARGUMENT" + + // FailedPrecondition is a OnePlatform error code. + FailedPrecondition ErrorCode = "FAILED_PRECONDITION" + + // OutOfRange is a OnePlatform error code. + OutOfRange ErrorCode = "OUT_OF_RANGE" + + // Unauthenticated is a OnePlatform error code. + Unauthenticated ErrorCode = "UNAUTHENTICATED" + + // PermissionDenied is a OnePlatform error code. + PermissionDenied ErrorCode = "PERMISSION_DENIED" + + // NotFound is a OnePlatform error code. + NotFound ErrorCode = "NOT_FOUND" + + // Conflict is a custom error code that represents HTTP 409 responses. + // + // OnePlatform APIs typically respond with ABORTED or ALREADY_EXISTS explicitly. But a few + // old APIs send HTTP 409 Conflict without any additional details to distinguish between the two + // cases. For these we currently use this error code. As more APIs adopt OnePlatform conventions + // this will become less important. + Conflict ErrorCode = "CONFLICT" + + // Aborted is a OnePlatform error code. + Aborted ErrorCode = "ABORTED" + + // AlreadyExists is a OnePlatform error code. + AlreadyExists ErrorCode = "ALREADY_EXISTS" + + // ResourceExhausted is a OnePlatform error code. + ResourceExhausted ErrorCode = "RESOURCE_EXHAUSTED" + + // Cancelled is a OnePlatform error code. + Cancelled ErrorCode = "CANCELLED" + + // DataLoss is a OnePlatform error code. + DataLoss ErrorCode = "DATA_LOSS" + + // Unknown is a OnePlatform error code. + Unknown ErrorCode = "UNKNOWN" - "firebase.google.com/go/v4/internal" + // Internal is a OnePlatform error code. + Internal ErrorCode = "INTERNAL" + + // Unavailable is a OnePlatform error code. + Unavailable ErrorCode = "UNAVAILABLE" + + // DeadlineExceeded is a OnePlatform error code. + DeadlineExceeded ErrorCode = "DEADLINE_EXCEEDED" ) +// FirebaseError is an error type containing an: +// - error code string. +// - error message. +// - HTTP response that caused this error, if any. +// - additional metadata about the error. +type FirebaseError struct { + ErrorCode ErrorCode + Message string + Response *http.Response + Ext map[string]interface{} +} + +// Error implements the error interface. +func (fe *FirebaseError) Error() string { + return fe.Message +} + +// Code returns the canonical error code associated with this error. +func (fe *FirebaseError) Code() ErrorCode { + return fe.ErrorCode +} + +// HTTPResponse returns the HTTP response that caused this error, if any. +// Returns nil if the error was not caused by an HTTP response. +func (fe *FirebaseError) HTTPResponse() *http.Response { + return fe.Response +} + +// Extensions returns additional metadata associated with this error. +// Returns nil if no extensions are available. +func (fe *FirebaseError) Extensions() map[string]interface{} { + return fe.Ext +} + // IsInvalidArgument checks if the given error was due to an invalid client argument. func IsInvalidArgument(err error) bool { - return internal.HasPlatformErrorCode(err, internal.InvalidArgument) + return HasPlatformErrorCode(err, InvalidArgument) } // IsFailedPrecondition checks if the given error was because a request could not be executed // in the current system state, such as deleting a non-empty directory. func IsFailedPrecondition(err error) bool { - return internal.HasPlatformErrorCode(err, internal.FailedPrecondition) + return HasPlatformErrorCode(err, FailedPrecondition) } // IsOutOfRange checks if the given error due to an invalid range specified by the client. func IsOutOfRange(err error) bool { - return internal.HasPlatformErrorCode(err, internal.OutOfRange) + return HasPlatformErrorCode(err, OutOfRange) } // IsUnauthenticated checks if the given error was caused by an unauthenticated request. // // Unauthenticated requests are due to missing, invalid, or expired OAuth token. func IsUnauthenticated(err error) bool { - return internal.HasPlatformErrorCode(err, internal.Unauthenticated) + return HasPlatformErrorCode(err, Unauthenticated) } -// IsPermissionDenied checks if the given error was due to a client not having suffificient +// IsPermissionDenied checks if the given error was due to a client not having sufficient // permissions. // // This can happen because the OAuth token does not have the right scopes, the client doesn't have // permission, or the API has not been enabled for the client project. func IsPermissionDenied(err error) bool { - return internal.HasPlatformErrorCode(err, internal.PermissionDenied) + return HasPlatformErrorCode(err, PermissionDenied) } // IsNotFound checks if the given error was due to a specified resource being not found. // // This may also occur when the request is rejected by undisclosed reasons, such as whitelisting. func IsNotFound(err error) bool { - return internal.HasPlatformErrorCode(err, internal.NotFound) + return HasPlatformErrorCode(err, NotFound) } // IsConflict checks if the given error was due to a concurrency conflict, such as a @@ -66,58 +157,58 @@ func IsNotFound(err error) bool { // This represents an HTTP 409 Conflict status code, without additional information to distinguish // between ABORTED or ALREADY_EXISTS error conditions. func IsConflict(err error) bool { - return internal.HasPlatformErrorCode(err, internal.Conflict) + return HasPlatformErrorCode(err, Conflict) } // IsAborted checks if the given error was due to a concurrency conflict, such as a // read-modify-write conflict. func IsAborted(err error) bool { - return internal.HasPlatformErrorCode(err, internal.Aborted) + return HasPlatformErrorCode(err, Aborted) } // IsAlreadyExists checks if the given error was because a resource that a client tried to create // already exists. func IsAlreadyExists(err error) bool { - return internal.HasPlatformErrorCode(err, internal.AlreadyExists) + return HasPlatformErrorCode(err, AlreadyExists) } // IsResourceExhausted checks if the given error was caused by either running out of a quota or // reaching a rate limit. func IsResourceExhausted(err error) bool { - return internal.HasPlatformErrorCode(err, internal.ResourceExhausted) + return HasPlatformErrorCode(err, ResourceExhausted) } // IsCancelled checks if the given error was due to the client cancelling a request. func IsCancelled(err error) bool { - return internal.HasPlatformErrorCode(err, internal.Cancelled) + return HasPlatformErrorCode(err, Cancelled) } // IsDataLoss checks if the given error was due to an unrecoverable data loss or corruption. // // The client should report such errors to the end user. func IsDataLoss(err error) bool { - return internal.HasPlatformErrorCode(err, internal.DataLoss) + return HasPlatformErrorCode(err, DataLoss) } -// IsUnknown checks if the given error was cuased by an unknown server error. +// IsUnknown checks if the given error was caused by an unknown server error. // // This typically indicates a server bug. func IsUnknown(err error) bool { - return internal.HasPlatformErrorCode(err, internal.Unknown) + return HasPlatformErrorCode(err, Unknown) } // IsInternal checks if the given error was due to an internal server error. // // This typically indicates a server bug. func IsInternal(err error) bool { - return internal.HasPlatformErrorCode(err, internal.Internal) + return HasPlatformErrorCode(err, Internal) } // IsUnavailable checks if the given error was caused by an unavailable service. // // This typically indicates that the target service is temporarily down. func IsUnavailable(err error) bool { - return internal.HasPlatformErrorCode(err, internal.Unavailable) + return HasPlatformErrorCode(err, Unavailable) } // IsDeadlineExceeded checks if the given error was due a request exceeding a deadline. @@ -126,7 +217,7 @@ func IsUnavailable(err error) bool { // deadline (i.e. requested deadline is not enough for the server to process the request) and the // request did not finish within the deadline. func IsDeadlineExceeded(err error) bool { - return internal.HasPlatformErrorCode(err, internal.DeadlineExceeded) + return HasPlatformErrorCode(err, DeadlineExceeded) } // HTTPResponse returns the http.Response instance that caused the given error. @@ -136,10 +227,16 @@ func IsDeadlineExceeded(err error) bool { // Returns a buffered copy of the original response received from the network stack. It is safe to // read the response content from the returned http.Response. func HTTPResponse(err error) *http.Response { - fe, ok := err.(*internal.FirebaseError) + fe, ok := err.(*FirebaseError) if ok { return fe.Response } return nil } + +// HasPlatformErrorCode checks if the given error contains a specific error code. +func HasPlatformErrorCode(err error, code ErrorCode) bool { + fe, ok := err.(*FirebaseError) + return ok && fe.ErrorCode == code +} diff --git a/errorutils/errorutils_test.go b/errorutils/errorutils_test.go new file mode 100644 index 00000000..60d8c568 --- /dev/null +++ b/errorutils/errorutils_test.go @@ -0,0 +1,267 @@ +// Copyright 2020 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package errorutils + +import ( + "errors" + "net/http" + "testing" +) + +func TestFirebaseErrorImplementsError(t *testing.T) { + fe := &FirebaseError{ + ErrorCode: NotFound, + Message: "resource not found", + } + + var err error = fe + if err.Error() != "resource not found" { + t.Errorf("Error() = %q; want = %q", err.Error(), "resource not found") + } +} + +func TestFirebaseErrorAccessors(t *testing.T) { + resp := &http.Response{StatusCode: http.StatusNotFound} + ext := map[string]interface{}{"key": "value"} + + fe := &FirebaseError{ + ErrorCode: NotFound, + Message: "resource not found", + Response: resp, + Ext: ext, + } + + if fe.Code() != NotFound { + t.Errorf("Code() = %q; want = %q", fe.Code(), NotFound) + } + + if fe.HTTPResponse() != resp { + t.Errorf("HTTPResponse() = %v; want = %v", fe.HTTPResponse(), resp) + } + + if fe.Extensions()["key"] != "value" { + t.Errorf("Extensions()[\"key\"] = %v; want = %q", fe.Extensions()["key"], "value") + } +} + +func TestFirebaseErrorAccessorsWithNilFields(t *testing.T) { + fe := &FirebaseError{ + ErrorCode: Internal, + Message: "internal error", + } + + if fe.HTTPResponse() != nil { + t.Errorf("HTTPResponse() = %v; want = nil", fe.HTTPResponse()) + } + + if fe.Extensions() != nil { + t.Errorf("Extensions() = %v; want = nil", fe.Extensions()) + } +} + +func TestHasPlatformErrorCode(t *testing.T) { + fe := &FirebaseError{ + ErrorCode: NotFound, + Message: "not found", + } + + if !HasPlatformErrorCode(fe, NotFound) { + t.Error("HasPlatformErrorCode(fe, NotFound) = false; want = true") + } + + if HasPlatformErrorCode(fe, Internal) { + t.Error("HasPlatformErrorCode(fe, Internal) = true; want = false") + } +} + +func TestHasPlatformErrorCodeWithNonFirebaseError(t *testing.T) { + err := errors.New("regular error") + + if HasPlatformErrorCode(err, NotFound) { + t.Error("HasPlatformErrorCode(err, NotFound) = true; want = false") + } +} + +func TestHasPlatformErrorCodeWithNil(t *testing.T) { + if HasPlatformErrorCode(nil, NotFound) { + t.Error("HasPlatformErrorCode(nil, NotFound) = true; want = false") + } +} + +func TestHTTPResponseFunction(t *testing.T) { + resp := &http.Response{StatusCode: http.StatusInternalServerError} + fe := &FirebaseError{ + ErrorCode: Internal, + Message: "internal error", + Response: resp, + } + + if HTTPResponse(fe) != resp { + t.Errorf("HTTPResponse(fe) = %v; want = %v", HTTPResponse(fe), resp) + } +} + +func TestHTTPResponseFunctionWithNonFirebaseError(t *testing.T) { + err := errors.New("regular error") + + if HTTPResponse(err) != nil { + t.Errorf("HTTPResponse(err) = %v; want = nil", HTTPResponse(err)) + } +} + +func TestHTTPResponseFunctionWithNil(t *testing.T) { + if HTTPResponse(nil) != nil { + t.Errorf("HTTPResponse(nil) = %v; want = nil", HTTPResponse(nil)) + } +} + +func TestIsErrorCodeFunctions(t *testing.T) { + testCases := []struct { + name string + code ErrorCode + checkFn func(error) bool + wantTrue bool + }{ + {"InvalidArgument", InvalidArgument, IsInvalidArgument, true}, + {"FailedPrecondition", FailedPrecondition, IsFailedPrecondition, true}, + {"OutOfRange", OutOfRange, IsOutOfRange, true}, + {"Unauthenticated", Unauthenticated, IsUnauthenticated, true}, + {"PermissionDenied", PermissionDenied, IsPermissionDenied, true}, + {"NotFound", NotFound, IsNotFound, true}, + {"Conflict", Conflict, IsConflict, true}, + {"Aborted", Aborted, IsAborted, true}, + {"AlreadyExists", AlreadyExists, IsAlreadyExists, true}, + {"ResourceExhausted", ResourceExhausted, IsResourceExhausted, true}, + {"Cancelled", Cancelled, IsCancelled, true}, + {"DataLoss", DataLoss, IsDataLoss, true}, + {"Unknown", Unknown, IsUnknown, true}, + {"Internal", Internal, IsInternal, true}, + {"Unavailable", Unavailable, IsUnavailable, true}, + {"DeadlineExceeded", DeadlineExceeded, IsDeadlineExceeded, true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fe := &FirebaseError{ + ErrorCode: tc.code, + Message: "test error", + } + + if tc.checkFn(fe) != tc.wantTrue { + t.Errorf("%s check = %v; want = %v", tc.name, tc.checkFn(fe), tc.wantTrue) + } + }) + } +} + +func TestIsErrorCodeFunctionsWithWrongCode(t *testing.T) { + fe := &FirebaseError{ + ErrorCode: NotFound, + Message: "not found", + } + + checks := []struct { + name string + checkFn func(error) bool + }{ + {"IsInvalidArgument", IsInvalidArgument}, + {"IsFailedPrecondition", IsFailedPrecondition}, + {"IsOutOfRange", IsOutOfRange}, + {"IsUnauthenticated", IsUnauthenticated}, + {"IsPermissionDenied", IsPermissionDenied}, + {"IsConflict", IsConflict}, + {"IsAborted", IsAborted}, + {"IsAlreadyExists", IsAlreadyExists}, + {"IsResourceExhausted", IsResourceExhausted}, + {"IsCancelled", IsCancelled}, + {"IsDataLoss", IsDataLoss}, + {"IsUnknown", IsUnknown}, + {"IsInternal", IsInternal}, + {"IsUnavailable", IsUnavailable}, + {"IsDeadlineExceeded", IsDeadlineExceeded}, + } + + for _, tc := range checks { + t.Run(tc.name, func(t *testing.T) { + if tc.checkFn(fe) { + t.Errorf("%s(NotFoundError) = true; want = false", tc.name) + } + }) + } +} + +func TestIsErrorCodeFunctionsWithNonFirebaseError(t *testing.T) { + err := errors.New("regular error") + + checks := []struct { + name string + checkFn func(error) bool + }{ + {"IsInvalidArgument", IsInvalidArgument}, + {"IsNotFound", IsNotFound}, + {"IsInternal", IsInternal}, + } + + for _, tc := range checks { + t.Run(tc.name, func(t *testing.T) { + if tc.checkFn(err) { + t.Errorf("%s(regularError) = true; want = false", tc.name) + } + }) + } +} + +func TestIsErrorCodeFunctionsWithNil(t *testing.T) { + if IsNotFound(nil) { + t.Error("IsNotFound(nil) = true; want = false") + } + + if IsInternal(nil) { + t.Error("IsInternal(nil) = true; want = false") + } +} + +func TestErrorCodeValues(t *testing.T) { + // Verify error codes have the expected string values + testCases := []struct { + code ErrorCode + want string + }{ + {InvalidArgument, "INVALID_ARGUMENT"}, + {FailedPrecondition, "FAILED_PRECONDITION"}, + {OutOfRange, "OUT_OF_RANGE"}, + {Unauthenticated, "UNAUTHENTICATED"}, + {PermissionDenied, "PERMISSION_DENIED"}, + {NotFound, "NOT_FOUND"}, + {Conflict, "CONFLICT"}, + {Aborted, "ABORTED"}, + {AlreadyExists, "ALREADY_EXISTS"}, + {ResourceExhausted, "RESOURCE_EXHAUSTED"}, + {Cancelled, "CANCELLED"}, + {DataLoss, "DATA_LOSS"}, + {Unknown, "UNKNOWN"}, + {Internal, "INTERNAL"}, + {Unavailable, "UNAVAILABLE"}, + {DeadlineExceeded, "DEADLINE_EXCEEDED"}, + } + + for _, tc := range testCases { + t.Run(tc.want, func(t *testing.T) { + if string(tc.code) != tc.want { + t.Errorf("ErrorCode = %q; want = %q", tc.code, tc.want) + } + }) + } +} diff --git a/iid/iid.go b/iid/iid.go index 7a3e9b55..0566f0e9 100644 --- a/iid/iid.go +++ b/iid/iid.go @@ -152,7 +152,7 @@ func createError(resp *internal.Response) error { if msg, ok := errorMessages[resp.Status]; ok { requestPath := resp.LowLevelResponse().Request.URL.Path idx := strings.LastIndex(requestPath, "/") - err.String = fmt.Sprintf("instance id %q: %s", requestPath[idx+1:], msg) + err.Message = fmt.Sprintf("instance id %q: %s", requestPath[idx+1:], msg) } return err diff --git a/internal/errors.go b/internal/errors.go index e209d158..284190aa 100644 --- a/internal/errors.go +++ b/internal/errors.go @@ -22,83 +22,39 @@ import ( "net/url" "os" "syscall" + + "firebase.google.com/go/v4/errorutils" ) -// ErrorCode represents the platform-wide error codes that can be raised by -// Admin SDK APIs. -type ErrorCode string +// ErrorCode alias to errorutils.ErrorCode +type ErrorCode = errorutils.ErrorCode +// Error code constants aliases to errorutils const ( - // InvalidArgument is a OnePlatform error code. - InvalidArgument ErrorCode = "INVALID_ARGUMENT" - - // FailedPrecondition is a OnePlatform error code. - FailedPrecondition ErrorCode = "FAILED_PRECONDITION" - - // OutOfRange is a OnePlatform error code. - OutOfRange ErrorCode = "OUT_OF_RANGE" - - // Unauthenticated is a OnePlatform error code. - Unauthenticated ErrorCode = "UNAUTHENTICATED" - - // PermissionDenied is a OnePlatform error code. - PermissionDenied ErrorCode = "PERMISSION_DENIED" - - // NotFound is a OnePlatform error code. - NotFound ErrorCode = "NOT_FOUND" - - // Conflict is a custom error code that represents HTTP 409 responses. - // - // OnePlatform APIs typically respond with ABORTED or ALREADY_EXISTS explicitly. But a few - // old APIs send HTTP 409 Conflict without any additional details to distinguish between the two - // cases. For these we currently use this error code. As more APIs adopt OnePlatform conventions - // this will become less important. - Conflict ErrorCode = "CONFLICT" - - // Aborted is a OnePlatform error code. - Aborted ErrorCode = "ABORTED" - - // AlreadyExists is a OnePlatform error code. - AlreadyExists ErrorCode = "ALREADY_EXISTS" - - // ResourceExhausted is a OnePlatform error code. - ResourceExhausted ErrorCode = "RESOURCE_EXHAUSTED" - - // Cancelled is a OnePlatform error code. - Cancelled ErrorCode = "CANCELLED" - - // DataLoss is a OnePlatform error code. - DataLoss ErrorCode = "DATA_LOSS" - - // Unknown is a OnePlatform error code. - Unknown ErrorCode = "UNKNOWN" - - // Internal is a OnePlatform error code. - Internal ErrorCode = "INTERNAL" - - // Unavailable is a OnePlatform error code. - Unavailable ErrorCode = "UNAVAILABLE" - - // DeadlineExceeded is a OnePlatform error code. - DeadlineExceeded ErrorCode = "DEADLINE_EXCEEDED" + InvalidArgument = errorutils.InvalidArgument + FailedPrecondition = errorutils.FailedPrecondition + OutOfRange = errorutils.OutOfRange + Unauthenticated = errorutils.Unauthenticated + PermissionDenied = errorutils.PermissionDenied + NotFound = errorutils.NotFound + Conflict = errorutils.Conflict + Aborted = errorutils.Aborted + AlreadyExists = errorutils.AlreadyExists + ResourceExhausted = errorutils.ResourceExhausted + Cancelled = errorutils.Cancelled + DataLoss = errorutils.DataLoss + Unknown = errorutils.Unknown + Internal = errorutils.Internal + Unavailable = errorutils.Unavailable + DeadlineExceeded = errorutils.DeadlineExceeded ) -// FirebaseError is an error type containing an error code string. -type FirebaseError struct { - ErrorCode ErrorCode - String string - Response *http.Response - Ext map[string]interface{} -} - -func (fe *FirebaseError) Error() string { - return fe.String -} +// FirebaseError is an alias to errorutils.FirebaseError for backwards compatibility. +type FirebaseError = errorutils.FirebaseError // HasPlatformErrorCode checks if the given error contains a specific error code. func HasPlatformErrorCode(err error, code ErrorCode) bool { - fe, ok := err.(*FirebaseError) - return ok && fe.ErrorCode == code + return errorutils.HasPlatformErrorCode(err, code) } var httpStatusToErrorCodes = map[int]ErrorCode{ @@ -121,7 +77,7 @@ func NewFirebaseError(resp *Response) *FirebaseError { return &FirebaseError{ ErrorCode: code, - String: fmt.Sprintf("unexpected http response with status: %d\n%s", resp.Status, string(resp.Body)), + Message: fmt.Sprintf("unexpected http response with status: %d\n%s", resp.Status, string(resp.Body)), Response: resp.LowLevelResponse(), Ext: make(map[string]interface{}), } @@ -147,7 +103,7 @@ func NewFirebaseErrorOnePlatform(resp *Response) *FirebaseError { } if gcpError.Error.Message != "" { - base.String = gcpError.Error.Message + base.Message = gcpError.Error.Message } return base @@ -169,7 +125,7 @@ func newFirebaseErrorTransport(err error) *FirebaseError { return &FirebaseError{ ErrorCode: code, - String: msg, + Message: msg, Ext: make(map[string]interface{}), } } diff --git a/messaging/messaging.go b/messaging/messaging.go index 8f484684..b5077c53 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -39,15 +39,6 @@ const ( apiFormatVersionHeader = "X-GOOG-API-FORMAT-VERSION" apiFormatVersion = "2" - apnsAuthError = "APNS_AUTH_ERROR" - internalError = "INTERNAL" - thirdPartyAuthError = "THIRD_PARTY_AUTH_ERROR" - invalidArgument = "INVALID_ARGUMENT" - quotaExceeded = "QUOTA_EXCEEDED" - senderIDMismatch = "SENDER_ID_MISMATCH" - unregistered = "UNREGISTERED" - unavailable = "UNAVAILABLE" - rfc3339Zulu = "2006-01-02T15:04:05.000000000Z" ) @@ -994,99 +985,6 @@ func (c *fcmClient) makeSendRequest(ctx context.Context, req *fcmRequest) (strin return result.Name, err } -// IsInternal checks if the given error was due to an internal server error. -func IsInternal(err error) bool { - return hasMessagingErrorCode(err, internalError) -} - -// IsInvalidAPNSCredentials checks if the given error was due to invalid APNS certificate or auth -// key. -// -// Deprecated: Use IsThirdPartyAuthError(). -func IsInvalidAPNSCredentials(err error) bool { - return IsThirdPartyAuthError(err) -} - -// IsThirdPartyAuthError checks if the given error was due to invalid APNS certificate or auth -// key. -func IsThirdPartyAuthError(err error) bool { - return hasMessagingErrorCode(err, thirdPartyAuthError) || hasMessagingErrorCode(err, apnsAuthError) -} - -// IsInvalidArgument checks if the given error was due to an invalid argument in the request. -func IsInvalidArgument(err error) bool { - return hasMessagingErrorCode(err, invalidArgument) -} - -// IsMessageRateExceeded checks if the given error was due to the client exceeding a quota. -// -// Deprecated: Use IsQuotaExceeded(). -func IsMessageRateExceeded(err error) bool { - return IsQuotaExceeded(err) -} - -// IsQuotaExceeded checks if the given error was due to the client exceeding a quota. -func IsQuotaExceeded(err error) bool { - return hasMessagingErrorCode(err, quotaExceeded) -} - -// IsMismatchedCredential checks if the given error was due to an invalid credential or permission -// error. -// -// Deprecated: Use IsSenderIDMismatch(). -func IsMismatchedCredential(err error) bool { - return IsSenderIDMismatch(err) -} - -// IsSenderIDMismatch checks if the given error was due to an invalid credential or permission -// error. -func IsSenderIDMismatch(err error) bool { - return hasMessagingErrorCode(err, senderIDMismatch) -} - -// IsRegistrationTokenNotRegistered checks if the given error was due to a registration token that -// became invalid. -// -// Deprecated: Use IsUnregistered(). -func IsRegistrationTokenNotRegistered(err error) bool { - return IsUnregistered(err) -} - -// IsUnregistered checks if the given error was due to a registration token that -// became invalid. -func IsUnregistered(err error) bool { - return hasMessagingErrorCode(err, unregistered) -} - -// IsServerUnavailable checks if the given error was due to the backend server being temporarily -// unavailable. -// -// Deprecated: Use IsUnavailable(). -func IsServerUnavailable(err error) bool { - return IsUnavailable(err) -} - -// IsUnavailable checks if the given error was due to the backend server being temporarily -// unavailable. -func IsUnavailable(err error) bool { - return hasMessagingErrorCode(err, unavailable) -} - -// IsTooManyTopics checks if the given error was due to the client exceeding the allowed number -// of topics. -// -// Deprecated: Always returns false. -func IsTooManyTopics(err error) bool { - return false -} - -// IsUnknown checks if the given error was due to unknown error returned by the backend server. -// -// Deprecated: Always returns false. -func IsUnknown(err error) bool { - return false -} - type fcmRequest struct { ValidateOnly bool `json:"validate_only,omitempty"` Message *Message `json:"message,omitempty"` @@ -1095,36 +993,3 @@ type fcmRequest struct { type fcmResponse struct { Name string `json:"name"` } - -type fcmErrorResponse struct { - Error struct { - Details []struct { - Type string `json:"@type"` - ErrorCode string `json:"errorCode"` - } - } `json:"error"` -} - -func handleFCMError(resp *internal.Response) error { - base := internal.NewFirebaseErrorOnePlatform(resp) - var fe fcmErrorResponse - json.Unmarshal(resp.Body, &fe) // ignore any json parse errors at this level - for _, d := range fe.Error.Details { - if d.Type == "type.googleapis.com/google.firebase.fcm.v1.FcmError" { - base.Ext["messagingErrorCode"] = d.ErrorCode - break - } - } - - return base -} - -func hasMessagingErrorCode(err error, code string) bool { - fe, ok := err.(*internal.FirebaseError) - if !ok { - return false - } - - got, ok := fe.Ext["messagingErrorCode"] - return ok && got == code -} diff --git a/messaging/messaging_errors.go b/messaging/messaging_errors.go new file mode 100644 index 00000000..90cf020c --- /dev/null +++ b/messaging/messaging_errors.go @@ -0,0 +1,241 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package messaging + +import ( + "encoding/json" + + "firebase.google.com/go/v4/errorutils" + "firebase.google.com/go/v4/internal" +) + +// FCM error codes +const ( + apnsAuthError = "APNS_AUTH_ERROR" + internalError = "INTERNAL" + thirdPartyAuthError = "THIRD_PARTY_AUTH_ERROR" + invalidArgument = "INVALID_ARGUMENT" + quotaExceeded = "QUOTA_EXCEEDED" + senderIDMismatch = "SENDER_ID_MISMATCH" + unregistered = "UNREGISTERED" + unavailable = "UNAVAILABLE" +) + +// QuotaViolation describes a single quota violation, identifying which quota +// was exceeded. +// See https://docs.cloud.google.com/tasks/docs/reference/rpc/google.rpc#google.rpc.QuotaFailure.Violation +// for more information on the google.rpc.QuotaFailure.Violation type. +type QuotaViolation struct { + // Subject is the subject on which the quota check failed. + // For example, "clientip:" or "project:". + Subject string + // Description explains how the quota check failed. + Description string + // APIService is the API service from which the QuotaFailure originates. + APIService string + // QuotaMetric is the metric of the violated quota (e.g., "compute.googleapis.com/cpus"). + QuotaMetric string + // QuotaID is the unique identifier of a quota (e.g., "CPUS-per-project-region"). + QuotaID string + // QuotaDimensions contains the dimensions of the violated quota. + QuotaDimensions map[string]string + // QuotaValue is the enforced quota value at the time of the failure. + QuotaValue int64 + // FutureQuotaValue is the new quota value being rolled out, if a rollout is in progress. + FutureQuotaValue int64 +} + +// QuotaFailure contains information about quota violations from FCM. +// This is returned when a rate limit is exceeded (device, topic, or overall). +type QuotaFailure struct { + Violations []*QuotaViolation +} + +// IsInternal checks if the given error was due to an internal server error. +func IsInternal(err error) bool { + return hasMessagingErrorCode(err, internalError) +} + +// IsInvalidAPNSCredentials checks if the given error was due to invalid APNS certificate or auth +// key. +// +// Deprecated: Use IsThirdPartyAuthError(). +func IsInvalidAPNSCredentials(err error) bool { + return IsThirdPartyAuthError(err) +} + +// IsThirdPartyAuthError checks if the given error was due to invalid APNS certificate or auth +// key. +func IsThirdPartyAuthError(err error) bool { + return hasMessagingErrorCode(err, thirdPartyAuthError) || hasMessagingErrorCode(err, apnsAuthError) +} + +// IsInvalidArgument checks if the given error was due to an invalid argument in the request. +func IsInvalidArgument(err error) bool { + return hasMessagingErrorCode(err, invalidArgument) +} + +// IsMessageRateExceeded checks if the given error was due to the client exceeding a quota. +// +// Deprecated: Use IsQuotaExceeded(). +func IsMessageRateExceeded(err error) bool { + return IsQuotaExceeded(err) +} + +// IsQuotaExceeded checks if the given error was due to the client exceeding a quota. +func IsQuotaExceeded(err error) bool { + return hasMessagingErrorCode(err, quotaExceeded) +} + +// GetQuotaFailure extracts the QuotaFailure details from a FirebaseError. +// Returns nil if the error does not contain quota failure information. +// The QuotaFailure indicates which rate limit was violated: device, topic, or overall. +func GetQuotaFailure(err *errorutils.FirebaseError) *QuotaFailure { + if err == nil || err.Ext == nil { + return nil + } + + qf, ok := err.Ext["quotaFailure"].(*QuotaFailure) + if !ok { + return nil + } + + return qf +} + +// IsMismatchedCredential checks if the given error was due to an invalid credential or permission +// error. +// +// Deprecated: Use IsSenderIDMismatch(). +func IsMismatchedCredential(err error) bool { + return IsSenderIDMismatch(err) +} + +// IsSenderIDMismatch checks if the given error was due to an invalid credential or permission +// error. +func IsSenderIDMismatch(err error) bool { + return hasMessagingErrorCode(err, senderIDMismatch) +} + +// IsRegistrationTokenNotRegistered checks if the given error was due to a registration token that +// became invalid. +// +// Deprecated: Use IsUnregistered(). +func IsRegistrationTokenNotRegistered(err error) bool { + return IsUnregistered(err) +} + +// IsUnregistered checks if the given error was due to a registration token that +// became invalid. +func IsUnregistered(err error) bool { + return hasMessagingErrorCode(err, unregistered) +} + +// IsServerUnavailable checks if the given error was due to the backend server being temporarily +// unavailable. +// +// Deprecated: Use IsUnavailable(). +func IsServerUnavailable(err error) bool { + return IsUnavailable(err) +} + +// IsUnavailable checks if the given error was due to the backend server being temporarily +// unavailable. +func IsUnavailable(err error) bool { + return hasMessagingErrorCode(err, unavailable) +} + +// IsTooManyTopics checks if the given error was due to the client exceeding the allowed number +// of topics. +// +// Deprecated: Always returns false. +func IsTooManyTopics(err error) bool { + return false +} + +// IsUnknown checks if the given error was due to unknown error returned by the backend server. +// +// Deprecated: Always returns false. +func IsUnknown(err error) bool { + return false +} + +type fcmErrorResponse struct { + Error struct { + Details []fcmErrorDetail `json:"details"` + } `json:"error"` +} + +type fcmErrorDetail struct { + Type string `json:"@type"` + ErrorCode string `json:"errorCode"` + Violations []struct { + Subject string `json:"subject"` + Description string `json:"description"` + APIService string `json:"api_service"` + QuotaMetric string `json:"quota_metric"` + QuotaID string `json:"quota_id"` + QuotaDimensions map[string]string `json:"quota_dimensions"` + QuotaValue int64 `json:"quota_value"` + FutureQuotaValue int64 `json:"future_quota_value"` + } `json:"violations"` +} + +func handleFCMError(resp *internal.Response) error { + base := internal.NewFirebaseErrorOnePlatform(resp) + var fe fcmErrorResponse + json.Unmarshal(resp.Body, &fe) // ignore any json parse errors at this level + + // FCM error responses include a "details" array with typed extensions. + // See https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode + for _, d := range fe.Error.Details { + // FcmError extension contains the FCM-specific error code. + // See https://firebase.google.com/docs/reference/fcm/rest/v1/FcmError + if d.Type == "type.googleapis.com/google.firebase.fcm.v1.FcmError" { + base.Ext["messagingErrorCode"] = d.ErrorCode + } + // QuotaFailure extension is returned when QUOTA_EXCEEDED error occurs. + // See https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode + // "An extension of type google.rpc.QuotaFailure is returned to specify which quota was exceeded." + if d.Type == "type.googleapis.com/google.rpc.QuotaFailure" && len(d.Violations) > 0 { + violations := make([]*QuotaViolation, len(d.Violations)) + for i, v := range d.Violations { + violations[i] = &QuotaViolation{ + Subject: v.Subject, + Description: v.Description, + APIService: v.APIService, + QuotaMetric: v.QuotaMetric, + QuotaID: v.QuotaID, + QuotaDimensions: v.QuotaDimensions, + QuotaValue: v.QuotaValue, + FutureQuotaValue: v.FutureQuotaValue, + } + } + base.Ext["quotaFailure"] = &QuotaFailure{Violations: violations} + } + } + + return base +} + +func hasMessagingErrorCode(err error, code string) bool { + fe, ok := err.(*internal.FirebaseError) + if !ok { + return false + } + + got, ok := fe.Ext["messagingErrorCode"] + return ok && got == code +} diff --git a/messaging/messaging_errors_test.go b/messaging/messaging_errors_test.go new file mode 100644 index 00000000..06b3e761 --- /dev/null +++ b/messaging/messaging_errors_test.go @@ -0,0 +1,208 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package messaging + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "firebase.google.com/go/v4/errorutils" + "firebase.google.com/go/v4/internal" +) + +func TestGetQuotaFailureEdgeCases(t *testing.T) { + tests := []struct { + name string + err *errorutils.FirebaseError + }{ + {"nil error", nil}, + {"no ext", &errorutils.FirebaseError{ErrorCode: internal.Unknown, Message: "test"}}, + {"empty ext", &errorutils.FirebaseError{ErrorCode: internal.Unknown, Ext: map[string]interface{}{}}}, + {"wrong type", &errorutils.FirebaseError{ErrorCode: internal.Unknown, Ext: map[string]interface{}{"quotaFailure": "string"}}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if qf := GetQuotaFailure(tc.err); qf != nil { + t.Errorf("GetQuotaFailure() = %v; want nil", qf) + } + }) + } +} + +// TestQuotaFailureParsing verifies all QuotaViolation fields are parsed correctly from FCM responses. +func TestQuotaFailureParsing(t *testing.T) { + resp := `{ + "error": { + "status": "RESOURCE_EXHAUSTED", + "message": "Quota exceeded", + "details": [ + {"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "QUOTA_EXCEEDED"}, + { + "@type": "type.googleapis.com/google.rpc.QuotaFailure", + "violations": [ + { + "subject": "project:test-project", + "description": "Device message rate exceeded", + "api_service": "firebasecloudmessaging.googleapis.com", + "quota_metric": "firebasecloudmessaging.googleapis.com/device_messages", + "quota_id": "DeviceMessagesPerMinute", + "quota_dimensions": {"device_token": "abc123"}, + "quota_value": 100, + "future_quota_value": 200 + }, + {"subject": "device:token123", "description": "Second violation"} + ] + } + ] + } + }` + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTooManyRequests) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(resp)) + })) + defer ts.Close() + + client, err := NewClient(context.Background(), testMessagingConfig) + if err != nil { + t.Fatal(err) + } + client.fcmEndpoint = ts.URL + client.fcmClient.httpClient.RetryConfig = nil + + _, sendErr := client.Send(context.Background(), &Message{Topic: "topic"}) + if sendErr == nil { + t.Fatal("Send() = nil; want error") + } + + // Verify error type checks work + if !IsQuotaExceeded(sendErr) { + t.Error("IsQuotaExceeded() = false; want true") + } + if !IsMessageRateExceeded(sendErr) { // deprecated alias + t.Error("IsMessageRateExceeded() = false; want true") + } + + fe := sendErr.(*errorutils.FirebaseError) + qf := GetQuotaFailure(fe) + if qf == nil { + t.Fatal("GetQuotaFailure() = nil; want non-nil") + } + if len(qf.Violations) != 2 { + t.Fatalf("len(Violations) = %d; want 2", len(qf.Violations)) + } + + // Verify all fields on first violation + v := qf.Violations[0] + if v.Subject != "project:test-project" { + t.Errorf("Subject = %q; want %q", v.Subject, "project:test-project") + } + if v.APIService != "firebasecloudmessaging.googleapis.com" { + t.Errorf("APIService = %q; want %q", v.APIService, "firebasecloudmessaging.googleapis.com") + } + if v.QuotaMetric != "firebasecloudmessaging.googleapis.com/device_messages" { + t.Errorf("QuotaMetric = %q; want correct value", v.QuotaMetric) + } + if v.QuotaID != "DeviceMessagesPerMinute" { + t.Errorf("QuotaID = %q; want %q", v.QuotaID, "DeviceMessagesPerMinute") + } + if v.QuotaDimensions == nil || v.QuotaDimensions["device_token"] != "abc123" { + t.Errorf("QuotaDimensions = %v; want map with device_token=abc123", v.QuotaDimensions) + } + if v.QuotaValue != 100 || v.FutureQuotaValue != 200 { + t.Errorf("QuotaValue=%d, FutureQuotaValue=%d; want 100, 200", v.QuotaValue, v.FutureQuotaValue) + } + + // Verify second violation + if qf.Violations[1].Subject != "device:token123" { + t.Errorf("Violations[1].Subject = %q; want %q", qf.Violations[1].Subject, "device:token123") + } +} + +// TestDeprecatedErrorFunctions verifies deprecated functions still work for backwards compatibility. +func TestDeprecatedErrorFunctions(t *testing.T) { + tests := []struct { + name string + httpStatus int + resp string + deprecated func(error) bool + current func(error) bool + }{ + { + name: "IsInvalidAPNSCredentials", + httpStatus: http.StatusUnauthorized, + resp: `{"error": {"status": "UNAUTHENTICATED", "message": "test", "details": [{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "THIRD_PARTY_AUTH_ERROR"}]}}`, + deprecated: IsInvalidAPNSCredentials, + current: IsThirdPartyAuthError, + }, + { + name: "IsMismatchedCredential", + httpStatus: http.StatusForbidden, + resp: `{"error": {"status": "PERMISSION_DENIED", "message": "test", "details": [{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "SENDER_ID_MISMATCH"}]}}`, + deprecated: IsMismatchedCredential, + current: IsSenderIDMismatch, + }, + { + name: "IsRegistrationTokenNotRegistered", + httpStatus: http.StatusNotFound, + resp: `{"error": {"status": "NOT_FOUND", "message": "test", "details": [{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "UNREGISTERED"}]}}`, + deprecated: IsRegistrationTokenNotRegistered, + current: IsUnregistered, + }, + { + name: "IsServerUnavailable", + httpStatus: http.StatusServiceUnavailable, + resp: `{"error": {"status": "UNAVAILABLE", "message": "test", "details": [{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "UNAVAILABLE"}]}}`, + deprecated: IsServerUnavailable, + current: IsUnavailable, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tc.httpStatus) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tc.resp)) + })) + defer ts.Close() + + client, _ := NewClient(context.Background(), testMessagingConfig) + client.fcmEndpoint = ts.URL + client.fcmClient.httpClient.RetryConfig = nil + + _, err := client.Send(context.Background(), &Message{Topic: "topic"}) + if !tc.deprecated(err) { + t.Errorf("%s() = false; want true", tc.name) + } + if !tc.current(err) { + t.Errorf("current function = false; want true") + } + }) + } +} + +// TestDeprecatedFunctionsAlwaysFalse verifies IsTooManyTopics and IsUnknown always return false. +func TestDeprecatedFunctionsAlwaysFalse(t *testing.T) { + if IsTooManyTopics(nil) { + t.Error("IsTooManyTopics(nil) = true; want false") + } + if IsUnknown(nil) { + t.Error("IsUnknown(nil) = true; want false") + } +} diff --git a/messaging/topic_mgt.go b/messaging/topic_mgt.go index 88492585..ef01ed9d 100644 --- a/messaging/topic_mgt.go +++ b/messaging/topic_mgt.go @@ -156,7 +156,7 @@ func handleIIDError(resp *internal.Response) error { var ie iidErrorResponse json.Unmarshal(resp.Body, &ie) // ignore any json parse errors at this level if ie.Error != "" { - base.String = fmt.Sprintf("error while calling the iid service: %s", ie.Error) + base.Message = fmt.Sprintf("error while calling the iid service: %s", ie.Error) } return base diff --git a/remoteconfig/remoteconfig.go b/remoteconfig/remoteconfig.go index 7c6228d1..b1ec9748 100644 --- a/remoteconfig/remoteconfig.go +++ b/remoteconfig/remoteconfig.go @@ -111,7 +111,7 @@ func handleRemoteConfigError(resp *internal.Response) error { } json.Unmarshal(resp.Body, &p) if p.Error != "" { - err.String = fmt.Sprintf("http error status: %d; reason: %s", resp.Status, p.Error) + err.Message = fmt.Sprintf("http error status: %d; reason: %s", resp.Status, p.Error) } return err