Skip to content

Commit 0beafb8

Browse files
authored
Implementing Functions to Test Error Codes (#131)
* Adding error codes to the auth package * Added error codes for messaging * Responding to review feedback; Updated changelog
1 parent 1b20f5a commit 0beafb8

File tree

8 files changed

+395
-84
lines changed

8 files changed

+395
-84
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Unreleased
22

3+
- [added] Added several new functions for testing errors
4+
(e.g. `auth.IsUserNotFound()`).
5+
- [added] Added support for setting the `mutable-content` property on
6+
FCM messages sent via APNS.
7+
- [changed] Updated the error messages returned by the `messaging`
8+
package. These errors now contain the full details sent by the
9+
back-end server.
10+
311
# v2.6.1
412

513
- [added] Added support for Go 1.6.

auth/auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ func (c *Client) VerifyIDTokenAndCheckRevoked(ctx context.Context, idToken strin
265265
}
266266

267267
if p.IssuedAt*1000 < user.TokensValidAfterMillis {
268-
return nil, fmt.Errorf("ID token has been revoked")
268+
return nil, internal.Error(idTokenRevoked, "ID token has been revoked")
269269
}
270270
return p, nil
271271
}

auth/auth_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ func TestVerifyIDTokenAndCheckRevokedInvalidated(t *testing.T) {
233233

234234
p, err := s.Client.VerifyIDTokenAndCheckRevoked(ctx, tok)
235235
we := "ID token has been revoked"
236-
if p != nil || err == nil || err.Error() != we {
236+
if p != nil || err == nil || err.Error() != we || !IsIDTokenRevoked(err) {
237237
t.Errorf("VerifyIDTokenAndCheckRevoked(ctx, token) =(%v, %v); want = (%v, %v)",
238238
p, err, nil, we)
239239
}

auth/user_mgt.go

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import (
2323
"strings"
2424
"time"
2525

26+
"firebase.google.com/go/internal"
2627
"golang.org/x/net/context"
2728

29+
"google.golang.org/api/googleapi"
2830
"google.golang.org/api/identitytoolkit/v3"
2931
"google.golang.org/api/iterator"
3032
)
@@ -215,8 +217,10 @@ func (c *Client) DeleteUser(ctx context.Context, uid string) error {
215217

216218
call := c.is.Relyingparty.DeleteAccount(request)
217219
c.setHeader(call)
218-
_, err := call.Context(ctx).Do()
219-
return err
220+
if _, err := call.Context(ctx).Do(); err != nil {
221+
return handleServerError(err)
222+
}
223+
return nil
220224
}
221225

222226
// GetUser gets the user data corresponding to the specified user ID.
@@ -279,7 +283,7 @@ func (it *UserIterator) fetch(pageSize int, pageToken string) (string, error) {
279283
it.client.setHeader(call)
280284
resp, err := call.Context(it.ctx).Do()
281285
if err != nil {
282-
return "", err
286+
return "", handleServerError(err)
283287
}
284288

285289
for _, u := range resp.Users {
@@ -345,10 +349,7 @@ func processClaims(p map[string]interface{}) error {
345349
return nil
346350
}
347351

348-
claims, ok := cc.(map[string]interface{})
349-
if !ok {
350-
return fmt.Errorf("unexpected type for custom claims")
351-
}
352+
claims := cc.(map[string]interface{})
352353
for _, key := range reservedClaims {
353354
if _, ok := claims[key]; ok {
354355
return fmt.Errorf("claim %q is reserved and must not be set", key)
@@ -372,6 +373,83 @@ func processClaims(p map[string]interface{}) error {
372373
return nil
373374
}
374375

376+
// Error handlers.
377+
378+
const (
379+
emailAlredyExists = "email-already-exists"
380+
idTokenRevoked = "id-token-revoked"
381+
insufficientPermission = "insufficient-permission"
382+
phoneNumberAlreadyExists = "phone-number-already-exists"
383+
projectNotFound = "project-not-found"
384+
uidAlreadyExists = "uid-already-exists"
385+
unknown = "unknown-error"
386+
userNotFound = "user-not-found"
387+
)
388+
389+
// IsEmailAlreadyExists checks if the given error was due to a duplicate email.
390+
func IsEmailAlreadyExists(err error) bool {
391+
return internal.HasErrorCode(err, emailAlredyExists)
392+
}
393+
394+
// IsIDTokenRevoked checks if the given error was due to a revoked ID token.
395+
func IsIDTokenRevoked(err error) bool {
396+
return internal.HasErrorCode(err, idTokenRevoked)
397+
}
398+
399+
// IsInsufficientPermission checks if the given error was due to insufficient permissions.
400+
func IsInsufficientPermission(err error) bool {
401+
return internal.HasErrorCode(err, insufficientPermission)
402+
}
403+
404+
// IsPhoneNumberAlreadyExists checks if the given error was due to a duplicate phone number.
405+
func IsPhoneNumberAlreadyExists(err error) bool {
406+
return internal.HasErrorCode(err, phoneNumberAlreadyExists)
407+
}
408+
409+
// IsProjectNotFound checks if the given error was due to a non-existing project.
410+
func IsProjectNotFound(err error) bool {
411+
return internal.HasErrorCode(err, projectNotFound)
412+
}
413+
414+
// IsUIDAlreadyExists checks if the given error was due to a duplicate uid.
415+
func IsUIDAlreadyExists(err error) bool {
416+
return internal.HasErrorCode(err, uidAlreadyExists)
417+
}
418+
419+
// IsUnknown checks if the given error was due to a unknown server error.
420+
func IsUnknown(err error) bool {
421+
return internal.HasErrorCode(err, unknown)
422+
}
423+
424+
// IsUserNotFound checks if the given error was due to non-existing user.
425+
func IsUserNotFound(err error) bool {
426+
return internal.HasErrorCode(err, userNotFound)
427+
}
428+
429+
var serverError = map[string]string{
430+
"CONFIGURATION_NOT_FOUND": projectNotFound,
431+
"DUPLICATE_EMAIL": emailAlredyExists,
432+
"DUPLICATE_LOCAL_ID": uidAlreadyExists,
433+
"EMAIL_EXISTS": emailAlredyExists,
434+
"INSUFFICIENT_PERMISSION": insufficientPermission,
435+
"PHONE_NUMBER_EXISTS": phoneNumberAlreadyExists,
436+
"PROJECT_NOT_FOUND": projectNotFound,
437+
}
438+
439+
func handleServerError(err error) error {
440+
gerr, ok := err.(*googleapi.Error)
441+
if !ok {
442+
// Not a back-end error
443+
return err
444+
}
445+
serverCode := gerr.Message
446+
clientCode, ok := serverError[serverCode]
447+
if !ok {
448+
clientCode = unknown
449+
}
450+
return internal.Error(clientCode, err.Error())
451+
}
452+
375453
// Validators.
376454

377455
func validateDisplayName(val interface{}) error {
@@ -532,7 +610,7 @@ func (c *Client) createUser(ctx context.Context, user *UserToCreate) (string, er
532610
c.setHeader(call)
533611
resp, err := call.Context(ctx).Do()
534612
if err != nil {
535-
return "", err
613+
return "", handleServerError(err)
536614
}
537615

538616
return resp.LocalId, nil
@@ -555,20 +633,29 @@ func (c *Client) updateUser(ctx context.Context, uid string, user *UserToUpdate)
555633

556634
call := c.is.Relyingparty.SetAccountInfo(request)
557635
c.setHeader(call)
558-
_, err := call.Context(ctx).Do()
559-
560-
return err
636+
if _, err := call.Context(ctx).Do(); err != nil {
637+
return handleServerError(err)
638+
}
639+
return nil
561640
}
562641

563642
func (c *Client) getUser(ctx context.Context, request *identitytoolkit.IdentitytoolkitRelyingpartyGetAccountInfoRequest) (*UserRecord, error) {
564643
call := c.is.Relyingparty.GetAccountInfo(request)
565644
c.setHeader(call)
566645
resp, err := call.Context(ctx).Do()
567646
if err != nil {
568-
return nil, err
647+
return nil, handleServerError(err)
569648
}
570649
if len(resp.Users) == 0 {
571-
return nil, fmt.Errorf("cannot find user given params: id:%v, phone:%v, email: %v", request.LocalId, request.PhoneNumber, request.Email)
650+
var msg string
651+
if len(request.LocalId) == 1 {
652+
msg = fmt.Sprintf("cannot find user from uid: %q", request.LocalId[0])
653+
} else if len(request.Email) == 1 {
654+
msg = fmt.Sprintf("cannot find user from email: %q", request.Email[0])
655+
} else {
656+
msg = fmt.Sprintf("cannot find user from phone number: %q", request.PhoneNumber[0])
657+
}
658+
return nil, internal.Error(userNotFound, msg)
572659
}
573660

574661
eu, err := makeExportedUser(resp.Users[0])
@@ -581,8 +668,7 @@ func (c *Client) getUser(ctx context.Context, request *identitytoolkit.Identityt
581668
func makeExportedUser(r *identitytoolkit.UserInfo) (*ExportedUserRecord, error) {
582669
var cc map[string]interface{}
583670
if r.CustomAttributes != "" {
584-
err := json.Unmarshal([]byte(r.CustomAttributes), &cc)
585-
if err != nil {
671+
if err := json.Unmarshal([]byte(r.CustomAttributes), &cc); err != nil {
586672
return nil, err
587673
}
588674
if len(cc) == 0 {

auth/user_mgt_test.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,23 +149,21 @@ func TestGetNonExistingUser(t *testing.T) {
149149
s := echoServer([]byte(resp), t)
150150
defer s.Close()
151151

152-
want := "cannot find user given params: id:[%s], phone:[%s], email: [%s]"
153-
154-
we := fmt.Sprintf(want, "id-nonexisting", "", "")
152+
we := `cannot find user from uid: "id-nonexisting"`
155153
user, err := s.Client.GetUser(context.Background(), "id-nonexisting")
156-
if user != nil || err == nil || err.Error() != we {
154+
if user != nil || err == nil || err.Error() != we || !IsUserNotFound(err) {
157155
t.Errorf("GetUser(non-existing) = (%v, %q); want = (nil, %q)", user, err, we)
158156
}
159157

160-
we = fmt.Sprintf(want, "", "", "[email protected]")
158+
we = `cannot find user from email: "[email protected]"`
161159
user, err = s.Client.GetUserByEmail(context.Background(), "[email protected]")
162-
if user != nil || err == nil || err.Error() != we {
160+
if user != nil || err == nil || err.Error() != we || !IsUserNotFound(err) {
163161
t.Errorf("GetUserByEmail(non-existing) = (%v, %q); want = (nil, %q)", user, err, we)
164162
}
165163

166-
we = fmt.Sprintf(want, "", "+12345678901", "")
164+
we = `cannot find user from phone number: "+12345678901"`
167165
user, err = s.Client.GetUserByPhoneNumber(context.Background(), "+12345678901")
168-
if user != nil || err == nil || err.Error() != we {
166+
if user != nil || err == nil || err.Error() != we || !IsUserNotFound(err) {
169167
t.Errorf("GetUserPhoneNumber(non-existing) = (%v, %q); want = (nil, %q)", user, err, we)
170168
}
171169
}
@@ -642,7 +640,6 @@ func TestInvalidDeleteUser(t *testing.T) {
642640
}
643641

644642
func TestMakeExportedUser(t *testing.T) {
645-
646643
rur := &identitytoolkit.UserInfo{
647644
LocalId: "testuser",
648645
@@ -704,11 +701,39 @@ func TestHTTPError(t *testing.T) {
704701
}
705702

706703
want := `googleapi: got HTTP response code 500 with body: {"error":"test"}`
707-
if err.Error() != want {
704+
if err.Error() != want || !IsUnknown(err) {
708705
t.Errorf("GetUser() = %v; want = %q", err, want)
709706
}
710707
}
711708

709+
func TestHTTPErrorWithCode(t *testing.T) {
710+
errorCodes := map[string]func(error) bool{
711+
"CONFIGURATION_NOT_FOUND": IsProjectNotFound,
712+
"DUPLICATE_EMAIL": IsEmailAlreadyExists,
713+
"DUPLICATE_LOCAL_ID": IsUIDAlreadyExists,
714+
"EMAIL_EXISTS": IsEmailAlreadyExists,
715+
"INSUFFICIENT_PERMISSION": IsInsufficientPermission,
716+
"PHONE_NUMBER_EXISTS": IsPhoneNumberAlreadyExists,
717+
"PROJECT_NOT_FOUND": IsProjectNotFound,
718+
}
719+
s := echoServer(nil, t)
720+
defer s.Close()
721+
s.Status = http.StatusInternalServerError
722+
723+
for code, check := range errorCodes {
724+
s.Resp = []byte(fmt.Sprintf(`{"error":{"message":"%s"}}`, code))
725+
u, err := s.Client.GetUser(context.Background(), "some uid")
726+
if u != nil || err == nil {
727+
t.Fatalf("GetUser() = (%v, %v); want = (nil, error)", u, err)
728+
}
729+
730+
want := fmt.Sprintf("googleapi: Error 500: %s", code)
731+
if err.Error() != want || !check(err) {
732+
t.Errorf("GetUser() = %v; want = %q", err, want)
733+
}
734+
}
735+
}
736+
712737
type mockAuthServer struct {
713738
Resp []byte
714739
Header map[string]string

internal/internal.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package internal
1717

1818
import (
19+
"fmt"
20+
1921
"golang.org/x/oauth2"
2022
"golang.org/x/oauth2/google"
2123
"google.golang.org/api/option"
@@ -59,18 +61,47 @@ type StorageConfig struct {
5961
Bucket string
6062
}
6163

62-
// MockTokenSource is a TokenSource implementation that can be used for testing.
63-
type MockTokenSource struct {
64-
AccessToken string
65-
}
66-
6764
// MessagingConfig represents the configuration of Firebase Cloud Messaging service.
6865
type MessagingConfig struct {
6966
Opts []option.ClientOption
7067
ProjectID string
7168
Version string
7269
}
7370

71+
// FirebaseError is an error type containing an error code string.
72+
type FirebaseError struct {
73+
Code string
74+
String string
75+
}
76+
77+
func (fe *FirebaseError) Error() string {
78+
return fe.String
79+
}
80+
81+
// HasErrorCode checks if the given error contain a specific error code.
82+
func HasErrorCode(err error, code string) bool {
83+
fe, ok := err.(*FirebaseError)
84+
return ok && fe.Code == code
85+
}
86+
87+
// Error creates a new FirebaseError from the specified error code and message.
88+
func Error(code string, msg string) *FirebaseError {
89+
return &FirebaseError{
90+
Code: code,
91+
String: msg,
92+
}
93+
}
94+
95+
// Errorf creates a new FirebaseError from the specified error code and message.
96+
func Errorf(code string, msg string, args ...interface{}) *FirebaseError {
97+
return Error(code, fmt.Sprintf(msg, args...))
98+
}
99+
100+
// MockTokenSource is a TokenSource implementation that can be used for testing.
101+
type MockTokenSource struct {
102+
AccessToken string
103+
}
104+
74105
// Token returns the test token associated with the TokenSource.
75106
func (ts *MockTokenSource) Token() (*oauth2.Token, error) {
76107
return &oauth2.Token{AccessToken: ts.AccessToken}, nil

0 commit comments

Comments
 (0)