From a62a8e8ad798500ed3e362cd30843d1c0b79b20e Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Mon, 23 Dec 2019 10:01:00 -0500 Subject: [PATCH 01/20] Add bulk get method --- auth/user_mgt.go | 276 +++++++++++++++++++++++++++++- auth/user_mgt_test.go | 262 ++++++++++++++++++++++++++++ integration/auth/user_mgt_test.go | 211 +++++++++++++++++++++++ 3 files changed, 746 insertions(+), 3 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index e1a18a0f..69011417 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -34,6 +34,9 @@ const ( maxLenPayloadCC = 1000 defaultProviderID = "firebase" idToolkitV1Endpoint = "https://identitytoolkit.googleapis.com/v1" + + // Maximum allowed number of users to batch get at one time. + maxGetAccountsBatchSize = 100 ) // 'REDACTED', encoded as a base64 string. @@ -331,6 +334,7 @@ const ( unauthorizedContinueURI = "unauthorized-continue-uri" unknown = "unknown-error" userNotFound = "user-not-found" + maximumUserCountExceeded = "maximum-user-count-exceeded" ) // IsConfigurationNotFound checks if the given error was due to a non-existing IdP configuration. @@ -491,6 +495,15 @@ func validatePhone(phone string) error { return nil } +func validateProvider(providerID string, providerUID string) error { + if providerID == "" { + return fmt.Errorf("providerID must be a non-empty string") + } else if providerUID == "" { + return fmt.Errorf("providerUID must be a non-empty string") + } + return nil +} + // End of validators // GetUser gets the user data corresponding to the specified user ID. @@ -545,10 +558,12 @@ func (q *userQuery) build() map[string]interface{} { } } +type getAccountInfoResponse struct { + Users []*userQueryResponse `json:"users"` +} + func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord, error) { - var parsed struct { - Users []*userQueryResponse `json:"users"` - } + var parsed getAccountInfoResponse _, err := c.post(ctx, "/accounts:lookup", query.build(), &parsed) if err != nil { return nil, err @@ -561,6 +576,261 @@ func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord return parsed.Users[0].makeUserRecord() } +// Identifies a user to be looked up. +type UserIdentifier interface { + String() string + + validate() error + matchesUserRecord(ur *UserRecord) (bool, error) + populateRequest(req *getAccountInfoRequest) error +} + +// Used for looking up an account by uid. +// +// See GetUsers function. +type UidIdentifier struct { + UID string +} + +func (id UidIdentifier) String() string { + return fmt.Sprintf("UidIdentifier{%s}", id.UID) +} + +func (id UidIdentifier) validate() error { + return validateUID(id.UID) +} + +func (id UidIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { + err := id.validate() + if err != nil { + return false, err + } + if id.UID == ur.UID { + return true, nil + } + return false, nil +} + +func (id UidIdentifier) populateRequest(req *getAccountInfoRequest) error { + err := id.validate() + if err != nil { + return err + } + req.localId = append(req.localId, id.UID) + return nil +} + +// Used for looking up an account by email. +// +// See GetUsers function. +type EmailIdentifier struct { + Email string +} + +func (id EmailIdentifier) String() string { + return fmt.Sprintf("EmailIdentifier{%s}", id.Email) +} + +func (id EmailIdentifier) validate() error { + return validateEmail(id.Email) +} + +func (id EmailIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { + err := id.validate() + if err != nil { + return false, err + } + if id.Email == ur.Email { + return true, nil + } + return false, nil +} + +func (id EmailIdentifier) populateRequest(req *getAccountInfoRequest) error { + err := id.validate() + if err != nil { + return err + } + req.email = append(req.email, id.Email) + return nil +} + +// Used for looking up an account by phone number. +// +// See GetUsers function. +type PhoneIdentifier struct { + PhoneNumber string +} + +func (id PhoneIdentifier) String() string { + return fmt.Sprintf("PhoneIdentifier{%s}", id.PhoneNumber) +} + +func (id PhoneIdentifier) validate() error { + return validatePhone(id.PhoneNumber) +} + +func (id PhoneIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { + err := id.validate() + if err != nil { + return false, err + } + if id.PhoneNumber == ur.PhoneNumber { + return true, nil + } + return false, nil +} + +func (id PhoneIdentifier) populateRequest(req *getAccountInfoRequest) error { + err := id.validate() + if err != nil { + return err + } + req.phoneNumber = append(req.phoneNumber, id.PhoneNumber) + return nil +} + +// Used for looking up an account by federated provider. +// +// See GetUsers function. +type ProviderIdentifier struct { + ProviderID string + ProviderUID string +} + +func (id ProviderIdentifier) String() string { + return fmt.Sprintf("ProviderIdentifier{%s, %s}", id.ProviderID, id.ProviderUID) +} + +func (id ProviderIdentifier) validate() error { + return validateProvider(id.ProviderID, id.ProviderUID) +} + +func (id ProviderIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { + err := id.validate() + if err != nil { + return false, err + } + for _, userInfo := range ur.ProviderUserInfo { + if id.ProviderID == userInfo.ProviderID && id.ProviderUID == userInfo.UID { + return true, nil + } + } + return false, nil +} + +func (id ProviderIdentifier) populateRequest(req *getAccountInfoRequest) error { + err := id.validate() + if err != nil { + return err + } + req.federatedUserId = append( + req.federatedUserId, + federatedUserIdentifier{providerId: id.ProviderID, rawId: id.ProviderUID}) + return nil +} + +// Represents the result of the GetUsers() API. +type GetUsersResult struct { + // Set of UserRecords, corresponding to the set of users that were requested. + // Only users that were found are listed here. The result set is unordered. + Users []*UserRecord + + // Set of UserIdentifiers that were requested, but not found. + NotFound []UserIdentifier +} + +type federatedUserIdentifier struct { + providerId string + rawId string +} + +type getAccountInfoRequest struct { + localId []string + email []string + phoneNumber []string + federatedUserId []federatedUserIdentifier +} + +func (req *getAccountInfoRequest) build() map[string]interface{} { + var builtFederatedUserId []map[string]interface{} + for i := range req.federatedUserId { + builtFederatedUserId = append(builtFederatedUserId, map[string]interface{}{ + "providerId": req.federatedUserId[i].providerId, + "rawId": req.federatedUserId[i].rawId, + }) + } + return map[string]interface{}{ + "localId": req.localId, + "email": req.email, + "phoneNumber": req.phoneNumber, + "federatedUserId": builtFederatedUserId, + } +} + +func isUserFound(id UserIdentifier, urs [](*UserRecord)) (bool, error) { + for i := range urs { + match, err := id.matchesUserRecord(urs[i]) + if err != nil { + return false, err + } else if match { + return true, nil + } + } + return false, nil +} + +func (c *baseClient) GetUsers( + ctx context.Context, identifiers []UserIdentifier, +) (*GetUsersResult, error) { + if len(identifiers) == 0 { + return &GetUsersResult{[](*UserRecord){}, [](UserIdentifier){}}, nil + } else if len(identifiers) > maxGetAccountsBatchSize { + return nil, internal.Errorf( + maximumUserCountExceeded, + "`identifiers` parameter must have <= %d entries.", maxGetAccountsBatchSize) + } + + var request getAccountInfoRequest + for i := range identifiers { + err := identifiers[i].populateRequest(&request) + if err != nil { + return nil, err + } + } + + var parsed getAccountInfoResponse + _, err := c.post(ctx, "/accounts:lookup", request.build(), &parsed) + if err != nil { + return nil, err + } + + var userRecords [](*UserRecord) + for _, user := range parsed.Users { + userRecord, err := user.makeUserRecord() + if err != nil { + return nil, err + } + userRecords = append(userRecords, userRecord) + } + if len(identifiers) < len(userRecords) { + return nil, errors.New("GetUsers() returned more results than requested") + } + + var notFound []UserIdentifier + for i := range identifiers { + userFound, err := isUserFound(identifiers[i], userRecords) + if err != nil { + return nil, err + } + if !userFound { + notFound = append(notFound, identifiers[i]) + } + } + + return &GetUsersResult{userRecords, notFound}, nil +} + type userQueryResponse struct { UID string `json:"localId,omitempty"` DisplayName string `json:"displayName,omitempty"` diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index b2591f1e..8bc91add 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -24,6 +24,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "sort" "strconv" "strings" "testing" @@ -157,6 +158,267 @@ func TestInvalidGetUser(t *testing.T) { } } +// Checks to see if the users list contain the given uids. Order is ignored. +// +// Behaviour is undefined if there are duplicate entries in either of the +// slices. +// +// This function is identical to the one in integration/auth/user_mgt_test.go +func sameUsers(users [](*UserRecord), uids []string) bool { + if len(users) != len(uids) { + return false + } + + sort.Slice(users, func(i, j int) bool { + return users[i].UID < users[j].UID + }) + sort.Slice(uids, func(i, j int) bool { + return uids[i] < uids[j] + }) + + for i := range users { + if users[i].UID != uids[i] { + return false + } + } + + return true +} + +func TestGetUsers(t *testing.T) { + t.Run("should be rejected when given more than 100 identifiers", func(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } + + var identifiers [101]UserIdentifier + for i := 0; i < 101; i++ { + identifiers[i] = &UidIdentifier{UID: fmt.Sprintf("id%d", i)} + } + + getUsersResult, err := client.GetUsers(context.Background(), identifiers[:]) + if getUsersResult != nil || err == nil { + t.Errorf("GetUsers(too-many-identifiers) should fail") + } + if !internal.HasErrorCode(err, "maximum-user-count-exceeded") { + t.Errorf( + "GetUsers(too-many-identifiers) returned an error of '%s'; "+ + "expected 'maximum-user-count-exceeded'", + err.(*internal.FirebaseError).Code) + } + }) + + t.Run("should return no results when given no identifiers", func(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } + + getUsersResult, err := client.GetUsers(context.Background(), [](UserIdentifier){}) + if getUsersResult == nil || err != nil { + t.Errorf("GetUsers([]) failed with: %s", err.Error()) + } else { + if len(getUsersResult.Users) != 0 { + t.Errorf("len(GetUsers([]).Users) = %d; want 0", len(getUsersResult.Users)) + } + if len(getUsersResult.NotFound) != 0 { + t.Errorf("len(GetUsers([]).NotFound) = %d; want 0", len(getUsersResult.NotFound)) + } + } + }) + + t.Run("should return no users when given identifiers that do not exist", func(t *testing.T) { + resp := `{ + "kind" : "identitytoolkit#GetAccountInfoResponse", + "users" : [] + }` + s := echoServer([]byte(resp), t) + defer s.Close() + + notFoundIds := []UserIdentifier{&UidIdentifier{"id that doesnt exist"}} + getUsersResult, err := s.Client.GetUsers(context.Background(), notFoundIds) + if err != nil { + t.Errorf("GetUsers(['id that doesnt exist']) failed with %v", err) + } else { + if len(getUsersResult.Users) != 0 { + t.Errorf( + "len(GetUsers(['id that doesnt exist']).Users) = %d; want 0", + len(getUsersResult.Users)) + } + if len(getUsersResult.NotFound) != len(notFoundIds) { + t.Errorf("len(GetUsers['id that doesnt exist']).NotFound) = %d; want %d", + len(getUsersResult.NotFound), len(notFoundIds)) + } else { + for i := range notFoundIds { + if getUsersResult.NotFound[i] != notFoundIds[i] { + t.Errorf("GetUsers['id that doesnt exist']).NotFound[%d] = %v; want %v", + i, getUsersResult.NotFound[i], notFoundIds[i]) + } + } + } + } + }) + + t.Run("should be rejected when given an invalid uid", func(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } + + getUsersResult, err := client.GetUsers( + context.Background(), + []UserIdentifier{&UidIdentifier{"too long " + strings.Repeat(".", 128)}}) + if getUsersResult != nil || err == nil { + t.Errorf("GetUsers([too-long]) should fail") + } + if err.Error() != "uid string must not be longer than 128 characters" { + t.Errorf( + "GetUsers([too-long]) returned an error of '%s'; "+ + "expected 'uid string must not be longer than 128 characters'", + err.Error()) + } + }) + + t.Run("should be rejected when given an invalid email", func(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } + + getUsersResult, err := client.GetUsers( + context.Background(), + []UserIdentifier{EmailIdentifier{"invalid email addr"}}) + if getUsersResult != nil || err == nil { + t.Errorf("GetUsers([invalid email addr]) should fail") + } + if err.Error() != `malformed email string: "invalid email addr"` { + t.Errorf( + "GetUsers([invalid email addr]) returned an error of '%s'; "+ + `"expected 'malformed email string: "invalid email addr"'"`, + err.Error()) + } + }) + + t.Run("should be rejected when given an invalid phone number", func(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } + + getUsersResult, err := client.GetUsers(context.Background(), []UserIdentifier{ + PhoneIdentifier{"invalid phone number"}, + }) + if getUsersResult != nil || err == nil { + t.Errorf("GetUsers([invalid phone number]) should fail") + } + if err.Error() != "phone number must be a valid, E.164 compliant identifier" { + t.Errorf( + "GetUsers([invalid phone number]) returned an error of '%s'; "+ + "expected 'phone number must be a valid, E.164 compliant identifier'", + err.Error()) + } + }) + + t.Run("should be rejected when given an invalid provider", func(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } + + getUsersResult, err := client.GetUsers(context.Background(), []UserIdentifier{ + ProviderIdentifier{ProviderID: "", ProviderUID: ""}, + }) + if getUsersResult != nil || err == nil { + t.Errorf("GetUsers([invalid provider]) should fail") + } + if err.Error() != "providerID must be a non-empty string" { + t.Errorf( + "GetUsers([invalid provider]) returned an error of '%s'; "+ + "expected 'providerID must be a non-empty string'", + err.Error()) + } + }) + + t.Run("should be rejected when given a single bad identifier", func(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } + + identifiers := []UserIdentifier{ + UidIdentifier{"valid_id1"}, + UidIdentifier{"valid_id2"}, + UidIdentifier{"invalid id; too long. " + strings.Repeat(".", 128)}, + UidIdentifier{"valid_id3"}, + UidIdentifier{"valid_id4"}, + } + + getUsersResult, err := client.GetUsers(context.Background(), identifiers) + if getUsersResult != nil || err == nil { + t.Errorf("GetUsers([list_with_bad_identifier]) should fail") + } + if err.Error() != "uid string must not be longer than 128 characters" { + t.Errorf( + "GetUsers([too-long]) returned an error of '%s'; "+ + "expected 'uid string must not be longer than 128 characters'", + err.Error()) + } + }) + + t.Run("returns users by various identifier types in a single call", func(t *testing.T) { + mockUsers := []byte(` + { + "users": [{ + "localId": "uid1", + "email": "user1@example.com", + "phoneNumber": "+15555550001" + }, { + "localId": "uid2", + "email": "user2@example.com", + "phoneNumber": "+15555550002" + }, { + "localId": "uid3", + "email": "user3@example.com", + "phoneNumber": "+15555550003" + }, { + "localId": "uid4", + "email": "user4@example.com", + "phoneNumber": "+15555550004", + "providerUserInfo": [{ + "providerId": "google.com", + "rawId": "google_uid4" + }] + }] + }`) + s := echoServer(mockUsers, t) + defer s.Close() + + identifiers := []UserIdentifier{ + &UidIdentifier{"uid1"}, + &EmailIdentifier{"user2@example.com"}, + &PhoneIdentifier{"+15555550003"}, + &ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, + &UidIdentifier{"this-user-doesnt-exist"}, + } + + getUsersResult, err := s.Client.GetUsers(context.Background(), identifiers) + if err != nil { + t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) + } else { + if !sameUsers(getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) { + t.Errorf("GetUsers([valid identifiers]) = %v; want = (uids from) %v (in any order)", + getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) + } + if len(getUsersResult.NotFound) != 1 { + t.Errorf("GetUsers([valid identifiers with one not found]) = %d; want = 1", + len(getUsersResult.NotFound)) + } else { + if id, ok := getUsersResult.NotFound[0].(*UidIdentifier); !ok { + t.Errorf("GetUsers([...]).NotFound[0] not a UidIdentifier") + } else { + if id.UID != "this-user-doesnt-exist" { + t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want = 'this-user-doesnt-exist'", id.UID) + } + } + } + } + }) +} + func TestGetNonExistingUser(t *testing.T) { resp := `{ "kind" : "identitytoolkit#GetAccountInfoResponse", diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index 1d4ef64e..458a5403 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -23,6 +23,7 @@ import ( "math/rand" "net/url" "reflect" + "sort" "strings" "testing" "time" @@ -91,6 +92,216 @@ func TestGetNonExistingUser(t *testing.T) { } } +func TestGetUsers(t *testing.T) { + testUser1 := (&auth.UserToCreate{}). + UID("uid1"). + Email("user1@example.com"). + PhoneNumber("+15555550001") + testUser2 := (&auth.UserToCreate{}). + UID("uid2"). + Email("user2@example.com"). + PhoneNumber("+15555550002") + testUser3 := (&auth.UserToCreate{}). + UID("uid3"). + Email("user3@example.com"). + PhoneNumber("+15555550003") + + importUser1 := (&auth.UserToImport{}). + UID("uid4"). + Email("user4@example.com"). + PhoneNumber("+15555550004"). + ProviderData([](*auth.UserProvider){ + &auth.UserProvider{ + ProviderID: "google.com", + UID: "google_uid4", + }, + }) + + // Creates/imports all test users. If a failure occurs, the remaining test + // users will not be created/imported. + createTestUsers := func() error { + var err error + + // Helper to create a user and return its UserRecord. Upon error, sets the + // err variable. + createUser := func(userToCreate *auth.UserToCreate) *auth.UserRecord { + if err != nil { + return nil + } + var userRecord *auth.UserRecord + userRecord, err = client.CreateUser(context.Background(), userToCreate) + return userRecord + } + + // Helper to import a user and return its UserRecord. Upon error, sets the + // err variable. `uid` must match the UID set on the `userToImport` + // parameter. + importUser := func(uid string, userToImport *auth.UserToImport) *auth.UserRecord { + if err != nil { + return nil + } + var userImportResult *auth.UserImportResult + userImportResult, err = client.ImportUsers( + context.Background(), [](*auth.UserToImport){userToImport}) + if err != nil { + return nil + } + + if userImportResult.FailureCount > 0 { + err = fmt.Errorf(userImportResult.Errors[0].Reason) + return nil + } + if userImportResult.SuccessCount != 1 { + err = fmt.Errorf("Import didn't fail, but it didn't succeed either?") + return nil + } + + var userRecord *auth.UserRecord + userRecord, err = client.GetUser(context.Background(), uid) + if err != nil { + return nil + } + return userRecord + } + + createUser(testUser1) + createUser(testUser2) + createUser(testUser3) + + importUser("uid4", importUser1) + + return err + } + + // Delete all test users. This function will attempt to delete all test users + // even if a failure occurs. + deleteTestUsers := func() { + createdUsersUids := []string{"uid1", "uid2", "uid3", "uid4"} + for i := range createdUsersUids { + client.DeleteUser(context.Background(), createdUsersUids[i]) + } + } + + // Checks to see if the users list contain the given uids. Order is ignored. + // + // Behaviour is undefined if there are duplicate entries in either of the + // slices. + // + // This function is identical to the one in integration/auth/user_mgt_test.go + sameUsers := func(users [](*auth.UserRecord), uids []string) bool { + if len(users) != len(uids) { + return false + } + + sort.Slice(users, func(i, j int) bool { + return users[i].UID < users[j].UID + }) + sort.Slice(uids, func(i, j int) bool { + return uids[i] < uids[j] + }) + + for i := range users { + if users[i].UID != uids[i] { + return false + } + } + + return true + } + + // Delete all the users that we're about to create (in case they were left + // over from a prior run). + deleteTestUsers() + + if err := createTestUsers(); err != nil { + t.Fatalf("Unable to create the test users: %v", err) + } + defer deleteTestUsers() + + t.Run("returns users by various identifier types in a single call", func(t *testing.T) { + getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ + auth.UidIdentifier{"uid1"}, + auth.EmailIdentifier{"user2@example.com"}, + auth.PhoneIdentifier{"+15555550003"}, + auth.ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, + }) + if err != nil { + t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) + } else { + if !sameUsers(getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) { + t.Errorf("GetUsers([valid identifiers]) = %v; want = %v (in any order)", + getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) + } + } + }) + + t.Run("returns found users and ignores non-existing users", func(t *testing.T) { + getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ + auth.UidIdentifier{"uid1"}, + auth.UidIdentifier{"uid_that_doesnt_exist"}, + auth.UidIdentifier{"uid3"}, + }) + if err != nil { + t.Errorf("GetUsers([...]) returned an error: %v", err) + } else { + if !sameUsers(getUsersResult.Users, []string{"uid1", "uid3"}) { + t.Errorf("GetUsers([valid identifiers]) = %v; want = %v (in any order)", + getUsersResult.Users, []string{"uid1", "uid3"}) + } + if len(getUsersResult.NotFound) != 1 { + t.Errorf("len(GetUsers([...]).NotFound) = %d; want 1", len(getUsersResult.NotFound)) + } else { + if getUsersResult.NotFound[0].(auth.UidIdentifier).UID != "uid_that_doesnt_exist" { + t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want 'uid_that_doesnt_exist'", + getUsersResult.NotFound[0].(auth.UidIdentifier).UID) + } + } + } + }) + + t.Run("returns nothing when queried for only non-existing users", func(t *testing.T) { + getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ + auth.UidIdentifier{"non-existing user"}, + }) + if err != nil { + t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) + } else { + if len(getUsersResult.Users) != 0 { + t.Errorf("len(GetUsers([...]).Users) = %d; want = 0", len(getUsersResult.Users)) + } + if len(getUsersResult.NotFound) != 1 { + t.Errorf("len(GetUsers([...]).NotFound) = %d; want = 1", len(getUsersResult.NotFound)) + } else { + if getUsersResult.NotFound[0].(auth.UidIdentifier).UID != "non-existing user" { + t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want 'non-existing user'", + getUsersResult.NotFound[0].(auth.UidIdentifier).UID) + } + } + } + }) + + t.Run("de-dups duplicate users", func(t *testing.T) { + getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ + auth.UidIdentifier{"uid1"}, + auth.UidIdentifier{"uid1"}, + }) + if err != nil { + t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) + } else { + if len(getUsersResult.Users) != 1 { + t.Errorf("len(GetUsers([...]).Users) = %d; want = 1", len(getUsersResult.Users)) + } else { + if getUsersResult.Users[0].UID != "uid1" { + t.Errorf("GetUsers([...]).Users[0].UID = %s; want = 'uid1'", getUsersResult.Users[0].UID) + } + } + if len(getUsersResult.NotFound) != 0 { + t.Errorf("len(GetUsers([...]).NotFound) = %d; want = 0", len(getUsersResult.NotFound)) + } + } + }) +} + func TestUpdateNonExistingUser(t *testing.T) { update := (&auth.UserToUpdate{}).Email("test@example.com") user, err := client.UpdateUser(context.Background(), "non.existing", update) From 7f11d8028866615ff1391c9c2a8a32825a89ea40 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Mon, 6 Jan 2020 16:36:53 -0500 Subject: [PATCH 02/20] Add LastRefreshTimestamp field to users returned via GetAccountInfo rpc --- auth/user_mgt.go | 18 ++++++++++-- integration/auth/auth_test.go | 5 ++-- integration/auth/user_mgt_test.go | 46 +++++++++++++++++++++++++++++-- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index 69011417..3591264c 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -60,6 +60,9 @@ type UserInfo struct { type UserMetadata struct { CreationTimestamp int64 LastLogInTimestamp int64 + // The time at which the user was last active (ID token refreshed), or 0 if + // the user was never active. + LastRefreshTimestamp int64 } // UserRecord contains metadata associated with a Firebase user account. @@ -839,6 +842,7 @@ type userQueryResponse struct { PhotoURL string `json:"photoUrl,omitempty"` CreationTimestamp int64 `json:"createdAt,string,omitempty"` LastLogInTimestamp int64 `json:"lastLoginAt,string,omitempty"` + LastRefreshAt string `json:"lastRefreshAt,omitempty"` ProviderID string `json:"providerId,omitempty"` CustomAttributes string `json:"customAttributes,omitempty"` Disabled bool `json:"disabled,omitempty"` @@ -879,6 +883,15 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error hash = "" } + var lastRefreshTimestamp int64 = 0 + if r.LastRefreshAt != "" { + t, err := time.Parse(time.RFC3339, r.LastRefreshAt) + if err != nil { + return nil, err + } + lastRefreshTimestamp = t.Unix() + } + return &ExportedUserRecord{ UserRecord: &UserRecord{ UserInfo: &UserInfo{ @@ -896,8 +909,9 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error TenantID: r.TenantID, TokensValidAfterMillis: r.ValidSinceSeconds * 1000, UserMetadata: &UserMetadata{ - LastLogInTimestamp: r.LastLogInTimestamp, - CreationTimestamp: r.CreationTimestamp, + LastLogInTimestamp: r.LastLogInTimestamp, + CreationTimestamp: r.CreationTimestamp, + LastRefreshTimestamp: lastRefreshTimestamp, }, }, PasswordHash: hash, diff --git a/integration/auth/auth_test.go b/integration/auth/auth_test.go index b5638f6a..15cf8d60 100644 --- a/integration/auth/auth_test.go +++ b/integration/auth/auth_test.go @@ -230,8 +230,9 @@ func signInWithCustomToken(token string) (string, error) { func signInWithPassword(email, password string) (string, error) { req, err := json.Marshal(map[string]interface{}{ - "email": email, - "password": password, + "email": email, + "password": password, + "returnSecureToken": true, }) if err != nil { return "", err diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index 458a5403..4283e612 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -130,6 +130,10 @@ func TestGetUsers(t *testing.T) { } var userRecord *auth.UserRecord userRecord, err = client.CreateUser(context.Background(), userToCreate) + if err != nil { + err = fmt.Errorf("Unable to create user %v: %w", *userToCreate, err) + return nil + } return userRecord } @@ -144,11 +148,12 @@ func TestGetUsers(t *testing.T) { userImportResult, err = client.ImportUsers( context.Background(), [](*auth.UserToImport){userToImport}) if err != nil { + err = fmt.Errorf("Unable to import user %v (uid %v): %w", *userToImport, uid, err) return nil } if userImportResult.FailureCount > 0 { - err = fmt.Errorf(userImportResult.Errors[0].Reason) + err = fmt.Errorf("Unable to import user %v (uid %v): %v", *userToImport, uid, userImportResult.Errors[0].Reason) return nil } if userImportResult.SuccessCount != 1 { @@ -213,10 +218,10 @@ func TestGetUsers(t *testing.T) { // over from a prior run). deleteTestUsers() + defer deleteTestUsers() if err := createTestUsers(); err != nil { t.Fatalf("Unable to create the test users: %v", err) } - defer deleteTestUsers() t.Run("returns users by various identifier types in a single call", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ @@ -300,6 +305,43 @@ func TestGetUsers(t *testing.T) { } } }) + + t.Run("returns users with a LastRefreshTimestamp", func(t *testing.T) { + // Delete user that we're about to create (in case it was left over from a + // prior run). + client.DeleteUser(context.Background(), "lastRefreshTimeUser") + defer client.DeleteUser(context.Background(), "lastRefreshTimeUser") + newUserRecord, err := client.CreateUser(context.Background(), (&auth.UserToCreate{}). + UID("lastRefreshTimeUser"). + Email("lastRefreshTimeUser@example.com"). + Password("p4ssword")) + if err != nil { + t.Fatalf("Unable to create lastRefreshTimeUser: %v", err) + } + + // New users should not have a LastRefreshTimestamp set. + if newUserRecord.UserMetadata.LastRefreshTimestamp != 0 { + t.Errorf( + "CreateUser(...).UserMetadata.LastRefreshTimestamp = %d; want = 0", + newUserRecord.UserMetadata.LastRefreshTimestamp) + } + + // Login to cause the LastRefreshTimestamp to be set + _, err = signInWithPassword("lastRefreshTimeUser@example.com", "p4ssword") + if err != nil { + t.Errorf("signInWithPassword failed: %v", err) + } + + getUsersResult, err := client.GetUser(context.Background(), "lastRefreshTimeUser") + if err != nil { + t.Errorf("GetUser(...) failed with error: %v", err) + } + if getUsersResult.UserMetadata.LastRefreshTimestamp <= 0 { + t.Errorf( + "GetUser(...).UserMetadata.LastRefreshTimestamp = %d; want > 0", + getUsersResult.UserMetadata.LastRefreshTimestamp) + } + }) } func TestUpdateNonExistingUser(t *testing.T) { From 2a212b37710bc030cf279745a186664545949332 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Wed, 15 Jan 2020 11:53:59 -0500 Subject: [PATCH 03/20] Add DeleteUsers() bulk deletion method --- auth/user_mgt.go | 97 +++++++++++++++++++++++ auth/user_mgt_test.go | 100 ++++++++++++++++++++++++ integration/auth/user_mgt_test.go | 125 ++++++++++++++++++++++++++++++ 3 files changed, 322 insertions(+) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index 3591264c..60cfc8e7 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -37,6 +37,9 @@ const ( // Maximum allowed number of users to batch get at one time. maxGetAccountsBatchSize = 100 + + // Maximum allowed numberof users to batch delete at one time. + maxDeleteAccountsBatchSize = 1000 ) // 'REDACTED', encoded as a base64 string. @@ -1012,6 +1015,100 @@ func (c *baseClient) DeleteUser(ctx context.Context, uid string) error { return err } +// Represents the result of the DeleteUsers() call. +type DeleteUsersResult struct { + // The number of users that were deleted successfully (possibly zero). Users + // that did not exist prior to calling DeleteUsers() will be considered to be + // successfully deleted. + SuccessCount int + + // The number of users that failed to be deleted (possibly zero). + FailureCount int + + // A list of describing the errors that were encountered + // during the deletion. Length of this list is equal to the value of + // FailureCount. + Errors []*DeleteUsersErrorInfo +} + +// ErrorInfo represents an error encountered while deleting a user account. +// +// The Index field corresponds to the index of the failed user in the uids +// array that was passed to DeleteUsers(). +type DeleteUsersErrorInfo struct { + Index int + Reason string +} + +// Deletes the users specified by the given identifiers. +// +// Deleting a non-existing user won't generate an error. (i.e. this method is idempotent.) +// Non-existing users will be considered to be successfully deleted, and will therefore be +// counted in the DeleteUsersResult.SuccessCount value. +// +// Only a maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are +// supplied, this method will immediately return an error. +// +// This API is currently rate limited at the server to 1 QPS. If you exceed this, you may get +// a quota exceeded error. Therefore, if you want to delete more than 1000 users, you may +// need to add a delay to ensure you don't go over this limit. +// +// Parameters: +// uids: The uids of the users to be deleted. Must have <= 1000 entries. +// +// Returns: The total number of successful/failed deletions, as well as the array of errors +// that correspond to the failed deletions. An error is returned if any of the identifiers +// are invalid or if more than 1000 identifiers are specified. +func (c *baseClient) DeleteUsers(ctx context.Context, uids []string) (*DeleteUsersResult, error) { + if len(uids) == 0 { + return &DeleteUsersResult{}, nil + } else if len(uids) > maxDeleteAccountsBatchSize { + return nil, internal.Errorf( + maximumUserCountExceeded, + "`uids` parameter must have <= %d entries.", maxDeleteAccountsBatchSize) + } + + var payload struct { + LocalIds []string `json:"localIds"` + Force bool `json:"force"` + } + payload.Force = true + + for i := range uids { + if err := validateUID(uids[i]); err != nil { + return nil, err + } + + payload.LocalIds = append(payload.LocalIds, uids[i]) + } + + type batchDeleteErrorInfo struct { + Index int `json:"index"` + LocalId string `json:"localId"` + Message string `json:"message"` + } + type batchDeleteAccountsResponse struct { + Errors []batchDeleteErrorInfo `json:"errors"` + } + + resp := batchDeleteAccountsResponse{} + _, err := c.post(ctx, "/accounts:batchDelete", payload, &resp) + + result := DeleteUsersResult{ + FailureCount: len(resp.Errors), + SuccessCount: len(uids) - len(resp.Errors), + } + + for i := range resp.Errors { + result.Errors = append(result.Errors, &DeleteUsersErrorInfo{ + Index: resp.Errors[i].Index, + Reason: resp.Errors[i].Message, + }) + } + + return &result, err +} + // SessionCookie creates a new Firebase session cookie from the given ID token and expiry // duration. The returned JWT can be set as a server-side session cookie with a custom cookie // policy. Expiry duration must be at least 5 minutes but may not exceed 14 days. diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index 8bc91add..d887251f 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -1341,6 +1341,106 @@ func TestInvalidDeleteUser(t *testing.T) { } } +func TestDeleteUsers(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } + + t.Run("should succeed given an empty list", func(t *testing.T) { + result, err := client.DeleteUsers(context.Background(), []string{}) + + if err != nil { + t.Errorf("DeleteUsers([]) error %v; want = nil", err) + } else { + if result.SuccessCount != 0 { + t.Errorf("DeleteUsers([]).SuccessCount = %d; want = 0", result.SuccessCount) + } + if result.FailureCount != 0 { + t.Errorf("DeleteUsers([]).FailureCount = %d; want = 0", result.FailureCount) + } + if len(result.Errors) != 0 { + t.Errorf("len(DeleteUsers([]).Errors) = %d; want = 0", len(result.Errors)) + } + } + }) + + t.Run("should be rejected when given more than 1000 identifiers", func(t *testing.T) { + uids := []string{} + for i := 0; i < 1001; i++ { + uids = append(uids, fmt.Sprintf("id%d", i)) + } + + _, err := client.DeleteUsers(context.Background(), uids) + if err == nil { + t.Errorf("DeleteUsers([too_many_uids]) error nil; want not nil") + } else if !internal.HasErrorCode(err, "maximum-user-count-exceeded") { + t.Errorf( + "DeleteUsers([too_many_uids]) returned an error of '%s'; "+ + "expected 'maximum-user-count-exceeded'", + err.(*internal.FirebaseError).Code) + } + }) + + t.Run("should immediately fail given an invalid id", func(t *testing.T) { + tooLongUid := "too long " + strings.Repeat(".", 128) + _, err := client.DeleteUsers(context.Background(), []string{tooLongUid}) + + if err == nil { + t.Errorf("DeleteUsers([too_long_uid]) error nil; want not nil") + } else if err.Error() != "uid string must not be longer than 128 characters" { + t.Errorf( + "DeleteUsers([too_long_uid]) returned an error of '%s'; "+ + "expected 'uid string must not be longer than 128 characters'", + err.Error()) + } + }) + + t.Run("should index errors correctly in result", func(t *testing.T) { + resp := `{ + "errors": [{ + "index": 0, + "localId": "uid1", + "message": "Error Message 1" + }, { + "index": 2, + "localId": "uid3", + "message": "Error Message 2" + }] + }` + s := echoServer([]byte(resp), t) + defer s.Close() + + result, err := s.Client.DeleteUsers(context.Background(), []string{"uid1", "uid2", "uid3", "uid4"}) + + if err != nil { + t.Errorf("DeleteUsers([...]) error %v; want = nil", err) + } else { + if result.SuccessCount != 2 { + t.Errorf("DeleteUsers([...]).SuccessCount = %d; want 2", result.SuccessCount) + } + if result.FailureCount != 2 { + t.Errorf("DeleteUsers([...]).FailureCount = %d; want 2", result.FailureCount) + } + if len(result.Errors) != 2 { + t.Errorf("len(DeleteUsers([...]).Errors) = %d; want 2", len(result.Errors)) + } else { + if result.Errors[0].Index != 0 { + t.Errorf("DeleteUsers([...]).Errors[0].Index = %d; want 0", result.Errors[0].Index) + } + if result.Errors[0].Reason != "Error Message 1" { + t.Errorf("DeleteUsers([...]).Errors[0].Reason = %s; want Error Message 1", result.Errors[0].Reason) + } + if result.Errors[1].Index != 2 { + t.Errorf("DeleteUsers([...]).Errors[1].Index = %d; want 2", result.Errors[1].Index) + } + if result.Errors[1].Reason != "Error Message 2" { + t.Errorf("DeleteUsers([...]).Errors[1].Reason = %s; want Error Message 2", result.Errors[1].Reason) + } + } + } + }) +} + func TestMakeExportedUser(t *testing.T) { queryResponse := &userQueryResponse{ UID: "testuser", diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index 4283e612..672fd220 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -587,6 +587,131 @@ func TestDeleteUser(t *testing.T) { } } +func TestDeleteUsers(t *testing.T) { + // Creates a user and returns its uid. Upon failure, triggers t.Fatalf(). + createTestUserOrDie := func(t *testing.T) string { + userRecord, err := client.CreateUser(context.Background(), &auth.UserToCreate{}) + if err != nil { + t.Fatalf("CreateUser({}) error %v; want nil", err) + } + return userRecord.UID + } + + // Ensures the specified users don't exist. Expected to be called after + // deleting the users to ensure the delete method worked. + ensureUsersNotFound := func(t *testing.T, uids []string) { + identifiers := []auth.UserIdentifier{} + for i := range uids { + identifiers = append(identifiers, auth.UidIdentifier{uids[i]}) + } + + getUsersResult, err := client.GetUsers(context.Background(), identifiers) + if err != nil { + t.Errorf("GetUsers(notfound_ids) error %v; want nil", err) + } else { + ok := true + if len(getUsersResult.Users) != 0 { + t.Errorf("len(GetUsers(notfound_ids).Users) = %d; want 0", len(getUsersResult.Users)) + ok = false + } + if len(getUsersResult.NotFound) != len(uids) { + t.Errorf("len(GetUsers(notfound_ids).NotFound) = %d; want %d", len(getUsersResult.NotFound), len(uids)) + ok = false + } + if !ok { + t.FailNow() + } + + sort.Strings(uids) + notFoundUids := []string{} + for i := range getUsersResult.NotFound { + notFoundUids = append(notFoundUids, getUsersResult.NotFound[i].(auth.UidIdentifier).UID) + } + sort.Strings(notFoundUids) + for i := range uids { + if notFoundUids[i] != uids[i] { + t.Errorf("GetUsers(deleted_ids).NotFound[%d] = %s; want %s", i, notFoundUids[i], uids[i]) + } + } + } + } + + t.Run("deletes users", func(t *testing.T) { + uids := []string{ + createTestUserOrDie(t), createTestUserOrDie(t), createTestUserOrDie(t), + } + + result, err := client.DeleteUsers(context.Background(), uids) + if err != nil { + t.Fatalf("DeleteUsers([valid_ids]) error %v; want nil", err) + } else { + ok := true + if result.SuccessCount != 3 { + t.Errorf("DeleteUsers([valid_ids]).SuccessCount = %d; want 3", result.SuccessCount) + ok = false + } + if result.FailureCount != 0 { + t.Errorf("DeleteUsers([valid_ids]).FailureCount = %d; want 0", result.FailureCount) + ok = false + } + if len(result.Errors) != 0 { + t.Errorf("len(DeleteUsers([valid_ids]).Errors) = %d; want 0", len(result.Errors)) + ok = false + } + if !ok { + t.FailNow() + } + } + + ensureUsersNotFound(t, uids) + }) + + t.Run("deletes users that exist even when non-existing users also specified", func(t *testing.T) { + uids := []string{createTestUserOrDie(t), "uid-that-doesnt-exist"} + result, err := client.DeleteUsers(context.Background(), uids) + if err != nil { + t.Errorf("DeleteUsers(uids) error %v; want nil", err) + } else { + if result.SuccessCount != 2 { + t.Errorf("DeleteUsers(uids).SuccessCount = %d; want 2", result.SuccessCount) + } + if result.FailureCount != 0 { + t.Errorf("DeleteUsers(uids).FailureCount = %d; want 0", result.FailureCount) + } + if len(result.Errors) != 0 { + t.Errorf("len(DeleteUsers(uids).Errors) = %d; want 0", len(result.Errors)) + } + + ensureUsersNotFound(t, uids) + } + }) + + t.Run("is idempotent", func(t *testing.T) { + deleteUserAndEnsureSuccess := func(t *testing.T, uids []string) { + result, err := client.DeleteUsers(context.Background(), uids) + if err != nil { + t.Errorf("DeleteUsers(uids) error %v; want nil", err) + } else { + if result.SuccessCount != 1 { + t.Errorf("DeleteUsers(uids).SuccessCount = %d; want 1", result.SuccessCount) + } + if result.FailureCount != 0 { + t.Errorf("DeleteUsers(uids).FailureCount = %d; want 0", result.FailureCount) + } + if len(result.Errors) != 0 { + t.Errorf("len(DeleteUsers(uids).Errors) = %d; want 0", len(result.Errors)) + } + } + } + + uids := []string{createTestUserOrDie(t)} + deleteUserAndEnsureSuccess(t, uids) + + // Delete the user again, ensuring that everything still counts as a success. + deleteUserAndEnsureSuccess(t, uids) + }) +} + func TestImportUsers(t *testing.T) { uid := randomUID() email := randomEmail(uid) From d82578b030f001d0f4bf5d91e08033eee6b12b00 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Wed, 15 Jan 2020 14:17:11 -0500 Subject: [PATCH 04/20] golint --- auth/user_mgt.go | 63 ++++++++++++++++--------------- auth/user_mgt_test.go | 28 +++++++------- integration/auth/user_mgt_test.go | 26 ++++++------- 3 files changed, 59 insertions(+), 58 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index 60cfc8e7..056f11db 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -582,7 +582,7 @@ func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord return parsed.Users[0].makeUserRecord() } -// Identifies a user to be looked up. +// A UserIdentifier identifies a user to be looked up. type UserIdentifier interface { String() string @@ -591,22 +591,22 @@ type UserIdentifier interface { populateRequest(req *getAccountInfoRequest) error } -// Used for looking up an account by uid. +// A UIDIdentifier is used for looking up an account by uid. // // See GetUsers function. -type UidIdentifier struct { +type UIDIdentifier struct { UID string } -func (id UidIdentifier) String() string { - return fmt.Sprintf("UidIdentifier{%s}", id.UID) +func (id UIDIdentifier) String() string { + return fmt.Sprintf("UIDIdentifier{%s}", id.UID) } -func (id UidIdentifier) validate() error { +func (id UIDIdentifier) validate() error { return validateUID(id.UID) } -func (id UidIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { +func (id UIDIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { err := id.validate() if err != nil { return false, err @@ -617,16 +617,16 @@ func (id UidIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { return false, nil } -func (id UidIdentifier) populateRequest(req *getAccountInfoRequest) error { +func (id UIDIdentifier) populateRequest(req *getAccountInfoRequest) error { err := id.validate() if err != nil { return err } - req.localId = append(req.localId, id.UID) + req.localID = append(req.localID, id.UID) return nil } -// Used for looking up an account by email. +// An EmailIdentifier is used for looking up an account by email. // // See GetUsers function. type EmailIdentifier struct { @@ -661,7 +661,7 @@ func (id EmailIdentifier) populateRequest(req *getAccountInfoRequest) error { return nil } -// Used for looking up an account by phone number. +// A PhoneIdentifier is used for looking up an account by phone number. // // See GetUsers function. type PhoneIdentifier struct { @@ -696,7 +696,7 @@ func (id PhoneIdentifier) populateRequest(req *getAccountInfoRequest) error { return nil } -// Used for looking up an account by federated provider. +// A ProviderIdentifier is used for looking up an account by federated provider. // // See GetUsers function. type ProviderIdentifier struct { @@ -730,13 +730,13 @@ func (id ProviderIdentifier) populateRequest(req *getAccountInfoRequest) error { if err != nil { return err } - req.federatedUserId = append( - req.federatedUserId, - federatedUserIdentifier{providerId: id.ProviderID, rawId: id.ProviderUID}) + req.federatedUserID = append( + req.federatedUserID, + federatedUserIdentifier{providerID: id.ProviderID, rawID: id.ProviderUID}) return nil } -// Represents the result of the GetUsers() API. +// A GetUsersResult represents the result of the GetUsers() API. type GetUsersResult struct { // Set of UserRecords, corresponding to the set of users that were requested. // Only users that were found are listed here. The result set is unordered. @@ -747,30 +747,30 @@ type GetUsersResult struct { } type federatedUserIdentifier struct { - providerId string - rawId string + providerID string + rawID string } type getAccountInfoRequest struct { - localId []string + localID []string email []string phoneNumber []string - federatedUserId []federatedUserIdentifier + federatedUserID []federatedUserIdentifier } func (req *getAccountInfoRequest) build() map[string]interface{} { - var builtFederatedUserId []map[string]interface{} - for i := range req.federatedUserId { - builtFederatedUserId = append(builtFederatedUserId, map[string]interface{}{ - "providerId": req.federatedUserId[i].providerId, - "rawId": req.federatedUserId[i].rawId, + var builtFederatedUserID []map[string]interface{} + for i := range req.federatedUserID { + builtFederatedUserID = append(builtFederatedUserID, map[string]interface{}{ + "providerId": req.federatedUserID[i].providerID, + "rawId": req.federatedUserID[i].rawID, }) } return map[string]interface{}{ - "localId": req.localId, + "localId": req.localID, "email": req.email, "phoneNumber": req.phoneNumber, - "federatedUserId": builtFederatedUserId, + "federatedUserId": builtFederatedUserID, } } @@ -886,7 +886,7 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error hash = "" } - var lastRefreshTimestamp int64 = 0 + var lastRefreshTimestamp int64 if r.LastRefreshAt != "" { t, err := time.Parse(time.RFC3339, r.LastRefreshAt) if err != nil { @@ -1015,7 +1015,7 @@ func (c *baseClient) DeleteUser(ctx context.Context, uid string) error { return err } -// Represents the result of the DeleteUsers() call. +// A DeleteUsersResult represents the result of the DeleteUsers() call. type DeleteUsersResult struct { // The number of users that were deleted successfully (possibly zero). Users // that did not exist prior to calling DeleteUsers() will be considered to be @@ -1031,7 +1031,8 @@ type DeleteUsersResult struct { Errors []*DeleteUsersErrorInfo } -// ErrorInfo represents an error encountered while deleting a user account. +// DeleteUsersErrorInfo represents an error encountered while deleting a user +// account. // // The Index field corresponds to the index of the failed user in the uids // array that was passed to DeleteUsers(). @@ -1084,7 +1085,7 @@ func (c *baseClient) DeleteUsers(ctx context.Context, uids []string) (*DeleteUse type batchDeleteErrorInfo struct { Index int `json:"index"` - LocalId string `json:"localId"` + LocalID string `json:"localId"` Message string `json:"message"` } type batchDeleteAccountsResponse struct { diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index d887251f..7d98b6e3 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -193,7 +193,7 @@ func TestGetUsers(t *testing.T) { var identifiers [101]UserIdentifier for i := 0; i < 101; i++ { - identifiers[i] = &UidIdentifier{UID: fmt.Sprintf("id%d", i)} + identifiers[i] = &UIDIdentifier{UID: fmt.Sprintf("id%d", i)} } getUsersResult, err := client.GetUsers(context.Background(), identifiers[:]) @@ -234,7 +234,7 @@ func TestGetUsers(t *testing.T) { s := echoServer([]byte(resp), t) defer s.Close() - notFoundIds := []UserIdentifier{&UidIdentifier{"id that doesnt exist"}} + notFoundIds := []UserIdentifier{&UIDIdentifier{"id that doesnt exist"}} getUsersResult, err := s.Client.GetUsers(context.Background(), notFoundIds) if err != nil { t.Errorf("GetUsers(['id that doesnt exist']) failed with %v", err) @@ -265,7 +265,7 @@ func TestGetUsers(t *testing.T) { getUsersResult, err := client.GetUsers( context.Background(), - []UserIdentifier{&UidIdentifier{"too long " + strings.Repeat(".", 128)}}) + []UserIdentifier{&UIDIdentifier{"too long " + strings.Repeat(".", 128)}}) if getUsersResult != nil || err == nil { t.Errorf("GetUsers([too-long]) should fail") } @@ -340,11 +340,11 @@ func TestGetUsers(t *testing.T) { } identifiers := []UserIdentifier{ - UidIdentifier{"valid_id1"}, - UidIdentifier{"valid_id2"}, - UidIdentifier{"invalid id; too long. " + strings.Repeat(".", 128)}, - UidIdentifier{"valid_id3"}, - UidIdentifier{"valid_id4"}, + UIDIdentifier{"valid_id1"}, + UIDIdentifier{"valid_id2"}, + UIDIdentifier{"invalid id; too long. " + strings.Repeat(".", 128)}, + UIDIdentifier{"valid_id3"}, + UIDIdentifier{"valid_id4"}, } getUsersResult, err := client.GetUsers(context.Background(), identifiers) @@ -388,11 +388,11 @@ func TestGetUsers(t *testing.T) { defer s.Close() identifiers := []UserIdentifier{ - &UidIdentifier{"uid1"}, + &UIDIdentifier{"uid1"}, &EmailIdentifier{"user2@example.com"}, &PhoneIdentifier{"+15555550003"}, &ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, - &UidIdentifier{"this-user-doesnt-exist"}, + &UIDIdentifier{"this-user-doesnt-exist"}, } getUsersResult, err := s.Client.GetUsers(context.Background(), identifiers) @@ -407,8 +407,8 @@ func TestGetUsers(t *testing.T) { t.Errorf("GetUsers([valid identifiers with one not found]) = %d; want = 1", len(getUsersResult.NotFound)) } else { - if id, ok := getUsersResult.NotFound[0].(*UidIdentifier); !ok { - t.Errorf("GetUsers([...]).NotFound[0] not a UidIdentifier") + if id, ok := getUsersResult.NotFound[0].(*UIDIdentifier); !ok { + t.Errorf("GetUsers([...]).NotFound[0] not a UIDIdentifier") } else { if id.UID != "this-user-doesnt-exist" { t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want = 'this-user-doesnt-exist'", id.UID) @@ -1382,8 +1382,8 @@ func TestDeleteUsers(t *testing.T) { }) t.Run("should immediately fail given an invalid id", func(t *testing.T) { - tooLongUid := "too long " + strings.Repeat(".", 128) - _, err := client.DeleteUsers(context.Background(), []string{tooLongUid}) + tooLongUID := "too long " + strings.Repeat(".", 128) + _, err := client.DeleteUsers(context.Background(), []string{tooLongUID}) if err == nil { t.Errorf("DeleteUsers([too_long_uid]) error nil; want not nil") diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index 672fd220..b1449917 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -225,7 +225,7 @@ func TestGetUsers(t *testing.T) { t.Run("returns users by various identifier types in a single call", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UidIdentifier{"uid1"}, + auth.UIDIdentifier{"uid1"}, auth.EmailIdentifier{"user2@example.com"}, auth.PhoneIdentifier{"+15555550003"}, auth.ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, @@ -242,9 +242,9 @@ func TestGetUsers(t *testing.T) { t.Run("returns found users and ignores non-existing users", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UidIdentifier{"uid1"}, - auth.UidIdentifier{"uid_that_doesnt_exist"}, - auth.UidIdentifier{"uid3"}, + auth.UIDIdentifier{"uid1"}, + auth.UIDIdentifier{"uid_that_doesnt_exist"}, + auth.UIDIdentifier{"uid3"}, }) if err != nil { t.Errorf("GetUsers([...]) returned an error: %v", err) @@ -256,9 +256,9 @@ func TestGetUsers(t *testing.T) { if len(getUsersResult.NotFound) != 1 { t.Errorf("len(GetUsers([...]).NotFound) = %d; want 1", len(getUsersResult.NotFound)) } else { - if getUsersResult.NotFound[0].(auth.UidIdentifier).UID != "uid_that_doesnt_exist" { + if getUsersResult.NotFound[0].(auth.UIDIdentifier).UID != "uid_that_doesnt_exist" { t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want 'uid_that_doesnt_exist'", - getUsersResult.NotFound[0].(auth.UidIdentifier).UID) + getUsersResult.NotFound[0].(auth.UIDIdentifier).UID) } } } @@ -266,7 +266,7 @@ func TestGetUsers(t *testing.T) { t.Run("returns nothing when queried for only non-existing users", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UidIdentifier{"non-existing user"}, + auth.UIDIdentifier{"non-existing user"}, }) if err != nil { t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) @@ -277,9 +277,9 @@ func TestGetUsers(t *testing.T) { if len(getUsersResult.NotFound) != 1 { t.Errorf("len(GetUsers([...]).NotFound) = %d; want = 1", len(getUsersResult.NotFound)) } else { - if getUsersResult.NotFound[0].(auth.UidIdentifier).UID != "non-existing user" { + if getUsersResult.NotFound[0].(auth.UIDIdentifier).UID != "non-existing user" { t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want 'non-existing user'", - getUsersResult.NotFound[0].(auth.UidIdentifier).UID) + getUsersResult.NotFound[0].(auth.UIDIdentifier).UID) } } } @@ -287,8 +287,8 @@ func TestGetUsers(t *testing.T) { t.Run("de-dups duplicate users", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UidIdentifier{"uid1"}, - auth.UidIdentifier{"uid1"}, + auth.UIDIdentifier{"uid1"}, + auth.UIDIdentifier{"uid1"}, }) if err != nil { t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) @@ -602,7 +602,7 @@ func TestDeleteUsers(t *testing.T) { ensureUsersNotFound := func(t *testing.T, uids []string) { identifiers := []auth.UserIdentifier{} for i := range uids { - identifiers = append(identifiers, auth.UidIdentifier{uids[i]}) + identifiers = append(identifiers, auth.UIDIdentifier{uids[i]}) } getUsersResult, err := client.GetUsers(context.Background(), identifiers) @@ -625,7 +625,7 @@ func TestDeleteUsers(t *testing.T) { sort.Strings(uids) notFoundUids := []string{} for i := range getUsersResult.NotFound { - notFoundUids = append(notFoundUids, getUsersResult.NotFound[i].(auth.UidIdentifier).UID) + notFoundUids = append(notFoundUids, getUsersResult.NotFound[i].(auth.UIDIdentifier).UID) } sort.Strings(notFoundUids) for i := range uids { From 44dae7a48ec977023d20fbd74706dd02d6ded943 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Wed, 15 Jan 2020 14:31:43 -0500 Subject: [PATCH 05/20] %w -> %v. (Old versions don't support it?) --- integration/auth/user_mgt_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index b1449917..bf2a1897 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -131,7 +131,7 @@ func TestGetUsers(t *testing.T) { var userRecord *auth.UserRecord userRecord, err = client.CreateUser(context.Background(), userToCreate) if err != nil { - err = fmt.Errorf("Unable to create user %v: %w", *userToCreate, err) + err = fmt.Errorf("Unable to create user %v: %v", *userToCreate, err) return nil } return userRecord @@ -148,7 +148,7 @@ func TestGetUsers(t *testing.T) { userImportResult, err = client.ImportUsers( context.Background(), [](*auth.UserToImport){userToImport}) if err != nil { - err = fmt.Errorf("Unable to import user %v (uid %v): %w", *userToImport, uid, err) + err = fmt.Errorf("Unable to import user %v (uid %v): %v", *userToImport, uid, err) return nil } From 2c9da85ca3c095be1d631977904931c986c1e3f0 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Wed, 15 Jan 2020 14:59:00 -0500 Subject: [PATCH 06/20] go vet --- integration/auth/user_mgt_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index bf2a1897..bd944825 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -225,9 +225,9 @@ func TestGetUsers(t *testing.T) { t.Run("returns users by various identifier types in a single call", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UIDIdentifier{"uid1"}, - auth.EmailIdentifier{"user2@example.com"}, - auth.PhoneIdentifier{"+15555550003"}, + auth.UIDIdentifier{UID: "uid1"}, + auth.EmailIdentifier{Email: "user2@example.com"}, + auth.PhoneIdentifier{PhoneNumber: "+15555550003"}, auth.ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, }) if err != nil { @@ -242,9 +242,9 @@ func TestGetUsers(t *testing.T) { t.Run("returns found users and ignores non-existing users", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UIDIdentifier{"uid1"}, - auth.UIDIdentifier{"uid_that_doesnt_exist"}, - auth.UIDIdentifier{"uid3"}, + auth.UIDIdentifier{UID: "uid1"}, + auth.UIDIdentifier{UID: "uid_that_doesnt_exist"}, + auth.UIDIdentifier{UID: "uid3"}, }) if err != nil { t.Errorf("GetUsers([...]) returned an error: %v", err) @@ -266,7 +266,7 @@ func TestGetUsers(t *testing.T) { t.Run("returns nothing when queried for only non-existing users", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UIDIdentifier{"non-existing user"}, + auth.UIDIdentifier{UID: "non-existing user"}, }) if err != nil { t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) @@ -287,8 +287,8 @@ func TestGetUsers(t *testing.T) { t.Run("de-dups duplicate users", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UIDIdentifier{"uid1"}, - auth.UIDIdentifier{"uid1"}, + auth.UIDIdentifier{UID: "uid1"}, + auth.UIDIdentifier{UID: "uid1"}, }) if err != nil { t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) @@ -602,7 +602,7 @@ func TestDeleteUsers(t *testing.T) { ensureUsersNotFound := func(t *testing.T, uids []string) { identifiers := []auth.UserIdentifier{} for i := range uids { - identifiers = append(identifiers, auth.UIDIdentifier{uids[i]}) + identifiers = append(identifiers, auth.UIDIdentifier{UID: uids[i]}) } getUsersResult, err := client.GetUsers(context.Background(), identifiers) From 43c7e77f89a7ab03dc8d1d09c6de33b4cf6e8603 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 16 Jan 2020 11:12:12 -0800 Subject: [PATCH 07/20] chore: Removed Travis CI integration (#326) --- .github/workflows/ci.yml | 6 +++--- .travis.gofmt.sh | 6 ------ .travis.yml | 27 --------------------------- README.md | 4 ++-- 4 files changed, 5 insertions(+), 38 deletions(-) delete mode 100755 .travis.gofmt.sh delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37149aec..c40ae862 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Go Continuous Integration +name: Continuous Integration on: [push, pull_request] jobs: @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v2 with: path: go/src/firebase.google.com/go - + - name: Get dependencies run: go get -t -v $(go list ./... | grep -v integration) @@ -43,7 +43,7 @@ jobs: echo "Go code is not formatted:" gofmt -d -s . exit 1 - fi + fi - name: Run Static Analyzer run: go vet -v firebase.google.com/go/... diff --git a/.travis.gofmt.sh b/.travis.gofmt.sh deleted file mode 100755 index e33451d7..00000000 --- a/.travis.gofmt.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -if [[ ! -z "$(gofmt -l -s .)" ]]; then - echo "Go code is not formatted:" - gofmt -d -s . - exit 1 -fi diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a4290f13..00000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: go - -go: - - "1.11.x" - - "1.12.x" - - "1.13.x" - - master - -matrix: - # Build OK if fails on unstable development versions of Go. - allow_failures: - - go: master - # Don't wait for tests to finish on allow_failures. - # Mark the build finished if tests pass on other versions of Go. - fast_finish: true - -go_import_path: firebase.google.com/go - -install: - - go get golang.org/x/lint/golint - - go get -t -v $(go list ./... | grep -v integration) - -script: - - golint -set_exit_status $(go list ./...) - - if [[ "$TRAVIS_GO_VERSION" =~ ^1\.11\.([0-9]+|x)$ ]]; then ./.travis.gofmt.sh; fi - - go test -v -race -test.short ./... # Run tests with the race detector. - - go vet -v ./... # Run Go static analyzer. diff --git a/README.md b/README.md index 4b0e87b8..899a88d4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/firebase/firebase-admin-go.svg?branch=master)](https://travis-ci.org/firebase/firebase-admin-go) +[![Build Status](https://github.com/firebase/firebase-admin-go/workflows/Continuous%20Integration/badge.svg)](https://github.com/firebase/firebase-admin-go/actions) [![GoDoc](https://godoc.org/firebase.google.com/go?status.svg)](https://godoc.org/firebase.google.com/go) [![Go Report Card](https://goreportcard.com/badge/github.com/firebase/firebase-admin-go)](https://goreportcard.com/report/github.com/firebase/firebase-admin-go) @@ -42,7 +42,7 @@ requests, code review feedback, and also pull requests. ## Supported Go Versions We support Go v1.11 and higher. -[Continuous integration](https://travis-ci.org/firebase/firebase-admin-go) system +[Continuous integration](https://github.com/firebase/firebase-admin-go/actions) system tests the code on Go v1.11 through v1.13. ## Documentation From 48b371f14c6273cdc9785e95d05a8fb88efadccb Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Wed, 22 Jan 2020 13:14:03 -0500 Subject: [PATCH 08/20] review feedback --- auth/user_mgt.go | 218 +++++++++++------------------- auth/user_mgt_test.go | 170 +++++++++++------------ integration/auth/user_mgt_test.go | 153 ++++++++++----------- 3 files changed, 238 insertions(+), 303 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index 056f11db..f678c089 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -570,8 +570,7 @@ type getAccountInfoResponse struct { func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord, error) { var parsed getAccountInfoResponse - _, err := c.post(ctx, "/accounts:lookup", query.build(), &parsed) - if err != nil { + if _, err := c.post(ctx, "/accounts:lookup", query.build(), &parsed); err != nil { return nil, err } @@ -584,10 +583,9 @@ func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord // A UserIdentifier identifies a user to be looked up. type UserIdentifier interface { - String() string + toString() string - validate() error - matchesUserRecord(ur *UserRecord) (bool, error) + matchesUserRecord(ur *UserRecord) bool populateRequest(req *getAccountInfoRequest) error } @@ -598,31 +596,16 @@ type UIDIdentifier struct { UID string } -func (id UIDIdentifier) String() string { +func (id UIDIdentifier) toString() string { return fmt.Sprintf("UIDIdentifier{%s}", id.UID) } -func (id UIDIdentifier) validate() error { - return validateUID(id.UID) -} - -func (id UIDIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { - err := id.validate() - if err != nil { - return false, err - } - if id.UID == ur.UID { - return true, nil - } - return false, nil +func (id UIDIdentifier) matchesUserRecord(ur *UserRecord) bool { + return id.UID == ur.UID } func (id UIDIdentifier) populateRequest(req *getAccountInfoRequest) error { - err := id.validate() - if err != nil { - return err - } - req.localID = append(req.localID, id.UID) + req.LocalID = append(req.LocalID, id.UID) return nil } @@ -633,31 +616,16 @@ type EmailIdentifier struct { Email string } -func (id EmailIdentifier) String() string { +func (id EmailIdentifier) toString() string { return fmt.Sprintf("EmailIdentifier{%s}", id.Email) } -func (id EmailIdentifier) validate() error { - return validateEmail(id.Email) -} - -func (id EmailIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { - err := id.validate() - if err != nil { - return false, err - } - if id.Email == ur.Email { - return true, nil - } - return false, nil +func (id EmailIdentifier) matchesUserRecord(ur *UserRecord) bool { + return id.Email == ur.Email } func (id EmailIdentifier) populateRequest(req *getAccountInfoRequest) error { - err := id.validate() - if err != nil { - return err - } - req.email = append(req.email, id.Email) + req.Email = append(req.Email, id.Email) return nil } @@ -668,31 +636,16 @@ type PhoneIdentifier struct { PhoneNumber string } -func (id PhoneIdentifier) String() string { +func (id PhoneIdentifier) toString() string { return fmt.Sprintf("PhoneIdentifier{%s}", id.PhoneNumber) } -func (id PhoneIdentifier) validate() error { - return validatePhone(id.PhoneNumber) -} - -func (id PhoneIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { - err := id.validate() - if err != nil { - return false, err - } - if id.PhoneNumber == ur.PhoneNumber { - return true, nil - } - return false, nil +func (id PhoneIdentifier) matchesUserRecord(ur *UserRecord) bool { + return id.PhoneNumber == ur.PhoneNumber } func (id PhoneIdentifier) populateRequest(req *getAccountInfoRequest) error { - err := id.validate() - if err != nil { - return err - } - req.phoneNumber = append(req.phoneNumber, id.PhoneNumber) + req.PhoneNumber = append(req.PhoneNumber, id.PhoneNumber) return nil } @@ -704,35 +657,23 @@ type ProviderIdentifier struct { ProviderUID string } -func (id ProviderIdentifier) String() string { +func (id ProviderIdentifier) toString() string { return fmt.Sprintf("ProviderIdentifier{%s, %s}", id.ProviderID, id.ProviderUID) } -func (id ProviderIdentifier) validate() error { - return validateProvider(id.ProviderID, id.ProviderUID) -} - -func (id ProviderIdentifier) matchesUserRecord(ur *UserRecord) (bool, error) { - err := id.validate() - if err != nil { - return false, err - } +func (id ProviderIdentifier) matchesUserRecord(ur *UserRecord) bool { for _, userInfo := range ur.ProviderUserInfo { if id.ProviderID == userInfo.ProviderID && id.ProviderUID == userInfo.UID { - return true, nil + return true } } - return false, nil + return false } func (id ProviderIdentifier) populateRequest(req *getAccountInfoRequest) error { - err := id.validate() - if err != nil { - return err - } - req.federatedUserID = append( - req.federatedUserID, - federatedUserIdentifier{providerID: id.ProviderID, rawID: id.ProviderUID}) + req.FederatedUserID = append( + req.FederatedUserID, + federatedUserIdentifier{ProviderID: id.ProviderID, RawID: id.ProviderUID}) return nil } @@ -747,43 +688,54 @@ type GetUsersResult struct { } type federatedUserIdentifier struct { - providerID string - rawID string + ProviderID string `json:"providerId,omitempty"` + RawID string `json:"rawId,omitempty"` } type getAccountInfoRequest struct { - localID []string - email []string - phoneNumber []string - federatedUserID []federatedUserIdentifier + LocalID []string `json:"localId"` + Email []string `json:"email"` + PhoneNumber []string `json:"phoneNumber,omitempty"` + FederatedUserID []federatedUserIdentifier `json:"federatedUserId,omitempty"` } -func (req *getAccountInfoRequest) build() map[string]interface{} { - var builtFederatedUserID []map[string]interface{} - for i := range req.federatedUserID { - builtFederatedUserID = append(builtFederatedUserID, map[string]interface{}{ - "providerId": req.federatedUserID[i].providerID, - "rawId": req.federatedUserID[i].rawID, - }) +func (req *getAccountInfoRequest) validate() error { + for i := range req.LocalID { + if err := validateUID(req.LocalID[i]); err != nil { + return err + } } - return map[string]interface{}{ - "localId": req.localID, - "email": req.email, - "phoneNumber": req.phoneNumber, - "federatedUserId": builtFederatedUserID, + + for i := range req.Email { + if err := validateEmail(req.Email[i]); err != nil { + return err + } } + + for i := range req.PhoneNumber { + if err := validatePhone(req.PhoneNumber[i]); err != nil { + return err + } + } + + for i := range req.FederatedUserID { + id := &req.FederatedUserID[i] + if err := validateProvider(id.ProviderID, id.RawID); err != nil { + return err + } + } + + return nil } -func isUserFound(id UserIdentifier, urs [](*UserRecord)) (bool, error) { +func isUserFound(id UserIdentifier, urs [](*UserRecord)) bool { for i := range urs { - match, err := id.matchesUserRecord(urs[i]) - if err != nil { - return false, err - } else if match { - return true, nil + match := id.matchesUserRecord(urs[i]) + if match { + return true } } - return false, nil + return false } func (c *baseClient) GetUsers( @@ -792,22 +744,23 @@ func (c *baseClient) GetUsers( if len(identifiers) == 0 { return &GetUsersResult{[](*UserRecord){}, [](UserIdentifier){}}, nil } else if len(identifiers) > maxGetAccountsBatchSize { - return nil, internal.Errorf( - maximumUserCountExceeded, - "`identifiers` parameter must have <= %d entries.", maxGetAccountsBatchSize) + return nil, fmt.Errorf( + "`identifiers` parameter must have <= %d entries", maxGetAccountsBatchSize) } var request getAccountInfoRequest for i := range identifiers { - err := identifiers[i].populateRequest(&request) - if err != nil { + if err := identifiers[i].populateRequest(&request); err != nil { return nil, err } } + if err := request.validate(); err != nil { + return nil, err + } + var parsed getAccountInfoResponse - _, err := c.post(ctx, "/accounts:lookup", request.build(), &parsed) - if err != nil { + if _, err := c.post(ctx, "/accounts:lookup", request, &parsed); err != nil { return nil, err } @@ -825,11 +778,7 @@ func (c *baseClient) GetUsers( var notFound []UserIdentifier for i := range identifiers { - userFound, err := isUserFound(identifiers[i], userRecords) - if err != nil { - return nil, err - } - if !userFound { + if !isUserFound(identifiers[i], userRecords) { notFound = append(notFound, identifiers[i]) } } @@ -869,8 +818,7 @@ func (r *userQueryResponse) makeUserRecord() (*UserRecord, error) { func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error) { var customClaims map[string]interface{} if r.CustomAttributes != "" { - err := json.Unmarshal([]byte(r.CustomAttributes), &customClaims) - if err != nil { + if err := json.Unmarshal([]byte(r.CustomAttributes), &customClaims); err != nil { return nil, err } if len(customClaims) == 0 { @@ -1037,8 +985,8 @@ type DeleteUsersResult struct { // The Index field corresponds to the index of the failed user in the uids // array that was passed to DeleteUsers(). type DeleteUsersErrorInfo struct { - Index int - Reason string + Index int `json:"index,omitEmpty"` + Reason string `json:"message,omitEmpty"` } // Deletes the users specified by the given identifiers. @@ -1064,9 +1012,8 @@ func (c *baseClient) DeleteUsers(ctx context.Context, uids []string) (*DeleteUse if len(uids) == 0 { return &DeleteUsersResult{}, nil } else if len(uids) > maxDeleteAccountsBatchSize { - return nil, internal.Errorf( - maximumUserCountExceeded, - "`uids` parameter must have <= %d entries.", maxDeleteAccountsBatchSize) + return nil, fmt.Errorf( + "`uids` parameter must have <= %d entries", maxDeleteAccountsBatchSize) } var payload struct { @@ -1083,31 +1030,22 @@ func (c *baseClient) DeleteUsers(ctx context.Context, uids []string) (*DeleteUse payload.LocalIds = append(payload.LocalIds, uids[i]) } - type batchDeleteErrorInfo struct { - Index int `json:"index"` - LocalID string `json:"localId"` - Message string `json:"message"` - } type batchDeleteAccountsResponse struct { - Errors []batchDeleteErrorInfo `json:"errors"` + Errors []*DeleteUsersErrorInfo `json:"errors"` } resp := batchDeleteAccountsResponse{} - _, err := c.post(ctx, "/accounts:batchDelete", payload, &resp) + if _, err := c.post(ctx, "/accounts:batchDelete", payload, &resp); err != nil { + return nil, err + } result := DeleteUsersResult{ FailureCount: len(resp.Errors), SuccessCount: len(uids) - len(resp.Errors), + Errors: resp.Errors, } - for i := range resp.Errors { - result.Errors = append(result.Errors, &DeleteUsersErrorInfo{ - Index: resp.Errors[i].Index, - Reason: resp.Errors[i].Message, - }) - } - - return &result, err + return &result, nil } // SessionCookie creates a new Firebase session cookie from the given ID token and expiry diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index 7d98b6e3..1afc5c8d 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -198,13 +198,13 @@ func TestGetUsers(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), identifiers[:]) if getUsersResult != nil || err == nil { - t.Errorf("GetUsers(too-many-identifiers) should fail") + t.Fatalf("GetUsers(too-many-identifiers) should fail") } - if !internal.HasErrorCode(err, "maximum-user-count-exceeded") { + if err.Error() != "`identifiers` parameter must have <= 100 entries" { t.Errorf( "GetUsers(too-many-identifiers) returned an error of '%s'; "+ - "expected 'maximum-user-count-exceeded'", - err.(*internal.FirebaseError).Code) + "expected '`identifiers` parameter must have <= 100 entries'", + err.Error()) } }) @@ -215,14 +215,14 @@ func TestGetUsers(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), [](UserIdentifier){}) if getUsersResult == nil || err != nil { - t.Errorf("GetUsers([]) failed with: %s", err.Error()) - } else { - if len(getUsersResult.Users) != 0 { - t.Errorf("len(GetUsers([]).Users) = %d; want 0", len(getUsersResult.Users)) - } - if len(getUsersResult.NotFound) != 0 { - t.Errorf("len(GetUsers([]).NotFound) = %d; want 0", len(getUsersResult.NotFound)) - } + t.Fatalf("GetUsers([]) failed with: %s", err.Error()) + } + + if len(getUsersResult.Users) != 0 { + t.Errorf("len(GetUsers([]).Users) = %d; want 0", len(getUsersResult.Users)) + } + if len(getUsersResult.NotFound) != 0 { + t.Errorf("len(GetUsers([]).NotFound) = %d; want 0", len(getUsersResult.NotFound)) } }) @@ -237,22 +237,22 @@ func TestGetUsers(t *testing.T) { notFoundIds := []UserIdentifier{&UIDIdentifier{"id that doesnt exist"}} getUsersResult, err := s.Client.GetUsers(context.Background(), notFoundIds) if err != nil { - t.Errorf("GetUsers(['id that doesnt exist']) failed with %v", err) + t.Fatalf("GetUsers(['id that doesnt exist']) failed with %v", err) + } + + if len(getUsersResult.Users) != 0 { + t.Errorf( + "len(GetUsers(['id that doesnt exist']).Users) = %d; want 0", + len(getUsersResult.Users)) + } + if len(getUsersResult.NotFound) != len(notFoundIds) { + t.Errorf("len(GetUsers['id that doesnt exist']).NotFound) = %d; want %d", + len(getUsersResult.NotFound), len(notFoundIds)) } else { - if len(getUsersResult.Users) != 0 { - t.Errorf( - "len(GetUsers(['id that doesnt exist']).Users) = %d; want 0", - len(getUsersResult.Users)) - } - if len(getUsersResult.NotFound) != len(notFoundIds) { - t.Errorf("len(GetUsers['id that doesnt exist']).NotFound) = %d; want %d", - len(getUsersResult.NotFound), len(notFoundIds)) - } else { - for i := range notFoundIds { - if getUsersResult.NotFound[i] != notFoundIds[i] { - t.Errorf("GetUsers['id that doesnt exist']).NotFound[%d] = %v; want %v", - i, getUsersResult.NotFound[i], notFoundIds[i]) - } + for i := range notFoundIds { + if getUsersResult.NotFound[i] != notFoundIds[i] { + t.Errorf("GetUsers['id that doesnt exist']).NotFound[%d] = %v; want %v", + i, getUsersResult.NotFound[i], notFoundIds[i]) } } } @@ -267,7 +267,7 @@ func TestGetUsers(t *testing.T) { context.Background(), []UserIdentifier{&UIDIdentifier{"too long " + strings.Repeat(".", 128)}}) if getUsersResult != nil || err == nil { - t.Errorf("GetUsers([too-long]) should fail") + t.Fatalf("GetUsers([too-long]) should fail") } if err.Error() != "uid string must not be longer than 128 characters" { t.Errorf( @@ -286,7 +286,7 @@ func TestGetUsers(t *testing.T) { context.Background(), []UserIdentifier{EmailIdentifier{"invalid email addr"}}) if getUsersResult != nil || err == nil { - t.Errorf("GetUsers([invalid email addr]) should fail") + t.Fatalf("GetUsers([invalid email addr]) should fail") } if err.Error() != `malformed email string: "invalid email addr"` { t.Errorf( @@ -305,7 +305,7 @@ func TestGetUsers(t *testing.T) { PhoneIdentifier{"invalid phone number"}, }) if getUsersResult != nil || err == nil { - t.Errorf("GetUsers([invalid phone number]) should fail") + t.Fatalf("GetUsers([invalid phone number]) should fail") } if err.Error() != "phone number must be a valid, E.164 compliant identifier" { t.Errorf( @@ -324,7 +324,7 @@ func TestGetUsers(t *testing.T) { ProviderIdentifier{ProviderID: "", ProviderUID: ""}, }) if getUsersResult != nil || err == nil { - t.Errorf("GetUsers([invalid provider]) should fail") + t.Fatalf("GetUsers([invalid provider]) should fail") } if err.Error() != "providerID must be a non-empty string" { t.Errorf( @@ -349,7 +349,7 @@ func TestGetUsers(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), identifiers) if getUsersResult != nil || err == nil { - t.Errorf("GetUsers([list_with_bad_identifier]) should fail") + t.Fatalf("GetUsers([list_with_bad_identifier]) should fail") } if err.Error() != "uid string must not be longer than 128 characters" { t.Errorf( @@ -397,22 +397,22 @@ func TestGetUsers(t *testing.T) { getUsersResult, err := s.Client.GetUsers(context.Background(), identifiers) if err != nil { - t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) + t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) + } + + if !sameUsers(getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) { + t.Errorf("GetUsers([valid identifiers]) = %v; want = (uids from) %v (in any order)", + getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) + } + if len(getUsersResult.NotFound) != 1 { + t.Errorf("GetUsers([valid identifiers with one not found]) = %d; want = 1", + len(getUsersResult.NotFound)) } else { - if !sameUsers(getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) { - t.Errorf("GetUsers([valid identifiers]) = %v; want = (uids from) %v (in any order)", - getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) - } - if len(getUsersResult.NotFound) != 1 { - t.Errorf("GetUsers([valid identifiers with one not found]) = %d; want = 1", - len(getUsersResult.NotFound)) + if id, ok := getUsersResult.NotFound[0].(*UIDIdentifier); !ok { + t.Errorf("GetUsers([...]).NotFound[0] not a UIDIdentifier") } else { - if id, ok := getUsersResult.NotFound[0].(*UIDIdentifier); !ok { - t.Errorf("GetUsers([...]).NotFound[0] not a UIDIdentifier") - } else { - if id.UID != "this-user-doesnt-exist" { - t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want = 'this-user-doesnt-exist'", id.UID) - } + if id.UID != "this-user-doesnt-exist" { + t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want = 'this-user-doesnt-exist'", id.UID) } } } @@ -1350,17 +1350,17 @@ func TestDeleteUsers(t *testing.T) { result, err := client.DeleteUsers(context.Background(), []string{}) if err != nil { - t.Errorf("DeleteUsers([]) error %v; want = nil", err) - } else { - if result.SuccessCount != 0 { - t.Errorf("DeleteUsers([]).SuccessCount = %d; want = 0", result.SuccessCount) - } - if result.FailureCount != 0 { - t.Errorf("DeleteUsers([]).FailureCount = %d; want = 0", result.FailureCount) - } - if len(result.Errors) != 0 { - t.Errorf("len(DeleteUsers([]).Errors) = %d; want = 0", len(result.Errors)) - } + t.Fatalf("DeleteUsers([]) error %v; want = nil", err) + } + + if result.SuccessCount != 0 { + t.Errorf("DeleteUsers([]).SuccessCount = %d; want = 0", result.SuccessCount) + } + if result.FailureCount != 0 { + t.Errorf("DeleteUsers([]).FailureCount = %d; want = 0", result.FailureCount) + } + if len(result.Errors) != 0 { + t.Errorf("len(DeleteUsers([]).Errors) = %d; want = 0", len(result.Errors)) } }) @@ -1372,12 +1372,14 @@ func TestDeleteUsers(t *testing.T) { _, err := client.DeleteUsers(context.Background(), uids) if err == nil { - t.Errorf("DeleteUsers([too_many_uids]) error nil; want not nil") - } else if !internal.HasErrorCode(err, "maximum-user-count-exceeded") { + t.Fatalf("DeleteUsers([too_many_uids]) error nil; want not nil") + } + + if err.Error() != "`uids` parameter must have <= 1000 entries" { t.Errorf( "DeleteUsers([too_many_uids]) returned an error of '%s'; "+ - "expected 'maximum-user-count-exceeded'", - err.(*internal.FirebaseError).Code) + "expected '`uids` parameter must have <= 1000 entries'", + err.Error()) } }) @@ -1386,8 +1388,10 @@ func TestDeleteUsers(t *testing.T) { _, err := client.DeleteUsers(context.Background(), []string{tooLongUID}) if err == nil { - t.Errorf("DeleteUsers([too_long_uid]) error nil; want not nil") - } else if err.Error() != "uid string must not be longer than 128 characters" { + t.Fatalf("DeleteUsers([too_long_uid]) error nil; want not nil") + } + + if err.Error() != "uid string must not be longer than 128 characters" { t.Errorf( "DeleteUsers([too_long_uid]) returned an error of '%s'; "+ "expected 'uid string must not be longer than 128 characters'", @@ -1413,29 +1417,29 @@ func TestDeleteUsers(t *testing.T) { result, err := s.Client.DeleteUsers(context.Background(), []string{"uid1", "uid2", "uid3", "uid4"}) if err != nil { - t.Errorf("DeleteUsers([...]) error %v; want = nil", err) + t.Fatalf("DeleteUsers([...]) error %v; want = nil", err) + } + + if result.SuccessCount != 2 { + t.Errorf("DeleteUsers([...]).SuccessCount = %d; want 2", result.SuccessCount) + } + if result.FailureCount != 2 { + t.Errorf("DeleteUsers([...]).FailureCount = %d; want 2", result.FailureCount) + } + if len(result.Errors) != 2 { + t.Errorf("len(DeleteUsers([...]).Errors) = %d; want 2", len(result.Errors)) } else { - if result.SuccessCount != 2 { - t.Errorf("DeleteUsers([...]).SuccessCount = %d; want 2", result.SuccessCount) + if result.Errors[0].Index != 0 { + t.Errorf("DeleteUsers([...]).Errors[0].Index = %d; want 0", result.Errors[0].Index) } - if result.FailureCount != 2 { - t.Errorf("DeleteUsers([...]).FailureCount = %d; want 2", result.FailureCount) + if result.Errors[0].Reason != "Error Message 1" { + t.Errorf("DeleteUsers([...]).Errors[0].Reason = %s; want Error Message 1", result.Errors[0].Reason) } - if len(result.Errors) != 2 { - t.Errorf("len(DeleteUsers([...]).Errors) = %d; want 2", len(result.Errors)) - } else { - if result.Errors[0].Index != 0 { - t.Errorf("DeleteUsers([...]).Errors[0].Index = %d; want 0", result.Errors[0].Index) - } - if result.Errors[0].Reason != "Error Message 1" { - t.Errorf("DeleteUsers([...]).Errors[0].Reason = %s; want Error Message 1", result.Errors[0].Reason) - } - if result.Errors[1].Index != 2 { - t.Errorf("DeleteUsers([...]).Errors[1].Index = %d; want 2", result.Errors[1].Index) - } - if result.Errors[1].Reason != "Error Message 2" { - t.Errorf("DeleteUsers([...]).Errors[1].Reason = %s; want Error Message 2", result.Errors[1].Reason) - } + if result.Errors[1].Index != 2 { + t.Errorf("DeleteUsers([...]).Errors[1].Index = %d; want 2", result.Errors[1].Index) + } + if result.Errors[1].Reason != "Error Message 2" { + t.Errorf("DeleteUsers([...]).Errors[1].Reason = %s; want Error Message 2", result.Errors[1].Reason) } } }) diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index bd944825..19fc8d4d 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -231,12 +231,12 @@ func TestGetUsers(t *testing.T) { auth.ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, }) if err != nil { - t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) - } else { - if !sameUsers(getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) { - t.Errorf("GetUsers([valid identifiers]) = %v; want = %v (in any order)", - getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) - } + t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) + } + + if !sameUsers(getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) { + t.Errorf("GetUsers([valid identifiers]) = %v; want = %v (in any order)", + getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) } }) @@ -247,19 +247,19 @@ func TestGetUsers(t *testing.T) { auth.UIDIdentifier{UID: "uid3"}, }) if err != nil { - t.Errorf("GetUsers([...]) returned an error: %v", err) + t.Fatalf("GetUsers([...]) returned an error: %v", err) + } + + if !sameUsers(getUsersResult.Users, []string{"uid1", "uid3"}) { + t.Errorf("GetUsers([valid identifiers]) = %v; want = %v (in any order)", + getUsersResult.Users, []string{"uid1", "uid3"}) + } + if len(getUsersResult.NotFound) != 1 { + t.Errorf("len(GetUsers([...]).NotFound) = %d; want 1", len(getUsersResult.NotFound)) } else { - if !sameUsers(getUsersResult.Users, []string{"uid1", "uid3"}) { - t.Errorf("GetUsers([valid identifiers]) = %v; want = %v (in any order)", - getUsersResult.Users, []string{"uid1", "uid3"}) - } - if len(getUsersResult.NotFound) != 1 { - t.Errorf("len(GetUsers([...]).NotFound) = %d; want 1", len(getUsersResult.NotFound)) - } else { - if getUsersResult.NotFound[0].(auth.UIDIdentifier).UID != "uid_that_doesnt_exist" { - t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want 'uid_that_doesnt_exist'", - getUsersResult.NotFound[0].(auth.UIDIdentifier).UID) - } + if getUsersResult.NotFound[0].(auth.UIDIdentifier).UID != "uid_that_doesnt_exist" { + t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want 'uid_that_doesnt_exist'", + getUsersResult.NotFound[0].(auth.UIDIdentifier).UID) } } }) @@ -269,18 +269,18 @@ func TestGetUsers(t *testing.T) { auth.UIDIdentifier{UID: "non-existing user"}, }) if err != nil { - t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) + t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) + } + + if len(getUsersResult.Users) != 0 { + t.Errorf("len(GetUsers([...]).Users) = %d; want = 0", len(getUsersResult.Users)) + } + if len(getUsersResult.NotFound) != 1 { + t.Errorf("len(GetUsers([...]).NotFound) = %d; want = 1", len(getUsersResult.NotFound)) } else { - if len(getUsersResult.Users) != 0 { - t.Errorf("len(GetUsers([...]).Users) = %d; want = 0", len(getUsersResult.Users)) - } - if len(getUsersResult.NotFound) != 1 { - t.Errorf("len(GetUsers([...]).NotFound) = %d; want = 1", len(getUsersResult.NotFound)) - } else { - if getUsersResult.NotFound[0].(auth.UIDIdentifier).UID != "non-existing user" { - t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want 'non-existing user'", - getUsersResult.NotFound[0].(auth.UIDIdentifier).UID) - } + if getUsersResult.NotFound[0].(auth.UIDIdentifier).UID != "non-existing user" { + t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want 'non-existing user'", + getUsersResult.NotFound[0].(auth.UIDIdentifier).UID) } } }) @@ -291,19 +291,19 @@ func TestGetUsers(t *testing.T) { auth.UIDIdentifier{UID: "uid1"}, }) if err != nil { - t.Errorf("GetUsers([valid identifiers]) returned an error: %v", err) + t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) + } + + if len(getUsersResult.Users) != 1 { + t.Errorf("len(GetUsers([...]).Users) = %d; want = 1", len(getUsersResult.Users)) } else { - if len(getUsersResult.Users) != 1 { - t.Errorf("len(GetUsers([...]).Users) = %d; want = 1", len(getUsersResult.Users)) - } else { - if getUsersResult.Users[0].UID != "uid1" { - t.Errorf("GetUsers([...]).Users[0].UID = %s; want = 'uid1'", getUsersResult.Users[0].UID) - } - } - if len(getUsersResult.NotFound) != 0 { - t.Errorf("len(GetUsers([...]).NotFound) = %d; want = 0", len(getUsersResult.NotFound)) + if getUsersResult.Users[0].UID != "uid1" { + t.Errorf("GetUsers([...]).Users[0].UID = %s; want = 'uid1'", getUsersResult.Users[0].UID) } } + if len(getUsersResult.NotFound) != 0 { + t.Errorf("len(GetUsers([...]).NotFound) = %d; want = 0", len(getUsersResult.NotFound)) + } }) t.Run("returns users with a LastRefreshTimestamp", func(t *testing.T) { @@ -334,7 +334,7 @@ func TestGetUsers(t *testing.T) { getUsersResult, err := client.GetUser(context.Background(), "lastRefreshTimeUser") if err != nil { - t.Errorf("GetUser(...) failed with error: %v", err) + t.Fatalf("GetUser(...) failed with error: %v", err) } if getUsersResult.UserMetadata.LastRefreshTimestamp <= 0 { t.Errorf( @@ -644,23 +644,16 @@ func TestDeleteUsers(t *testing.T) { result, err := client.DeleteUsers(context.Background(), uids) if err != nil { t.Fatalf("DeleteUsers([valid_ids]) error %v; want nil", err) - } else { - ok := true - if result.SuccessCount != 3 { - t.Errorf("DeleteUsers([valid_ids]).SuccessCount = %d; want 3", result.SuccessCount) - ok = false - } - if result.FailureCount != 0 { - t.Errorf("DeleteUsers([valid_ids]).FailureCount = %d; want 0", result.FailureCount) - ok = false - } - if len(result.Errors) != 0 { - t.Errorf("len(DeleteUsers([valid_ids]).Errors) = %d; want 0", len(result.Errors)) - ok = false - } - if !ok { - t.FailNow() - } + } + + if result.SuccessCount != 3 { + t.Errorf("DeleteUsers([valid_ids]).SuccessCount = %d; want 3", result.SuccessCount) + } + if result.FailureCount != 0 { + t.Errorf("DeleteUsers([valid_ids]).FailureCount = %d; want 0", result.FailureCount) + } + if len(result.Errors) != 0 { + t.Errorf("len(DeleteUsers([valid_ids]).Errors) = %d; want 0", len(result.Errors)) } ensureUsersNotFound(t, uids) @@ -670,37 +663,37 @@ func TestDeleteUsers(t *testing.T) { uids := []string{createTestUserOrDie(t), "uid-that-doesnt-exist"} result, err := client.DeleteUsers(context.Background(), uids) if err != nil { - t.Errorf("DeleteUsers(uids) error %v; want nil", err) - } else { - if result.SuccessCount != 2 { - t.Errorf("DeleteUsers(uids).SuccessCount = %d; want 2", result.SuccessCount) - } - if result.FailureCount != 0 { - t.Errorf("DeleteUsers(uids).FailureCount = %d; want 0", result.FailureCount) - } - if len(result.Errors) != 0 { - t.Errorf("len(DeleteUsers(uids).Errors) = %d; want 0", len(result.Errors)) - } + t.Fatalf("DeleteUsers(uids) error %v; want nil", err) + } - ensureUsersNotFound(t, uids) + if result.SuccessCount != 2 { + t.Errorf("DeleteUsers(uids).SuccessCount = %d; want 2", result.SuccessCount) } + if result.FailureCount != 0 { + t.Errorf("DeleteUsers(uids).FailureCount = %d; want 0", result.FailureCount) + } + if len(result.Errors) != 0 { + t.Errorf("len(DeleteUsers(uids).Errors) = %d; want 0", len(result.Errors)) + } + + ensureUsersNotFound(t, uids) }) t.Run("is idempotent", func(t *testing.T) { deleteUserAndEnsureSuccess := func(t *testing.T, uids []string) { result, err := client.DeleteUsers(context.Background(), uids) if err != nil { - t.Errorf("DeleteUsers(uids) error %v; want nil", err) - } else { - if result.SuccessCount != 1 { - t.Errorf("DeleteUsers(uids).SuccessCount = %d; want 1", result.SuccessCount) - } - if result.FailureCount != 0 { - t.Errorf("DeleteUsers(uids).FailureCount = %d; want 0", result.FailureCount) - } - if len(result.Errors) != 0 { - t.Errorf("len(DeleteUsers(uids).Errors) = %d; want 0", len(result.Errors)) - } + t.Fatalf("DeleteUsers(uids) error %v; want nil", err) + } + + if result.SuccessCount != 1 { + t.Errorf("DeleteUsers(uids).SuccessCount = %d; want 1", result.SuccessCount) + } + if result.FailureCount != 0 { + t.Errorf("DeleteUsers(uids).FailureCount = %d; want 0", result.FailureCount) + } + if len(result.Errors) != 0 { + t.Errorf("len(DeleteUsers(uids).Errors) = %d; want 0", len(result.Errors)) } } From ae498f74bb51f4c8de9893bf295cf43e8e198444 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Wed, 22 Jan 2020 14:21:38 -0500 Subject: [PATCH 09/20] Remove matchesUserRecord interface method And inline the implementation. --- auth/user_mgt.go | 68 +++++++++++++++++++++++++++---------------- auth/user_mgt_test.go | 2 +- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index f678c089..5b843364 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -585,7 +585,6 @@ func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord type UserIdentifier interface { toString() string - matchesUserRecord(ur *UserRecord) bool populateRequest(req *getAccountInfoRequest) error } @@ -600,10 +599,6 @@ func (id UIDIdentifier) toString() string { return fmt.Sprintf("UIDIdentifier{%s}", id.UID) } -func (id UIDIdentifier) matchesUserRecord(ur *UserRecord) bool { - return id.UID == ur.UID -} - func (id UIDIdentifier) populateRequest(req *getAccountInfoRequest) error { req.LocalID = append(req.LocalID, id.UID) return nil @@ -620,10 +615,6 @@ func (id EmailIdentifier) toString() string { return fmt.Sprintf("EmailIdentifier{%s}", id.Email) } -func (id EmailIdentifier) matchesUserRecord(ur *UserRecord) bool { - return id.Email == ur.Email -} - func (id EmailIdentifier) populateRequest(req *getAccountInfoRequest) error { req.Email = append(req.Email, id.Email) return nil @@ -640,10 +631,6 @@ func (id PhoneIdentifier) toString() string { return fmt.Sprintf("PhoneIdentifier{%s}", id.PhoneNumber) } -func (id PhoneIdentifier) matchesUserRecord(ur *UserRecord) bool { - return id.PhoneNumber == ur.PhoneNumber -} - func (id PhoneIdentifier) populateRequest(req *getAccountInfoRequest) error { req.PhoneNumber = append(req.PhoneNumber, id.PhoneNumber) return nil @@ -661,15 +648,6 @@ func (id ProviderIdentifier) toString() string { return fmt.Sprintf("ProviderIdentifier{%s, %s}", id.ProviderID, id.ProviderUID) } -func (id ProviderIdentifier) matchesUserRecord(ur *UserRecord) bool { - for _, userInfo := range ur.ProviderUserInfo { - if id.ProviderID == userInfo.ProviderID && id.ProviderUID == userInfo.UID { - return true - } - } - return false -} - func (id ProviderIdentifier) populateRequest(req *getAccountInfoRequest) error { req.FederatedUserID = append( req.FederatedUserID, @@ -730,9 +708,49 @@ func (req *getAccountInfoRequest) validate() error { func isUserFound(id UserIdentifier, urs [](*UserRecord)) bool { for i := range urs { - match := id.matchesUserRecord(urs[i]) - if match { - return true + if uidIdentifier, ok := id.(*UIDIdentifier); ok { + if uidIdentifier.UID == urs[i].UID { + return true + } + } + if uidIdentifier, ok := id.(UIDIdentifier); ok { + if uidIdentifier.UID == urs[i].UID { + return true + } + } + if emailIdentifier, ok := id.(*EmailIdentifier); ok { + if emailIdentifier.Email == urs[i].Email { + return true + } + } + if emailIdentifier, ok := id.(EmailIdentifier); ok { + if emailIdentifier.Email == urs[i].Email { + return true + } + } + if phoneIdentifier, ok := id.(*PhoneIdentifier); ok { + if phoneIdentifier.PhoneNumber == urs[i].PhoneNumber { + return true + } + } + if phoneIdentifier, ok := id.(PhoneIdentifier); ok { + if phoneIdentifier.PhoneNumber == urs[i].PhoneNumber { + return true + } + } + if providerIdentifier, ok := id.(*ProviderIdentifier); ok { + for _, userInfo := range urs[i].ProviderUserInfo { + if providerIdentifier.ProviderID == userInfo.ProviderID && providerIdentifier.ProviderUID == userInfo.UID { + return true + } + } + } + if providerIdentifier, ok := id.(ProviderIdentifier); ok { + for _, userInfo := range urs[i].ProviderUserInfo { + if providerIdentifier.ProviderID == userInfo.ProviderID && providerIdentifier.ProviderUID == userInfo.UID { + return true + } + } } } return false diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index 1afc5c8d..61de5622 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -388,7 +388,7 @@ func TestGetUsers(t *testing.T) { defer s.Close() identifiers := []UserIdentifier{ - &UIDIdentifier{"uid1"}, + UIDIdentifier{"uid1"}, &EmailIdentifier{"user2@example.com"}, &PhoneIdentifier{"+15555550003"}, &ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, From f665813d00feeaae18b23d7f80e733ba6e5e53f6 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Thu, 23 Jan 2020 12:22:03 -0500 Subject: [PATCH 10/20] Revert "Remove matchesUserRecord interface method" This reverts commit ae498f74bb51f4c8de9893bf295cf43e8e198444. --- auth/user_mgt.go | 68 ++++++++++++++++--------------------------- auth/user_mgt_test.go | 2 +- 2 files changed, 26 insertions(+), 44 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index 5b843364..f678c089 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -585,6 +585,7 @@ func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord type UserIdentifier interface { toString() string + matchesUserRecord(ur *UserRecord) bool populateRequest(req *getAccountInfoRequest) error } @@ -599,6 +600,10 @@ func (id UIDIdentifier) toString() string { return fmt.Sprintf("UIDIdentifier{%s}", id.UID) } +func (id UIDIdentifier) matchesUserRecord(ur *UserRecord) bool { + return id.UID == ur.UID +} + func (id UIDIdentifier) populateRequest(req *getAccountInfoRequest) error { req.LocalID = append(req.LocalID, id.UID) return nil @@ -615,6 +620,10 @@ func (id EmailIdentifier) toString() string { return fmt.Sprintf("EmailIdentifier{%s}", id.Email) } +func (id EmailIdentifier) matchesUserRecord(ur *UserRecord) bool { + return id.Email == ur.Email +} + func (id EmailIdentifier) populateRequest(req *getAccountInfoRequest) error { req.Email = append(req.Email, id.Email) return nil @@ -631,6 +640,10 @@ func (id PhoneIdentifier) toString() string { return fmt.Sprintf("PhoneIdentifier{%s}", id.PhoneNumber) } +func (id PhoneIdentifier) matchesUserRecord(ur *UserRecord) bool { + return id.PhoneNumber == ur.PhoneNumber +} + func (id PhoneIdentifier) populateRequest(req *getAccountInfoRequest) error { req.PhoneNumber = append(req.PhoneNumber, id.PhoneNumber) return nil @@ -648,6 +661,15 @@ func (id ProviderIdentifier) toString() string { return fmt.Sprintf("ProviderIdentifier{%s, %s}", id.ProviderID, id.ProviderUID) } +func (id ProviderIdentifier) matchesUserRecord(ur *UserRecord) bool { + for _, userInfo := range ur.ProviderUserInfo { + if id.ProviderID == userInfo.ProviderID && id.ProviderUID == userInfo.UID { + return true + } + } + return false +} + func (id ProviderIdentifier) populateRequest(req *getAccountInfoRequest) error { req.FederatedUserID = append( req.FederatedUserID, @@ -708,49 +730,9 @@ func (req *getAccountInfoRequest) validate() error { func isUserFound(id UserIdentifier, urs [](*UserRecord)) bool { for i := range urs { - if uidIdentifier, ok := id.(*UIDIdentifier); ok { - if uidIdentifier.UID == urs[i].UID { - return true - } - } - if uidIdentifier, ok := id.(UIDIdentifier); ok { - if uidIdentifier.UID == urs[i].UID { - return true - } - } - if emailIdentifier, ok := id.(*EmailIdentifier); ok { - if emailIdentifier.Email == urs[i].Email { - return true - } - } - if emailIdentifier, ok := id.(EmailIdentifier); ok { - if emailIdentifier.Email == urs[i].Email { - return true - } - } - if phoneIdentifier, ok := id.(*PhoneIdentifier); ok { - if phoneIdentifier.PhoneNumber == urs[i].PhoneNumber { - return true - } - } - if phoneIdentifier, ok := id.(PhoneIdentifier); ok { - if phoneIdentifier.PhoneNumber == urs[i].PhoneNumber { - return true - } - } - if providerIdentifier, ok := id.(*ProviderIdentifier); ok { - for _, userInfo := range urs[i].ProviderUserInfo { - if providerIdentifier.ProviderID == userInfo.ProviderID && providerIdentifier.ProviderUID == userInfo.UID { - return true - } - } - } - if providerIdentifier, ok := id.(ProviderIdentifier); ok { - for _, userInfo := range urs[i].ProviderUserInfo { - if providerIdentifier.ProviderID == userInfo.ProviderID && providerIdentifier.ProviderUID == userInfo.UID { - return true - } - } + match := id.matchesUserRecord(urs[i]) + if match { + return true } } return false diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index 61de5622..1afc5c8d 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -388,7 +388,7 @@ func TestGetUsers(t *testing.T) { defer s.Close() identifiers := []UserIdentifier{ - UIDIdentifier{"uid1"}, + &UIDIdentifier{"uid1"}, &EmailIdentifier{"user2@example.com"}, &PhoneIdentifier{"+15555550003"}, &ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, From 7937ffffc3df202ca94f5758d21d4c137773aac4 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Fri, 24 Jan 2020 13:58:37 -0500 Subject: [PATCH 11/20] review feedback 2 --- auth/user_mgt.go | 40 +--- auth/user_mgt_test.go | 352 +++++++++++++++--------------- integration/auth/user_mgt_test.go | 248 +++++++++------------ 3 files changed, 291 insertions(+), 349 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index f678c089..7aaa3880 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -583,10 +583,8 @@ func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord // A UserIdentifier identifies a user to be looked up. type UserIdentifier interface { - toString() string - matchesUserRecord(ur *UserRecord) bool - populateRequest(req *getAccountInfoRequest) error + populateRequest(req *getAccountInfoRequest) } // A UIDIdentifier is used for looking up an account by uid. @@ -596,17 +594,12 @@ type UIDIdentifier struct { UID string } -func (id UIDIdentifier) toString() string { - return fmt.Sprintf("UIDIdentifier{%s}", id.UID) -} - func (id UIDIdentifier) matchesUserRecord(ur *UserRecord) bool { return id.UID == ur.UID } -func (id UIDIdentifier) populateRequest(req *getAccountInfoRequest) error { +func (id UIDIdentifier) populateRequest(req *getAccountInfoRequest) { req.LocalID = append(req.LocalID, id.UID) - return nil } // An EmailIdentifier is used for looking up an account by email. @@ -616,17 +609,12 @@ type EmailIdentifier struct { Email string } -func (id EmailIdentifier) toString() string { - return fmt.Sprintf("EmailIdentifier{%s}", id.Email) -} - func (id EmailIdentifier) matchesUserRecord(ur *UserRecord) bool { return id.Email == ur.Email } -func (id EmailIdentifier) populateRequest(req *getAccountInfoRequest) error { +func (id EmailIdentifier) populateRequest(req *getAccountInfoRequest) { req.Email = append(req.Email, id.Email) - return nil } // A PhoneIdentifier is used for looking up an account by phone number. @@ -636,17 +624,12 @@ type PhoneIdentifier struct { PhoneNumber string } -func (id PhoneIdentifier) toString() string { - return fmt.Sprintf("PhoneIdentifier{%s}", id.PhoneNumber) -} - func (id PhoneIdentifier) matchesUserRecord(ur *UserRecord) bool { return id.PhoneNumber == ur.PhoneNumber } -func (id PhoneIdentifier) populateRequest(req *getAccountInfoRequest) error { +func (id PhoneIdentifier) populateRequest(req *getAccountInfoRequest) { req.PhoneNumber = append(req.PhoneNumber, id.PhoneNumber) - return nil } // A ProviderIdentifier is used for looking up an account by federated provider. @@ -657,10 +640,6 @@ type ProviderIdentifier struct { ProviderUID string } -func (id ProviderIdentifier) toString() string { - return fmt.Sprintf("ProviderIdentifier{%s, %s}", id.ProviderID, id.ProviderUID) -} - func (id ProviderIdentifier) matchesUserRecord(ur *UserRecord) bool { for _, userInfo := range ur.ProviderUserInfo { if id.ProviderID == userInfo.ProviderID && id.ProviderUID == userInfo.UID { @@ -670,11 +649,10 @@ func (id ProviderIdentifier) matchesUserRecord(ur *UserRecord) bool { return false } -func (id ProviderIdentifier) populateRequest(req *getAccountInfoRequest) error { +func (id ProviderIdentifier) populateRequest(req *getAccountInfoRequest) { req.FederatedUserID = append( req.FederatedUserID, federatedUserIdentifier{ProviderID: id.ProviderID, RawID: id.ProviderUID}) - return nil } // A GetUsersResult represents the result of the GetUsers() API. @@ -693,8 +671,8 @@ type federatedUserIdentifier struct { } type getAccountInfoRequest struct { - LocalID []string `json:"localId"` - Email []string `json:"email"` + LocalID []string `json:"localId,omitempty"` + Email []string `json:"email,omitempty"` PhoneNumber []string `json:"phoneNumber,omitempty"` FederatedUserID []federatedUserIdentifier `json:"federatedUserId,omitempty"` } @@ -750,9 +728,7 @@ func (c *baseClient) GetUsers( var request getAccountInfoRequest for i := range identifiers { - if err := identifiers[i].populateRequest(&request); err != nil { - return nil, err - } + identifiers[i].populateRequest(&request) } if err := request.validate(); err != nil { diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index 1afc5c8d..4100e52d 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -185,182 +185,181 @@ func sameUsers(users [](*UserRecord), uids []string) bool { return true } -func TestGetUsers(t *testing.T) { - t.Run("should be rejected when given more than 100 identifiers", func(t *testing.T) { - client := &Client{ - baseClient: &baseClient{}, - } +func TestGetUsersExceeds100(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } - var identifiers [101]UserIdentifier - for i := 0; i < 101; i++ { - identifiers[i] = &UIDIdentifier{UID: fmt.Sprintf("id%d", i)} - } + var identifiers [101]UserIdentifier + for i := 0; i < 101; i++ { + identifiers[i] = &UIDIdentifier{UID: fmt.Sprintf("id%d", i)} + } - getUsersResult, err := client.GetUsers(context.Background(), identifiers[:]) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers(too-many-identifiers) should fail") - } - if err.Error() != "`identifiers` parameter must have <= 100 entries" { - t.Errorf( - "GetUsers(too-many-identifiers) returned an error of '%s'; "+ - "expected '`identifiers` parameter must have <= 100 entries'", - err.Error()) - } - }) + getUsersResult, err := client.GetUsers(context.Background(), identifiers[:]) + if getUsersResult != nil || err == nil { + t.Fatalf("GetUsers(too-many-identifiers) should fail") + } + if err.Error() != "`identifiers` parameter must have <= 100 entries" { + t.Errorf( + "GetUsers(too-many-identifiers) returned an error of '%s'; "+ + "expected '`identifiers` parameter must have <= 100 entries'", + err.Error()) + } +} - t.Run("should return no results when given no identifiers", func(t *testing.T) { - client := &Client{ - baseClient: &baseClient{}, - } +func TestGetUsersEmpty(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } - getUsersResult, err := client.GetUsers(context.Background(), [](UserIdentifier){}) - if getUsersResult == nil || err != nil { - t.Fatalf("GetUsers([]) failed with: %s", err.Error()) - } + getUsersResult, err := client.GetUsers(context.Background(), [](UserIdentifier){}) + if getUsersResult == nil || err != nil { + t.Fatalf("GetUsers([]) failed with: %s", err.Error()) + } - if len(getUsersResult.Users) != 0 { - t.Errorf("len(GetUsers([]).Users) = %d; want 0", len(getUsersResult.Users)) - } - if len(getUsersResult.NotFound) != 0 { - t.Errorf("len(GetUsers([]).NotFound) = %d; want 0", len(getUsersResult.NotFound)) - } - }) + if len(getUsersResult.Users) != 0 { + t.Errorf("len(GetUsers([]).Users) = %d; want 0", len(getUsersResult.Users)) + } + if len(getUsersResult.NotFound) != 0 { + t.Errorf("len(GetUsers([]).NotFound) = %d; want 0", len(getUsersResult.NotFound)) + } +} - t.Run("should return no users when given identifiers that do not exist", func(t *testing.T) { - resp := `{ +func TestGetUsersAllNonExisting(t *testing.T) { + resp := `{ "kind" : "identitytoolkit#GetAccountInfoResponse", "users" : [] }` - s := echoServer([]byte(resp), t) - defer s.Close() - - notFoundIds := []UserIdentifier{&UIDIdentifier{"id that doesnt exist"}} - getUsersResult, err := s.Client.GetUsers(context.Background(), notFoundIds) - if err != nil { - t.Fatalf("GetUsers(['id that doesnt exist']) failed with %v", err) - } + s := echoServer([]byte(resp), t) + defer s.Close() - if len(getUsersResult.Users) != 0 { - t.Errorf( - "len(GetUsers(['id that doesnt exist']).Users) = %d; want 0", - len(getUsersResult.Users)) - } - if len(getUsersResult.NotFound) != len(notFoundIds) { - t.Errorf("len(GetUsers['id that doesnt exist']).NotFound) = %d; want %d", - len(getUsersResult.NotFound), len(notFoundIds)) - } else { - for i := range notFoundIds { - if getUsersResult.NotFound[i] != notFoundIds[i] { - t.Errorf("GetUsers['id that doesnt exist']).NotFound[%d] = %v; want %v", - i, getUsersResult.NotFound[i], notFoundIds[i]) - } + notFoundIds := []UserIdentifier{&UIDIdentifier{"id that doesnt exist"}} + getUsersResult, err := s.Client.GetUsers(context.Background(), notFoundIds) + if err != nil { + t.Fatalf("GetUsers(['id that doesnt exist']) failed with %v", err) + } + + if len(getUsersResult.Users) != 0 { + t.Errorf( + "len(GetUsers(['id that doesnt exist']).Users) = %d; want 0", + len(getUsersResult.Users)) + } + if len(getUsersResult.NotFound) != len(notFoundIds) { + t.Errorf("len(GetUsers['id that doesnt exist']).NotFound) = %d; want %d", + len(getUsersResult.NotFound), len(notFoundIds)) + } else { + for i := range notFoundIds { + if getUsersResult.NotFound[i] != notFoundIds[i] { + t.Errorf("GetUsers['id that doesnt exist']).NotFound[%d] = %v; want %v", + i, getUsersResult.NotFound[i], notFoundIds[i]) } } - }) + } +} - t.Run("should be rejected when given an invalid uid", func(t *testing.T) { - client := &Client{ - baseClient: &baseClient{}, - } +func TestGetUsersInvalidUid(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } - getUsersResult, err := client.GetUsers( - context.Background(), - []UserIdentifier{&UIDIdentifier{"too long " + strings.Repeat(".", 128)}}) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers([too-long]) should fail") - } - if err.Error() != "uid string must not be longer than 128 characters" { - t.Errorf( - "GetUsers([too-long]) returned an error of '%s'; "+ - "expected 'uid string must not be longer than 128 characters'", - err.Error()) - } - }) + getUsersResult, err := client.GetUsers( + context.Background(), + []UserIdentifier{&UIDIdentifier{"too long " + strings.Repeat(".", 128)}}) + if getUsersResult != nil || err == nil { + t.Fatalf("GetUsers([too-long]) should fail") + } + if err.Error() != "uid string must not be longer than 128 characters" { + t.Errorf( + "GetUsers([too-long]) returned an error of '%s'; "+ + "expected 'uid string must not be longer than 128 characters'", + err.Error()) + } +} - t.Run("should be rejected when given an invalid email", func(t *testing.T) { - client := &Client{ - baseClient: &baseClient{}, - } +func TestGetUsersInvalidEmail(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } - getUsersResult, err := client.GetUsers( - context.Background(), - []UserIdentifier{EmailIdentifier{"invalid email addr"}}) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers([invalid email addr]) should fail") - } - if err.Error() != `malformed email string: "invalid email addr"` { - t.Errorf( - "GetUsers([invalid email addr]) returned an error of '%s'; "+ - `"expected 'malformed email string: "invalid email addr"'"`, - err.Error()) - } - }) + getUsersResult, err := client.GetUsers( + context.Background(), + []UserIdentifier{EmailIdentifier{"invalid email addr"}}) + if getUsersResult != nil || err == nil { + t.Fatalf("GetUsers([invalid email addr]) should fail") + } + if err.Error() != `malformed email string: "invalid email addr"` { + t.Errorf( + "GetUsers([invalid email addr]) returned an error of '%s'; "+ + `"expected 'malformed email string: "invalid email addr"'"`, + err.Error()) + } +} - t.Run("should be rejected when given an invalid phone number", func(t *testing.T) { - client := &Client{ - baseClient: &baseClient{}, - } +func TestGetUsersInvalidPhoneNumber(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } - getUsersResult, err := client.GetUsers(context.Background(), []UserIdentifier{ - PhoneIdentifier{"invalid phone number"}, - }) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers([invalid phone number]) should fail") - } - if err.Error() != "phone number must be a valid, E.164 compliant identifier" { - t.Errorf( - "GetUsers([invalid phone number]) returned an error of '%s'; "+ - "expected 'phone number must be a valid, E.164 compliant identifier'", - err.Error()) - } + getUsersResult, err := client.GetUsers(context.Background(), []UserIdentifier{ + PhoneIdentifier{"invalid phone number"}, }) + if getUsersResult != nil || err == nil { + t.Fatalf("GetUsers([invalid phone number]) should fail") + } + if err.Error() != "phone number must be a valid, E.164 compliant identifier" { + t.Errorf( + "GetUsers([invalid phone number]) returned an error of '%s'; "+ + "expected 'phone number must be a valid, E.164 compliant identifier'", + err.Error()) + } +} - t.Run("should be rejected when given an invalid provider", func(t *testing.T) { - client := &Client{ - baseClient: &baseClient{}, - } +func TestGetUsersInvalidProvider(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } - getUsersResult, err := client.GetUsers(context.Background(), []UserIdentifier{ - ProviderIdentifier{ProviderID: "", ProviderUID: ""}, - }) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers([invalid provider]) should fail") - } - if err.Error() != "providerID must be a non-empty string" { - t.Errorf( - "GetUsers([invalid provider]) returned an error of '%s'; "+ - "expected 'providerID must be a non-empty string'", - err.Error()) - } + getUsersResult, err := client.GetUsers(context.Background(), []UserIdentifier{ + ProviderIdentifier{ProviderID: "", ProviderUID: ""}, }) + if getUsersResult != nil || err == nil { + t.Fatalf("GetUsers([invalid provider]) should fail") + } + if err.Error() != "providerID must be a non-empty string" { + t.Errorf( + "GetUsers([invalid provider]) returned an error of '%s'; "+ + "expected 'providerID must be a non-empty string'", + err.Error()) + } +} - t.Run("should be rejected when given a single bad identifier", func(t *testing.T) { - client := &Client{ - baseClient: &baseClient{}, - } +func TestGetUsersSingleBadIdentifier(t *testing.T) { + client := &Client{ + baseClient: &baseClient{}, + } - identifiers := []UserIdentifier{ - UIDIdentifier{"valid_id1"}, - UIDIdentifier{"valid_id2"}, - UIDIdentifier{"invalid id; too long. " + strings.Repeat(".", 128)}, - UIDIdentifier{"valid_id3"}, - UIDIdentifier{"valid_id4"}, - } + identifiers := []UserIdentifier{ + UIDIdentifier{"valid_id1"}, + UIDIdentifier{"valid_id2"}, + UIDIdentifier{"invalid id; too long. " + strings.Repeat(".", 128)}, + UIDIdentifier{"valid_id3"}, + UIDIdentifier{"valid_id4"}, + } - getUsersResult, err := client.GetUsers(context.Background(), identifiers) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers([list_with_bad_identifier]) should fail") - } - if err.Error() != "uid string must not be longer than 128 characters" { - t.Errorf( - "GetUsers([too-long]) returned an error of '%s'; "+ - "expected 'uid string must not be longer than 128 characters'", - err.Error()) - } - }) + getUsersResult, err := client.GetUsers(context.Background(), identifiers) + if getUsersResult != nil || err == nil { + t.Fatalf("GetUsers([list_with_bad_identifier]) should fail") + } + if err.Error() != "uid string must not be longer than 128 characters" { + t.Errorf( + "GetUsers([too-long]) returned an error of '%s'; "+ + "expected 'uid string must not be longer than 128 characters'", + err.Error()) + } +} - t.Run("returns users by various identifier types in a single call", func(t *testing.T) { - mockUsers := []byte(` +func TestGetUsersMultipleIdentifierTypes(t *testing.T) { + mockUsers := []byte(` { "users": [{ "localId": "uid1", @@ -384,39 +383,38 @@ func TestGetUsers(t *testing.T) { }] }] }`) - s := echoServer(mockUsers, t) - defer s.Close() + s := echoServer(mockUsers, t) + defer s.Close() - identifiers := []UserIdentifier{ - &UIDIdentifier{"uid1"}, - &EmailIdentifier{"user2@example.com"}, - &PhoneIdentifier{"+15555550003"}, - &ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, - &UIDIdentifier{"this-user-doesnt-exist"}, - } + identifiers := []UserIdentifier{ + &UIDIdentifier{"uid1"}, + &EmailIdentifier{"user2@example.com"}, + &PhoneIdentifier{"+15555550003"}, + &ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, + &UIDIdentifier{"this-user-doesnt-exist"}, + } - getUsersResult, err := s.Client.GetUsers(context.Background(), identifiers) - if err != nil { - t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) - } + getUsersResult, err := s.Client.GetUsers(context.Background(), identifiers) + if err != nil { + t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) + } - if !sameUsers(getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) { - t.Errorf("GetUsers([valid identifiers]) = %v; want = (uids from) %v (in any order)", - getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) - } - if len(getUsersResult.NotFound) != 1 { - t.Errorf("GetUsers([valid identifiers with one not found]) = %d; want = 1", - len(getUsersResult.NotFound)) + if !sameUsers(getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) { + t.Errorf("GetUsers([valid identifiers]) = %v; want = (uids from) %v (in any order)", + getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) + } + if len(getUsersResult.NotFound) != 1 { + t.Errorf("GetUsers([valid identifiers with one not found]) = %d; want = 1", + len(getUsersResult.NotFound)) + } else { + if id, ok := getUsersResult.NotFound[0].(*UIDIdentifier); !ok { + t.Errorf("GetUsers([...]).NotFound[0] not a UIDIdentifier") } else { - if id, ok := getUsersResult.NotFound[0].(*UIDIdentifier); !ok { - t.Errorf("GetUsers([...]).NotFound[0] not a UIDIdentifier") - } else { - if id.UID != "this-user-doesnt-exist" { - t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want = 'this-user-doesnt-exist'", id.UID) - } + if id.UID != "this-user-doesnt-exist" { + t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want = 'this-user-doesnt-exist'", id.UID) } } - }) + } } func TestGetNonExistingUser(t *testing.T) { diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index 19fc8d4d..d9d8581d 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -93,98 +93,27 @@ func TestGetNonExistingUser(t *testing.T) { } func TestGetUsers(t *testing.T) { - testUser1 := (&auth.UserToCreate{}). - UID("uid1"). - Email("user1@example.com"). - PhoneNumber("+15555550001") - testUser2 := (&auth.UserToCreate{}). - UID("uid2"). - Email("user2@example.com"). - PhoneNumber("+15555550002") - testUser3 := (&auth.UserToCreate{}). - UID("uid3"). - Email("user3@example.com"). - PhoneNumber("+15555550003") + var testUser1 *auth.UserRecord + var testUser2 *auth.UserRecord + var testUser3 *auth.UserRecord + importUser1UID := randomUID() importUser1 := (&auth.UserToImport{}). - UID("uid4"). - Email("user4@example.com"). - PhoneNumber("+15555550004"). + UID(importUser1UID). + Email(randomEmail(importUser1UID)). + PhoneNumber(randomPhoneNumber()). ProviderData([](*auth.UserProvider){ &auth.UserProvider{ ProviderID: "google.com", - UID: "google_uid4", + UID: "google_" + importUser1UID, }, }) - // Creates/imports all test users. If a failure occurs, the remaining test - // users will not be created/imported. - createTestUsers := func() error { - var err error - - // Helper to create a user and return its UserRecord. Upon error, sets the - // err variable. - createUser := func(userToCreate *auth.UserToCreate) *auth.UserRecord { - if err != nil { - return nil - } - var userRecord *auth.UserRecord - userRecord, err = client.CreateUser(context.Background(), userToCreate) - if err != nil { - err = fmt.Errorf("Unable to create user %v: %v", *userToCreate, err) - return nil - } - return userRecord - } - - // Helper to import a user and return its UserRecord. Upon error, sets the - // err variable. `uid` must match the UID set on the `userToImport` - // parameter. - importUser := func(uid string, userToImport *auth.UserToImport) *auth.UserRecord { - if err != nil { - return nil - } - var userImportResult *auth.UserImportResult - userImportResult, err = client.ImportUsers( - context.Background(), [](*auth.UserToImport){userToImport}) - if err != nil { - err = fmt.Errorf("Unable to import user %v (uid %v): %v", *userToImport, uid, err) - return nil - } - - if userImportResult.FailureCount > 0 { - err = fmt.Errorf("Unable to import user %v (uid %v): %v", *userToImport, uid, userImportResult.Errors[0].Reason) - return nil - } - if userImportResult.SuccessCount != 1 { - err = fmt.Errorf("Import didn't fail, but it didn't succeed either?") - return nil - } - - var userRecord *auth.UserRecord - userRecord, err = client.GetUser(context.Background(), uid) - if err != nil { - return nil - } - return userRecord - } - - createUser(testUser1) - createUser(testUser2) - createUser(testUser3) - - importUser("uid4", importUser1) - - return err - } - - // Delete all test users. This function will attempt to delete all test users - // even if a failure occurs. - deleteTestUsers := func() { - createdUsersUids := []string{"uid1", "uid2", "uid3", "uid4"} - for i := range createdUsersUids { - client.DeleteUser(context.Background(), createdUsersUids[i]) - } + createTestUsers := func(t *testing.T) { + testUser1 = newUserWithParams(t) + testUser2 = newUserWithParams(t) + testUser3 = newUserWithParams(t) + importUser(t, importUser1UID, importUser1) } // Checks to see if the users list contain the given uids. Order is ignored. @@ -192,7 +121,7 @@ func TestGetUsers(t *testing.T) { // Behaviour is undefined if there are duplicate entries in either of the // slices. // - // This function is identical to the one in integration/auth/user_mgt_test.go + // This function is identical to the one in auth/user_mgt_test.go sameUsers := func(users [](*auth.UserRecord), uids []string) bool { if len(users) != len(uids) { return false @@ -214,45 +143,50 @@ func TestGetUsers(t *testing.T) { return true } - // Delete all the users that we're about to create (in case they were left - // over from a prior run). - deleteTestUsers() + createTestUsers(t) + defer deleteUser(testUser1.UID) + defer deleteUser(testUser2.UID) + defer deleteUser(testUser3.UID) + defer deleteUser(importUser1UID) - defer deleteTestUsers() - if err := createTestUsers(); err != nil { - t.Fatalf("Unable to create the test users: %v", err) + userRecordsToUIDs := func(users [](*auth.UserRecord)) []string { + results := []string{} + for i := range users { + results = append(results, users[i].UID) + } + return results } t.Run("returns users by various identifier types in a single call", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UIDIdentifier{UID: "uid1"}, - auth.EmailIdentifier{Email: "user2@example.com"}, - auth.PhoneIdentifier{PhoneNumber: "+15555550003"}, - auth.ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid4"}, + auth.UIDIdentifier{UID: testUser1.UID}, + auth.EmailIdentifier{Email: testUser2.Email}, + auth.PhoneIdentifier{PhoneNumber: testUser3.PhoneNumber}, + auth.ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_" + importUser1UID}, }) if err != nil { t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) } - if !sameUsers(getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) { + if !sameUsers(getUsersResult.Users, []string{testUser1.UID, testUser2.UID, testUser3.UID, importUser1UID}) { t.Errorf("GetUsers([valid identifiers]) = %v; want = %v (in any order)", - getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) + userRecordsToUIDs(getUsersResult.Users), []string{testUser1.UID, testUser2.UID, testUser3.UID, importUser1UID}) } }) t.Run("returns found users and ignores non-existing users", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UIDIdentifier{UID: "uid1"}, + auth.UIDIdentifier{UID: testUser1.UID}, auth.UIDIdentifier{UID: "uid_that_doesnt_exist"}, - auth.UIDIdentifier{UID: "uid3"}, + auth.UIDIdentifier{UID: testUser3.UID}, }) if err != nil { t.Fatalf("GetUsers([...]) returned an error: %v", err) } - if !sameUsers(getUsersResult.Users, []string{"uid1", "uid3"}) { + if !sameUsers(getUsersResult.Users, []string{testUser1.UID, testUser3.UID}) { t.Errorf("GetUsers([valid identifiers]) = %v; want = %v (in any order)", - getUsersResult.Users, []string{"uid1", "uid3"}) + getUsersResult.Users, []string{testUser1.UID, testUser3.UID}) } if len(getUsersResult.NotFound) != 1 { t.Errorf("len(GetUsers([...]).NotFound) = %d; want 1", len(getUsersResult.NotFound)) @@ -287,8 +221,8 @@ func TestGetUsers(t *testing.T) { t.Run("de-dups duplicate users", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ - auth.UIDIdentifier{UID: "uid1"}, - auth.UIDIdentifier{UID: "uid1"}, + auth.UIDIdentifier{UID: testUser1.UID}, + auth.UIDIdentifier{UID: testUser1.UID}, }) if err != nil { t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) @@ -297,8 +231,8 @@ func TestGetUsers(t *testing.T) { if len(getUsersResult.Users) != 1 { t.Errorf("len(GetUsers([...]).Users) = %d; want = 1", len(getUsersResult.Users)) } else { - if getUsersResult.Users[0].UID != "uid1" { - t.Errorf("GetUsers([...]).Users[0].UID = %s; want = 'uid1'", getUsersResult.Users[0].UID) + if getUsersResult.Users[0].UID != testUser1.UID { + t.Errorf("GetUsers([...]).Users[0].UID = %s; want = '%s'", getUsersResult.Users[0].UID, testUser1.UID) } } if len(getUsersResult.NotFound) != 0 { @@ -344,6 +278,33 @@ func TestGetUsers(t *testing.T) { }) } +func TestLastRefreshTime(t *testing.T) { + userRecord := newUserWithParams(t) + defer deleteUser(userRecord.UID) + + // New users should not have a LastRefreshTimestamp set. + if userRecord.UserMetadata.LastRefreshTimestamp != 0 { + t.Errorf( + "CreateUser(...).UserMetadata.LastRefreshTimestamp = %d; want = 0", + userRecord.UserMetadata.LastRefreshTimestamp) + } + + // Login to cause the LastRefreshTimestamp to be set + if _, err := signInWithPassword(userRecord.Email, "password"); err != nil { + t.Errorf("signInWithPassword failed: %v", err) + } + + getUsersResult, err := client.GetUser(context.Background(), userRecord.UID) + if err != nil { + t.Fatalf("GetUser(...) failed with error: %v", err) + } + if getUsersResult.UserMetadata.LastRefreshTimestamp <= 0 { + t.Errorf( + "GetUser(...).UserMetadata.LastRefreshTimestamp = %d; want > 0", + getUsersResult.UserMetadata.LastRefreshTimestamp) + } +} + func TestUpdateNonExistingUser(t *testing.T) { update := (&auth.UserToUpdate{}).Email("test@example.com") user, err := client.UpdateUser(context.Background(), "non.existing", update) @@ -588,15 +549,6 @@ func TestDeleteUser(t *testing.T) { } func TestDeleteUsers(t *testing.T) { - // Creates a user and returns its uid. Upon failure, triggers t.Fatalf(). - createTestUserOrDie := func(t *testing.T) string { - userRecord, err := client.CreateUser(context.Background(), &auth.UserToCreate{}) - if err != nil { - t.Fatalf("CreateUser({}) error %v; want nil", err) - } - return userRecord.UID - } - // Ensures the specified users don't exist. Expected to be called after // deleting the users to ensure the delete method worked. ensureUsersNotFound := func(t *testing.T, uids []string) { @@ -608,37 +560,30 @@ func TestDeleteUsers(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), identifiers) if err != nil { t.Errorf("GetUsers(notfound_ids) error %v; want nil", err) - } else { - ok := true - if len(getUsersResult.Users) != 0 { - t.Errorf("len(GetUsers(notfound_ids).Users) = %d; want 0", len(getUsersResult.Users)) - ok = false - } - if len(getUsersResult.NotFound) != len(uids) { - t.Errorf("len(GetUsers(notfound_ids).NotFound) = %d; want %d", len(getUsersResult.NotFound), len(uids)) - ok = false - } - if !ok { - t.FailNow() - } + return + } - sort.Strings(uids) - notFoundUids := []string{} - for i := range getUsersResult.NotFound { - notFoundUids = append(notFoundUids, getUsersResult.NotFound[i].(auth.UIDIdentifier).UID) - } - sort.Strings(notFoundUids) - for i := range uids { - if notFoundUids[i] != uids[i] { - t.Errorf("GetUsers(deleted_ids).NotFound[%d] = %s; want %s", i, notFoundUids[i], uids[i]) - } + if len(getUsersResult.NotFound) != len(uids) { + t.Errorf("len(GetUsers(notfound_ids).NotFound) = %d; want %d", len(getUsersResult.NotFound), len(uids)) + return + } + + sort.Strings(uids) + notFoundUids := []string{} + for i := range getUsersResult.NotFound { + notFoundUids = append(notFoundUids, getUsersResult.NotFound[i].(auth.UIDIdentifier).UID) + } + sort.Strings(notFoundUids) + for i := range uids { + if notFoundUids[i] != uids[i] { + t.Errorf("GetUsers(deleted_ids).NotFound[%d] = %s; want %s", i, notFoundUids[i], uids[i]) } } } t.Run("deletes users", func(t *testing.T) { uids := []string{ - createTestUserOrDie(t), createTestUserOrDie(t), createTestUserOrDie(t), + newUserWithParams(t).UID, newUserWithParams(t).UID, newUserWithParams(t).UID, } result, err := client.DeleteUsers(context.Background(), uids) @@ -660,7 +605,7 @@ func TestDeleteUsers(t *testing.T) { }) t.Run("deletes users that exist even when non-existing users also specified", func(t *testing.T) { - uids := []string{createTestUserOrDie(t), "uid-that-doesnt-exist"} + uids := []string{newUserWithParams(t).UID, "uid-that-doesnt-exist"} result, err := client.DeleteUsers(context.Background(), uids) if err != nil { t.Fatalf("DeleteUsers(uids) error %v; want nil", err) @@ -697,7 +642,7 @@ func TestDeleteUsers(t *testing.T) { } } - uids := []string{createTestUserOrDie(t)} + uids := []string{newUserWithParams(t).UID} deleteUserAndEnsureSuccess(t, uids) // Delete the user again, ensuring that everything still counts as a success. @@ -1031,3 +976,26 @@ func newUserWithParams(t *testing.T) *auth.UserRecord { } return user } + +// Helper to import a user and return its UserRecord. Upon error, exits via +// t.Fatalf. `uid` must match the UID set on the `userToImport` parameter. +func importUser(t *testing.T, uid string, userToImport *auth.UserToImport) *auth.UserRecord { + userImportResult, err := client.ImportUsers( + context.Background(), [](*auth.UserToImport){userToImport}) + if err != nil { + t.Fatalf("Unable to import user %v (uid %v): %v", *userToImport, uid, err) + } + + if userImportResult.FailureCount > 0 { + t.Fatalf("Unable to import user %v (uid %v): %v", *userToImport, uid, userImportResult.Errors[0].Reason) + } + if userImportResult.SuccessCount != 1 { + t.Fatalf("Import didn't fail, but it didn't succeed either?") + } + + userRecord, err := client.GetUser(context.Background(), uid) + if err != nil { + t.Fatalf("GetUser(%s) for imported user failed: %v", uid, err) + } + return userRecord +} From 14c7264f5e719e375d80338c85375b6b33ab9a55 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Tue, 28 Jan 2020 14:35:24 -0500 Subject: [PATCH 12/20] review feedback 3 --- auth/user_mgt.go | 44 +++++++----- auth/user_mgt_test.go | 87 ++++++++--------------- integration/auth/user_mgt_test.go | 111 +++++++++--------------------- 3 files changed, 90 insertions(+), 152 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index 7aaa3880..81d3c673 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -583,8 +583,8 @@ func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord // A UserIdentifier identifies a user to be looked up. type UserIdentifier interface { - matchesUserRecord(ur *UserRecord) bool - populateRequest(req *getAccountInfoRequest) + matches(ur *UserRecord) bool + populate(req *getAccountInfoRequest) } // A UIDIdentifier is used for looking up an account by uid. @@ -594,11 +594,11 @@ type UIDIdentifier struct { UID string } -func (id UIDIdentifier) matchesUserRecord(ur *UserRecord) bool { +func (id UIDIdentifier) matches(ur *UserRecord) bool { return id.UID == ur.UID } -func (id UIDIdentifier) populateRequest(req *getAccountInfoRequest) { +func (id UIDIdentifier) populate(req *getAccountInfoRequest) { req.LocalID = append(req.LocalID, id.UID) } @@ -609,11 +609,11 @@ type EmailIdentifier struct { Email string } -func (id EmailIdentifier) matchesUserRecord(ur *UserRecord) bool { +func (id EmailIdentifier) matches(ur *UserRecord) bool { return id.Email == ur.Email } -func (id EmailIdentifier) populateRequest(req *getAccountInfoRequest) { +func (id EmailIdentifier) populate(req *getAccountInfoRequest) { req.Email = append(req.Email, id.Email) } @@ -624,11 +624,11 @@ type PhoneIdentifier struct { PhoneNumber string } -func (id PhoneIdentifier) matchesUserRecord(ur *UserRecord) bool { +func (id PhoneIdentifier) matches(ur *UserRecord) bool { return id.PhoneNumber == ur.PhoneNumber } -func (id PhoneIdentifier) populateRequest(req *getAccountInfoRequest) { +func (id PhoneIdentifier) populate(req *getAccountInfoRequest) { req.PhoneNumber = append(req.PhoneNumber, id.PhoneNumber) } @@ -640,7 +640,7 @@ type ProviderIdentifier struct { ProviderUID string } -func (id ProviderIdentifier) matchesUserRecord(ur *UserRecord) bool { +func (id ProviderIdentifier) matches(ur *UserRecord) bool { for _, userInfo := range ur.ProviderUserInfo { if id.ProviderID == userInfo.ProviderID && id.ProviderUID == userInfo.UID { return true @@ -649,7 +649,7 @@ func (id ProviderIdentifier) matchesUserRecord(ur *UserRecord) bool { return false } -func (id ProviderIdentifier) populateRequest(req *getAccountInfoRequest) { +func (id ProviderIdentifier) populate(req *getAccountInfoRequest) { req.FederatedUserID = append( req.FederatedUserID, federatedUserIdentifier{ProviderID: id.ProviderID, RawID: id.ProviderUID}) @@ -708,14 +708,29 @@ func (req *getAccountInfoRequest) validate() error { func isUserFound(id UserIdentifier, urs [](*UserRecord)) bool { for i := range urs { - match := id.matchesUserRecord(urs[i]) - if match { + if id.matches(urs[i]) { return true } } return false } +// Gets the user data corresponding to the specified identifiers. +// +// There are no ordering guarantees; in particular, the nth entry in the users +// result list is not guaranteed to correspond to the nth entry in the input +// parameters list. +// +// Only a maximum of 100 identifiers may be supplied. If more than 100 +// identifiers are supplied, this method will immediately return an error. +// +// Parameters: +//   identifiers: The identifiers used to indicate which user records should be +// returned. Must have <= 100 entries. +// +// Returns: The corresponding user records. An error is returned instead if any +// of the identifiers are invalid or if more than 100 identifiers are +// specified. func (c *baseClient) GetUsers( ctx context.Context, identifiers []UserIdentifier, ) (*GetUsersResult, error) { @@ -728,7 +743,7 @@ func (c *baseClient) GetUsers( var request getAccountInfoRequest for i := range identifiers { - identifiers[i].populateRequest(&request) + identifiers[i].populate(&request) } if err := request.validate(); err != nil { @@ -748,9 +763,6 @@ func (c *baseClient) GetUsers( } userRecords = append(userRecords, userRecord) } - if len(identifiers) < len(userRecords) { - return nil, errors.New("GetUsers() returned more results than requested") - } var notFound []UserIdentifier for i := range identifiers { diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index 4100e52d..0ee5c678 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -196,14 +196,11 @@ func TestGetUsersExceeds100(t *testing.T) { } getUsersResult, err := client.GetUsers(context.Background(), identifiers[:]) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers(too-many-identifiers) should fail") - } - if err.Error() != "`identifiers` parameter must have <= 100 entries" { + want := "`identifiers` parameter must have <= 100 entries" + if getUsersResult != nil || err == nil || err.Error() != want { t.Errorf( - "GetUsers(too-many-identifiers) returned an error of '%s'; "+ - "expected '`identifiers` parameter must have <= 100 entries'", - err.Error()) + "GetUsers() = (%v, %q); want = (nil, %q)", + getUsersResult, err, want) } } @@ -214,7 +211,7 @@ func TestGetUsersEmpty(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), [](UserIdentifier){}) if getUsersResult == nil || err != nil { - t.Fatalf("GetUsers([]) failed with: %s", err.Error()) + t.Fatalf("GetUsers([]) = %q", err) } if len(getUsersResult.Users) != 0 { @@ -236,21 +233,21 @@ func TestGetUsersAllNonExisting(t *testing.T) { notFoundIds := []UserIdentifier{&UIDIdentifier{"id that doesnt exist"}} getUsersResult, err := s.Client.GetUsers(context.Background(), notFoundIds) if err != nil { - t.Fatalf("GetUsers(['id that doesnt exist']) failed with %v", err) + t.Fatalf("GetUsers() = %q", err) } if len(getUsersResult.Users) != 0 { t.Errorf( - "len(GetUsers(['id that doesnt exist']).Users) = %d; want 0", + "len(GetUsers().Users) = %d; want 0", len(getUsersResult.Users)) } if len(getUsersResult.NotFound) != len(notFoundIds) { - t.Errorf("len(GetUsers['id that doesnt exist']).NotFound) = %d; want %d", + t.Errorf("len(GetUsers()).NotFound) = %d; want %d", len(getUsersResult.NotFound), len(notFoundIds)) } else { for i := range notFoundIds { if getUsersResult.NotFound[i] != notFoundIds[i] { - t.Errorf("GetUsers['id that doesnt exist']).NotFound[%d] = %v; want %v", + t.Errorf("GetUsers().NotFound[%d] = %v; want %v", i, getUsersResult.NotFound[i], notFoundIds[i]) } } @@ -265,14 +262,9 @@ func TestGetUsersInvalidUid(t *testing.T) { getUsersResult, err := client.GetUsers( context.Background(), []UserIdentifier{&UIDIdentifier{"too long " + strings.Repeat(".", 128)}}) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers([too-long]) should fail") - } - if err.Error() != "uid string must not be longer than 128 characters" { - t.Errorf( - "GetUsers([too-long]) returned an error of '%s'; "+ - "expected 'uid string must not be longer than 128 characters'", - err.Error()) + want := "uid string must not be longer than 128 characters" + if getUsersResult != nil || err == nil || err.Error() != want { + t.Errorf("GetUsers() = (%v, %q); want = (nil, %q)", getUsersResult, err, want) } } @@ -284,14 +276,9 @@ func TestGetUsersInvalidEmail(t *testing.T) { getUsersResult, err := client.GetUsers( context.Background(), []UserIdentifier{EmailIdentifier{"invalid email addr"}}) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers([invalid email addr]) should fail") - } - if err.Error() != `malformed email string: "invalid email addr"` { - t.Errorf( - "GetUsers([invalid email addr]) returned an error of '%s'; "+ - `"expected 'malformed email string: "invalid email addr"'"`, - err.Error()) + want := `malformed email string: "invalid email addr"` + if getUsersResult != nil || err == nil || err.Error() != want { + t.Errorf("GetUsers() = (%v, %q); want = (nil, %q)", getUsersResult, err, want) } } @@ -303,14 +290,9 @@ func TestGetUsersInvalidPhoneNumber(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []UserIdentifier{ PhoneIdentifier{"invalid phone number"}, }) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers([invalid phone number]) should fail") - } - if err.Error() != "phone number must be a valid, E.164 compliant identifier" { - t.Errorf( - "GetUsers([invalid phone number]) returned an error of '%s'; "+ - "expected 'phone number must be a valid, E.164 compliant identifier'", - err.Error()) + want := "phone number must be a valid, E.164 compliant identifier" + if getUsersResult != nil || err == nil || err.Error() != want { + t.Errorf("GetUsers() = (%v, %q); want = (nil, %q)", getUsersResult, err, want) } } @@ -322,14 +304,9 @@ func TestGetUsersInvalidProvider(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []UserIdentifier{ ProviderIdentifier{ProviderID: "", ProviderUID: ""}, }) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers([invalid provider]) should fail") - } - if err.Error() != "providerID must be a non-empty string" { - t.Errorf( - "GetUsers([invalid provider]) returned an error of '%s'; "+ - "expected 'providerID must be a non-empty string'", - err.Error()) + want := "providerID must be a non-empty string" + if getUsersResult != nil || err == nil || err.Error() != want { + t.Errorf("GetUsers() = (%v, %q); want = (nil, %q)", getUsersResult, err, want) } } @@ -347,14 +324,9 @@ func TestGetUsersSingleBadIdentifier(t *testing.T) { } getUsersResult, err := client.GetUsers(context.Background(), identifiers) - if getUsersResult != nil || err == nil { - t.Fatalf("GetUsers([list_with_bad_identifier]) should fail") - } - if err.Error() != "uid string must not be longer than 128 characters" { - t.Errorf( - "GetUsers([too-long]) returned an error of '%s'; "+ - "expected 'uid string must not be longer than 128 characters'", - err.Error()) + want := "uid string must not be longer than 128 characters" + if getUsersResult != nil || err == nil || err.Error() != want { + t.Errorf("GetUsers() = (%v, %q); want = (nil, %q)", getUsersResult, err, want) } } @@ -396,22 +368,21 @@ func TestGetUsersMultipleIdentifierTypes(t *testing.T) { getUsersResult, err := s.Client.GetUsers(context.Background(), identifiers) if err != nil { - t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) + t.Fatalf("GetUsers() = %q", err) } if !sameUsers(getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) { - t.Errorf("GetUsers([valid identifiers]) = %v; want = (uids from) %v (in any order)", + t.Errorf("GetUsers() = %v; want = (uids from) %v (in any order)", getUsersResult.Users, []string{"uid1", "uid2", "uid3", "uid4"}) } if len(getUsersResult.NotFound) != 1 { - t.Errorf("GetUsers([valid identifiers with one not found]) = %d; want = 1", - len(getUsersResult.NotFound)) + t.Errorf("GetUsers() = %d; want = 1", len(getUsersResult.NotFound)) } else { if id, ok := getUsersResult.NotFound[0].(*UIDIdentifier); !ok { - t.Errorf("GetUsers([...]).NotFound[0] not a UIDIdentifier") + t.Errorf("GetUsers().NotFound[0] not a UIDIdentifier") } else { if id.UID != "this-user-doesnt-exist" { - t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want = 'this-user-doesnt-exist'", id.UID) + t.Errorf("GetUsers().NotFound[0].UID = %s; want = 'this-user-doesnt-exist'", id.UID) } } } diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index d9d8581d..3c1011e2 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -93,29 +93,6 @@ func TestGetNonExistingUser(t *testing.T) { } func TestGetUsers(t *testing.T) { - var testUser1 *auth.UserRecord - var testUser2 *auth.UserRecord - var testUser3 *auth.UserRecord - - importUser1UID := randomUID() - importUser1 := (&auth.UserToImport{}). - UID(importUser1UID). - Email(randomEmail(importUser1UID)). - PhoneNumber(randomPhoneNumber()). - ProviderData([](*auth.UserProvider){ - &auth.UserProvider{ - ProviderID: "google.com", - UID: "google_" + importUser1UID, - }, - }) - - createTestUsers := func(t *testing.T) { - testUser1 = newUserWithParams(t) - testUser2 = newUserWithParams(t) - testUser3 = newUserWithParams(t) - importUser(t, importUser1UID, importUser1) - } - // Checks to see if the users list contain the given uids. Order is ignored. // // Behaviour is undefined if there are duplicate entries in either of the @@ -143,10 +120,25 @@ func TestGetUsers(t *testing.T) { return true } - createTestUsers(t) + testUser1 := newUserWithParams(t) defer deleteUser(testUser1.UID) + testUser2 := newUserWithParams(t) defer deleteUser(testUser2.UID) + testUser3 := newUserWithParams(t) defer deleteUser(testUser3.UID) + + importUser1UID := randomUID() + importUser1 := (&auth.UserToImport{}). + UID(importUser1UID). + Email(randomEmail(importUser1UID)). + PhoneNumber(randomPhoneNumber()). + ProviderData([](*auth.UserProvider){ + &auth.UserProvider{ + ProviderID: "google.com", + UID: "google_" + importUser1UID, + }, + }) + importUser(t, importUser1UID, importUser1) defer deleteUser(importUser1UID) userRecordsToUIDs := func(users [](*auth.UserRecord)) []string { @@ -157,7 +149,7 @@ func TestGetUsers(t *testing.T) { return results } - t.Run("returns users by various identifier types in a single call", func(t *testing.T) { + t.Run("various identifier types", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ auth.UIDIdentifier{UID: testUser1.UID}, auth.EmailIdentifier{Email: testUser2.Email}, @@ -165,55 +157,55 @@ func TestGetUsers(t *testing.T) { auth.ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_" + importUser1UID}, }) if err != nil { - t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) + t.Fatalf("GetUsers() = %q", err) } if !sameUsers(getUsersResult.Users, []string{testUser1.UID, testUser2.UID, testUser3.UID, importUser1UID}) { - t.Errorf("GetUsers([valid identifiers]) = %v; want = %v (in any order)", + t.Errorf("GetUsers() = %v; want = %v (in any order)", userRecordsToUIDs(getUsersResult.Users), []string{testUser1.UID, testUser2.UID, testUser3.UID, importUser1UID}) } }) - t.Run("returns found users and ignores non-existing users", func(t *testing.T) { + t.Run("mix of existing and non-existing users", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ auth.UIDIdentifier{UID: testUser1.UID}, auth.UIDIdentifier{UID: "uid_that_doesnt_exist"}, auth.UIDIdentifier{UID: testUser3.UID}, }) if err != nil { - t.Fatalf("GetUsers([...]) returned an error: %v", err) + t.Fatalf("GetUsers() = %q", err) } if !sameUsers(getUsersResult.Users, []string{testUser1.UID, testUser3.UID}) { - t.Errorf("GetUsers([valid identifiers]) = %v; want = %v (in any order)", + t.Errorf("GetUsers() = %v; want = %v (in any order)", getUsersResult.Users, []string{testUser1.UID, testUser3.UID}) } if len(getUsersResult.NotFound) != 1 { - t.Errorf("len(GetUsers([...]).NotFound) = %d; want 1", len(getUsersResult.NotFound)) + t.Errorf("len(GetUsers().NotFound) = %d; want 1", len(getUsersResult.NotFound)) } else { if getUsersResult.NotFound[0].(auth.UIDIdentifier).UID != "uid_that_doesnt_exist" { - t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want 'uid_that_doesnt_exist'", + t.Errorf("GetUsers().NotFound[0].UID = %s; want 'uid_that_doesnt_exist'", getUsersResult.NotFound[0].(auth.UIDIdentifier).UID) } } }) - t.Run("returns nothing when queried for only non-existing users", func(t *testing.T) { + t.Run("only non-existing users", func(t *testing.T) { getUsersResult, err := client.GetUsers(context.Background(), []auth.UserIdentifier{ auth.UIDIdentifier{UID: "non-existing user"}, }) if err != nil { - t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) + t.Fatalf("GetUsers() = %q", err) } if len(getUsersResult.Users) != 0 { - t.Errorf("len(GetUsers([...]).Users) = %d; want = 0", len(getUsersResult.Users)) + t.Errorf("len(GetUsers().Users) = %d; want = 0", len(getUsersResult.Users)) } if len(getUsersResult.NotFound) != 1 { - t.Errorf("len(GetUsers([...]).NotFound) = %d; want = 1", len(getUsersResult.NotFound)) + t.Errorf("len(GetUsers().NotFound) = %d; want = 1", len(getUsersResult.NotFound)) } else { if getUsersResult.NotFound[0].(auth.UIDIdentifier).UID != "non-existing user" { - t.Errorf("GetUsers([...]).NotFound[0].UID = %s; want 'non-existing user'", + t.Errorf("GetUsers().NotFound[0].UID = %s; want 'non-existing user'", getUsersResult.NotFound[0].(auth.UIDIdentifier).UID) } } @@ -225,55 +217,18 @@ func TestGetUsers(t *testing.T) { auth.UIDIdentifier{UID: testUser1.UID}, }) if err != nil { - t.Fatalf("GetUsers([valid identifiers]) returned an error: %v", err) + t.Fatalf("GetUsers() returned an error: %v", err) } if len(getUsersResult.Users) != 1 { - t.Errorf("len(GetUsers([...]).Users) = %d; want = 1", len(getUsersResult.Users)) + t.Errorf("len(GetUsers().Users) = %d; want = 1", len(getUsersResult.Users)) } else { if getUsersResult.Users[0].UID != testUser1.UID { - t.Errorf("GetUsers([...]).Users[0].UID = %s; want = '%s'", getUsersResult.Users[0].UID, testUser1.UID) + t.Errorf("GetUsers().Users[0].UID = %s; want = '%s'", getUsersResult.Users[0].UID, testUser1.UID) } } if len(getUsersResult.NotFound) != 0 { - t.Errorf("len(GetUsers([...]).NotFound) = %d; want = 0", len(getUsersResult.NotFound)) - } - }) - - t.Run("returns users with a LastRefreshTimestamp", func(t *testing.T) { - // Delete user that we're about to create (in case it was left over from a - // prior run). - client.DeleteUser(context.Background(), "lastRefreshTimeUser") - defer client.DeleteUser(context.Background(), "lastRefreshTimeUser") - newUserRecord, err := client.CreateUser(context.Background(), (&auth.UserToCreate{}). - UID("lastRefreshTimeUser"). - Email("lastRefreshTimeUser@example.com"). - Password("p4ssword")) - if err != nil { - t.Fatalf("Unable to create lastRefreshTimeUser: %v", err) - } - - // New users should not have a LastRefreshTimestamp set. - if newUserRecord.UserMetadata.LastRefreshTimestamp != 0 { - t.Errorf( - "CreateUser(...).UserMetadata.LastRefreshTimestamp = %d; want = 0", - newUserRecord.UserMetadata.LastRefreshTimestamp) - } - - // Login to cause the LastRefreshTimestamp to be set - _, err = signInWithPassword("lastRefreshTimeUser@example.com", "p4ssword") - if err != nil { - t.Errorf("signInWithPassword failed: %v", err) - } - - getUsersResult, err := client.GetUser(context.Background(), "lastRefreshTimeUser") - if err != nil { - t.Fatalf("GetUser(...) failed with error: %v", err) - } - if getUsersResult.UserMetadata.LastRefreshTimestamp <= 0 { - t.Errorf( - "GetUser(...).UserMetadata.LastRefreshTimestamp = %d; want > 0", - getUsersResult.UserMetadata.LastRefreshTimestamp) + t.Errorf("len(GetUsers().NotFound) = %d; want = 0", len(getUsersResult.NotFound)) } }) } From 454e81c1f909aeba3e0272c7d0c65fbc0d828fac Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Wed, 29 Jan 2020 11:46:24 -0500 Subject: [PATCH 13/20] review feedback 4 --- auth/user_mgt.go | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index 81d3c673..a0fd1ea0 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -715,7 +715,7 @@ func isUserFound(id UserIdentifier, urs [](*UserRecord)) bool { return false } -// Gets the user data corresponding to the specified identifiers. +// GetUsers returns the user data corresponding to the specified identifiers. // // There are no ordering guarantees; in particular, the nth entry in the users // result list is not guaranteed to correspond to the nth entry in the input @@ -724,13 +724,9 @@ func isUserFound(id UserIdentifier, urs [](*UserRecord)) bool { // Only a maximum of 100 identifiers may be supplied. If more than 100 // identifiers are supplied, this method will immediately return an error. // -// Parameters: -//   identifiers: The identifiers used to indicate which user records should be -// returned. Must have <= 100 entries. -// -// Returns: The corresponding user records. An error is returned instead if any -// of the identifiers are invalid or if more than 100 identifiers are -// specified. +// Returns the corresponding user records. An error is returned instead if any +// of the identifiers are invalid or if more than 100 identifiers are +// specified. func (c *baseClient) GetUsers( ctx context.Context, identifiers []UserIdentifier, ) (*GetUsersResult, error) { @@ -977,25 +973,25 @@ type DeleteUsersErrorInfo struct { Reason string `json:"message,omitEmpty"` } -// Deletes the users specified by the given identifiers. -// -// Deleting a non-existing user won't generate an error. (i.e. this method is idempotent.) -// Non-existing users will be considered to be successfully deleted, and will therefore be -// counted in the DeleteUsersResult.SuccessCount value. +// DeleteUsers deletes the users specified by the given identifiers. // -// Only a maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are -// supplied, this method will immediately return an error. +// Deleting a non-existing user won't generate an error. (i.e. this method is +// idempotent.) Non-existing users will be considered to be successfully +// deleted, and will therefore be counted in the DeleteUsersResult.SuccessCount +// value. // -// This API is currently rate limited at the server to 1 QPS. If you exceed this, you may get -// a quota exceeded error. Therefore, if you want to delete more than 1000 users, you may -// need to add a delay to ensure you don't go over this limit. +// Only a maximum of 1000 identifiers may be supplied. If more than 1000 +// identifiers are supplied, this method will immediately return an error. // -// Parameters: -// uids: The uids of the users to be deleted. Must have <= 1000 entries. +// This API is currently rate limited at the server to 1 QPS. If you exceed +// this, you may get a quota exceeded error. Therefore, if you want to delete +// more than 1000 users, you may need to add a delay to ensure you don't go +// over this limit. // -// Returns: The total number of successful/failed deletions, as well as the array of errors -// that correspond to the failed deletions. An error is returned if any of the identifiers -// are invalid or if more than 1000 identifiers are specified. +// Returns the total number of successful/failed deletions, as well as the +// array of errors that correspond to the failed deletions. An error is +// returned if any of the identifiers are invalid or if more than 1000 +// identifiers are specified. func (c *baseClient) DeleteUsers(ctx context.Context, uids []string) (*DeleteUsersResult, error) { if len(uids) == 0 { return &DeleteUsersResult{}, nil From aa4e7a42c1f55a3c756fc70d0ef6d885c14e8b40 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 19 Feb 2020 13:10:06 -0800 Subject: [PATCH 14/20] chore: Added Actions-based release workflow (#331) * chore: Added Actions-based release workflow * Set GOPATH * Fixed working directory for tests * Decrypting credentials into the testdata directory * Added preflight and post check scripts --- .../resources/integ-service-account.json.gpg | Bin 0 -> 1730 bytes .github/scripts/generate_changelog.sh | 79 +++++++++++++ .github/scripts/publish_post_check.sh | 105 ++++++++++++++++++ .github/scripts/publish_preflight_check.sh | 97 ++++++++++++++++ .github/scripts/run_all_tests.sh | 25 +++++ .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 64 +++++++++++ .github/workflows/stage.yml | 74 ++++++++++++ 8 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 .github/resources/integ-service-account.json.gpg create mode 100755 .github/scripts/generate_changelog.sh create mode 100755 .github/scripts/publish_post_check.sh create mode 100755 .github/scripts/publish_preflight_check.sh create mode 100755 .github/scripts/run_all_tests.sh create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/stage.yml diff --git a/.github/resources/integ-service-account.json.gpg b/.github/resources/integ-service-account.json.gpg new file mode 100644 index 0000000000000000000000000000000000000000..145389e0e839675ef92e2712638f3d39dc030fb0 GIT binary patch literal 1730 zcmV;z20i(V4Fm}T0?6uq1v?nDCI8au0gw$8IMvg~=Xcz9UM=*NZ5RCsFH>eO?p z`a^EFF$25U3A=Oe?N{hlAYsHIn1<-8dZs!_SjOD zNc0><*irbMG5XMAmkxEg7Vq=0eMf__RhX2*c>b%81@)u`1gZMPQ zGKW`slEt)@cq-#;Eka6FiLO2c17RPvwoe_$O7(KK55Ff^Y`gYK`IqN1^#GzsAzAJ= z2iDr$?bAYi^{Gd`W)N9aE82E}*GEJlD6Ne!1RN^h|GY+|I?_TzBrmGNd3k|BCt=0+ zlq$1BYyQT)E`HaX^j&P|2@!^L|L&AB=Lf3#hL**Mv%2jw}VoJ1h*g?JA6|73=1pI1!mf43=2 zb%CDko(ScSX&(QBwwJ#&0r2Z3yYpYQ#-@G3e0UUcdCF!@XBNWXu60L$0p+B zKGg9onju-)=jcEDq)n52gLbLRwjRV214Vkq>K)yt_%R!86j(%WjUkm}Z zkld=xUJ}5pRVi0DDM65R?RzmS0v4PdgPj2M!b?qkZeH2kblF4TE5-> z+wx^D+a8D9wMjvx4~3%w==`~DNy)e&VRq>gnoi~d3iv0n9CQwklD^ngCt&HclPN;sYPq-!;(NBWa-ANlB1|rWZQxe7e9&%C!t66F|w!x@^9{lBd zm@=95Fb+n~I{Gu3ck*o*973|Z*l`yJ&93|4`QmqDBP}U6XB)cr6-2&7LXU_`bGFyk z$Y)%4T^<`}!M7))ymrZhN^Q57bN)#fV)M1RDq1%H?Fta$qSF9MyS!B5`8nVdJ51rQPEAZ^O?zsM}O zPhp<9H7#qk*1~nY?Kwyy63(aXTuneshSsSuy&}{cmv3>jcURN+wzjMPjZ|=n$dPnu z3pt(#iEFy1Xy0d@+7OFlqfp{;Fb4x(T2gG= zxu=dWI=75#Cn`d2=HA$qYRp{goK~aRQ`g;(YjSYZc(^(j8n&X~78N1#$D!&gS(uVy zTk&c$0>XvD4+B$~g!xb<3yDfSCiPguMZ9Q}3jwv`{5kx)_{JLg1FHR6&o*QSe6RzF z4GnVbVJgjce+fXr>&#Wqkv)y#R2IFNldAdS%NJm;I{0nCTqEAhTe=%Tt z<4JG}+xXl(>w*ZIV`zr}LJNUbi)+LMRpFZE@Qk0%|Ag_xvgt)NC z%K0pcz3{!a01i+|RU%UUOua$O*?k5)SMr^Ww2)NKENK7-4bml?WYi~(Wm#GVOSk*k z?|?mdMfkR3z-u?0Ep@}-*86k*+PM7w71x@zPV)}3(x|hmy#3B z;;&>t_G6>4eDTRLb+rsozx@J<8Y`Etwhw&i0PZz#$84|6_7N*%xz*%dvH$=8 literal 0 HcmV?d00001 diff --git a/.github/scripts/generate_changelog.sh b/.github/scripts/generate_changelog.sh new file mode 100755 index 00000000..e393f40e --- /dev/null +++ b/.github/scripts/generate_changelog.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# Copyright 2020 Google Inc. +# +# 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. + +set -e +set -u + +function printChangelog() { + local TITLE=$1 + shift + # Skip the sentinel value. + local ENTRIES=("${@:2}") + if [ ${#ENTRIES[@]} -ne 0 ]; then + echo "### ${TITLE}" + echo "" + for ((i = 0; i < ${#ENTRIES[@]}; i++)) + do + echo "* ${ENTRIES[$i]}" + done + echo "" + fi +} + +if [[ -z "${GITHUB_SHA}" ]]; then + GITHUB_SHA="HEAD" +fi + +LAST_TAG=`git describe --tags $(git rev-list --tags --max-count=1) 2> /dev/null` || true +if [[ -z "${LAST_TAG}" ]]; then + echo "[INFO] No tags found. Including all commits up to ${GITHUB_SHA}." + VERSION_RANGE="${GITHUB_SHA}" +else + echo "[INFO] Last release tag: ${LAST_TAG}." + COMMIT_SHA=`git show-ref -s ${LAST_TAG}` + echo "[INFO] Last release commit: ${COMMIT_SHA}." + VERSION_RANGE="${COMMIT_SHA}..${GITHUB_SHA}" + echo "[INFO] Including all commits in the range ${VERSION_RANGE}." +fi + +echo "" + +# Older versions of Bash (< 4.4) treat empty arrays as unbound variables, which triggers +# errors when referencing them. Therefore we initialize each of these arrays with an empty +# sentinel value, and later skip them. +CHANGES=("") +FIXES=("") +FEATS=("") +MISC=("") + +while read -r line +do + COMMIT_MSG=`echo ${line} | cut -d ' ' -f 2-` + if [[ $COMMIT_MSG =~ ^change(\(.*\))?: ]]; then + CHANGES+=("$COMMIT_MSG") + elif [[ $COMMIT_MSG =~ ^fix(\(.*\))?: ]]; then + FIXES+=("$COMMIT_MSG") + elif [[ $COMMIT_MSG =~ ^feat(\(.*\))?: ]]; then + FEATS+=("$COMMIT_MSG") + else + MISC+=("${COMMIT_MSG}") + fi +done < <(git log ${VERSION_RANGE} --oneline) + +printChangelog "Breaking Changes" "${CHANGES[@]}" +printChangelog "New Features" "${FEATS[@]}" +printChangelog "Bug Fixes" "${FIXES[@]}" +printChangelog "Miscellaneous" "${MISC[@]}" diff --git a/.github/scripts/publish_post_check.sh b/.github/scripts/publish_post_check.sh new file mode 100755 index 00000000..8311ec25 --- /dev/null +++ b/.github/scripts/publish_post_check.sh @@ -0,0 +1,105 @@ + +#!/bin/bash + +# Copyright 2020 Google Inc. +# +# 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. + + +###################################### Outputs ##################################### + +# 1. version: The version of this release including the 'v' prefix (e.g. v1.2.3). +# 2. changelog: Formatted changelog text for this release. + +#################################################################################### + +set -e +set -u + +function echo_info() { + local MESSAGE=$1 + echo "[INFO] ${MESSAGE}" +} + +function echo_warn() { + local MESSAGE=$1 + echo "[WARN] ${MESSAGE}" +} + +function terminate() { + echo "" + echo_warn "--------------------------------------------" + echo_warn "POST CHECK FAILED" + echo_warn "--------------------------------------------" + exit 1 +} + + +echo_info "Starting publish post check..." +echo_info "Git revision : ${GITHUB_SHA}" +echo_info "Git ref : ${GITHUB_REF}" +echo_info "Workflow triggered by : ${GITHUB_ACTOR}" +echo_info "GitHub event : ${GITHUB_EVENT_NAME}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Extracting release version" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "Loading version from: firebase.go" + +readonly RELEASE_VERSION=`grep "const Version" firebase.go | awk '{print $4}' | tr -d \"` || true +if [[ -z "${RELEASE_VERSION}" ]]; then + echo_warn "Failed to extract release version from: firebase.go" + terminate +fi + +if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then + echo_warn "Malformed release version string: ${RELEASE_VERSION}. Exiting." + terminate +fi + +echo_info "Extracted release version: ${RELEASE_VERSION}" +echo "::set-output name=version::v${RELEASE_VERSION}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Generating changelog" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "---< git fetch origin master --prune --unshallow >---" +git fetch origin master --prune --unshallow +echo "" + +echo_info "Generating changelog from history..." +readonly CURRENT_DIR=$(dirname "$0") +readonly CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh` +echo "$CHANGELOG" + +# Parse and preformat the text to handle multi-line output. +# See https://github.community/t5/GitHub-Actions/set-output-Truncates-Multiline-Strings/td-p/37870 +FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[INFO\\]"` +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//'%'/'%25'}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\n'/'%0A'}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\r'/'%0D'}" +echo "::set-output name=changelog::${FILTERED_CHANGELOG}" + + +echo "" +echo_info "--------------------------------------------" +echo_info "POST CHECK SUCCESSFUL" +echo_info "--------------------------------------------" diff --git a/.github/scripts/publish_preflight_check.sh b/.github/scripts/publish_preflight_check.sh new file mode 100755 index 00000000..9dad71bb --- /dev/null +++ b/.github/scripts/publish_preflight_check.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +# Copyright 2020 Google Inc. +# +# 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. + + +set -e +set -u + +function echo_info() { + local MESSAGE=$1 + echo "[INFO] ${MESSAGE}" +} + +function echo_warn() { + local MESSAGE=$1 + echo "[WARN] ${MESSAGE}" +} + +function terminate() { + echo "" + echo_warn "--------------------------------------------" + echo_warn "PREFLIGHT FAILED" + echo_warn "--------------------------------------------" + exit 1 +} + + +echo_info "Starting publish preflight check..." +echo_info "Git revision : ${GITHUB_SHA}" +echo_info "Git ref : ${GITHUB_REF}" +echo_info "Workflow triggered by : ${GITHUB_ACTOR}" +echo_info "GitHub event : ${GITHUB_EVENT_NAME}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Extracting release version" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "Loading version from: firebase.go" + +readonly RELEASE_VERSION=`grep "const Version" firebase.go | awk '{print $4}' | tr -d \"` || true +if [[ -z "${RELEASE_VERSION}" ]]; then + echo_warn "Failed to extract release version from: firebase.go" + terminate +fi + +if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then + echo_warn "Malformed release version string: ${RELEASE_VERSION}. Exiting." + terminate +fi + +echo_info "Extracted release version: ${RELEASE_VERSION}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Checking release tag" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "---< git fetch --depth=1 origin +refs/tags/*:refs/tags/* >---" +git fetch --depth=1 origin +refs/tags/*:refs/tags/* +echo "" + +readonly EXISTING_TAG=`git rev-parse -q --verify "refs/tags/v${RELEASE_VERSION}"` || true +if [[ -n "${EXISTING_TAG}" ]]; then + echo_warn "Tag v${RELEASE_VERSION} already exists. Exiting." + echo_warn "If the tag was created in a previous unsuccessful attempt, delete it and try again." + echo_warn " $ git tag -d v${RELEASE_VERSION}" + echo_warn " $ git push --delete origin v${RELEASE_VERSION}" + + readonly RELEASE_URL="https://github.com/firebase/firebase-admin-go/releases/tag/v${RELEASE_VERSION}" + echo_warn "Delete any corresponding releases at ${RELEASE_URL}." + terminate +fi + +echo_info "Tag v${RELEASE_VERSION} does not exist." + + +echo "" +echo_info "--------------------------------------------" +echo_info "PREFLIGHT SUCCESSFUL" +echo_info "--------------------------------------------" diff --git a/.github/scripts/run_all_tests.sh b/.github/scripts/run_all_tests.sh new file mode 100755 index 00000000..b52b283c --- /dev/null +++ b/.github/scripts/run_all_tests.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Copyright 2020 Google Inc. +# +# 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. + +set -e +set -u + +gpg --quiet --batch --yes --decrypt --passphrase="${FIREBASE_SERVICE_ACCT_KEY}" \ + --output testdata/integration_cert.json .github/resources/integ-service-account.json.gpg + +echo "${FIREBASE_API_KEY}" > testdata/integration_apikey.txt + +go test -v -race firebase.google.com/go/... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c40ae862..3f20d5c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,5 @@ name: Continuous Integration -on: [push, pull_request] +on: push jobs: build: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..26609cab --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,64 @@ +# Copyright 2020 Google Inc. +# +# 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. + +name: Publish Release + +on: + # Only run the workflow when a PR is merged to the master branch. + pull_request: + branches: master + types: closed + +jobs: + publish_release: + if: github.event.pull_request.merged + + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v2 + + - name: Publish post check + id: postcheck + run: ./.github/scripts/publish_post_check.sh + + # We pull this action from a custom fork of a contributor until + # https://github.com/actions/create-release/pull/32 is merged. Also note that v1 of + # this action does not support the "body" parameter. + - name: Create release tag + uses: fleskesvor/create-release@1a72e235c178bf2ae6c51a8ae36febc24568c5fe + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.postcheck.outputs.version }} + release_name: Firebase Admin Go SDK ${{ steps.postcheck.outputs.version }} + body: ${{ steps.postcheck.outputs.changelog }} + draft: false + prerelease: false + + # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. + - name: Post to Twitter + if: success() && + contains(github.event.pull_request.labels.*.name, 'release:tweet') + uses: firebase/firebase-admin-node/.github/actions/send-tweet@master + with: + status: > + ${{ steps.postcheck.outputs.version }} of @Firebase Admin Go SDK is avaialble. + https://github.com/firebase/firebase-admin-go/releases/tag/${{ steps.postcheck.outputs.version }} + consumer-key: ${{ secrets.FIREBASE_TWITTER_CONSUMER_KEY }} + consumer-secret: ${{ secrets.FIREBASE_TWITTER_CONSUMER_SECRET }} + access-token: ${{ secrets.FIREBASE_TWITTER_ACCESS_TOKEN }} + access-token-secret: ${{ secrets.FIREBASE_TWITTER_ACCESS_TOKEN_SECRET }} + continue-on-error: true diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml new file mode 100644 index 00000000..39ff581a --- /dev/null +++ b/.github/workflows/stage.yml @@ -0,0 +1,74 @@ +# Copyright 2020 Google Inc. +# +# 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. + +name: Stage Release + +on: + # Only run the workflow when a PR is updated or when a developer explicitly requests + # a build by sending a 'firebase_build' event. + pull_request: + types: [opened, synchronize] + + repository_dispatch: + types: + - firebase_build + +jobs: + stage_release: + # To stage a release without publishing it, send a 'firebase_build' event or apply + # the 'release:stage' label to a PR. PRs targetting the master branch are always + # staged. + if: github.event.action == 'firebase_build' || + contains(github.event.pull_request.labels.*.name, 'release:stage') || + github.event.pull_request.base.ref == 'master' + + runs-on: ubuntu-latest + + env: + GOPATH: ${{ github.workspace }}/go + + # When manually triggering the build, the requester can specify a target branch or a tag + # via the 'ref' client parameter. + steps: + - name: Check out code into GOPATH + uses: actions/checkout@v2 + with: + path: go/src/firebase.google.com/go + ref: ${{ github.event.client_payload.ref || github.ref }} + + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: 1.11 + + - name: Get dependencies + run: go get -t -v $(go list ./... | grep -v integration) + + - name: Run Linter + run: | + echo + go get golang.org/x/lint/golint + $GOPATH/bin/golint -set_exit_status firebase.google.com/go/... + + - name: Run Tests + working-directory: ./go/src/firebase.google.com/go + run: ./.github/scripts/run_all_tests.sh + env: + FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }} + FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} + + # If triggered by a PR against the master branch, run additional checks. + - name: Publish preflight check + if: github.event.pull_request.base.ref == 'master' + run: ./.github/scripts/publish_preflight_check.sh From 87fd378af5b284952b880b3c99d3968968805001 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Thu, 20 Feb 2020 12:04:39 -0500 Subject: [PATCH 15/20] doc review feedback --- auth/user_mgt.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index a0fd1ea0..e03c704c 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -657,7 +657,7 @@ func (id ProviderIdentifier) populate(req *getAccountInfoRequest) { // A GetUsersResult represents the result of the GetUsers() API. type GetUsersResult struct { - // Set of UserRecords, corresponding to the set of users that were requested. + // Set of UserRecords corresponding to the set of users that were requested. // Only users that were found are listed here. The result set is unordered. Users []*UserRecord @@ -721,8 +721,8 @@ func isUserFound(id UserIdentifier, urs [](*UserRecord)) bool { // result list is not guaranteed to correspond to the nth entry in the input // parameters list. // -// Only a maximum of 100 identifiers may be supplied. If more than 100 -// identifiers are supplied, this method will immediately return an error. +// A maximum of 100 identifiers may be supplied. If more than 100 +// identifiers are supplied, this method returns an error. // // Returns the corresponding user records. An error is returned instead if any // of the identifiers are invalid or if more than 100 identifiers are @@ -950,16 +950,16 @@ func (c *baseClient) DeleteUser(ctx context.Context, uid string) error { // A DeleteUsersResult represents the result of the DeleteUsers() call. type DeleteUsersResult struct { // The number of users that were deleted successfully (possibly zero). Users - // that did not exist prior to calling DeleteUsers() will be considered to be + // that did not exist prior to calling DeleteUsers() are considered to be // successfully deleted. SuccessCount int // The number of users that failed to be deleted (possibly zero). FailureCount int - // A list of describing the errors that were encountered - // during the deletion. Length of this list is equal to the value of - // FailureCount. + // A list of DeleteUsersErrorInfo instances describing the errors that were + // encountered during the deletion. Length of this list is equal to the value + // of FailureCount. Errors []*DeleteUsersErrorInfo } @@ -976,12 +976,12 @@ type DeleteUsersErrorInfo struct { // DeleteUsers deletes the users specified by the given identifiers. // // Deleting a non-existing user won't generate an error. (i.e. this method is -// idempotent.) Non-existing users will be considered to be successfully -// deleted, and will therefore be counted in the DeleteUsersResult.SuccessCount +// idempotent.) Non-existing users are considered to be successfully +// deleted, and are therefore counted in the DeleteUsersResult.SuccessCount // value. // -// Only a maximum of 1000 identifiers may be supplied. If more than 1000 -// identifiers are supplied, this method will immediately return an error. +// A maximum of 1000 identifiers may be supplied. If more than 1000 +// identifiers are supplied, this method returns an error. // // This API is currently rate limited at the server to 1 QPS. If you exceed // this, you may get a quota exceeded error. Therefore, if you want to delete From 4fa1fbafe0ed41568ee3cbfa8c1c5f3b8e7eca4b Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Thu, 20 Feb 2020 13:18:59 -0500 Subject: [PATCH 16/20] go fmt --- auth/user_mgt.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index e03c704c..bdf61cb8 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -957,9 +957,9 @@ type DeleteUsersResult struct { // The number of users that failed to be deleted (possibly zero). FailureCount int - // A list of DeleteUsersErrorInfo instances describing the errors that were - // encountered during the deletion. Length of this list is equal to the value - // of FailureCount. + // A list of DeleteUsersErrorInfo instances describing the errors that were + // encountered during the deletion. Length of this list is equal to the value + // of FailureCount. Errors []*DeleteUsersErrorInfo } From 4b82bdd24358d5d29fba9d648e5fcfbd1259db19 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Fri, 13 Mar 2020 14:14:20 -0400 Subject: [PATCH 17/20] review feedback (docs) --- auth/user_mgt.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index bdf61cb8..df5aeb87 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -35,10 +35,10 @@ const ( defaultProviderID = "firebase" idToolkitV1Endpoint = "https://identitytoolkit.googleapis.com/v1" - // Maximum allowed number of users to batch get at one time. + // Maximum number of users allowed to batch get at a time. maxGetAccountsBatchSize = 100 - // Maximum allowed numberof users to batch delete at one time. + // Maximum number of users allowed to batch delete at a time. maxDeleteAccountsBatchSize = 1000 ) From 89b033f3abe6e652ec590d0938855982ee741168 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Tue, 19 May 2020 18:19:03 -0400 Subject: [PATCH 18/20] Tighten bounds of lastRefreshTime in the test (and fix bug!) --- auth/user_mgt.go | 2 +- integration/auth/user_mgt_test.go | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/auth/user_mgt.go b/auth/user_mgt.go index df5aeb87..0ce85566 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -824,7 +824,7 @@ func (r *userQueryResponse) makeExportedUserRecord() (*ExportedUserRecord, error if err != nil { return nil, err } - lastRefreshTimestamp = t.Unix() + lastRefreshTimestamp = t.Unix() * 1000 } return &ExportedUserRecord{ diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index 3c1011e2..d3d1bc91 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -253,11 +253,16 @@ func TestLastRefreshTime(t *testing.T) { if err != nil { t.Fatalf("GetUser(...) failed with error: %v", err) } - if getUsersResult.UserMetadata.LastRefreshTimestamp <= 0 { - t.Errorf( - "GetUser(...).UserMetadata.LastRefreshTimestamp = %d; want > 0", - getUsersResult.UserMetadata.LastRefreshTimestamp) - } + + // Ensure last refresh time is approx now (with tollerance of 10m) + now_millis := time.Now().Unix() * 1000 + lastRefreshTimestamp := getUsersResult.UserMetadata.LastRefreshTimestamp + if lastRefreshTimestamp < now_millis - 10*60*1000 { + t.Errorf("GetUser(...).UserMetadata.LastRefreshTimestamp = %d; want >= %d", lastRefreshTimestamp, now_millis - 10*60*1000) + } + if now_millis + 10*60*1000 < lastRefreshTimestamp { + t.Errorf("GetUser(...).UserMetadata.LastRefreshTimestamp = %d; want <= %d", lastRefreshTimestamp, now_millis + 10*60*1000) + } } func TestUpdateNonExistingUser(t *testing.T) { From c73a300ec9f57195f727666cd99ad29c065af305 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Tue, 19 May 2020 18:32:51 -0400 Subject: [PATCH 19/20] lint/fmt --- integration/auth/user_mgt_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index d3d1bc91..f5348299 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -254,15 +254,15 @@ func TestLastRefreshTime(t *testing.T) { t.Fatalf("GetUser(...) failed with error: %v", err) } - // Ensure last refresh time is approx now (with tollerance of 10m) - now_millis := time.Now().Unix() * 1000 - lastRefreshTimestamp := getUsersResult.UserMetadata.LastRefreshTimestamp - if lastRefreshTimestamp < now_millis - 10*60*1000 { - t.Errorf("GetUser(...).UserMetadata.LastRefreshTimestamp = %d; want >= %d", lastRefreshTimestamp, now_millis - 10*60*1000) - } - if now_millis + 10*60*1000 < lastRefreshTimestamp { - t.Errorf("GetUser(...).UserMetadata.LastRefreshTimestamp = %d; want <= %d", lastRefreshTimestamp, now_millis + 10*60*1000) - } + // Ensure last refresh time is approx now (with tollerance of 10m) + nowMillis := time.Now().Unix() * 1000 + lastRefreshTimestamp := getUsersResult.UserMetadata.LastRefreshTimestamp + if lastRefreshTimestamp < nowMillis-10*60*1000 { + t.Errorf("GetUser(...).UserMetadata.LastRefreshTimestamp = %d; want >= %d", lastRefreshTimestamp, nowMillis-10*60*1000) + } + if nowMillis+10*60*1000 < lastRefreshTimestamp { + t.Errorf("GetUser(...).UserMetadata.LastRefreshTimestamp = %d; want <= %d", lastRefreshTimestamp, nowMillis+10*60*1000) + } } func TestUpdateNonExistingUser(t *testing.T) { From 07747220bbf1f493d0955805e9a5e2be11254740 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Wed, 20 May 2020 13:06:01 -0400 Subject: [PATCH 20/20] review feedback --- .github/scripts/publish_post_check.sh | 105 -------------------------- .github/workflows/publish.yml | 64 ---------------- .github/workflows/stage.yml | 74 ------------------ auth/user_mgt.go | 1 - 4 files changed, 244 deletions(-) delete mode 100755 .github/scripts/publish_post_check.sh delete mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/stage.yml diff --git a/.github/scripts/publish_post_check.sh b/.github/scripts/publish_post_check.sh deleted file mode 100755 index 8311ec25..00000000 --- a/.github/scripts/publish_post_check.sh +++ /dev/null @@ -1,105 +0,0 @@ - -#!/bin/bash - -# Copyright 2020 Google Inc. -# -# 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. - - -###################################### Outputs ##################################### - -# 1. version: The version of this release including the 'v' prefix (e.g. v1.2.3). -# 2. changelog: Formatted changelog text for this release. - -#################################################################################### - -set -e -set -u - -function echo_info() { - local MESSAGE=$1 - echo "[INFO] ${MESSAGE}" -} - -function echo_warn() { - local MESSAGE=$1 - echo "[WARN] ${MESSAGE}" -} - -function terminate() { - echo "" - echo_warn "--------------------------------------------" - echo_warn "POST CHECK FAILED" - echo_warn "--------------------------------------------" - exit 1 -} - - -echo_info "Starting publish post check..." -echo_info "Git revision : ${GITHUB_SHA}" -echo_info "Git ref : ${GITHUB_REF}" -echo_info "Workflow triggered by : ${GITHUB_ACTOR}" -echo_info "GitHub event : ${GITHUB_EVENT_NAME}" - - -echo_info "" -echo_info "--------------------------------------------" -echo_info "Extracting release version" -echo_info "--------------------------------------------" -echo_info "" - -echo_info "Loading version from: firebase.go" - -readonly RELEASE_VERSION=`grep "const Version" firebase.go | awk '{print $4}' | tr -d \"` || true -if [[ -z "${RELEASE_VERSION}" ]]; then - echo_warn "Failed to extract release version from: firebase.go" - terminate -fi - -if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then - echo_warn "Malformed release version string: ${RELEASE_VERSION}. Exiting." - terminate -fi - -echo_info "Extracted release version: ${RELEASE_VERSION}" -echo "::set-output name=version::v${RELEASE_VERSION}" - - -echo_info "" -echo_info "--------------------------------------------" -echo_info "Generating changelog" -echo_info "--------------------------------------------" -echo_info "" - -echo_info "---< git fetch origin master --prune --unshallow >---" -git fetch origin master --prune --unshallow -echo "" - -echo_info "Generating changelog from history..." -readonly CURRENT_DIR=$(dirname "$0") -readonly CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh` -echo "$CHANGELOG" - -# Parse and preformat the text to handle multi-line output. -# See https://github.community/t5/GitHub-Actions/set-output-Truncates-Multiline-Strings/td-p/37870 -FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[INFO\\]"` -FILTERED_CHANGELOG="${FILTERED_CHANGELOG//'%'/'%25'}" -FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\n'/'%0A'}" -FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\r'/'%0D'}" -echo "::set-output name=changelog::${FILTERED_CHANGELOG}" - - -echo "" -echo_info "--------------------------------------------" -echo_info "POST CHECK SUCCESSFUL" -echo_info "--------------------------------------------" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 26609cab..00000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2020 Google Inc. -# -# 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. - -name: Publish Release - -on: - # Only run the workflow when a PR is merged to the master branch. - pull_request: - branches: master - types: closed - -jobs: - publish_release: - if: github.event.pull_request.merged - - runs-on: ubuntu-latest - - steps: - - name: Checkout source - uses: actions/checkout@v2 - - - name: Publish post check - id: postcheck - run: ./.github/scripts/publish_post_check.sh - - # We pull this action from a custom fork of a contributor until - # https://github.com/actions/create-release/pull/32 is merged. Also note that v1 of - # this action does not support the "body" parameter. - - name: Create release tag - uses: fleskesvor/create-release@1a72e235c178bf2ae6c51a8ae36febc24568c5fe - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.postcheck.outputs.version }} - release_name: Firebase Admin Go SDK ${{ steps.postcheck.outputs.version }} - body: ${{ steps.postcheck.outputs.changelog }} - draft: false - prerelease: false - - # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. - - name: Post to Twitter - if: success() && - contains(github.event.pull_request.labels.*.name, 'release:tweet') - uses: firebase/firebase-admin-node/.github/actions/send-tweet@master - with: - status: > - ${{ steps.postcheck.outputs.version }} of @Firebase Admin Go SDK is avaialble. - https://github.com/firebase/firebase-admin-go/releases/tag/${{ steps.postcheck.outputs.version }} - consumer-key: ${{ secrets.FIREBASE_TWITTER_CONSUMER_KEY }} - consumer-secret: ${{ secrets.FIREBASE_TWITTER_CONSUMER_SECRET }} - access-token: ${{ secrets.FIREBASE_TWITTER_ACCESS_TOKEN }} - access-token-secret: ${{ secrets.FIREBASE_TWITTER_ACCESS_TOKEN_SECRET }} - continue-on-error: true diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml deleted file mode 100644 index 39ff581a..00000000 --- a/.github/workflows/stage.yml +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2020 Google Inc. -# -# 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. - -name: Stage Release - -on: - # Only run the workflow when a PR is updated or when a developer explicitly requests - # a build by sending a 'firebase_build' event. - pull_request: - types: [opened, synchronize] - - repository_dispatch: - types: - - firebase_build - -jobs: - stage_release: - # To stage a release without publishing it, send a 'firebase_build' event or apply - # the 'release:stage' label to a PR. PRs targetting the master branch are always - # staged. - if: github.event.action == 'firebase_build' || - contains(github.event.pull_request.labels.*.name, 'release:stage') || - github.event.pull_request.base.ref == 'master' - - runs-on: ubuntu-latest - - env: - GOPATH: ${{ github.workspace }}/go - - # When manually triggering the build, the requester can specify a target branch or a tag - # via the 'ref' client parameter. - steps: - - name: Check out code into GOPATH - uses: actions/checkout@v2 - with: - path: go/src/firebase.google.com/go - ref: ${{ github.event.client_payload.ref || github.ref }} - - - name: Set up Go - uses: actions/setup-go@v1 - with: - go-version: 1.11 - - - name: Get dependencies - run: go get -t -v $(go list ./... | grep -v integration) - - - name: Run Linter - run: | - echo - go get golang.org/x/lint/golint - $GOPATH/bin/golint -set_exit_status firebase.google.com/go/... - - - name: Run Tests - working-directory: ./go/src/firebase.google.com/go - run: ./.github/scripts/run_all_tests.sh - env: - FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }} - FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} - - # If triggered by a PR against the master branch, run additional checks. - - name: Publish preflight check - if: github.event.pull_request.base.ref == 'master' - run: ./.github/scripts/publish_preflight_check.sh diff --git a/auth/user_mgt.go b/auth/user_mgt.go index 0ce85566..e601d8bc 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -340,7 +340,6 @@ const ( unauthorizedContinueURI = "unauthorized-continue-uri" unknown = "unknown-error" userNotFound = "user-not-found" - maximumUserCountExceeded = "maximum-user-count-exceeded" ) // IsConfigurationNotFound checks if the given error was due to a non-existing IdP configuration.