Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased

- [added] Added new functions for testing errors in the `iid` package
(e.g. `iid.IsNotFound()`).
- [fixed] `auth.UpdateUser()` and `auth.DeleteUser()` return the expected
`UserNotFound` error when called with a non-existing uid.
- [added] Implemented the `auth.ImportUsers()` function for importing
Expand Down
89 changes: 77 additions & 12 deletions iid/iid.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,77 @@ import (

const iidEndpoint = "https://console.firebase.google.com/v1"

var errorCodes = map[int]string{
http.StatusBadRequest: "malformed instance id argument",
http.StatusUnauthorized: "request not authorized",
http.StatusForbidden: "project does not match instance ID or the client does not have sufficient privileges",
http.StatusNotFound: "failed to find the instance id",
http.StatusConflict: "already deleted",
http.StatusTooManyRequests: "request throttled out by the backend server",
http.StatusInternalServerError: "internal server error",
http.StatusServiceUnavailable: "backend servers are over capacity",
const (
invalidArgument = "invalid-argument"
unauthorized = "unauthorized"
insufficientPermission = "insufficient-permission"
notFound = "instance-id-not-found"
alreadyDeleted = "instance-id-already-deleted"
tooManyRequests = "too-many-requests"
internalError = "internal-error"
serverUnavailable = "server-unavailable"
unknown = "unknown-error"
)

var errorCodes = map[int]struct {
code, message string
}{
http.StatusBadRequest: {invalidArgument, "malformed instance id argument"},
http.StatusUnauthorized: {insufficientPermission, "request not authorized"},
http.StatusForbidden: {
insufficientPermission,
"project does not match instance ID or the client does not have sufficient privileges",
},
http.StatusNotFound: {notFound, "failed to find the instance id"},
http.StatusConflict: {alreadyDeleted, "already deleted"},
http.StatusTooManyRequests: {tooManyRequests, "request throttled out by the backend server"},
http.StatusInternalServerError: {internalError, "internal server error"},
http.StatusServiceUnavailable: {serverUnavailable, "backend servers are over capacity"},
}

// IsInvalidArgument checks if the given error was due to an invalid instance ID argument.
func IsInvalidArgument(err error) bool {
return internal.HasErrorCode(err, invalidArgument)
}

// IsInsufficientPermission checks if the given error was due to the request not having the
// required authorization. This could be due to the client not having the required permission
// or the specified instance ID not matching the target Firebase project.
func IsInsufficientPermission(err error) bool {
return internal.HasErrorCode(err, insufficientPermission)
}

// IsNotFound checks if the given error was due to a non existing instance ID.
func IsNotFound(err error) bool {
return internal.HasErrorCode(err, notFound)
}

// IsAlreadyDeleted checks if the given error was due to the instance ID being already deleted from
// the project.
func IsAlreadyDeleted(err error) bool {
return internal.HasErrorCode(err, alreadyDeleted)
}

// IsTooManyRequests checks if the given error was due to the client sending too many requests
// causing a server quota to exceed.
func IsTooManyRequests(err error) bool {
return internal.HasErrorCode(err, tooManyRequests)
}

// IsInternal checks if the given error was due to an internal server error.
func IsInternal(err error) bool {
return internal.HasErrorCode(err, internalError)
}

// IsServerUnavailable checks if the given error was due to the backend server being temporarily
// unavailable.
func IsServerUnavailable(err error) bool {
return internal.HasErrorCode(err, serverUnavailable)
}

// IsUnknown checks if the given error was due to unknown error returned by the backend server.
func IsUnknown(err error) bool {
return internal.HasErrorCode(err, unknown)
}

// Client is the interface for the Firebase Instance ID service.
Expand Down Expand Up @@ -84,8 +146,11 @@ func (c *Client) DeleteInstanceID(ctx context.Context, iid string) error {
return err
}

if msg, ok := errorCodes[resp.Status]; ok {
return fmt.Errorf("instance id %q: %s", iid, msg)
if info, ok := errorCodes[resp.Status]; ok {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice we're only using the http status code here. Can't our APIs return more specific error codes in the body of the response? Or am I thinking of something else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The endpoint we use for this functionality today signals errors via HTTP status code. In the future the backend service will send more detailed and well-formed error responses.

return internal.Errorf(info.code, "instance id %q: %s", iid, info.message)
}
if err := resp.CheckStatus(http.StatusOK); err != nil {
return internal.Error(unknown, err.Error())
}
return resp.CheckStatus(http.StatusOK)
return nil
}
31 changes: 17 additions & 14 deletions iid/iid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,22 @@ func TestDeleteInstanceIDError(t *testing.T) {
}
client.endpoint = ts.URL

for k, v := range errorCodes {
status = k
errorHandlers := map[int]func(error) bool{
http.StatusBadRequest: IsInvalidArgument,
http.StatusUnauthorized: IsInsufficientPermission,
http.StatusForbidden: IsInsufficientPermission,
http.StatusNotFound: IsNotFound,
http.StatusConflict: IsAlreadyDeleted,
http.StatusTooManyRequests: IsTooManyRequests,
http.StatusInternalServerError: IsInternal,
http.StatusServiceUnavailable: IsServerUnavailable,
}

for code, check := range errorHandlers {
status = code
want := fmt.Sprintf("instance id %q: %s", "test-iid", errorCodes[code].message)
err := client.DeleteInstanceID(ctx, "test-iid")
if err == nil {
t.Fatal("DeleteInstanceID() = nil; want = error")
}

want := fmt.Sprintf("instance id %q: %s", "test-iid", v)
if err.Error() != want {
if err == nil || !check(err) || err.Error() != want {
t.Errorf("DeleteInstanceID() = %v; want = %v", err, want)
}

Expand Down Expand Up @@ -149,13 +156,9 @@ func TestDeleteInstanceIDUnexpectedError(t *testing.T) {
}
client.endpoint = ts.URL

err = client.DeleteInstanceID(ctx, "test-iid")
if err == nil {
t.Fatal("DeleteInstanceID() = nil; want = error")
}

want := "http error status: 511; reason: {}"
if err.Error() != want {
err = client.DeleteInstanceID(ctx, "test-iid")
if err == nil || !IsUnknown(err) || err.Error() != want {
t.Errorf("DeleteInstanceID() = %v; want = %v", err, want)
}

Expand Down
2 changes: 1 addition & 1 deletion integration/iid/iid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func TestNonExisting(t *testing.T) {
t.Errorf("DeleteInstanceID(non-existing) = nil; want error")
}
want := `instance id "fictive-ID0": failed to find the instance id`
if err.Error() != want {
if !iid.IsNotFound(err) || err.Error() != want {
t.Errorf("DeleteInstanceID(non-existing) = %v; want = %v", err, want)
}
}