From 31bcfc91d8bd7f9ef8bf367a31d7099eb5d6c7f0 Mon Sep 17 00:00:00 2001 From: avishalom Date: Thu, 11 Jan 2018 17:12:42 -0500 Subject: [PATCH] Release Version 2.4.0 (#69) * Documentation fix md file (#43) * Clarify certificate download comment * Fix Link * Adding User Management utils, including Custom Claims in the auth package. (#42) Adding the user management go SDK. This includes the customClaims and the iterator over all users. (as well as Create, Update, Delete User, and GetUser (by UID, Phone or Email)) Proposal : go/firebase-go-user-mgt Code snippets: https://firebase-dot-devsite.googleplex.com/docs/auth/admin/manage-users https://firebase-dot-devsite.googleplex.com/docs/auth/admin/custom-claims TODO: clean up the case of an http.DefaultClient when there are no options. * Minor improvements to user management code (#44) * some changes to auth * Implemented a reusable HTTP client API * Added test cases * Comment clean up * Simplified the usage by adding HTTPClient * Using the old ctx import * Support for arbitrary entity types in the request * Renamed fields; Added documentation * Removing a redundant else case * initial * more integration tests * set custom - still needs guessing the type for unmarshaling. * tests * server * server * . * json * json * move testdata * get * tests * updated to param struct of pointers to call create and update * Comments * cleanup * cleanup * cleanup * cleanup minor test changes * Changing the iteraator pattern * last page iterator test fix * clean up tests next. * make the fetch tidier * fetch cleanup * cc change * custom claims * newline * adding error propagation to makeExportedUser * remove trivial claims map type * list users test data * rename p ptr, and remove the with... options for the iterator * rename p ptr * some unit tests * adding integration tests for custom claims * NO ERROR * unit tests * comments hkj * addressing comments * delete unneeded json file * phone NUMBER * typo * remove cc from create * refactor param structs * remove package ptr * test refactor * cleanup comments * cleanup debug.test * Adding back the default http client * fix httpClient for tests * creds * creds * fix default http client * cleanupPrintfs * disable * Reanme payload vars * Revert newHTTPKeySource function * add back defaultClient in newClient * reenable testNewHTTPClientNoOpts) * reverting keysource tests) * Take the httpClient from the keysource * Removethe +1 second for the token timestamp. * Adding tests * White spaces * Redesign the error validators * prepare returns an error. * cleanup * dissolve * dissolve * clean tests * split integration tests * addressing comments * addressing comments opt branch/ BEFORE hc change * Removing the defaultClient from the NewClient, and extracting the NewClient creation outside of KeySource * closer from echoServer * cleanup + 500 error unit test * unify error messages * Refactor stop side effect for params preparation * +1 to timestamp for flakiness * removing +1 * disallow defaultClient * whitespaces * http default * add TODO * Code clean up and refactoring * Refactored integration tests (#46) * Refactored integration tests * Minor cleanup * Auth Unit Test Improvements (#45) * Cleaning up new auth tests * More updates to tests; Dissolved commonParams type * More test updates * More argument validation for auth * Fixed a bug in enable/disable user; Added more tests; Cleaned up unit tests * Removed debug file * Create the 5th user in the integration tests for user management. (#47) * Bump version to 2.2.0 (#49) * Adding version, stat counter header and test (#50) * Adding version to the header for the stat counter + tests * bump version to 2.2.1 * Adding comment for manual cleanup * Auth Package Internal Cleanup (#53) * Release v2.2.1 (#51) * Adding version, stat counter header and test (#50) * Adding version to the header for the stat counter + tests * bump version to 2.2.1 * Adding comment for manual cleanup * Experimental auth cleanup * Using a mock token source in tests * Further cleaning up the test cases * Implemented IID Delete API (#41) * Implemented IID Delete API * Cleaned up code * Updated test case * Updating comments * Improved error handling * Fixing malformed error messages due to extra format placeholders; Using the recommended Go error message format (no caps or punctuation) * Bumped version to 2.3.0 (#56) * use const for Common HTTP methods and HTTP status codes (#58) * use const for Common HTTP methods and HTTP status codes * fix format verbs %q to %d * golint - remove redundant if ...; err != nil check (#59) * golint - remove redundant if ...; err != nil check * add golint check on travis.yml * update readme with Authentication Guide & Release Notes (#61) * update readme with Authentication Guide & Release Notes * fix a misspelling : separately * fix missing newline before package * Format JSON test data (#63) * use identitytoolkit/v3 go client instead of makeHTTPCall (#60) * use identitytoolkit/v3 go client instead of makeHTTPCall * remove unnecessary if because empty values are not sent with identitytoolkit * refactoring to avoid multiple header set * change execute func to setHeader * remove url from Client type, rename relayingpartyCall interface + fix providerID mistake * change rawId value in test to 'testuid' * Firebase App auto init (#54) * FIREBASE_CONFIG env variable auto init, allows calling initialize_app with not arguments using the env variable * allow env var to contain json * add tests for json string in env file * constant provider id plus tests (#64) * Constant provider id plus tests * Cleaning up the json tags in the structs following the identitytoolkit PR * Expand comment about ProviderID * Contributing (#65) * [docs update] How to view test coverage. * Release Version 2.3.0 (#57) (#67) * Documentation fix md file (#43) * Clarify certificate download comment * Fix Link * Adding User Management utils, including Custom Claims in the auth package. (#42) Adding the user management go SDK. This includes the customClaims and the iterator over all users. (as well as Create, Update, Delete User, and GetUser (by UID, Phone or Email)) Proposal : go/firebase-go-user-mgt Code snippets: https://firebase-dot-devsite.googleplex.com/docs/auth/admin/manage-users https://firebase-dot-devsite.googleplex.com/docs/auth/admin/custom-claims TODO: clean up the case of an http.DefaultClient when there are no options. * Minor improvements to user management code (#44) * some changes to auth * Implemented a reusable HTTP client API * Added test cases * Comment clean up * Simplified the usage by adding HTTPClient * Using the old ctx import * Support for arbitrary entity types in the request * Renamed fields; Added documentation * Removing a redundant else case * initial * more integration tests * set custom - still needs guessing the type for unmarshaling. * tests * server * server * . * json * json * move testdata * get * tests * updated to param struct of pointers to call create and update * Comments * cleanup * cleanup * cleanup * cleanup minor test changes * Changing the iteraator pattern * last page iterator test fix * clean up tests next. * make the fetch tidier * fetch cleanup * cc change * custom claims * newline * adding error propagation to makeExportedUser * remove trivial claims map type * list users test data * rename p ptr, and remove the with... options for the iterator * rename p ptr * some unit tests * adding integration tests for custom claims * NO ERROR * unit tests * comments hkj * addressing comments * delete unneeded json file * phone NUMBER * typo * remove cc from create * refactor param structs * remove package ptr * test refactor * cleanup comments * cleanup debug.test * Adding back the default http client * fix httpClient for tests * creds * creds * fix default http client * cleanupPrintfs * disable * Reanme payload vars * Revert newHTTPKeySource function * add back defaultClient in newClient * reenable testNewHTTPClientNoOpts) * reverting keysource tests) * Take the httpClient from the keysource * Removethe +1 second for the token timestamp. * Adding tests * White spaces * Redesign the error validators * prepare returns an error. * cleanup * dissolve * dissolve * clean tests * split integration tests * addressing comments * addressing comments opt branch/ BEFORE hc change * Removing the defaultClient from the NewClient, and extracting the NewClient creation outside of KeySource * closer from echoServer * cleanup + 500 error unit test * unify error messages * Refactor stop side effect for params preparation * +1 to timestamp for flakiness * removing +1 * disallow defaultClient * whitespaces * http default * add TODO * Code clean up and refactoring * Refactored integration tests (#46) * Refactored integration tests * Minor cleanup * Auth Unit Test Improvements (#45) * Cleaning up new auth tests * More updates to tests; Dissolved commonParams type * More test updates * More argument validation for auth * Fixed a bug in enable/disable user; Added more tests; Cleaned up unit tests * Removed debug file * Create the 5th user in the integration tests for user management. (#47) * Bump version to 2.2.0 (#49) * Adding version, stat counter header and test (#50) * Adding version to the header for the stat counter + tests * bump version to 2.2.1 * Adding comment for manual cleanup * Auth Package Internal Cleanup (#53) * Release v2.2.1 (#51) * Adding version, stat counter header and test (#50) * Adding version to the header for the stat counter + tests * bump version to 2.2.1 * Adding comment for manual cleanup * Experimental auth cleanup * Using a mock token source in tests * Further cleaning up the test cases * Implemented IID Delete API (#41) * Implemented IID Delete API * Cleaned up code * Updated test case * Updating comments * Improved error handling * Fixing malformed error messages due to extra format placeholders; Using the recommended Go error message format (no caps or punctuation) * Bumped version to 2.3.0 (#56) * Bumped Version 2.4.0 (#66) * Release Version 2.3.0 (#57) (#68) --- .travis.yml | 6 +- CONTRIBUTING.md | 15 ++ README.md | 2 + auth/auth.go | 27 +-- auth/crypto_test.go | 2 +- auth/user_mgt.go | 249 +++++++++++++++------- auth/user_mgt_test.go | 70 ++++-- firebase.go | 41 +++- firebase_test.go | 198 +++++++++++++++-- iid/iid.go | 18 +- iid/iid_test.go | 14 +- integration/auth/auth_test.go | 2 +- integration/auth/user_mgt_test.go | 10 +- integration/storage/storage_test.go | 5 +- internal/http_client_test.go | 30 +-- testdata/firebase_config.json | 4 + testdata/firebase_config_empty.json | 0 testdata/firebase_config_invalid.json | 1 + testdata/firebase_config_invalid_key.json | 4 + testdata/firebase_config_partial.json | 3 + testdata/get_user.json | 63 +++--- testdata/list_users.json | 165 +++++++------- 22 files changed, 642 insertions(+), 287 deletions(-) create mode 100644 testdata/firebase_config.json create mode 100644 testdata/firebase_config_empty.json create mode 100644 testdata/firebase_config_invalid.json create mode 100644 testdata/firebase_config_invalid_key.json create mode 100644 testdata/firebase_config_partial.json diff --git a/.travis.yml b/.travis.yml index 11a5af12..b1d02a82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,8 @@ language: go go_import_path: firebase.google.com/go -script: go test -v -test.short ./... +before_install: + - go get github.com/golang/lint/golint +script: + - golint -set_exit_status $(go list ./...) + - go test -v -test.short ./... diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59c4edab..6aa95251 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -137,3 +137,18 @@ go test firebase.google.com/go/... ``` This will execute both unit and integration test suites. + +### Test Coverage + +Coverage can be measured per package by passing the `-cover` flag to the test invocation: + +```bash +go test -cover firebase.google.com/go/auth +``` + +To view the detailed coverage reports (per package): + +```bash +go test -cover -coverprofile=coverage.out firebase.google.com/go +go tool cover -html=coverage.out +``` diff --git a/README.md b/README.md index 06a6e7e5..441b5fad 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,9 @@ requests, code review feedback, and also pull requests. ## Documentation * [Setup Guide](https://firebase.google.com/docs/admin/setup/) +* [Authentication Guide](https://firebase.google.com/docs/auth/admin/) * [API Reference](https://godoc.org/firebase.google.com/go) +* [Release Notes](https://firebase.google.com/support/release-notes/admin/go) ## License and Terms diff --git a/auth/auth.go b/auth/auth.go index aa72c6c5..cc798182 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -26,12 +26,12 @@ import ( "firebase.google.com/go/internal" "golang.org/x/net/context" + "google.golang.org/api/identitytoolkit/v3" "google.golang.org/api/transport" ) const firebaseAudience = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit" const googleCertURL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com" -const idToolKitURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" const issuerPrefix = "https://securetoken.google.com/" const tokenExpSeconds = 3600 @@ -63,10 +63,10 @@ type Token struct { // by Firebase backend services. type Client struct { hc *internal.HTTPClient + is *identitytoolkit.Service ks keySource projectID string snr signer - url string version string } @@ -117,32 +117,21 @@ func NewClient(ctx context.Context, c *internal.AuthConfig) (*Client, error) { return nil, err } + is, err := identitytoolkit.New(hc) + if err != nil { + return nil, err + } + return &Client{ hc: &internal.HTTPClient{Client: hc}, + is: is, ks: newHTTPKeySource(googleCertURL, hc), projectID: c.ProjectID, snr: snr, - url: idToolKitURL, version: "Go/Admin/" + c.Version, }, nil } -// Passes the request struct, returns a byte array of the json -func (c *Client) makeHTTPCall(ctx context.Context, serviceName string, payload interface{}, result interface{}) error { - versionHeader := internal.WithHeader("X-Client-Version", c.version) - request := &internal.Request{ - Method: "POST", - URL: c.url + serviceName, - Body: internal.NewJSONEntity(payload), - Opts: []internal.HTTPOption{versionHeader}, - } - resp, err := c.hc.Do(ctx, request) - if err != nil { - return err - } - return resp.Unmarshal(200, result) -} - // CustomToken creates a signed custom authentication token with the specified user ID. The resulting // JWT can be used in a Firebase client SDK to trigger an authentication flow. See // https://firebase.google.com/docs/auth/admin/create-custom-tokens#sign_in_using_custom_tokens_on_clients diff --git a/auth/crypto_test.go b/auth/crypto_test.go index 6c3306ef..ab5e642a 100644 --- a/auth/crypto_test.go +++ b/auth/crypto_test.go @@ -48,7 +48,7 @@ func newTestHTTPClient(data []byte) (*http.Client, *mockReadCloser) { Transport: &mockHTTPResponse{ Response: http.Response{ Status: "200 OK", - StatusCode: 200, + StatusCode: http.StatusOK, Header: http.Header{ "Cache-Control": {"public, max-age=100"}, }, diff --git a/auth/user_mgt.go b/auth/user_mgt.go index f1dea1f3..9d0098dc 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -17,17 +17,21 @@ package auth import ( "encoding/json" "fmt" + "net/http" + "reflect" "regexp" "strings" - "google.golang.org/api/iterator" - "golang.org/x/net/context" + "google.golang.org/api/identitytoolkit/v3" + "google.golang.org/api/iterator" ) const maxReturnedResults = 1000 const maxLenPayloadCC = 1000 +const defaultProviderID = "firebase" + var commonValidators = map[string]func(interface{}) error{ "displayName": validateDisplayName, "email": validateEmail, @@ -37,16 +41,27 @@ var commonValidators = map[string]func(interface{}) error{ "localId": validateUID, } +// Create a new interface +type identitytoolkitCall interface { + Header() http.Header +} + +// set header +func (c *Client) setHeader(ic identitytoolkitCall) { + ic.Header().Set("X-Client-Version", c.version) +} + // UserInfo is a collection of standard profile information for a user. type UserInfo struct { - DisplayName string `json:"displayName,omitempty"` - Email string `json:"email,omitempty"` - PhoneNumber string `json:"phoneNumber,omitempty"` - PhotoURL string `json:"photoUrl,omitempty"` - // ProviderID can be a short domain name (e.g. google.com), + DisplayName string + Email string + PhoneNumber string + PhotoURL string + // In the ProviderUserInfo[] ProviderID can be a short domain name (e.g. google.com), // or the identity of an OpenID identity provider. - ProviderID string `json:"providerId,omitempty"` - UID string `json:"localId,omitempty"` + // In UserRecord.UserInfo it will return the constant string "firebase". + ProviderID string + UID string } // UserMetadata contains additional metadata associated with a user account. @@ -182,9 +197,14 @@ func (c *Client) DeleteUser(ctx context.Context, uid string) error { if err := validateUID(uid); err != nil { return err } - var resp map[string]interface{} - deleteParams := map[string]interface{}{"localId": []string{uid}} - return c.makeHTTPCall(ctx, "deleteAccount", deleteParams, &resp) + request := &identitytoolkit.IdentitytoolkitRelyingpartyDeleteAccountRequest{ + LocalId: uid, + } + + call := c.is.Relyingparty.DeleteAccount(request) + c.setHeader(call) + _, err := call.Context(ctx).Do() + return err } // GetUser gets the user data corresponding to the specified user ID. @@ -192,7 +212,10 @@ func (c *Client) GetUser(ctx context.Context, uid string) (*UserRecord, error) { if err := validateUID(uid); err != nil { return nil, err } - return c.getUser(ctx, map[string]interface{}{"localId": []string{uid}}) + request := &identitytoolkit.IdentitytoolkitRelyingpartyGetAccountInfoRequest{ + LocalId: []string{uid}, + } + return c.getUser(ctx, request) } // GetUserByPhoneNumber gets the user data corresponding to the specified user phone number. @@ -200,7 +223,10 @@ func (c *Client) GetUserByPhoneNumber(ctx context.Context, phone string) (*UserR if err := validatePhone(phone); err != nil { return nil, err } - return c.getUser(ctx, map[string]interface{}{"phoneNumber": []string{phone}}) + request := &identitytoolkit.IdentitytoolkitRelyingpartyGetAccountInfoRequest{ + PhoneNumber: []string{phone}, + } + return c.getUser(ctx, request) } // GetUserByEmail gets the user data corresponding to the specified email. @@ -208,7 +234,10 @@ func (c *Client) GetUserByEmail(ctx context.Context, email string) (*UserRecord, if err := validateEmail(email); err != nil { return nil, err } - return c.getUser(ctx, map[string]interface{}{"email": []string{email}}) + request := &identitytoolkit.IdentitytoolkitRelyingpartyGetAccountInfoRequest{ + Email: []string{email}, + } + return c.getUser(ctx, request) } // Users returns an iterator over Users. @@ -230,25 +259,26 @@ func (c *Client) Users(ctx context.Context, nextPageToken string) *UserIterator } func (it *UserIterator) fetch(pageSize int, pageToken string) (string, error) { - params := map[string]interface{}{"maxResults": pageSize} - if pageToken != "" { - params["nextPageToken"] = pageToken + request := &identitytoolkit.IdentitytoolkitRelyingpartyDownloadAccountRequest{ + MaxResults: int64(pageSize), + NextPageToken: pageToken, } - - var lur listUsersResponse - err := it.client.makeHTTPCall(it.ctx, "downloadAccount", params, &lur) + call := it.client.is.Relyingparty.DownloadAccount(request) + it.client.setHeader(call) + resp, err := call.Context(it.ctx).Do() if err != nil { return "", err } - for _, u := range lur.Users { + + for _, u := range resp.Users { eu, err := makeExportedUser(u) if err != nil { return "", err } it.users = append(it.users, eu) } - it.pageInfo.Token = lur.NextPage - return lur.NextPage, nil + it.pageInfo.Token = resp.NextPageToken + return resp.NextPageToken, nil } // PageInfo supports pagination. See the google.golang.org/api/iterator package for details. @@ -386,10 +416,10 @@ func validatePhone(val interface{}) error { return nil } -func (u *UserToCreate) preparePayload() (map[string]interface{}, error) { +func (u *UserToCreate) preparePayload(user *identitytoolkit.IdentitytoolkitRelyingpartySignupNewUserRequest) error { params := map[string]interface{}{} if u.params == nil { - return params, nil + return nil } for k, v := range u.params { @@ -398,14 +428,28 @@ func (u *UserToCreate) preparePayload() (map[string]interface{}, error) { for key, validate := range commonValidators { if v, ok := params[key]; ok { if err := validate(v); err != nil { - return nil, err + return err } + reflect.ValueOf(user).Elem().FieldByName(strings.Title(key)).SetString(params[key].(string)) + } + } + if params["disabled"] != nil { + user.Disabled = params["disabled"].(bool) + if !user.Disabled { + user.ForceSendFields = append(user.ForceSendFields, "Disabled") + } + } + if params["emailVerified"] != nil { + user.EmailVerified = params["emailVerified"].(bool) + if !user.EmailVerified { + user.ForceSendFields = append(user.ForceSendFields, "EmailVerified") } } - return params, nil + + return nil } -func (u *UserToUpdate) preparePayload() (map[string]interface{}, error) { +func (u *UserToUpdate) preparePayload(user *identitytoolkit.IdentitytoolkitRelyingpartySetAccountInfoRequest) error { params := map[string]interface{}{} for k, v := range u.params { params[k] = v @@ -415,17 +459,41 @@ func (u *UserToUpdate) preparePayload() (map[string]interface{}, error) { processDeletion(params, "phoneNumber", "deleteProvider", "phone") if err := processClaims(params); err != nil { - return nil, err + return err + } + + if params["customAttributes"] != nil { + user.CustomAttributes = params["customAttributes"].(string) } for key, validate := range commonValidators { if v, ok := params[key]; ok { if err := validate(v); err != nil { - return nil, err + return err } + reflect.ValueOf(user).Elem().FieldByName(strings.Title(key)).SetString(params[key].(string)) + } + } + if params["disableUser"] != nil { + user.DisableUser = params["disableUser"].(bool) + if !user.DisableUser { + user.ForceSendFields = append(user.ForceSendFields, "DisableUser") } } - return params, nil + if params["emailVerified"] != nil { + user.EmailVerified = params["emailVerified"].(bool) + if !user.EmailVerified { + user.ForceSendFields = append(user.ForceSendFields, "EmailVerified") + } + } + if params["deleteAttribute"] != nil { + user.DeleteAttribute = params["deleteAttribute"].([]string) + } + if params["deleteProvider"] != nil { + user.DeleteProvider = params["deleteProvider"].([]string) + } + + return nil } // End of validators @@ -433,32 +501,32 @@ func (u *UserToUpdate) preparePayload() (map[string]interface{}, error) { // Response Types ------------------------------- type getUserResponse struct { - RequestType string `json:"kind,omitempty"` - Users []responseUserRecord `json:"users,omitempty"` + RequestType string + Users []responseUserRecord } type responseUserRecord struct { - UID string `json:"localId,omitempty"` - DisplayName string `json:"displayName,omitempty"` - Email string `json:"email,omitempty"` - PhoneNumber string `json:"phoneNumber,omitempty"` - PhotoURL string `json:"photoUrl,omitempty"` - CreationTimestamp int64 `json:"createdAt,string,omitempty"` - LastLogInTimestamp int64 `json:"lastLoginAt,string,omitempty"` - ProviderID string `json:"providerId,omitempty"` - CustomClaims string `json:"customAttributes,omitempty"` - Disabled bool `json:"disabled,omitempty"` - EmailVerified bool `json:"emailVerified,omitempty"` - ProviderUserInfo []*UserInfo `json:"providerUserInfo,omitempty"` - PasswordHash string `json:"passwordHash,omitempty"` - PasswordSalt string `json:"salt,omitempty"` - ValidSince int64 `json:"validSince,string,omitempty"` + UID string + DisplayName string + Email string + PhoneNumber string + PhotoURL string + CreationTimestamp int64 + LastLogInTimestamp int64 + ProviderID string + CustomClaims string + Disabled bool + EmailVerified bool + ProviderUserInfo []*UserInfo + PasswordHash string + PasswordSalt string + ValidSince int64 } type listUsersResponse struct { - RequestType string `json:"kind,omitempty"` - Users []responseUserRecord `json:"users,omitempty"` - NextPage string `json:"nextPageToken,omitempty"` + RequestType string + Users []responseUserRecord + NextPage string } // Helper functions for retrieval and HTTP calls. @@ -468,15 +536,20 @@ func (c *Client) createUser(ctx context.Context, user *UserToCreate) (string, er user = &UserToCreate{} } - payload, err := user.preparePayload() - if err != nil { + request := &identitytoolkit.IdentitytoolkitRelyingpartySignupNewUserRequest{} + + if err := user.preparePayload(request); err != nil { return "", err } - var rur responseUserRecord - if err := c.makeHTTPCall(ctx, "signupNewUser", payload, &rur); err != nil { + + call := c.is.Relyingparty.SignupNewUser(request) + c.setHeader(call) + resp, err := call.Context(ctx).Do() + if err != nil { return "", err } - return rur.UID, nil + + return resp.LocalId, nil } func (c *Client) updateUser(ctx context.Context, uid string, user *UserToUpdate) error { @@ -486,37 +559,44 @@ func (c *Client) updateUser(ctx context.Context, uid string, user *UserToUpdate) if user == nil || user.params == nil { return fmt.Errorf("update parameters must not be nil or empty") } - user.params["localId"] = uid - payload, err := user.preparePayload() - if err != nil { + request := &identitytoolkit.IdentitytoolkitRelyingpartySetAccountInfoRequest{ + LocalId: uid, + } + + if err := user.preparePayload(request); err != nil { return err } - var rur responseUserRecord - return c.makeHTTPCall(ctx, "setAccountInfo", payload, &rur) + call := c.is.Relyingparty.SetAccountInfo(request) + c.setHeader(call) + _, err := call.Context(ctx).Do() + + return err } -func (c *Client) getUser(ctx context.Context, params map[string]interface{}) (*UserRecord, error) { - var gur getUserResponse - err := c.makeHTTPCall(ctx, "getAccountInfo", params, &gur) +func (c *Client) getUser(ctx context.Context, request *identitytoolkit.IdentitytoolkitRelyingpartyGetAccountInfoRequest) (*UserRecord, error) { + call := c.is.Relyingparty.GetAccountInfo(request) + c.setHeader(call) + resp, err := call.Context(ctx).Do() if err != nil { return nil, err } - if len(gur.Users) == 0 { - return nil, fmt.Errorf("cannot find user from params: %v", params) + if len(resp.Users) == 0 { + return nil, fmt.Errorf("cannot find user from params: %v", request) } - eu, err := makeExportedUser(gur.Users[0]) + + eu, err := makeExportedUser(resp.Users[0]) if err != nil { return nil, err } return eu.UserRecord, nil } -func makeExportedUser(r responseUserRecord) (*ExportedUserRecord, error) { +func makeExportedUser(r *identitytoolkit.UserInfo) (*ExportedUserRecord, error) { var cc map[string]interface{} - if r.CustomClaims != "" { - err := json.Unmarshal([]byte(r.CustomClaims), &cc) + if r.CustomAttributes != "" { + err := json.Unmarshal([]byte(r.CustomAttributes), &cc) if err != nil { return nil, err } @@ -525,27 +605,40 @@ func makeExportedUser(r responseUserRecord) (*ExportedUserRecord, error) { } } + var providerUserInfo []*UserInfo + for _, u := range r.ProviderUserInfo { + info := &UserInfo{ + DisplayName: u.DisplayName, + Email: u.Email, + PhoneNumber: u.PhoneNumber, + PhotoURL: u.PhotoUrl, + ProviderID: u.ProviderId, + UID: u.RawId, + } + providerUserInfo = append(providerUserInfo, info) + } + resp := &ExportedUserRecord{ UserRecord: &UserRecord{ UserInfo: &UserInfo{ DisplayName: r.DisplayName, Email: r.Email, PhoneNumber: r.PhoneNumber, - PhotoURL: r.PhotoURL, - ProviderID: r.ProviderID, - UID: r.UID, + PhotoURL: r.PhotoUrl, + ProviderID: defaultProviderID, + UID: r.LocalId, }, CustomClaims: cc, Disabled: r.Disabled, EmailVerified: r.EmailVerified, - ProviderUserInfo: r.ProviderUserInfo, + ProviderUserInfo: providerUserInfo, UserMetadata: &UserMetadata{ - LastLogInTimestamp: r.LastLogInTimestamp, - CreationTimestamp: r.CreationTimestamp, + LastLogInTimestamp: r.LastLoginAt, + CreationTimestamp: r.CreatedAt, }, }, PasswordHash: r.PasswordHash, - PasswordSalt: r.PasswordSalt, + PasswordSalt: r.Salt, } return resp, nil } diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index edf5db0f..5c75fdf9 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -15,6 +15,7 @@ package auth import ( + "bytes" "encoding/json" "fmt" "io/ioutil" @@ -28,6 +29,7 @@ import ( "golang.org/x/net/context" "golang.org/x/oauth2" + "google.golang.org/api/identitytoolkit/v3" "google.golang.org/api/iterator" "google.golang.org/api/option" ) @@ -39,6 +41,7 @@ var testUser = &UserRecord{ PhoneNumber: "+1234567890", DisplayName: "Test User", PhotoURL: "http://www.example.com/testuser/photo.png", + ProviderID: defaultProviderID, }, Disabled: false, @@ -49,9 +52,11 @@ var testUser = &UserRecord{ DisplayName: "Test User", PhotoURL: "http://www.example.com/testuser/photo.png", Email: "testuser@example.com", + UID: "testuid", }, { ProviderID: "phone", PhoneNumber: "+1234567890", + UID: "testuid", }, }, UserMetadata: &UserMetadata{ @@ -314,10 +319,18 @@ func TestCreateUser(t *testing.T) { (&UserToCreate{}).Disabled(true), map[string]interface{}{"disabled": true}, }, + { + (&UserToCreate{}).Disabled(false), + map[string]interface{}{"disabled": false}, + }, { (&UserToCreate{}).EmailVerified(true), map[string]interface{}{"emailVerified": true}, }, + { + (&UserToCreate{}).EmailVerified(false), + map[string]interface{}{"emailVerified": false}, + }, { (&UserToCreate{}).PhotoURL("http://some.url"), map[string]interface{}{"photoUrl": "http://some.url"}, @@ -420,10 +433,18 @@ func TestUpdateUser(t *testing.T) { (&UserToUpdate{}).Disabled(true), map[string]interface{}{"disableUser": true}, }, + { + (&UserToUpdate{}).Disabled(false), + map[string]interface{}{"disableUser": false}, + }, { (&UserToUpdate{}).EmailVerified(true), map[string]interface{}{"emailVerified": true}, }, + { + (&UserToUpdate{}).EmailVerified(false), + map[string]interface{}{"emailVerified": false}, + }, { (&UserToUpdate{}).PhotoURL("http://some.url"), map[string]interface{}{"photoUrl": "http://some.url"}, @@ -576,32 +597,35 @@ func TestInvalidDeleteUser(t *testing.T) { } func TestMakeExportedUser(t *testing.T) { - rur := responseUserRecord{ - UID: "testuser", - Email: "testuser@example.com", - PhoneNumber: "+1234567890", - EmailVerified: true, - DisplayName: "Test User", - ProviderUserInfo: []*UserInfo{ + + rur := &identitytoolkit.UserInfo{ + LocalId: "testuser", + Email: "testuser@example.com", + PhoneNumber: "+1234567890", + EmailVerified: true, + DisplayName: "Test User", + Salt: "salt", + PhotoUrl: "http://www.example.com/testuser/photo.png", + PasswordHash: "passwordhash", + ValidSince: 1494364393, + Disabled: false, + CreatedAt: 1234567890, + LastLoginAt: 1233211232, + CustomAttributes: `{"admin": true, "package": "gold"}`, + ProviderUserInfo: []*identitytoolkit.UserInfoProviderUserInfo{ { - ProviderID: "password", + ProviderId: "password", DisplayName: "Test User", - PhotoURL: "http://www.example.com/testuser/photo.png", + PhotoUrl: "http://www.example.com/testuser/photo.png", Email: "testuser@example.com", + RawId: "testuid", }, { - ProviderID: "phone", + ProviderId: "phone", PhoneNumber: "+1234567890", + RawId: "testuid", }}, - PhotoURL: "http://www.example.com/testuser/photo.png", - PasswordHash: "passwordhash", - PasswordSalt: "salt", - - ValidSince: 1494364393, - Disabled: false, - CreationTimestamp: 1234567890, - LastLogInTimestamp: 1233211232, - CustomClaims: `{"admin": true, "package": "gold"}`, } + want := &ExportedUserRecord{ UserRecord: testUser, PasswordHash: "passwordhash", @@ -626,14 +650,14 @@ func TestMakeExportedUser(t *testing.T) { func TestHTTPError(t *testing.T) { s := echoServer([]byte(`{"error":"test"}`), t) defer s.Close() - s.Status = 500 + s.Status = http.StatusInternalServerError u, err := s.Client.GetUser(context.Background(), "some uid") if u != nil || err == nil { t.Fatalf("GetUser() = (%v, %v); want = (nil, error)", u, err) } - want := `http error status: 500; reason: {"error":"test"}` + want := `googleapi: got HTTP response code 500 with body: {"error":"test"}` if err.Error() != want { t.Errorf("GetUser() = %v; want = %q", err, want) } @@ -682,7 +706,7 @@ func echoServer(resp interface{}, t *testing.T) *mockAuthServer { if err != nil { t.Fatal(err) } - s.Rbody = reqBody + s.Rbody = bytes.TrimSpace(reqBody) s.Req = append(s.Req, r) gh := r.Header.Get("Authorization") @@ -719,7 +743,7 @@ func echoServer(resp interface{}, t *testing.T) *mockAuthServer { if err != nil { t.Fatal(err) } - authClient.url = s.Srv.URL + "/" + authClient.is.BasePath = s.Srv.URL + "/" s.Client = authClient return &s } diff --git a/firebase.go b/firebase.go index c0ed3569..f9b9db49 100644 --- a/firebase.go +++ b/firebase.go @@ -18,7 +18,10 @@ package firebase import ( + "encoding/json" "errors" + "io/ioutil" + "os" "cloud.google.com/go/firestore" @@ -27,8 +30,6 @@ import ( "firebase.google.com/go/internal" "firebase.google.com/go/storage" - "os" - "golang.org/x/net/context" "golang.org/x/oauth2/google" "google.golang.org/api/option" @@ -45,7 +46,10 @@ var firebaseScopes = []string{ } // Version of the Firebase Go Admin SDK. -const Version = "2.3.0" +const Version = "2.4.0" + +// firebaseEnvName is the name of the environment variable with the Config. +const firebaseEnvName = "FIREBASE_CONFIG" // An App holds configuration and state common to all Firebase services that are exposed from the SDK. type App struct { @@ -57,8 +61,8 @@ type App struct { // Config represents the configuration used to initialize an App. type Config struct { - ProjectID string - StorageBucket string + ProjectID string `json:"projectId"` + StorageBucket string `json:"storageBucket"` } // Auth returns an instance of auth.Client. @@ -107,14 +111,14 @@ func (a *App) InstanceID(ctx context.Context) (*iid.Client, error) { func NewApp(ctx context.Context, config *Config, opts ...option.ClientOption) (*App, error) { o := []option.ClientOption{option.WithScopes(firebaseScopes...)} o = append(o, opts...) - creds, err := transport.Creds(ctx, o...) if err != nil { return nil, err } - if config == nil { - config = &Config{} + if config, err = getConfigDefaults(); err != nil { + return nil, err + } } var pid string @@ -133,3 +137,24 @@ func NewApp(ctx context.Context, config *Config, opts ...option.ClientOption) (* opts: o, }, nil } + +// getConfigDefaults reads the default config file, defined by the FIREBASE_CONFIG +// env variable, used only when options are nil. +func getConfigDefaults() (*Config, error) { + fbc := &Config{} + confFileName := os.Getenv(firebaseEnvName) + if confFileName == "" { + return fbc, nil + } + var dat []byte + if confFileName[0] == byte('{') { + dat = []byte(confFileName) + } else { + var err error + if dat, err = ioutil.ReadFile(confFileName); err != nil { + return nil, err + } + } + err := json.Unmarshal(dat, fbc) + return fbc, err +} diff --git a/firebase_test.go b/firebase_test.go index 9e2388ce..686d6af5 100644 --- a/firebase_test.go +++ b/firebase_test.go @@ -16,6 +16,7 @@ package firebase import ( "io/ioutil" + "log" "net/http" "net/http/httptest" "os" @@ -35,6 +36,17 @@ import ( "google.golang.org/api/option" ) +const credEnvVar = "GOOGLE_APPLICATION_CREDENTIALS" + +func TestMain(m *testing.M) { + // This isolates the tests from a possiblity that the default config env + // variable is set to a valid file containing the wanted default config, + // but we the test is not expecting it. + configOld := overwriteEnv(firebaseEnvName, "") + defer reinstateEnv(firebaseEnvName, configOld) + os.Exit(m.Run()) +} + func TestServiceAcctFile(t *testing.T) { app, err := NewApp(context.Background(), nil, option.WithCredentialsFile("testdata/service_account.json")) if err != nil { @@ -85,8 +97,8 @@ func TestClientOptions(t *testing.T) { if err != nil { t.Fatal(err) } - if resp.StatusCode != 200 { - t.Errorf("Status: %d; want: 200", resp.StatusCode) + if resp.StatusCode != http.StatusOK { + t.Errorf("Status: %d; want: %d", resp.StatusCode, http.StatusOK) } if bearer != "Bearer mock-token" { t.Errorf("Bearer token: %q; want: %q", bearer, "Bearer mock-token") @@ -151,13 +163,12 @@ func TestRefreshTokenWithEnvVar(t *testing.T) { } func TestAppDefault(t *testing.T) { - varName := "GOOGLE_APPLICATION_CREDENTIALS" - current := os.Getenv(varName) + current := os.Getenv(credEnvVar) - if err := os.Setenv(varName, "testdata/service_account.json"); err != nil { + if err := os.Setenv(credEnvVar, "testdata/service_account.json"); err != nil { t.Fatal(err) } - defer os.Setenv(varName, current) + defer os.Setenv(credEnvVar, current) app, err := NewApp(context.Background(), nil) if err != nil { @@ -175,13 +186,12 @@ func TestAppDefault(t *testing.T) { } func TestAppDefaultWithInvalidFile(t *testing.T) { - varName := "GOOGLE_APPLICATION_CREDENTIALS" - current := os.Getenv(varName) + current := os.Getenv(credEnvVar) - if err := os.Setenv(varName, "testdata/non_existing.json"); err != nil { + if err := os.Setenv(credEnvVar, "testdata/non_existing.json"); err != nil { t.Fatal(err) } - defer os.Setenv(varName, current) + defer os.Setenv(credEnvVar, current) app, err := NewApp(context.Background(), nil) if app != nil || err == nil { @@ -318,8 +328,8 @@ func TestCustomTokenSource(t *testing.T) { if err != nil { t.Fatal(err) } - if resp.StatusCode != 200 { - t.Errorf("Status: %d; want: 200", resp.StatusCode) + if resp.StatusCode != http.StatusOK { + t.Errorf("Status: %d; want: %d", resp.StatusCode, http.StatusOK) } if bearer != "Bearer "+ts.AccessToken { t.Errorf("Bearer token: %q; want: %q", bearer, "Bearer "+ts.AccessToken) @@ -337,6 +347,139 @@ func TestVersion(t *testing.T) { } } } +func TestAutoInit(t *testing.T) { + tests := []struct { + name string + optionsConfig string + initOptions *Config + wantOptions *Config + }{ + { + "No environment variable, no explicit options", + "", + nil, + &Config{ProjectID: "mock-project-id"}, // from default creds here and below. + }, { + "Environment variable set to file, no explicit options", + "testdata/firebase_config.json", + nil, + &Config{ + ProjectID: "hipster-chat-mock", + StorageBucket: "hipster-chat.appspot.mock", + }, + }, { + "Environment variable set to string, no explicit options", + `{ + "projectId": "hipster-chat-mock", + "storageBucket": "hipster-chat.appspot.mock" + }`, + nil, + &Config{ + ProjectID: "hipster-chat-mock", + StorageBucket: "hipster-chat.appspot.mock", + }, + }, { + "Environment variable set to file with some values missing, no explicit options", + "testdata/firebase_config_partial.json", + nil, + &Config{ProjectID: "hipster-chat-mock"}, + }, { + "Environment variable set to string with some values missing, no explicit options", + `{"projectId": "hipster-chat-mock"}`, + nil, + &Config{ProjectID: "hipster-chat-mock"}, + }, { + "Environment variable set to file which is ignored as some explicit options are passed", + "testdata/firebase_config_partial.json", + &Config{StorageBucket: "sb1-mock"}, + &Config{ + ProjectID: "mock-project-id", + StorageBucket: "sb1-mock", + }, + }, { + "Environment variable set to string which is ignored as some explicit options are passed", + `{"projectId": "hipster-chat-mock"}`, + &Config{StorageBucket: "sb1-mock"}, + &Config{ + ProjectID: "mock-project-id", + StorageBucket: "sb1-mock", + }, + }, { + "Environment variable set to file which is ignored as options are explicitly empty", + "testdata/firebase_config_partial.json", + &Config{}, + &Config{ProjectID: "mock-project-id"}, + }, { + "Environment variable set to file with an unknown key which is ignored, no explicit options", + "testdata/firebase_config_invalid_key.json", + nil, + &Config{ + ProjectID: "mock-project-id", // from default creds + StorageBucket: "hipster-chat.appspot.mock", + }, + }, { + "Environment variable set to string with an unknown key which is ignored, no explicit options", + `{ + "obviously_bad_key": "hipster-chat-mock", + "storageBucket": "hipster-chat.appspot.mock" + }`, + nil, + &Config{ + ProjectID: "mock-project-id", + StorageBucket: "hipster-chat.appspot.mock", + }, + }, + } + + credOld := overwriteEnv(credEnvVar, "testdata/service_account.json") + defer reinstateEnv(credEnvVar, credOld) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + overwriteEnv(firebaseEnvName, test.optionsConfig) + app, err := NewApp(context.Background(), test.initOptions) + if err != nil { + t.Error(err) + } else { + compareConfig(app, test.wantOptions, t) + } + }) + } +} + +func TestAutoInitInvalidFiles(t *testing.T) { + tests := []struct { + name string + filename string + wantError string + }{ + { + "nonexistant file", + "testdata/no_such_file.json", + "open testdata/no_such_file.json: no such file or directory", + }, { + "invalid JSON", + "testdata/firebase_config_invalid.json", + "invalid character 'b' looking for beginning of value", + }, { + "empty file", + "testdata/firebase_config_empty.json", + "unexpected end of JSON input", + }, + } + credOld := overwriteEnv(credEnvVar, "testdata/service_account.json") + defer reinstateEnv(credEnvVar, credOld) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + overwriteEnv(firebaseEnvName, test.filename) + _, err := NewApp(context.Background(), nil) + if err == nil || err.Error() != test.wantError { + t.Errorf("got error = %s; want = %s", err, test.wantError) + } + }) + } +} type testTokenSource struct { AccessToken string @@ -350,6 +493,15 @@ func (t *testTokenSource) Token() (*oauth2.Token, error) { }, nil } +func compareConfig(got *App, want *Config, t *testing.T) { + if got.projectID != want.ProjectID { + t.Errorf("app.projectID = %q; want = %q", got.projectID, want.ProjectID) + } + if got.storageBucket != want.StorageBucket { + t.Errorf("app.storageBucket = %q; want = %q", got.storageBucket, want.StorageBucket) + } +} + // mockServiceAcct generates a service account configuration with the provided URL as the // token_url value. func mockServiceAcct(tokenURL string) ([]byte, error) { @@ -379,3 +531,25 @@ func initMockTokenServer() *httptest.Server { }`)) })) } + +// overwriteEnv overwrites env variables, used in testsing. +func overwriteEnv(varName, newVal string) string { + oldVal := os.Getenv(varName) + if newVal == "" { + if err := os.Unsetenv(varName); err != nil { + log.Fatal(err) + } + } else if err := os.Setenv(varName, newVal); err != nil { + log.Fatal(err) + } + return oldVal +} + +// reinstateEnv restores the enviornment variable, will usually be used deferred with overwriteEnv. +func reinstateEnv(varName, oldVal string) { + if len(varName) > 0 { + os.Setenv(varName, oldVal) + } else { + os.Unsetenv(varName) + } +} diff --git a/iid/iid.go b/iid/iid.go index d7798e1a..980a7bed 100644 --- a/iid/iid.go +++ b/iid/iid.go @@ -30,14 +30,14 @@ import ( const iidEndpoint = "https://console.firebase.google.com/v1" var errorCodes = map[int]string{ - 400: "malformed instance id argument", - 401: "request not authorized", - 403: "project does not match instance ID or the client does not have sufficient privileges", - 404: "failed to find the instance id", - 409: "already deleted", - 429: "request throttled out by the backend server", - 500: "internal server error", - 503: "backend servers are over capacity", + http.StatusBadRequest: "malformed instance id argument", + http.StatusUnauthorized: "request not authorized", + http.StatusForbidden: "project does not match instance ID or the client does not have sufficient privileges", + http.StatusNotFound: "failed to find the instance id", + http.StatusConflict: "already deleted", + http.StatusTooManyRequests: "request throttled out by the backend server", + http.StatusInternalServerError: "internal server error", + http.StatusServiceUnavailable: "backend servers are over capacity", } // Client is the interface for the Firebase Instance ID service. @@ -79,7 +79,7 @@ func (c *Client) DeleteInstanceID(ctx context.Context, iid string) error { } url := fmt.Sprintf("%s/project/%s/instanceId/%s", c.endpoint, c.project, iid) - resp, err := c.client.Do(ctx, &internal.Request{Method: "DELETE", URL: url}) + resp, err := c.client.Do(ctx, &internal.Request{Method: http.MethodDelete, URL: url}) if err != nil { return err } diff --git a/iid/iid_test.go b/iid/iid_test.go index 9a7c6d15..6d154650 100644 --- a/iid/iid_test.go +++ b/iid/iid_test.go @@ -75,8 +75,8 @@ func TestDeleteInstanceID(t *testing.T) { if tr == nil { t.Fatalf("Request = nil; want non-nil") } - if tr.Method != "DELETE" { - t.Errorf("Method = %q; want = %q", tr.Method, "DELETE") + if tr.Method != http.MethodDelete { + t.Errorf("Method = %q; want = %q", tr.Method, http.MethodDelete) } if tr.URL.Path != "/project/test-project/instanceId/test-iid" { t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/instanceId/test-iid") @@ -87,7 +87,7 @@ func TestDeleteInstanceID(t *testing.T) { } func TestDeleteInstanceIDError(t *testing.T) { - status := 200 + status := http.StatusOK var tr *http.Request ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tr = r @@ -119,8 +119,8 @@ func TestDeleteInstanceIDError(t *testing.T) { if tr == nil { t.Fatalf("Request = nil; want non-nil") } - if tr.Method != "DELETE" { - t.Errorf("Method = %q; want = %q", tr.Method, "DELETE") + if tr.Method != http.MethodDelete { + t.Errorf("Method = %q; want = %q", tr.Method, http.MethodDelete) } if tr.URL.Path != "/project/test-project/instanceId/test-iid" { t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/instanceId/test-iid") @@ -162,8 +162,8 @@ func TestDeleteInstanceIDUnexpectedError(t *testing.T) { if tr == nil { t.Fatalf("Request = nil; want non-nil") } - if tr.Method != "DELETE" { - t.Errorf("Method = %q; want = %q", tr.Method, "DELETE") + if tr.Method != http.MethodDelete { + t.Errorf("Method = %q; want = %q", tr.Method, http.MethodDelete) } if tr.URL.Path != "/project/test-project/instanceId/test-iid" { t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/instanceId/test-iid") diff --git a/integration/auth/auth_test.go b/integration/auth/auth_test.go index 63e26295..ee0dc0b3 100644 --- a/integration/auth/auth_test.go +++ b/integration/auth/auth_test.go @@ -139,7 +139,7 @@ func postRequest(url string, req []byte) ([]byte, error) { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected http status code: %d", resp.StatusCode) } return ioutil.ReadAll(resp.Body) diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index d626c4f7..8227599f 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -212,7 +212,10 @@ func testUpdateUser(t *testing.T) { } want := &auth.UserRecord{ - UserInfo: &auth.UserInfo{UID: testFixtures.sampleUserBlank.UID}, + UserInfo: &auth.UserInfo{ + UID: testFixtures.sampleUserBlank.UID, + ProviderID: "firebase", + }, UserMetadata: &auth.UserMetadata{ CreationTimestamp: testFixtures.sampleUserBlank.UserMetadata.CreationTimestamp, }, @@ -241,6 +244,7 @@ func testUpdateUser(t *testing.T) { DisplayName: "name", PhoneNumber: "+12345678901", PhotoURL: "http://photo.png", + ProviderID: "firebase", Email: "abc@ab.ab", }, UserMetadata: &auth.UserMetadata{ @@ -257,10 +261,12 @@ func testUpdateUser(t *testing.T) { Email: "abc@ab.ab", PhotoURL: "http://photo.png", ProviderID: "password", + UID: "abc@ab.ab", } phoneUI := &auth.UserInfo{ PhoneNumber: "+12345678901", ProviderID: "phone", + UID: "+12345678901", } var compareWith *auth.UserInfo @@ -277,7 +283,7 @@ func testUpdateUser(t *testing.T) { } } - // compare provider info seperatley since the order of the providers isn't guaranteed. + // compare provider info separately since the order of the providers isn't guaranteed. testProviderInfo(u.ProviderUserInfo, t) // now compare the rest of the record, without the ProviderInfo diff --git a/integration/storage/storage_test.go b/integration/storage/storage_test.go index 078e61c6..5efe92d2 100644 --- a/integration/storage/storage_test.go +++ b/integration/storage/storage_test.go @@ -116,8 +116,5 @@ func verifyBucket(bucket *gcs.BucketHandle) error { } // Delete the object - if err := o.Delete(ctx); err != nil { - return err - } - return nil + return o.Delete(ctx) } diff --git a/internal/http_client_test.go b/internal/http_client_test.go index 67b1c35b..bdac7474 100644 --- a/internal/http_client_test.go +++ b/internal/http_client_test.go @@ -33,25 +33,25 @@ var cases = []struct { }{ { req: &Request{ - Method: "GET", + Method: http.MethodGet, }, - method: "GET", + method: http.MethodGet, }, { req: &Request{ - Method: "GET", + Method: http.MethodGet, Opts: []HTTPOption{ WithHeader("Test-Header", "value1"), WithQueryParam("testParam", "value2"), }, }, - method: "GET", + method: http.MethodGet, headers: map[string]string{"Test-Header": "value1"}, query: map[string]string{"testParam": "value2"}, }, { req: &Request{ - Method: "POST", + Method: http.MethodPost, Body: NewJSONEntity(map[string]string{"foo": "bar"}), Opts: []HTTPOption{ WithHeader("Test-Header", "value1"), @@ -59,35 +59,35 @@ var cases = []struct { WithQueryParam("testParam2", "value3"), }, }, - method: "POST", + method: http.MethodPost, body: "{\"foo\":\"bar\"}", headers: map[string]string{"Test-Header": "value1"}, query: map[string]string{"testParam1": "value2", "testParam2": "value3"}, }, { req: &Request{ - Method: "POST", + Method: http.MethodPost, Body: NewJSONEntity("body"), Opts: []HTTPOption{ WithHeader("Test-Header", "value1"), WithQueryParams(map[string]string{"testParam1": "value2", "testParam2": "value3"}), }, }, - method: "POST", + method: http.MethodPost, body: "\"body\"", headers: map[string]string{"Test-Header": "value1"}, query: map[string]string{"testParam1": "value2", "testParam2": "value3"}, }, { req: &Request{ - Method: "PUT", + Method: http.MethodPut, Body: NewJSONEntity(nil), Opts: []HTTPOption{ WithHeader("Test-Header", "value1"), WithQueryParams(map[string]string{"testParam1": "value2", "testParam2": "value3"}), }, }, - method: "PUT", + method: http.MethodPut, body: "null", headers: map[string]string{"Test-Header": "value1"}, query: map[string]string{"testParam1": "value2", "testParam2": "value3"}, @@ -184,7 +184,7 @@ func TestContext(t *testing.T) { client := &HTTPClient{Client: http.DefaultClient} ctx, cancel := context.WithCancel(context.Background()) resp, err := client.Do(ctx, &Request{ - Method: "GET", + Method: http.MethodGet, URL: server.URL, }) if err != nil { @@ -196,7 +196,7 @@ func TestContext(t *testing.T) { cancel() resp, err = client.Do(ctx, &Request{ - Method: "GET", + Method: http.MethodGet, URL: server.URL, }) if resp != nil || err == nil { @@ -234,7 +234,7 @@ func TestErrorParser(t *testing.T) { Client: http.DefaultClient, ErrParser: ep, } - req := &Request{Method: "GET", URL: server.URL} + req := &Request{Method: http.MethodGet, URL: server.URL} resp, err := client.Do(context.Background(), req) if err != nil { t.Fatal(err) @@ -255,7 +255,7 @@ func TestErrorParser(t *testing.T) { func TestInvalidURL(t *testing.T) { req := &Request{ - Method: "GET", + Method: http.MethodGet, URL: "http://localhost:250/mock.url", } client := &HTTPClient{Client: http.DefaultClient} @@ -281,7 +281,7 @@ func TestUnmarshalError(t *testing.T) { server := httptest.NewServer(handler) defer server.Close() - req := &Request{Method: "GET", URL: server.URL} + req := &Request{Method: http.MethodGet, URL: server.URL} client := &HTTPClient{Client: http.DefaultClient} resp, err := client.Do(context.Background(), req) if err != nil { diff --git a/testdata/firebase_config.json b/testdata/firebase_config.json new file mode 100644 index 00000000..d249fe76 --- /dev/null +++ b/testdata/firebase_config.json @@ -0,0 +1,4 @@ +{ + "projectId": "hipster-chat-mock", + "storageBucket": "hipster-chat.appspot.mock" +} diff --git a/testdata/firebase_config_empty.json b/testdata/firebase_config_empty.json new file mode 100644 index 00000000..e69de29b diff --git a/testdata/firebase_config_invalid.json b/testdata/firebase_config_invalid.json new file mode 100644 index 00000000..485aba4f --- /dev/null +++ b/testdata/firebase_config_invalid.json @@ -0,0 +1 @@ +baaad diff --git a/testdata/firebase_config_invalid_key.json b/testdata/firebase_config_invalid_key.json new file mode 100644 index 00000000..8fad82c8 --- /dev/null +++ b/testdata/firebase_config_invalid_key.json @@ -0,0 +1,4 @@ +{ + "project1d_bad_key": "hipster-chat-mock", + "storageBucket": "hipster-chat.appspot.mock" +} diff --git a/testdata/firebase_config_partial.json b/testdata/firebase_config_partial.json new file mode 100644 index 00000000..1775043e --- /dev/null +++ b/testdata/firebase_config_partial.json @@ -0,0 +1,3 @@ +{ + "projectId": "hipster-chat-mock" +} diff --git a/testdata/get_user.json b/testdata/get_user.json index dc992a64..a56ef9f3 100644 --- a/testdata/get_user.json +++ b/testdata/get_user.json @@ -1,31 +1,36 @@ { - "kind" : "identitytoolkit#GetAccountInfoResponse", - "users" : [ { - "localId" : "testuser", - "email" : "testuser@example.com", - "phoneNumber" : "+1234567890", - "emailVerified" : true, - "displayName" : "Test User", - "providerUserInfo" : [ { - "providerId" : "password", - "displayName" : "Test User", - "photoUrl" : "http://www.example.com/testuser/photo.png", - "federatedId" : "testuser@example.com", - "email" : "testuser@example.com", - "rawId" : "testuser@example.com" - }, { - "providerId" : "phone", - "phoneNumber" : "+1234567890", - "rawId" : "+1234567890" - } ], - "photoUrl" : "http://www.example.com/testuser/photo.png", - "passwordHash" : "passwordhash", - "salt" : "salt===", - "passwordUpdatedAt" : 1.494364393E+12, - "validSince" : "1494364393", - "disabled" : false, - "createdAt" : "1234567890", - "lastLoginAt" :"1233211232", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" - } ] + "kind": "identitytoolkit#GetAccountInfoResponse", + "users": [ + { + "localId": "testuser", + "email": "testuser@example.com", + "phoneNumber": "+1234567890", + "emailVerified": true, + "displayName": "Test User", + "providerUserInfo": [ + { + "providerId": "password", + "displayName": "Test User", + "photoUrl": "http://www.example.com/testuser/photo.png", + "federatedId": "testuser@example.com", + "email": "testuser@example.com", + "rawId": "testuid" + }, + { + "providerId": "phone", + "phoneNumber": "+1234567890", + "rawId": "testuid" + } + ], + "photoUrl": "http://www.example.com/testuser/photo.png", + "passwordHash": "passwordhash", + "salt": "salt===", + "passwordUpdatedAt": 1.494364393E+12, + "validSince": "1494364393", + "disabled": false, + "createdAt": "1234567890", + "lastLoginAt": "1233211232", + "customAttributes": "{\"admin\": true, \"package\": \"gold\"}" + } + ] } diff --git a/testdata/list_users.json b/testdata/list_users.json index e4749ab3..21d152fc 100644 --- a/testdata/list_users.json +++ b/testdata/list_users.json @@ -2,88 +2,97 @@ "kind": "identitytoolkit#DownloadAccountResponse", "users": [ { - "localId" : "testuser", - "email" : "testuser@example.com", - "phoneNumber" : "+1234567890", - "emailVerified" : true, - "displayName" : "Test User", - "providerUserInfo" : [ { - "providerId" : "password", - "displayName" : "Test User", - "photoUrl" : "http://www.example.com/testuser/photo.png", - "federatedId" : "testuser@example.com", - "email" : "testuser@example.com", - "rawId" : "testuser@example.com" - }, { - "providerId" : "phone", - "phoneNumber" : "+1234567890", - "rawId" : "+1234567890" - } ], - "photoUrl" : "http://www.example.com/testuser/photo.png", - "passwordHash" : "passwordhash1", - "salt" : "salt1", - "passwordUpdatedAt" : 1.494364393E+12, - "validSince" : "1494364393", - "disabled" : false, - "createdAt" : "1234567890", - "lastLoginAt" :"1233211232", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "localId": "testuser", + "email": "testuser@example.com", + "phoneNumber": "+1234567890", + "emailVerified": true, + "displayName": "Test User", + "providerUserInfo": [ + { + "providerId": "password", + "displayName": "Test User", + "photoUrl": "http://www.example.com/testuser/photo.png", + "federatedId": "testuser@example.com", + "email": "testuser@example.com", + "rawId": "testuid" + }, + { + "providerId": "phone", + "phoneNumber": "+1234567890", + "rawId": "testuid" + } + ], + "photoUrl": "http://www.example.com/testuser/photo.png", + "passwordHash": "passwordhash1", + "salt": "salt1", + "passwordUpdatedAt": 1.494364393E+12, + "validSince": "1494364393", + "disabled": false, + "createdAt": "1234567890", + "lastLoginAt": "1233211232", + "customAttributes": "{\"admin\": true, \"package\": \"gold\"}" }, { - "localId" : "testuser", - "email" : "testuser@example.com", - "phoneNumber" : "+1234567890", - "emailVerified" : true, - "displayName" : "Test User", - "providerUserInfo" : [ { - "providerId" : "password", - "displayName" : "Test User", - "photoUrl" : "http://www.example.com/testuser/photo.png", - "federatedId" : "testuser@example.com", - "email" : "testuser@example.com", - "rawId" : "testuser@example.com" - }, { - "providerId" : "phone", - "phoneNumber" : "+1234567890", - "rawId" : "+1234567890" - } ], - "photoUrl" : "http://www.example.com/testuser/photo.png", - "passwordHash" : "passwordhash2", - "salt" : "salt2", - "passwordUpdatedAt" : 1.494364393E+12, - "validSince" : "1494364393", - "disabled" : false, - "createdAt" : "1234567890", - "lastLoginAt" :"1233211232", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "localId": "testuser", + "email": "testuser@example.com", + "phoneNumber": "+1234567890", + "emailVerified": true, + "displayName": "Test User", + "providerUserInfo": [ + { + "providerId": "password", + "displayName": "Test User", + "photoUrl": "http://www.example.com/testuser/photo.png", + "federatedId": "testuser@example.com", + "email": "testuser@example.com", + "rawId": "testuid" + }, + { + "providerId": "phone", + "phoneNumber": "+1234567890", + "rawId": "testuid" + } + ], + "photoUrl": "http://www.example.com/testuser/photo.png", + "passwordHash": "passwordhash2", + "salt": "salt2", + "passwordUpdatedAt": 1.494364393E+12, + "validSince": "1494364393", + "disabled": false, + "createdAt": "1234567890", + "lastLoginAt": "1233211232", + "customAttributes": "{\"admin\": true, \"package\": \"gold\"}" }, { - "localId" : "testuser", - "email" : "testuser@example.com", - "phoneNumber" : "+1234567890", - "emailVerified" : true, - "displayName" : "Test User", - "providerUserInfo" : [ { - "providerId" : "password", - "displayName" : "Test User", - "photoUrl" : "http://www.example.com/testuser/photo.png", - "federatedId" : "testuser@example.com", - "email" : "testuser@example.com", - "rawId" : "testuser@example.com" - }, { - "providerId" : "phone", - "phoneNumber" : "+1234567890", - "rawId" : "+1234567890" - } ], - "photoUrl" : "http://www.example.com/testuser/photo.png", - "passwordHash" : "passwordhash3", - "salt" : "salt3", - "passwordUpdatedAt" : 1.494364393E+12, - "validSince" : "1494364393", - "disabled" : false, - "createdAt" : "1234567890", - "lastLoginAt" :"1233211232", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "localId": "testuser", + "email": "testuser@example.com", + "phoneNumber": "+1234567890", + "emailVerified": true, + "displayName": "Test User", + "providerUserInfo": [ + { + "providerId": "password", + "displayName": "Test User", + "photoUrl": "http://www.example.com/testuser/photo.png", + "federatedId": "testuser@example.com", + "email": "testuser@example.com", + "rawId": "testuid" + }, + { + "providerId": "phone", + "phoneNumber": "+1234567890", + "rawId": "testuid" + } + ], + "photoUrl": "http://www.example.com/testuser/photo.png", + "passwordHash": "passwordhash3", + "salt": "salt3", + "passwordUpdatedAt": 1.494364393E+12, + "validSince": "1494364393", + "disabled": false, + "createdAt": "1234567890", + "lastLoginAt": "1233211232", + "customAttributes": "{\"admin\": true, \"package\": \"gold\"}" } ], "nextPageToken": ""