From 0e4edef05fe0d99f8ee715ef8f9fba35003e0f14 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 28 May 2020 21:17:49 +0300 Subject: [PATCH 1/7] fix(fcm): Add ability to override default FCM endpoint via ClientOptions (#373) * Add FIREBASE_MESSAGING_ENDPOINT environment variable to override default FCM endpoint * Use endpoint while creating new messaging client * Add tests for custom endpoint * Simplify setting default endpoint * Remove redundant code * Fix formatting * Fix imports order Co-authored-by: Hiranya Jayathilaka --- firebase_test.go | 38 +++++++++++++++++++++++++++++++++++++ messaging/messaging.go | 12 ++++++++---- messaging/messaging_test.go | 37 ++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/firebase_test.go b/firebase_test.go index 1b367f1c..f29bb73b 100644 --- a/firebase_test.go +++ b/firebase_test.go @@ -29,6 +29,7 @@ import ( "testing" "time" + "firebase.google.com/go/messaging" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/option" @@ -361,6 +362,43 @@ func TestMessaging(t *testing.T) { } } +func TestMessagingSendWithCustomEndpoint(t *testing.T) { + name := "custom-endpoint-ok" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{ \"name\":\"" + name + "\" }")) + })) + defer ts.Close() + + ctx := context.Background() + + tokenSource := &testTokenSource{AccessToken: "mock-token-from-custom"} + app, err := NewApp( + ctx, + nil, + option.WithCredentialsFile("testdata/service_account.json"), + option.WithTokenSource(tokenSource), + option.WithEndpoint(ts.URL), + ) + if err != nil { + t.Fatal(err) + } + + c, err := app.Messaging(ctx) + if c == nil || err != nil { + t.Fatalf("Messaging() = (%v, %v); want (iid, nil)", c, err) + } + + msg := &messaging.Message{ + Token: "...", + } + n, err := c.Send(ctx, msg) + if n != name || err != nil { + t.Errorf("Send() = (%q, %v); want (%q, nil)", n, err, name) + } +} + func TestCustomTokenSource(t *testing.T) { ctx := context.Background() ts := &testTokenSource{AccessToken: "mock-token-from-custom"} diff --git a/messaging/messaging.go b/messaging/messaging.go index 37f61b00..aabfce3d 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -913,13 +913,17 @@ func NewClient(ctx context.Context, c *internal.MessagingConfig) (*Client, error return nil, errors.New("project ID is required to access Firebase Cloud Messaging client") } - hc, _, err := transport.NewHTTPClient(ctx, c.Opts...) + hc, endpoint, err := transport.NewHTTPClient(ctx, c.Opts...) if err != nil { return nil, err } + if endpoint == "" { + endpoint = messagingEndpoint + } + return &Client{ - fcmClient: newFCMClient(hc, c), + fcmClient: newFCMClient(hc, c, endpoint), iidClient: newIIDClient(hc), }, nil } @@ -932,7 +936,7 @@ type fcmClient struct { httpClient *internal.HTTPClient } -func newFCMClient(hc *http.Client, conf *internal.MessagingConfig) *fcmClient { +func newFCMClient(hc *http.Client, conf *internal.MessagingConfig, endpoint string) *fcmClient { client := internal.WithDefaultRetryConfig(hc) client.CreateErrFn = handleFCMError client.SuccessFn = internal.HasSuccessStatus @@ -944,7 +948,7 @@ func newFCMClient(hc *http.Client, conf *internal.MessagingConfig) *fcmClient { } return &fcmClient{ - fcmEndpoint: messagingEndpoint, + fcmEndpoint: endpoint, batchEndpoint: batchEndpoint, project: conf.ProjectID, version: version, diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go index 97f03205..be966513 100644 --- a/messaging/messaging_test.go +++ b/messaging/messaging_test.go @@ -1126,6 +1126,43 @@ func TestSend(t *testing.T) { } } +func TestSendWithCustomEndpoint(t *testing.T) { + var tr *http.Request + var b []byte + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tr = r + b, _ = ioutil.ReadAll(r.Body) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{ \"name\":\"" + testMessageID + "\" }")) + })) + defer ts.Close() + + ctx := context.Background() + + conf := *testMessagingConfig + optEndpoint := option.WithEndpoint(ts.URL) + conf.Opts = append(conf.Opts, optEndpoint) + + client, err := NewClient(ctx, &conf) + if err != nil { + t.Fatal(err) + } + + if ts.URL != client.fcmEndpoint { + t.Errorf("client.fcmEndpoint = %q; want = %q", client.fcmEndpoint, ts.URL) + } + + for _, tc := range validMessages { + t.Run(tc.name, func(t *testing.T) { + name, err := client.Send(ctx, tc.req) + if name != testMessageID || err != nil { + t.Errorf("Send(%s) = (%q, %v); want = (%q, nil)", tc.name, name, err, testMessageID) + } + checkFCMRequest(t, b, tr, tc.want, false) + }) + } +} + func TestSendDryRun(t *testing.T) { var tr *http.Request var b []byte From f2eb40b57ad9507e55c21d58e0b8a7e577bb1a14 Mon Sep 17 00:00:00 2001 From: rsgowman Date: Fri, 29 May 2020 16:38:30 -0400 Subject: [PATCH 2/7] Snippets for bulk get/delete function (#328) Corresponding doc change to enable it: http://cl/282953279 --- snippets/auth.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/snippets/auth.go b/snippets/auth.go index a1a87eaa..259a5334 100644 --- a/snippets/auth.go +++ b/snippets/auth.go @@ -185,6 +185,30 @@ func getUserByPhone(ctx context.Context, client *auth.Client) *auth.UserRecord { return u } +func bulkGetUsers(ctx context.Context, client *auth.Client) { + // [START bulk_get_users_golang] + getUsersResult, err := client.GetUsers(ctx, []auth.UserIdentifier{ + auth.UIDIdentifier{UID: "uid1"}, + auth.EmailIdentifier{Email: "user@example.com"}, + auth.PhoneIdentifier{PhoneNumber: "+15555551234"}, + auth.ProviderIdentifier{ProviderID: "google.com", ProviderUID: "google_uid1"}, + }) + if err != nil { + log.Fatalf("error retriving multiple users: %v\n", err) + } + + log.Printf("Successfully fetched user data:") + for _, u := range getUsersResult.Users { + log.Printf("%v", u) + } + + log.Printf("Unable to find users corresponding to these identifiers:") + for _, id := range getUsersResult.NotFound { + log.Printf("%v", id) + } + // [END bulk_get_users_golang] +} + func createUser(ctx context.Context, client *auth.Client) *auth.UserRecord { // [START create_user_golang] params := (&auth.UserToCreate{}). @@ -250,6 +274,21 @@ func deleteUser(ctx context.Context, client *auth.Client) { // [END delete_user_golang] } +func bulkDeleteUsers(ctx context.Context, client *auth.Client) { + // [START bulk_delete_users_golang] + deleteUsersResult, err := client.DeleteUsers(ctx, []string{"uid1", "uid2", "uid3"}) + if err != nil { + log.Fatalf("error deleting users: %v\n", err) + } + + log.Printf("Successfully deleted %d users", deleteUsersResult.SuccessCount) + log.Printf("Failed to delete %d users", deleteUsersResult.FailureCount) + for _, err := range deleteUsersResult.Errors { + log.Printf("%v", err) + } + // [END bulk_delete_users_golang] +} + func customClaimsSet(ctx context.Context, app *firebase.App) { uid := "uid" // [START set_custom_user_claims_golang] From 81eddc5df347bfe626ab24f99f38412a7465b8f6 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 11 Jun 2020 11:34:14 -0700 Subject: [PATCH 3/7] chore: Merging v4 branch into dev (#370) * Renamed WebpushFcmOptions to WebpushFCMOptions (#334) * Added modules support; Added semantic import versioning (#336) * feat: New error handling APIs (#342) * fix: Error handling revamp basic structure * Added more documentation and tests * Updated docs * Exhaustive unit tests; Updated integration test * Fixed some typos in comments * Renamed function in comment * FCM error handling revamp (#343) * FCM error handling revamp * Updated unit tests * RTDB error handling revamp (#345) * Error handling revamp of Auth APIs (#348) * Error handling revamp for Auth APIs * Minor code clean up * Error handling support in the token verification APIs (#349) * New error handling scheme for token verification APIs * Make InvalidToken condition conjunctive * Removing deprecated HTTP and error handling utils (#350) * Removed deprecated APIs from internal * Made HasSuccessStatus the default error checking function * Added unit test for error response handling * Handling timeouts, connection and other network errors (#353) * Handling timeouts, connection and other network errors * Handling wrapped errors * Updated comment * Apply suggestions from code review * Removing old release workflow files * Removing an erroneous import * Fixed bad merge * Fixed failing test due to old dependency * chore: Updated release flow for modules support (#378) * chore: Updated release flow for modules support * Fixed failing unit test * Removed GOPATH from release workflow --- .github/scripts/run_all_tests.sh | 2 +- .github/workflows/ci.yml | 22 +- .github/workflows/release.yml | 24 +- auth/auth.go | 66 ++++- auth/auth_appengine.go | 2 +- auth/auth_std.go | 2 +- auth/auth_test.go | 63 +++-- auth/export_users.go | 2 +- auth/hash/hash.go | 2 +- auth/hash/hash_test.go | 4 +- auth/import_users.go | 2 +- auth/provider_config.go | 18 +- auth/provider_config_test.go | 17 +- auth/tenant_mgt.go | 8 +- auth/tenant_mgt_test.go | 13 +- auth/token_generator.go | 66 ++--- auth/token_generator_test.go | 23 +- auth/token_verifier.go | 157 +++++++++-- auth/token_verifier_test.go | 2 +- auth/user_mgt.go | 209 ++++++++------ auth/user_mgt_test.go | 148 ++++++++-- db/db.go | 51 ++-- db/db_test.go | 2 +- db/query.go | 15 +- db/query_test.go | 16 +- db/ref.go | 152 ++++++---- db/ref_test.go | 111 +++++++- errorutils/errorutils.go | 143 ++++++++++ firebase.go | 14 +- firebase_test.go | 7 +- go.mod | 11 + go.sum | 179 ++++++++++++ iid/iid.go | 93 +++---- iid/iid_test.go | 39 ++- integration/auth/auth_test.go | 6 +- integration/auth/provider_config_test.go | 2 +- integration/auth/tenant_mgt_test.go | 2 +- integration/auth/user_mgt_test.go | 4 +- integration/db/db_test.go | 86 ++++-- integration/db/query_test.go | 2 +- integration/firestore/firestore_test.go | 2 +- integration/iid/iid_test.go | 7 +- integration/internal/internal.go | 7 +- integration/messaging/messaging_test.go | 4 +- integration/storage/storage_test.go | 6 +- internal/errors.go | 197 +++++++++++++ internal/errors_test.go | 337 +++++++++++++++++++++++ internal/http_client.go | 140 +++------- internal/http_client_test.go | 211 ++++---------- internal/internal.go | 32 +-- internal/json_http_client_test.go | 5 +- messaging/messaging.go | 173 ++++++------ messaging/messaging_batch.go | 2 +- messaging/messaging_test.go | 110 +++++--- messaging/messaging_utils.go | 4 +- messaging/topic_mgt.go | 48 +--- messaging/topic_mgt_test.go | 58 ++-- snippets/auth.go | 6 +- snippets/db.go | 4 +- snippets/init.go | 4 +- snippets/messaging.go | 4 +- snippets/storage.go | 2 +- storage/storage.go | 2 +- storage/storage_test.go | 2 +- 64 files changed, 2175 insertions(+), 979 deletions(-) create mode 100644 errorutils/errorutils.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/errors.go create mode 100644 internal/errors_test.go diff --git a/.github/scripts/run_all_tests.sh b/.github/scripts/run_all_tests.sh index b52b283c..c47961b3 100755 --- a/.github/scripts/run_all_tests.sh +++ b/.github/scripts/run_all_tests.sh @@ -22,4 +22,4 @@ gpg --quiet --batch --yes --decrypt --passphrase="${FIREBASE_SERVICE_ACCT_KEY}" echo "${FIREBASE_API_KEY}" > testdata/integration_apikey.txt -go test -v -race firebase.google.com/go/... +go test -v -race ./... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6153e812..eca8b98c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,38 +5,30 @@ jobs: build: name: Build runs-on: ubuntu-latest - env: - GOPATH: ${{ github.workspace }}/go strategy: matrix: go: [1.12, 1.13, 1.14] - steps: + steps: - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v1 with: go-version: ${{ matrix.go }} - id: go - - name: Check out code into GOPATH + - name: Check out code 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) - name: Run Linter run: | - go get golang.org/x/lint/golint - $GOPATH/bin/golint -set_exit_status firebase.google.com/go/... + go get -u golang.org/x/lint/golint + GOLINT=`go list -f {{.Target}} golang.org/x/lint/golint` + $GOLINT -set_exit_status ./... - name: Run Unit Tests if: success() || failure() - run: go test -v -race -test.short firebase.google.com/go/... + run: go test -v -race -test.short ./... - name: Run Formatter - working-directory: ./go/src/firebase.google.com/go run: | if [[ ! -z "$(gofmt -l -s .)" ]]; then echo "Go code is not formatted:" @@ -45,4 +37,4 @@ jobs: fi - name: Run Static Analyzer - run: go vet -v firebase.google.com/go/... + run: go vet -v ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3bfccd48..bdd55650 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,34 +36,26 @@ jobs: 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 + go-version: 1.12 - - name: Get dependencies - run: go get -t -v $(go list ./... | grep -v integration) + - name: Check out code + uses: actions/checkout@v2 + with: + ref: ${{ github.event.client_payload.ref || github.ref }} - name: Run Linter run: | - echo - go get golang.org/x/lint/golint - $GOPATH/bin/golint -set_exit_status firebase.google.com/go/... + go get -u golang.org/x/lint/golint + GOLINT=`go list -f {{.Target}} golang.org/x/lint/golint` + $GOLINT -set_exit_status ./... - 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 }} diff --git a/auth/auth.go b/auth/auth.go index 8a86e583..35600b84 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -23,13 +23,19 @@ import ( "strings" "time" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "google.golang.org/api/transport" ) const ( + authErrorCode = "authErrorCode" firebaseAudience = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit" oneHourInSeconds = 3600 + + // SDK-generated error codes + idTokenRevoked = "ID_TOKEN_REVOKED" + sessionCookieRevoked = "SESSION_COOKIE_REVOKED" + tenantIDMismatch = "TENANT_ID_MISMATCH" ) var reservedClaims = []string{ @@ -102,7 +108,6 @@ func NewClient(ctx context.Context, conf *internal.AuthConfig) (*Client, error) hc := internal.WithDefaultRetryConfig(transport) hc.CreateErrFn = handleHTTPError - hc.SuccessFn = internal.HasSuccessStatus hc.Opts = []internal.HTTPOption{ internal.WithHeader("X-Client-Version", fmt.Sprintf("Go/Admin/%s", conf.Version)), } @@ -261,12 +266,23 @@ func (c *baseClient) withTenantID(tenantID string) *baseClient { func (c *baseClient) VerifyIDToken(ctx context.Context, idToken string) (*Token, error) { decoded, err := c.idTokenVerifier.VerifyToken(ctx, idToken) if err == nil && c.tenantID != "" && c.tenantID != decoded.Firebase.Tenant { - return nil, internal.Errorf(tenantIDMismatch, "invalid tenant id: %q", decoded.Firebase.Tenant) + return nil, &internal.FirebaseError{ + ErrorCode: internal.InvalidArgument, + String: fmt.Sprintf("invalid tenant id: %q", decoded.Firebase.Tenant), + Ext: map[string]interface{}{ + authErrorCode: tenantIDMismatch, + }, + } } return decoded, err } +// IsTenantIDMismatch checks if the given error was due to a mismatched tenant ID in a JWT. +func IsTenantIDMismatch(err error) bool { + return hasAuthErrorCode(err, tenantIDMismatch) +} + // VerifyIDTokenAndCheckRevoked verifies the provided ID token, and additionally checks that the // token has not been revoked. // @@ -284,12 +300,27 @@ func (c *baseClient) VerifyIDTokenAndCheckRevoked(ctx context.Context, idToken s if err != nil { return nil, err } + if revoked { - return nil, internal.Error(idTokenRevoked, "ID token has been revoked") + return nil, &internal.FirebaseError{ + ErrorCode: internal.InvalidArgument, + String: "ID token has been revoked", + Ext: map[string]interface{}{ + authErrorCode: idTokenRevoked, + }, + } } + return decoded, nil } +// IsIDTokenRevoked checks if the given error was due to a revoked ID token. +// +// When IsIDTokenRevoked returns true, IsIDTokenInvalid is guranteed to return true. +func IsIDTokenRevoked(err error) bool { + return hasAuthErrorCode(err, idTokenRevoked) +} + // VerifySessionCookie verifies the signature and payload of the provided Firebase session cookie. // // VerifySessionCookie accepts a signed JWT token string, and verifies that it is current, issued for the @@ -324,12 +355,27 @@ func (c *Client) VerifySessionCookieAndCheckRevoked(ctx context.Context, session if err != nil { return nil, err } + if revoked { - return nil, internal.Error(sessionCookieRevoked, "session cookie has been revoked") + return nil, &internal.FirebaseError{ + ErrorCode: internal.InvalidArgument, + String: "session cookie has been revoked", + Ext: map[string]interface{}{ + authErrorCode: sessionCookieRevoked, + }, + } } + return decoded, nil } +// IsSessionCookieRevoked checks if the given error was due to a revoked session cookie. +// +// When IsSessionCookieRevoked returns true, IsSessionCookieInvalid is guranteed to return true. +func IsSessionCookieRevoked(err error) bool { + return hasAuthErrorCode(err, sessionCookieRevoked) +} + func (c *baseClient) checkRevoked(ctx context.Context, token *Token) (bool, error) { user, err := c.GetUser(ctx, token.UID) if err != nil { @@ -338,3 +384,13 @@ func (c *baseClient) checkRevoked(ctx context.Context, token *Token) (bool, erro return token.IssuedAt*1000 < user.TokensValidAfterMillis, nil } + +func hasAuthErrorCode(err error, code string) bool { + fe, ok := err.(*internal.FirebaseError) + if !ok { + return false + } + + got, ok := fe.Ext[authErrorCode] + return ok && got == code +} diff --git a/auth/auth_appengine.go b/auth/auth_appengine.go index 9b8ad6e1..18a23f54 100644 --- a/auth/auth_appengine.go +++ b/auth/auth_appengine.go @@ -19,7 +19,7 @@ package auth import ( "context" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "google.golang.org/appengine" ) diff --git a/auth/auth_std.go b/auth/auth_std.go index 7c757175..46a3b4b5 100644 --- a/auth/auth_std.go +++ b/auth/auth_std.go @@ -19,7 +19,7 @@ package auth // import "firebase.google.com/go/auth" import ( "context" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" ) func newCryptoSigner(ctx context.Context, conf *internal.AuthConfig) (cryptoSigner, error) { diff --git a/auth/auth_test.go b/auth/auth_test.go index f42df871..cecea7b7 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -27,7 +27,7 @@ import ( "testing" "time" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "golang.org/x/oauth2/google" "google.golang.org/api/option" "google.golang.org/api/transport" @@ -390,6 +390,7 @@ func TestCustomTokenInvalidCredential(t *testing.T) { t.Fatal(err) } + s.signer.(*iamSigner).httpClient.RetryConfig = nil token, err := s.CustomToken(ctx, "user1") if token != "" || err == nil { t.Errorf("CustomTokenWithClaims() = (%q, %v); want = (\"\", error)", token, err) @@ -510,8 +511,9 @@ func TestVerifyIDTokenInvalidSignature(t *testing.T) { parts := strings.Split(testIDToken, ".") token := fmt.Sprintf("%s:%s:invalidsignature", parts[0], parts[1]) - if ft, err := client.VerifyIDToken(context.Background(), token); ft != nil || err == nil { - t.Errorf("VerifyIDToken('invalid-signature') = (%v, %v); want = (nil, error)", ft, err) + ft, err := client.VerifyIDToken(context.Background(), token) + if ft != nil || !IsIDTokenInvalid(err) { + t.Errorf("VerifyIDToken('invalid-signature') = (%v, %v); want = (nil, IDTokenInvalid)", ft, err) } } @@ -606,9 +608,12 @@ func TestVerifyIDTokenError(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := client.VerifyIDToken(context.Background(), tc.token) - if err == nil || !strings.HasPrefix(err.Error(), tc.want) { + if !IsIDTokenInvalid(err) || !strings.HasPrefix(err.Error(), tc.want) { t.Errorf("VerifyIDToken(%q) = %v; want = %q", tc.name, err, tc.want) } + if tc.name == "ExpiredToken" && !IsIDTokenExpired(err) { + t.Errorf("VerifyIDToken(%q) = %v; want = IDTokenExpired", tc.name, err) + } }) } } @@ -637,8 +642,9 @@ func TestVerifyIDTokenInvalidAlgorithm(t *testing.T) { idTokenVerifier: testIDTokenVerifier, }, } - if _, err := client.VerifyIDToken(context.Background(), token); err == nil { - t.Errorf("VerifyIDToken(InvalidAlgorithm) = nil; want error") + _, err = client.VerifyIDToken(context.Background(), token) + if !IsIDTokenInvalid(err) { + t.Errorf("VerifyIDToken(InvalidAlgorithm) = nil; want = IDTokenInvalid") } } @@ -670,8 +676,8 @@ func TestCustomTokenVerification(t *testing.T) { t.Fatal(err) } - if _, err := client.VerifyIDToken(context.Background(), token); err == nil { - t.Error("VeridyIDToken() = nil; want error") + if _, err := client.VerifyIDToken(context.Background(), token); !IsIDTokenInvalid(err) { + t.Error("VeridyIDToken() = nil; want = IDTokenInvalid") } } @@ -686,8 +692,8 @@ func TestCertificateRequestError(t *testing.T) { idTokenVerifier: tv, }, } - if _, err := client.VerifyIDToken(context.Background(), testIDToken); err == nil { - t.Error("VeridyIDToken() = nil; want error") + if _, err := client.VerifyIDToken(context.Background(), testIDToken); !IsCertificateFetchFailed(err) { + t.Error("VeridyIDToken() = nil; want = CertificateFetchFailed") } } @@ -732,8 +738,8 @@ func TestInvalidTokenDoesNotCheckRevoked(t *testing.T) { s.Client.idTokenVerifier = testIDTokenVerifier ft, err := s.Client.VerifyIDTokenAndCheckRevoked(context.Background(), "") - if ft != nil || err == nil { - t.Errorf("VerifyIDTokenAndCheckRevoked() = (%v, %v); want = (nil, error)", ft, err) + if ft != nil || !IsIDTokenInvalid(err) || IsIDTokenRevoked(err) { + t.Errorf("VerifyIDTokenAndCheckRevoked() = (%v, %v); want = (nil, IDTokenInvalid)", ft, err) } if len(s.Req) != 0 { t.Errorf("Revocation checks = %d; want = 0", len(s.Req)) @@ -748,7 +754,7 @@ func TestVerifyIDTokenAndCheckRevokedError(t *testing.T) { p, err := s.Client.VerifyIDTokenAndCheckRevoked(context.Background(), revokedToken) we := "ID token has been revoked" - if p != nil || err == nil || err.Error() != we || !IsIDTokenRevoked(err) { + if p != nil || !IsIDTokenRevoked(err) || !IsIDTokenInvalid(err) || err.Error() != we { t.Errorf("VerifyIDTokenAndCheckRevoked(ctx, token) =(%v, %v); want = (%v, %v)", p, err, nil, we) } @@ -765,8 +771,8 @@ func TestIDTokenRevocationCheckUserMgtError(t *testing.T) { s.Client.idTokenVerifier = testIDTokenVerifier p, err := s.Client.VerifyIDTokenAndCheckRevoked(context.Background(), revokedToken) - if p != nil || err == nil || !IsUserNotFound(err) { - t.Errorf("VerifyIDTokenAndCheckRevoked(ctx, token) =(%v, %v); want = (%v, user-not-found)", p, err, nil) + if p != nil || !IsUserNotFound(err) { + t.Errorf("VerifyIDTokenAndCheckRevoked(ctx, token) =(%v, %v); want = (%v, UserNotFound)", p, err, nil) } } @@ -917,9 +923,12 @@ func TestVerifySessionCookieError(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := client.VerifySessionCookie(context.Background(), tc.token) - if err == nil || !strings.HasPrefix(err.Error(), tc.want) { + if !IsSessionCookieInvalid(err) || !strings.HasPrefix(err.Error(), tc.want) { t.Errorf("VerifySessionCookie(%q) = %v; want = %q", tc.name, err, tc.want) } + if tc.name == "ExpiredToken" && !IsSessionCookieExpired(err) { + t.Errorf("VerifySessionCookie(%q) = %v; want = SessionCookieExpired", tc.name, err) + } }) } } @@ -965,8 +974,8 @@ func TestInvalidCookieDoesNotCheckRevoked(t *testing.T) { s.Client.cookieVerifier = testCookieVerifier ft, err := s.Client.VerifySessionCookieAndCheckRevoked(context.Background(), "") - if ft != nil || err == nil { - t.Errorf("VerifySessionCookieAndCheckRevoked() = (%v, %v); want = (nil, error)", ft, err) + if ft != nil || !IsSessionCookieInvalid(err) { + t.Errorf("VerifySessionCookieAndCheckRevoked() = (%v, %v); want = (nil, SessionCookieInvalid)", ft, err) } if len(s.Req) != 0 { t.Errorf("Revocation checks = %d; want = 0", len(s.Req)) @@ -981,7 +990,7 @@ func TestVerifySessionCookieAndCheckRevokedError(t *testing.T) { p, err := s.Client.VerifySessionCookieAndCheckRevoked(context.Background(), revokedCookie) we := "session cookie has been revoked" - if p != nil || err == nil || err.Error() != we || !IsSessionCookieRevoked(err) { + if p != nil || !IsSessionCookieRevoked(err) || !IsSessionCookieInvalid(err) || err.Error() != we { t.Errorf("VerifySessionCookieAndCheckRevoked(ctx, token) =(%v, %v); want = (%v, %v)", p, err, nil, we) } @@ -998,8 +1007,8 @@ func TestCookieRevocationCheckUserMgtError(t *testing.T) { s.Client.cookieVerifier = testCookieVerifier p, err := s.Client.VerifySessionCookieAndCheckRevoked(context.Background(), revokedCookie) - if p != nil || err == nil || !IsUserNotFound(err) { - t.Errorf("VerifySessionCookieAndCheckRevoked(ctx, token) =(%v, %v); want = (%v, user-not-found)", p, err, nil) + if p != nil || !IsUserNotFound(err) { + t.Errorf("VerifySessionCookieAndCheckRevoked(ctx, token) =(%v, %v); want = (%v, UserNotFound)", p, err, nil) } } @@ -1125,6 +1134,12 @@ func checkIDTokenVerifier(tv *tokenVerifier, projectID string) error { if tv.shortName != "ID token" { return fmt.Errorf("shortName = %q; want = %q", tv.shortName, "ID token") } + if tv.invalidTokenCode != idTokenInvalid { + return fmt.Errorf("invalidTokenCode = %q; want = %q", tv.invalidTokenCode, idTokenInvalid) + } + if tv.expiredTokenCode != idTokenExpired { + return fmt.Errorf("expiredTokenCode = %q; want = %q", tv.expiredTokenCode, idTokenExpired) + } return nil } @@ -1138,6 +1153,12 @@ func checkCookieVerifier(tv *tokenVerifier, projectID string) error { if tv.shortName != "session cookie" { return fmt.Errorf("shortName = %q; want = %q", tv.shortName, "session cookie") } + if tv.invalidTokenCode != sessionCookieInvalid { + return fmt.Errorf("invalidTokenCode = %q; want = %q", tv.invalidTokenCode, sessionCookieInvalid) + } + if tv.expiredTokenCode != sessionCookieExpired { + return fmt.Errorf("expiredTokenCode = %q; want = %q", tv.expiredTokenCode, sessionCookieExpired) + } return nil } diff --git a/auth/export_users.go b/auth/export_users.go index baeb5bbc..7e5670a7 100644 --- a/auth/export_users.go +++ b/auth/export_users.go @@ -21,7 +21,7 @@ import ( "net/url" "strconv" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "google.golang.org/api/iterator" ) diff --git a/auth/hash/hash.go b/auth/hash/hash.go index 114562a5..67aa51af 100644 --- a/auth/hash/hash.go +++ b/auth/hash/hash.go @@ -22,7 +22,7 @@ import ( "errors" "fmt" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" ) // Bcrypt represents the BCRYPT hash algorithm. diff --git a/auth/hash/hash_test.go b/auth/hash/hash_test.go index 6fe47bca..7817eb42 100644 --- a/auth/hash/hash_test.go +++ b/auth/hash/hash_test.go @@ -19,8 +19,8 @@ import ( "reflect" "testing" - "firebase.google.com/go/auth" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/auth" + "firebase.google.com/go/v4/internal" ) var ( diff --git a/auth/import_users.go b/auth/import_users.go index cfb92493..0febf31f 100644 --- a/auth/import_users.go +++ b/auth/import_users.go @@ -20,7 +20,7 @@ import ( "errors" "fmt" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" ) const maxImportUsers = 1000 diff --git a/auth/provider_config.go b/auth/provider_config.go index ac87f4a4..496e9914 100644 --- a/auth/provider_config.go +++ b/auth/provider_config.go @@ -23,7 +23,7 @@ import ( "strconv" "strings" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "google.golang.org/api/iterator" ) @@ -92,8 +92,8 @@ func (nm nestedMap) Set(key string, value interface{}) { } } -func (nm nestedMap) UpdateMask() ([]string, error) { - return buildMask(nm), nil +func (nm nestedMap) UpdateMask() []string { + return buildMask(nm) } func buildMask(data map[string]interface{}) []string { @@ -654,11 +654,7 @@ func (c *baseClient) UpdateOIDCProviderConfig(ctx context.Context, id string, co return nil, err } - mask, err := body.UpdateMask() - if err != nil { - return nil, fmt.Errorf("failed to construct update mask: %v", err) - } - + mask := body.UpdateMask() req := &internal.Request{ Method: http.MethodPatch, URL: fmt.Sprintf("/oauthIdpConfigs/%s", id), @@ -766,11 +762,7 @@ func (c *baseClient) UpdateSAMLProviderConfig(ctx context.Context, id string, co return nil, err } - mask, err := body.UpdateMask() - if err != nil { - return nil, fmt.Errorf("failed to construct update mask: %v", err) - } - + mask := body.UpdateMask() req := &internal.Request{ Method: http.MethodPatch, URL: fmt.Sprintf("/inboundSamlConfigs/%s", id), diff --git a/auth/provider_config_test.go b/auth/provider_config_test.go index 0bde823f..7884375c 100644 --- a/auth/provider_config_test.go +++ b/auth/provider_config_test.go @@ -24,6 +24,7 @@ import ( "strings" "testing" + "firebase.google.com/go/v4/errorutils" "google.golang.org/api/iterator" ) @@ -247,8 +248,8 @@ func TestCreateOIDCProviderConfigError(t *testing.T) { ClientID(oidcProviderConfig.ClientID). Issuer(oidcProviderConfig.Issuer) oidc, err := client.CreateOIDCProviderConfig(context.Background(), options) - if oidc != nil || !IsUnknown(err) { - t.Errorf("CreateOIDCProviderConfig() = (%v, %v); want = (nil, %q)", oidc, err, "unknown-error") + if oidc != nil || !errorutils.IsInternal(err) { + t.Errorf("CreateOIDCProviderConfig() = (%v, %v); want = (nil, %q)", oidc, err, "internal-error") } } @@ -583,8 +584,8 @@ func TestOIDCProviderConfigsError(t *testing.T) { client.baseClient.httpClient.RetryConfig = nil it := client.OIDCProviderConfigs(context.Background(), "") config, err := it.Next() - if config != nil || err == nil || !IsUnknown(err) { - t.Errorf("OIDCProviderConfigs() = (%v, %v); want = (nil, %q)", config, err, "unknown-error") + if config != nil || err == nil || !errorutils.IsInternal(err) { + t.Errorf("OIDCProviderConfigs() = (%v, %v); want = (nil, %q)", config, err, "internal-error") } } @@ -775,8 +776,8 @@ func TestCreateSAMLProviderConfigError(t *testing.T) { RPEntityID(samlProviderConfig.RPEntityID). CallbackURL(samlProviderConfig.CallbackURL) saml, err := client.CreateSAMLProviderConfig(context.Background(), options) - if saml != nil || !IsUnknown(err) { - t.Errorf("CreateSAMLProviderConfig() = (%v, %v); want = (nil, %q)", saml, err, "unknown-error") + if saml != nil || !errorutils.IsInternal(err) { + t.Errorf("CreateSAMLProviderConfig() = (%v, %v); want = (nil, %q)", saml, err, "internal-error") } } @@ -1211,8 +1212,8 @@ func TestSAMLProviderConfigsError(t *testing.T) { client.baseClient.httpClient.RetryConfig = nil it := client.SAMLProviderConfigs(context.Background(), "") config, err := it.Next() - if config != nil || err == nil || !IsUnknown(err) { - t.Errorf("SAMLProviderConfigs() = (%v, %v); want = (nil, %q)", config, err, "unknown-error") + if config != nil || err == nil || !errorutils.IsInternal(err) { + t.Errorf("SAMLProviderConfigs() = (%v, %v); want = (nil, %q)", config, err, "internal-error") } } diff --git a/auth/tenant_mgt.go b/auth/tenant_mgt.go index 3496b47b..f35c6d7d 100644 --- a/auth/tenant_mgt.go +++ b/auth/tenant_mgt.go @@ -22,7 +22,7 @@ import ( "strconv" "strings" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "google.golang.org/api/iterator" ) @@ -153,11 +153,7 @@ func (tm *TenantManager) UpdateTenant(ctx context.Context, tenantID string, tena return nil, errors.New("tenant must not be nil") } - mask, err := tenant.params.UpdateMask() - if err != nil { - return nil, fmt.Errorf("failed to construct update mask: %v", err) - } - + mask := tenant.params.UpdateMask() if len(mask) == 0 { return nil, errors.New("no parameters specified in the update request") } diff --git a/auth/tenant_mgt_test.go b/auth/tenant_mgt_test.go index 3a4e35f4..c5d6b340 100644 --- a/auth/tenant_mgt_test.go +++ b/auth/tenant_mgt_test.go @@ -27,6 +27,7 @@ import ( "testing" "time" + "firebase.google.com/go/v4/errorutils" "google.golang.org/api/iterator" ) @@ -1252,8 +1253,8 @@ func TestCreateTenantError(t *testing.T) { client := s.Client client.TenantManager.httpClient.RetryConfig = nil tenant, err := client.TenantManager.CreateTenant(context.Background(), &TenantToCreate{}) - if tenant != nil || !IsUnknown(err) { - t.Errorf("CreateTenant() = (%v, %v); want = (nil, %q)", tenant, err, "unknown-error") + if tenant != nil || !errorutils.IsInternal(err) { + t.Errorf("CreateTenant() = (%v, %v); want = (nil, %q)", tenant, err, "internal-error") } } @@ -1356,8 +1357,8 @@ func TestUpdateTenantError(t *testing.T) { client.TenantManager.httpClient.RetryConfig = nil options := (&TenantToUpdate{}).DisplayName("") tenant, err := client.TenantManager.UpdateTenant(context.Background(), "tenantID", options) - if tenant != nil || !IsUnknown(err) { - t.Errorf("UpdateTenant() = (%v, %v); want = (nil, %q)", tenant, err, "unknown-error") + if tenant != nil || !errorutils.IsInternal(err) { + t.Errorf("UpdateTenant() = (%v, %v); want = (nil, %q)", tenant, err, "internal-error") } } @@ -1502,8 +1503,8 @@ func TestTenantsError(t *testing.T) { client.TenantManager.httpClient.RetryConfig = nil it := client.TenantManager.Tenants(context.Background(), "") config, err := it.Next() - if config != nil || err == nil || !IsUnknown(err) { - t.Errorf("Tenants() = (%v, %v); want = (nil, %q)", config, err, "unknown-error") + if config != nil || !errorutils.IsInternal(err) { + t.Errorf("Tenants() = (%v, %v); want = (nil, %q)", config, err, "internal-error") } } diff --git a/auth/token_generator.go b/auth/token_generator.go index 98acad5a..83e12020 100644 --- a/auth/token_generator.go +++ b/auth/token_generator.go @@ -30,8 +30,7 @@ import ( "strings" "sync" - "firebase.google.com/go/internal" - "google.golang.org/api/transport" + "firebase.google.com/go/v4/internal" ) type jwtHeader struct { @@ -160,13 +159,14 @@ type iamSigner struct { } func newIAMSigner(ctx context.Context, config *internal.AuthConfig) (*iamSigner, error) { - hc, _, err := transport.NewHTTPClient(ctx, config.Opts...) + hc, _, err := internal.NewHTTPClient(ctx, config.Opts...) if err != nil { return nil, err } + return &iamSigner{ mutex: &sync.Mutex{}, - httpClient: &internal.HTTPClient{Client: hc}, + httpClient: hc, serviceAcct: config.ServiceAccountID, metadataHost: "http://metadata.google.internal", iamHost: "https://iam.googleapis.com", @@ -178,53 +178,31 @@ func (s iamSigner) Sign(ctx context.Context, b []byte) ([]byte, error) { if err != nil { return nil, err } + url := fmt.Sprintf("%s/v1/projects/-/serviceAccounts/%s:signBlob", s.iamHost, account) body := map[string]interface{}{ "bytesToSign": base64.StdEncoding.EncodeToString(b), } req := &internal.Request{ - Method: "POST", + Method: http.MethodPost, URL: url, Body: internal.NewJSONEntity(body), } - resp, err := s.httpClient.Do(ctx, req) - if err != nil { - return nil, err - } else if resp.Status == http.StatusOK { - var signResponse struct { - Signature string `json:"signature"` - } - if err := json.Unmarshal(resp.Body, &signResponse); err != nil { - return nil, err - } - return base64.StdEncoding.DecodeString(signResponse.Signature) + var signResponse struct { + Signature string `json:"signature"` } - var signError struct { - Error struct { - Message string `json:"message"` - Status string `json:"status"` - } `json:"error"` - } - json.Unmarshal(resp.Body, &signError) // ignore any json parse errors at this level - var ( - clientCode, msg string - ok bool - ) - clientCode, ok = serverError[signError.Error.Status] - if !ok { - clientCode = unknown - } - msg = signError.Error.Message - if msg == "" { - msg = fmt.Sprintf("client encountered an unknown error; response: %s", string(resp.Body)) + if _, err := s.httpClient.DoAndUnmarshal(ctx, req, &signResponse); err != nil { + return nil, err } - return nil, internal.Errorf(clientCode, "http error status: %d; reason: %s", resp.Status, msg) + + return base64.StdEncoding.DecodeString(signResponse.Signature) } func (s iamSigner) Email(ctx context.Context) (string, error) { if s.serviceAcct != "" { return s.serviceAcct, nil } + s.mutex.Lock() defer s.mutex.Unlock() result, err := s.callMetadataService(ctx) @@ -235,29 +213,35 @@ func (s iamSigner) Email(ctx context.Context) (string, error) { "for more details on creating custom tokens" return "", fmt.Errorf(msg, err) } + + s.serviceAcct = result return result, nil } func (s iamSigner) callMetadataService(ctx context.Context) (string, error) { + // Use the built-in default client without request authorization or retries for this call. + noAuthClient := &internal.HTTPClient{ + Client: http.DefaultClient, + } + url := fmt.Sprintf("%s/computeMetadata/v1/instance/service-accounts/default/email", s.metadataHost) req := &internal.Request{ - Method: "GET", + Method: http.MethodGet, URL: url, Opts: []internal.HTTPOption{ internal.WithHeader("Metadata-Flavor", "Google"), }, } - resp, err := s.httpClient.Do(ctx, req) + + resp, err := noAuthClient.Do(ctx, req) if err != nil { return "", err } - if err := resp.CheckStatus(http.StatusOK); err != nil { - return "", err - } + result := strings.TrimSpace(string(resp.Body)) if result == "" { return "", errors.New("unexpected response from metadata service") } - s.serviceAcct = result + return result, nil } diff --git a/auth/token_generator_test.go b/auth/token_generator_test.go index 5a408f19..fee005ab 100644 --- a/auth/token_generator_test.go +++ b/auth/token_generator_test.go @@ -26,7 +26,8 @@ import ( "strings" "testing" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/errorutils" + "firebase.google.com/go/v4/internal" ) func TestEncodeToken(t *testing.T) { @@ -161,9 +162,9 @@ func TestIAMSignerHTTPError(t *testing.T) { defer server.Close() signer.iamHost = server.URL - want := "http error status: 403; reason: test reason" + want := "test reason" _, err = signer.Sign(context.Background(), []byte("input")) - if err == nil || !IsInsufficientPermission(err) || err.Error() != want { + if err == nil || !errorutils.IsPermissionDenied(err) || err.Error() != want { t.Errorf("Sign() = %v; want = %q", err, want) } } @@ -188,9 +189,9 @@ func TestIAMSignerUnknownHTTPError(t *testing.T) { defer server.Close() signer.iamHost = server.URL - want := "http error status: 403; reason: client encountered an unknown error; response: not json" + want := "unexpected http response with status: 403\nnot json" _, err = signer.Sign(context.Background(), []byte("input")) - if err == nil || !IsUnknown(err) || err.Error() != want { + if err == nil || !errorutils.IsPermissionDenied(err) || err.Error() != want { t.Errorf("Sign() = %v; want = %q", err, want) } } @@ -251,11 +252,15 @@ func TestIAMSignerNoMetadataService(t *testing.T) { t.Fatal(err) } - if _, err = signer.Email(ctx); err == nil { - t.Errorf("Email() = nil; want = error") + want := "failed to determine service account: " + _, err = signer.Email(ctx) + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("Email() = %v; want = %q", err, want) } - if _, err = signer.Sign(ctx, []byte("input")); err == nil { - t.Errorf("Sign() = nil; want = error") + + _, err = signer.Sign(ctx, []byte("input")) + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("Sign() = %v; want = %q", err, want) } } diff --git a/auth/token_verifier.go b/auth/token_verifier.go index 51b16cc1..d91f64d8 100644 --- a/auth/token_verifier.go +++ b/auth/token_verifier.go @@ -33,7 +33,7 @@ import ( "sync" "time" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "google.golang.org/api/option" "google.golang.org/api/transport" ) @@ -44,8 +44,50 @@ const ( sessionCookieCertURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys" sessionCookieIssuerPrefix = "https://session.firebase.google.com/" clockSkewSeconds = 300 + certificateFetchFailed = "CERTIFICATE_FETCH_FAILED" + idTokenExpired = "ID_TOKEN_EXPIRED" + idTokenInvalid = "ID_TOKEN_INVALID" + sessionCookieExpired = "SESSION_COOKIE_EXPIRED" + sessionCookieInvalid = "SESSION_COOKIE_INVALID" ) +// IsCertificateFetchFailed checks if the given error was caused by a failure to fetch public key +// certificates required to verify a JWT. +func IsCertificateFetchFailed(err error) bool { + return hasAuthErrorCode(err, certificateFetchFailed) +} + +// IsIDTokenExpired checks if the given error was due to an expired ID token. +// +// When IsIDTokenExpired returns true, IsIDTokenInvalid is guranteed to return true. +func IsIDTokenExpired(err error) bool { + return hasAuthErrorCode(err, idTokenExpired) +} + +// IsIDTokenInvalid checks if the given error was due to an invalid ID token. +// +// An ID token is considered invalid when it is malformed (i.e. contains incorrect data), expired +// or revoked. +func IsIDTokenInvalid(err error) bool { + return hasAuthErrorCode(err, idTokenInvalid) || IsIDTokenExpired(err) || IsIDTokenRevoked(err) +} + +// IsSessionCookieExpired checks if the given error was due to an expired session cookie. +// +// When IsSessionCookieExpired returns true, IsSessionCookieInvalid is guranteed to return true. +func IsSessionCookieExpired(err error) bool { + return hasAuthErrorCode(err, sessionCookieExpired) +} + +// IsSessionCookieInvalid checks if the given error was due to an invalid session cookie. +// +// A session cookie is considered invalid when it is malformed (i.e. contains incorrect data), +// expired or revoked. +func IsSessionCookieInvalid(err error) bool { + return hasAuthErrorCode(err, sessionCookieInvalid) || IsSessionCookieExpired(err) || + IsSessionCookieRevoked(err) +} + // tokenVerifier verifies different types of Firebase token strings, including ID tokens and // session cookies. type tokenVerifier struct { @@ -54,6 +96,8 @@ type tokenVerifier struct { docURL string projectID string issuerPrefix string + invalidTokenCode string + expiredTokenCode string keySource keySource clock internal.Clock } @@ -63,12 +107,15 @@ func newIDTokenVerifier(ctx context.Context, projectID string) (*tokenVerifier, if err != nil { return nil, err } + return &tokenVerifier{ shortName: "ID token", articledShortName: "an ID token", docURL: "https://firebase.google.com/docs/auth/admin/verify-id-tokens", projectID: projectID, issuerPrefix: idTokenIssuerPrefix, + invalidTokenCode: idTokenInvalid, + expiredTokenCode: idTokenExpired, keySource: newHTTPKeySource(idTokenCertURL, noAuthHTTPClient), clock: internal.SystemClock, }, nil @@ -79,12 +126,15 @@ func newSessionCookieVerifier(ctx context.Context, projectID string) (*tokenVeri if err != nil { return nil, err } + return &tokenVerifier{ shortName: "session cookie", articledShortName: "a session cookie", docURL: "https://firebase.google.com/docs/auth/admin/manage-cookies", projectID: projectID, issuerPrefix: sessionCookieIssuerPrefix, + invalidTokenCode: sessionCookieInvalid, + expiredTokenCode: sessionCookieExpired, keySource: newHTTPKeySource(sessionCookieCertURL, noAuthHTTPClient), clock: internal.SystemClock, }, nil @@ -105,17 +155,14 @@ func newSessionCookieVerifier(ctx context.Context, projectID string) (*tokenVeri // decoded Token is returned. func (tv *tokenVerifier) VerifyToken(ctx context.Context, token string) (*Token, error) { if tv.projectID == "" { + // Configuration error. return nil, errors.New("project id not available") } - if token == "" { - return nil, fmt.Errorf("%s must be a non-empty string", tv.shortName) - } // Validate the token content first. This is fast and cheap. payload, err := tv.verifyContent(token) if err != nil { - return nil, fmt.Errorf("%s; see %s for details on how to retrieve a valid %s", - err.Error(), tv.docURL, tv.shortName) + return nil, err } if err := tv.verifyTimestamps(payload); err != nil { @@ -127,10 +174,75 @@ func (tv *tokenVerifier) VerifyToken(ctx context.Context, token string) (*Token, if err := tv.verifySignature(ctx, token); err != nil { return nil, err } + return payload, nil } func (tv *tokenVerifier) verifyContent(token string) (*Token, error) { + if token == "" { + return nil, &internal.FirebaseError{ + ErrorCode: internal.InvalidArgument, + String: fmt.Sprintf("%s must be a non-empty string", tv.shortName), + Ext: map[string]interface{}{authErrorCode: tv.invalidTokenCode}, + } + } + + payload, err := tv.verifyHeaderAndBody(token) + if err != nil { + return nil, &internal.FirebaseError{ + ErrorCode: internal.InvalidArgument, + String: fmt.Sprintf( + "%s; see %s for details on how to retrieve a valid %s", + err.Error(), tv.docURL, tv.shortName), + Ext: map[string]interface{}{authErrorCode: tv.invalidTokenCode}, + } + } + + return payload, nil +} + +func (tv *tokenVerifier) verifyTimestamps(payload *Token) error { + if (payload.IssuedAt - clockSkewSeconds) > tv.clock.Now().Unix() { + return &internal.FirebaseError{ + ErrorCode: internal.InvalidArgument, + String: fmt.Sprintf("%s issued at future timestamp: %d", tv.shortName, payload.IssuedAt), + Ext: map[string]interface{}{authErrorCode: tv.invalidTokenCode}, + } + } + + if (payload.Expires + clockSkewSeconds) < tv.clock.Now().Unix() { + return &internal.FirebaseError{ + ErrorCode: internal.InvalidArgument, + String: fmt.Sprintf("%s has expired at: %d", tv.shortName, payload.Expires), + Ext: map[string]interface{}{authErrorCode: tv.expiredTokenCode}, + } + } + + return nil +} + +func (tv *tokenVerifier) verifySignature(ctx context.Context, token string) error { + keys, err := tv.keySource.Keys(ctx) + if err != nil { + return &internal.FirebaseError{ + ErrorCode: internal.Unknown, + String: err.Error(), + Ext: map[string]interface{}{authErrorCode: certificateFetchFailed}, + } + } + + if !tv.verifySignatureWithKeys(ctx, token, keys) { + return &internal.FirebaseError{ + ErrorCode: internal.InvalidArgument, + String: "failed to verify token signature", + Ext: map[string]interface{}{authErrorCode: tv.invalidTokenCode}, + } + } + + return nil +} + +func (tv *tokenVerifier) verifyHeaderAndBody(token string) (*Token, error) { var ( header jwtHeader payload Token @@ -190,27 +302,10 @@ func (tv *tokenVerifier) verifyContent(token string) (*Token, error) { return &payload, nil } -func (tv *tokenVerifier) verifyTimestamps(payload *Token) error { - if (payload.IssuedAt - clockSkewSeconds) > tv.clock.Now().Unix() { - return fmt.Errorf("%s issued at future timestamp: %d", tv.shortName, payload.IssuedAt) - } else if (payload.Expires + clockSkewSeconds) < tv.clock.Now().Unix() { - return fmt.Errorf("%s has expired at: %d", tv.shortName, payload.Expires) - } - return nil -} - -func (tv *tokenVerifier) verifySignature(ctx context.Context, token string) error { +func (tv *tokenVerifier) verifySignatureWithKeys(ctx context.Context, token string, keys []*publicKey) bool { segments := strings.Split(token, ".") - var h jwtHeader - if err := decode(segments[0], &h); err != nil { - return err - } - - keys, err := tv.keySource.Keys(ctx) - if err != nil { - return err - } + decode(segments[0], &h) verified := false for _, k := range keys { @@ -221,10 +316,8 @@ func (tv *tokenVerifier) verifySignature(ctx context.Context, token string) erro } } } - if !verified { - return errors.New("failed to verify token signature") - } - return nil + + return verified } func (tv *tokenVerifier) getProjectIDMatchMessage() string { @@ -308,7 +401,7 @@ func (k *httpKeySource) hasExpired() bool { func (k *httpKeySource) refreshKeys(ctx context.Context) error { k.CachedKeys = nil - req, err := http.NewRequest("GET", k.KeyURI, nil) + req, err := http.NewRequest(http.MethodGet, k.KeyURI, nil) if err != nil { return err } @@ -323,18 +416,22 @@ func (k *httpKeySource) refreshKeys(ctx context.Context) error { if err != nil { return err } + if resp.StatusCode != http.StatusOK { return fmt.Errorf("invalid response (%d) while retrieving public keys: %s", resp.StatusCode, string(contents)) } + newKeys, err := parsePublicKeys(contents) if err != nil { return err } + maxAge, err := findMaxAge(resp) if err != nil { return err } + k.CachedKeys = append([]*publicKey(nil), newKeys...) k.ExpiryTime = k.Clock.Now().Add(*maxAge) return nil diff --git a/auth/token_verifier_test.go b/auth/token_verifier_test.go index 9d9edd1b..f226fd3d 100644 --- a/auth/token_verifier_test.go +++ b/auth/token_verifier_test.go @@ -24,7 +24,7 @@ import ( "testing" "time" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" ) func TestNewIDTokenVerifier(t *testing.T) { diff --git a/auth/user_mgt.go b/auth/user_mgt.go index e601d8bc..5c84ed81 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -26,8 +26,7 @@ import ( "strings" "time" - "firebase.google.com/go/internal" - "google.golang.org/api/googleapi" + "firebase.google.com/go/v4/internal" ) const ( @@ -325,126 +324,83 @@ func marshalCustomClaims(claims map[string]interface{}) (string, error) { // Error handlers. const ( - configurationNotFound = "configuration-not-found" - emailAlreadyExists = "email-already-exists" - idTokenRevoked = "id-token-revoked" - insufficientPermission = "insufficient-permission" - invalidDynamicLinkDomain = "invalid-dynamic-link-domain" - invalidEmail = "invalid-email" - phoneNumberAlreadyExists = "phone-number-already-exists" - projectNotFound = "project-not-found" - sessionCookieRevoked = "session-cookie-revoked" - tenantIDMismatch = "tenant-id-mismatch" - tenantNotFound = "tenant-not-found" - uidAlreadyExists = "uid-already-exists" - unauthorizedContinueURI = "unauthorized-continue-uri" - unknown = "unknown-error" - userNotFound = "user-not-found" + // Backend-generated error codes + configurationNotFound = "CONFIGURATION_NOT_FOUND" + emailAlreadyExists = "EMAIL_ALREADY_EXISTS" + invalidDynamicLinkDomain = "INVALID_DYNAMIC_LINK_DOMAIN" + phoneNumberAlreadyExists = "PHONE_NUMBER_ALREADY_EXISTS" + tenantNotFound = "TENANT_NOT_FOUND" + uidAlreadyExists = "UID_ALREADY_EXISTS" + unauthorizedContinueURI = "UNAUTHORIZED_CONTINUE_URI" + userNotFound = "USER_NOT_FOUND" ) // IsConfigurationNotFound checks if the given error was due to a non-existing IdP configuration. func IsConfigurationNotFound(err error) bool { - return internal.HasErrorCode(err, configurationNotFound) + return hasAuthErrorCode(err, configurationNotFound) } // IsEmailAlreadyExists checks if the given error was due to a duplicate email. func IsEmailAlreadyExists(err error) bool { - return internal.HasErrorCode(err, emailAlreadyExists) -} - -// IsIDTokenRevoked checks if the given error was due to a revoked ID token. -func IsIDTokenRevoked(err error) bool { - return internal.HasErrorCode(err, idTokenRevoked) + return hasAuthErrorCode(err, emailAlreadyExists) } // IsInsufficientPermission checks if the given error was due to insufficient permissions. +// +// Deprecated. Always returns false. func IsInsufficientPermission(err error) bool { - return internal.HasErrorCode(err, insufficientPermission) + return false } // IsInvalidDynamicLinkDomain checks if the given error was due to an invalid dynamic link domain. func IsInvalidDynamicLinkDomain(err error) bool { - return internal.HasErrorCode(err, invalidDynamicLinkDomain) + return hasAuthErrorCode(err, invalidDynamicLinkDomain) } // IsInvalidEmail checks if the given error was due to an invalid email. +// +// Deprecated. Always returns false. func IsInvalidEmail(err error) bool { - return internal.HasErrorCode(err, invalidEmail) + return false } // IsPhoneNumberAlreadyExists checks if the given error was due to a duplicate phone number. func IsPhoneNumberAlreadyExists(err error) bool { - return internal.HasErrorCode(err, phoneNumberAlreadyExists) + return hasAuthErrorCode(err, phoneNumberAlreadyExists) } // IsProjectNotFound checks if the given error was due to a non-existing project. +// +// Deprecated. Always returns false. func IsProjectNotFound(err error) bool { - return internal.HasErrorCode(err, projectNotFound) -} - -// IsSessionCookieRevoked checks if the given error was due to a revoked session cookie. -func IsSessionCookieRevoked(err error) bool { - return internal.HasErrorCode(err, sessionCookieRevoked) -} - -// IsTenantIDMismatch checks if the given error was due to a mismatched tenant ID in a JWT. -func IsTenantIDMismatch(err error) bool { - return internal.HasErrorCode(err, tenantIDMismatch) + return false } // IsTenantNotFound checks if the given error was due to a non-existing tenant ID. func IsTenantNotFound(err error) bool { - return internal.HasErrorCode(err, tenantNotFound) + return hasAuthErrorCode(err, tenantNotFound) } // IsUIDAlreadyExists checks if the given error was due to a duplicate uid. func IsUIDAlreadyExists(err error) bool { - return internal.HasErrorCode(err, uidAlreadyExists) + return hasAuthErrorCode(err, uidAlreadyExists) } // IsUnauthorizedContinueURI checks if the given error was due to an unauthorized continue URI domain. func IsUnauthorizedContinueURI(err error) bool { - return internal.HasErrorCode(err, unauthorizedContinueURI) + return hasAuthErrorCode(err, unauthorizedContinueURI) } // IsUnknown checks if the given error was due to a unknown server error. +// +// Deprecated. Always returns false. func IsUnknown(err error) bool { - return internal.HasErrorCode(err, unknown) + return false } // IsUserNotFound checks if the given error was due to non-existing user. func IsUserNotFound(err error) bool { - return internal.HasErrorCode(err, userNotFound) -} - -var serverError = map[string]string{ - "CONFIGURATION_NOT_FOUND": configurationNotFound, - "DUPLICATE_EMAIL": emailAlreadyExists, - "DUPLICATE_LOCAL_ID": uidAlreadyExists, - "EMAIL_EXISTS": emailAlreadyExists, - "INSUFFICIENT_PERMISSION": insufficientPermission, - "INVALID_DYNAMIC_LINK_DOMAIN": invalidDynamicLinkDomain, - "INVALID_EMAIL": invalidEmail, - "PERMISSION_DENIED": insufficientPermission, - "PHONE_NUMBER_EXISTS": phoneNumberAlreadyExists, - "PROJECT_NOT_FOUND": projectNotFound, - "TENANT_NOT_FOUND": tenantNotFound, - "UNAUTHORIZED_DOMAIN": unauthorizedContinueURI, - "USER_NOT_FOUND": userNotFound, -} - -func handleServerError(err error) error { - gerr, ok := err.(*googleapi.Error) - if !ok { - // Not a back-end error - return err - } - serverCode := gerr.Message - clientCode, ok := serverError[serverCode] - if !ok { - clientCode = unknown - } - return internal.Error(clientCode, err.Error()) + return hasAuthErrorCode(err, userNotFound) } // Validators. @@ -569,12 +525,20 @@ type getAccountInfoResponse struct { func (c *baseClient) getUser(ctx context.Context, query *userQuery) (*UserRecord, error) { var parsed getAccountInfoResponse - if _, err := c.post(ctx, "/accounts:lookup", query.build(), &parsed); err != nil { + resp, err := c.post(ctx, "/accounts:lookup", query.build(), &parsed) + if err != nil { return nil, err } if len(parsed.Users) == 0 { - return nil, internal.Errorf(userNotFound, "cannot find user from %s", query.description()) + return nil, &internal.FirebaseError{ + ErrorCode: internal.NotFound, + String: fmt.Sprintf("no user exists with the %s", query.description()), + Response: resp.LowLevelResponse(), + Ext: map[string]interface{}{ + authErrorCode: userNotFound, + }, + } } return parsed.Users[0].makeUserRecord() @@ -1096,21 +1060,92 @@ func (c *baseClient) makeUserMgtURL(path string) (string, error) { return url, nil } +type authError struct { + code internal.ErrorCode + message string + authCode string +} + +var serverError = map[string]*authError{ + "CONFIGURATION_NOT_FOUND": { + code: internal.NotFound, + message: "no IdP configuration corresponding to the provided identifier", + authCode: configurationNotFound, + }, + "DUPLICATE_EMAIL": { + code: internal.AlreadyExists, + message: "user with the provided email already exists", + authCode: emailAlreadyExists, + }, + "DUPLICATE_LOCAL_ID": { + code: internal.AlreadyExists, + message: "user with the provided uid already exists", + authCode: uidAlreadyExists, + }, + "EMAIL_EXISTS": { + code: internal.AlreadyExists, + message: "user with the provided email already exists", + authCode: emailAlreadyExists, + }, + "INVALID_DYNAMIC_LINK_DOMAIN": { + code: internal.InvalidArgument, + message: "the provided dynamic link domain is not configured or authorized for the current project", + authCode: invalidDynamicLinkDomain, + }, + "PHONE_NUMBER_EXISTS": { + code: internal.AlreadyExists, + message: "user with the provided phone number already exists", + authCode: phoneNumberAlreadyExists, + }, + "TENANT_NOT_FOUND": { + code: internal.NotFound, + message: "tenant with the specified ID does not exist", + authCode: tenantNotFound, + }, + "UNAUTHORIZED_DOMAIN": { + code: internal.InvalidArgument, + message: "domain of the continue url is not whitelisted", + authCode: unauthorizedContinueURI, + }, + "USER_NOT_FOUND": { + code: internal.NotFound, + message: "no user record found for the given identifier", + authCode: userNotFound, + }, +} + func handleHTTPError(resp *internal.Response) error { + err := internal.NewFirebaseError(resp) + code, detail := parseErrorResponse(resp) + if authErr, ok := serverError[code]; ok { + err.ErrorCode = authErr.code + err.Ext[authErrorCode] = authErr.authCode + if detail != "" { + err.String = fmt.Sprintf("%s: %s", authErr.message, detail) + } else { + err.String = authErr.message + } + } + + return err +} + +func parseErrorResponse(resp *internal.Response) (string, string) { var httpErr struct { Error struct { Message string `json:"message"` } `json:"error"` } - json.Unmarshal(resp.Body, &httpErr) // ignore any json parse errors at this level - serverCode := httpErr.Error.Message - clientCode, ok := serverError[serverCode] - if !ok { - clientCode = unknown - } - return internal.Errorf( - clientCode, - "http error status: %d; body: %s", - resp.Status, - string(resp.Body)) + // ignore any json parse errors at this level + json.Unmarshal(resp.Body, &httpErr) + + // Auth error response format: {"error": {"message": "AUTH_ERROR_CODE: Optional text"}} + code, detail := httpErr.Error.Message, "" + idx := strings.Index(code, ":") + if idx != -1 { + detail = strings.TrimSpace(code[idx+1:]) + code = code[:idx] + } + + return code, detail } diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index 0ee5c678..e8393a7e 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -30,7 +30,8 @@ import ( "testing" "time" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/errorutils" + "firebase.google.com/go/v4/internal" "google.golang.org/api/iterator" ) @@ -396,19 +397,19 @@ func TestGetNonExistingUser(t *testing.T) { s := echoServer([]byte(resp), t) defer s.Close() - we := `cannot find user from uid: "id-nonexisting"` + we := `no user exists with the uid: "id-nonexisting"` user, err := s.Client.GetUser(context.Background(), "id-nonexisting") if user != nil || err == nil || err.Error() != we || !IsUserNotFound(err) { t.Errorf("GetUser(non-existing) = (%v, %q); want = (nil, %q)", user, err, we) } - we = `cannot find user from email: "foo@bar.nonexisting"` + we = `no user exists with the email: "foo@bar.nonexisting"` user, err = s.Client.GetUserByEmail(context.Background(), "foo@bar.nonexisting") if user != nil || err == nil || err.Error() != we || !IsUserNotFound(err) { t.Errorf("GetUserByEmail(non-existing) = (%v, %q); want = (nil, %q)", user, err, we) } - we = `cannot find user from phone number: "+12345678901"` + we = `no user exists with the phone number: "+12345678901"` user, err = s.Client.GetUserByPhoneNumber(context.Background(), "+12345678901") if user != nil || err == nil || err.Error() != we || !IsUserNotFound(err) { t.Errorf("GetUserPhoneNumber(non-existing) = (%v, %q); want = (nil, %q)", user, err, we) @@ -1542,8 +1543,8 @@ func TestSessionCookieError(t *testing.T) { t.Fatalf("SessionCookie() = (%q, %v); want = (%q, error)", cookie, err, "") } - want := fmt.Sprintf("http error status: 403; body: %s", resp) - if err.Error() != want || !IsInsufficientPermission(err) { + want := fmt.Sprintf("unexpected http response with status: 403\n%s", resp) + if err.Error() != want || !errorutils.IsPermissionDenied(err) { t.Errorf("SessionCookie() error = %v; want = %q", err, want) } } @@ -1599,37 +1600,144 @@ func TestHTTPError(t *testing.T) { t.Fatalf("GetUser() = (%v, %v); want = (nil, error)", u, err) } - want := `http error status: 500; body: {"error":"test"}` - if err.Error() != want || !IsUnknown(err) { + want := "unexpected http response with status: 500\n{\"error\":\"test\"}" + if err.Error() != want || !errorutils.IsInternal(err) { t.Errorf("GetUser() = %v; want = %q", err, want) } } func TestHTTPErrorWithCode(t *testing.T) { - errorCodes := map[string]func(error) bool{ - "CONFIGURATION_NOT_FOUND": IsConfigurationNotFound, - "DUPLICATE_EMAIL": IsEmailAlreadyExists, - "DUPLICATE_LOCAL_ID": IsUIDAlreadyExists, - "EMAIL_EXISTS": IsEmailAlreadyExists, - "INSUFFICIENT_PERMISSION": IsInsufficientPermission, - "INVALID_EMAIL": IsInvalidEmail, - "PHONE_NUMBER_EXISTS": IsPhoneNumberAlreadyExists, - "PROJECT_NOT_FOUND": IsProjectNotFound, + errorCodes := map[string]struct { + authCheck func(error) bool + platformCheck func(error) bool + want string + }{ + "CONFIGURATION_NOT_FOUND": { + IsConfigurationNotFound, + errorutils.IsNotFound, + "no IdP configuration corresponding to the provided identifier", + }, + "DUPLICATE_EMAIL": { + IsEmailAlreadyExists, + errorutils.IsAlreadyExists, + "user with the provided email already exists", + }, + "DUPLICATE_LOCAL_ID": { + IsUIDAlreadyExists, + errorutils.IsAlreadyExists, + "user with the provided uid already exists", + }, + "EMAIL_EXISTS": { + IsEmailAlreadyExists, + errorutils.IsAlreadyExists, + "user with the provided email already exists", + }, + "INVALID_DYNAMIC_LINK_DOMAIN": { + IsInvalidDynamicLinkDomain, + errorutils.IsInvalidArgument, + "the provided dynamic link domain is not configured or authorized for the current project", + }, + "PHONE_NUMBER_EXISTS": { + IsPhoneNumberAlreadyExists, + errorutils.IsAlreadyExists, + "user with the provided phone number already exists", + }, + "UNAUTHORIZED_DOMAIN": { + IsUnauthorizedContinueURI, + errorutils.IsInvalidArgument, + "domain of the continue url is not whitelisted", + }, + "USER_NOT_FOUND": { + IsUserNotFound, + errorutils.IsNotFound, + "no user record found for the given identifier", + }, + } + s := echoServer(nil, t) + defer s.Close() + s.Client.baseClient.httpClient.RetryConfig = nil + s.Status = http.StatusInternalServerError + + for code, conf := range errorCodes { + s.Resp = []byte(fmt.Sprintf(`{"error":{"message":"%s"}}`, code)) + u, err := s.Client.GetUser(context.Background(), "some uid") + if u != nil || err == nil { + t.Fatalf("GetUser() = (%v, %v); want = (nil, error)", u, err) + } + + if err.Error() != conf.want || !conf.authCheck(err) || !conf.platformCheck(err) { + t.Errorf("GetUser() = %v; want = %q", err, conf.want) + } + } +} + +func TestAuthErrorWithCodeAndDetails(t *testing.T) { + resp := []byte(`{"error":{"message":"USER_NOT_FOUND: extra details"}}`) + s := echoServer(resp, t) + defer s.Close() + s.Client.baseClient.httpClient.RetryConfig = nil + 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 := "no user record found for the given identifier: extra details" + if err.Error() != want || !IsUserNotFound(err) || !errorutils.IsNotFound(err) { + t.Errorf("GetUser() = %v; want = %q", err, want) + } +} + +func TestAuthErrorWithUnknownCode(t *testing.T) { + resp := `{"error":{"message":"UNKNOWN_CODE: extra details"}}` + s := echoServer([]byte(resp), t) + defer s.Close() + s.Client.baseClient.httpClient.RetryConfig = nil + 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 := fmt.Sprintf("unexpected http response with status: 500\n%s", resp) + if err.Error() != want || !errorutils.IsInternal(err) { + t.Errorf("GetUser() = %v; want = %q", err, want) + } +} + +func TestUnmappedHTTPError(t *testing.T) { + errorCodes := map[string]struct { + authCheck func(error) bool + }{ + "PROJECT_NOT_FOUND": { + IsProjectNotFound, + }, + "INVALID_EMAIL": { + IsInvalidEmail, + }, + "INSUFFICIENT_PERMISSION": { + IsInsufficientPermission, + }, + "UNKNOWN": { + IsUnknown, + }, } s := echoServer(nil, t) defer s.Close() s.Client.baseClient.httpClient.RetryConfig = nil s.Status = http.StatusInternalServerError - for code, check := range errorCodes { + for code, conf := range errorCodes { s.Resp = []byte(fmt.Sprintf(`{"error":{"message":"%s"}}`, code)) 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 := fmt.Sprintf(`http error status: 500; body: {"error":{"message":"%s"}}`, code) - if err.Error() != want || !check(err) { + want := fmt.Sprintf("unexpected http response with status: 500\n%s", string(s.Resp)) + if err.Error() != want || conf.authCheck(err) || !errorutils.IsInternal(err) { t.Errorf("GetUser() = %v; want = %q", err, want) } } diff --git a/db/db.go b/db/db.go index 728509a4..6d2774b8 100644 --- a/db/db.go +++ b/db/db.go @@ -23,7 +23,7 @@ import ( "runtime" "strings" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "google.golang.org/api/option" ) @@ -68,17 +68,7 @@ func NewClient(ctx context.Context, c *internal.DatabaseConfig) (*Client, error) return nil, err } - ep := func(b []byte) string { - var p struct { - Error string `json:"error"` - } - if err := json.Unmarshal(b, &p); err != nil { - return "" - } - return p.Error - } - hc.ErrParser = ep - + hc.CreateErrFn = handleRTDBError return &Client{ hc: hc, url: fmt.Sprintf("https://%s", p.Host), @@ -102,24 +92,18 @@ func (c *Client) NewRef(path string) *Ref { } } -func (c *Client) send( - ctx context.Context, - method, path string, - body internal.HTTPEntity, - opts ...internal.HTTPOption) (*internal.Response, error) { - - if strings.ContainsAny(path, invalidChars) { - return nil, fmt.Errorf("invalid path with illegal characters: %q", path) +func (c *Client) sendAndUnmarshal( + ctx context.Context, req *internal.Request, v interface{}) (*internal.Response, error) { + if strings.ContainsAny(req.URL, invalidChars) { + return nil, fmt.Errorf("invalid path with illegal characters: %q", req.URL) } + + req.URL = fmt.Sprintf("%s%s.json", c.url, req.URL) if c.authOverride != "" { - opts = append(opts, internal.WithQueryParam(authVarOverride, c.authOverride)) + req.Opts = append(req.Opts, internal.WithQueryParam(authVarOverride, c.authOverride)) } - return c.hc.Do(ctx, &internal.Request{ - Method: method, - URL: fmt.Sprintf("%s%s.json", c.url, path), - Body: body, - Opts: opts, - }) + + return c.hc.DoAndUnmarshal(ctx, req, v) } func parsePath(path string) []string { @@ -131,3 +115,16 @@ func parsePath(path string) []string { } return segs } + +func handleRTDBError(resp *internal.Response) error { + err := internal.NewFirebaseError(resp) + var p struct { + Error string `json:"error"` + } + json.Unmarshal(resp.Body, &p) + if p.Error != "" { + err.String = fmt.Sprintf("http error status: %d; reason: %s", resp.Status, p.Error) + } + + return err +} diff --git a/db/db_test.go b/db/db_test.go index 3c32a018..1ec624fb 100644 --- a/db/db_test.go +++ b/db/db_test.go @@ -28,7 +28,7 @@ import ( "runtime" "testing" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "google.golang.org/api/option" ) diff --git a/db/query.go b/db/query.go index ca88b133..ff6a394e 100644 --- a/db/query.go +++ b/db/query.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package db // import "firebase.google.com/go/db" +package db // import "firebase.google.com/go/v4/db" import ( "context" @@ -23,7 +23,7 @@ import ( "strconv" "strings" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" ) // QueryNode represents a data node retrieved from an ordered query. @@ -109,11 +109,14 @@ func (q *Query) Get(ctx context.Context, v interface{}) error { if err := initQueryParams(q, qp); err != nil { return err } - resp, err := q.client.send(ctx, "GET", q.path, nil, internal.WithQueryParams(qp)) - if err != nil { - return err + + req := &internal.Request{ + Method: http.MethodGet, + URL: q.path, + Opts: []internal.HTTPOption{internal.WithQueryParams(qp)}, } - return resp.Unmarshal(http.StatusOK, v) + _, err := q.client.sendAndUnmarshal(ctx, req, v) + return err } // GetOrdered executes the Query and returns the results as an ordered slice. diff --git a/db/query_test.go b/db/query_test.go index c20ad7f4..c540bd14 100644 --- a/db/query_test.go +++ b/db/query_test.go @@ -16,8 +16,11 @@ package db import ( "context" "fmt" + "net/http" "reflect" "testing" + + "firebase.google.com/go/v4/errorutils" ) var sortableKeysResp = map[string]interface{}{ @@ -768,10 +771,15 @@ func TestQueryHttpError(t *testing.T) { want := "http error status: 500; reason: test error" result, err := testref.OrderByChild("child").GetOrdered(context.Background()) - if err == nil || err.Error() != want { - t.Errorf("GetOrdered() = %v; want = %v", err, want) + if result != nil || err == nil || err.Error() != want { + t.Fatalf("GetOrdered() = (%v, %v); want = (nil, %v)", result, err, want) } - if result != nil { - t.Errorf("GetOrdered() = %v; want = nil", result) + if !errorutils.IsInternal(err) { + t.Errorf("IsInternal(err) = false; want = true") + } + + resp := errorutils.HTTPResponse(err) + if resp == nil || resp.StatusCode != http.StatusInternalServerError { + t.Errorf("HTTPResponse(err) = %v; want = {StatusCode: %d}", resp, http.StatusInternalServerError) } } diff --git a/db/ref.go b/db/ref.go index 821ce859..bb26a531 100644 --- a/db/ref.go +++ b/db/ref.go @@ -21,7 +21,7 @@ import ( "net/http" "strings" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" ) // txnRetires is the maximum number of times a transaction is retried before giving up. Transaction @@ -75,21 +75,26 @@ func (r *Ref) Child(path string) *Ref { // therefore v has the same requirements as the json package. Specifically, it must be a pointer, // and must not be nil. func (r *Ref) Get(ctx context.Context, v interface{}) error { - resp, err := r.send(ctx, "GET") - if err != nil { - return err + req := &internal.Request{ + Method: http.MethodGet, } - return resp.Unmarshal(http.StatusOK, v) + _, err := r.sendAndUnmarshal(ctx, req, v) + return err } // GetWithETag retrieves the value at the current database location, along with its ETag. func (r *Ref) GetWithETag(ctx context.Context, v interface{}) (string, error) { - resp, err := r.send(ctx, "GET", internal.WithHeader("X-Firebase-ETag", "true")) + req := &internal.Request{ + Method: http.MethodGet, + Opts: []internal.HTTPOption{ + internal.WithHeader("X-Firebase-ETag", "true"), + }, + } + resp, err := r.sendAndUnmarshal(ctx, req, v) if err != nil { return "", err - } else if err := resp.Unmarshal(http.StatusOK, v); err != nil { - return "", err } + return resp.Header.Get("Etag"), nil } @@ -97,11 +102,14 @@ func (r *Ref) GetWithETag(ctx context.Context, v interface{}) (string, error) { // // Shallow reads do not retrieve the child nodes of the current reference. func (r *Ref) GetShallow(ctx context.Context, v interface{}) error { - resp, err := r.send(ctx, "GET", internal.WithQueryParam("shallow", "true")) - if err != nil { - return err + req := &internal.Request{ + Method: http.MethodGet, + Opts: []internal.HTTPOption{ + internal.WithQueryParam("shallow", "true"), + }, } - return resp.Unmarshal(http.StatusOK, v) + _, err := r.sendAndUnmarshal(ctx, req, v) + return err } // GetIfChanged retrieves the value and ETag of the current database location only if the specified @@ -112,16 +120,26 @@ func (r *Ref) GetShallow(ctx context.Context, v interface{}) error { // If the etag matches, returns false along with the same ETag passed into the function. No data // will be stored in v in this case. func (r *Ref) GetIfChanged(ctx context.Context, etag string, v interface{}) (bool, string, error) { - resp, err := r.send(ctx, "GET", internal.WithHeader("If-None-Match", etag)) + req := &internal.Request{ + Method: http.MethodGet, + Opts: []internal.HTTPOption{ + internal.WithHeader("If-None-Match", etag), + }, + SuccessFn: successOrNotModified, + } + resp, err := r.sendAndUnmarshal(ctx, req, nil) if err != nil { return false, "", err } + if resp.Status == http.StatusNotModified { return false, etag, nil } - if err := resp.Unmarshal(http.StatusOK, v); err != nil { + + if err := json.Unmarshal(resp.Body, v); err != nil { return false, "", err } + return true, resp.Header.Get("ETag"), nil } @@ -131,11 +149,15 @@ func (r *Ref) GetIfChanged(ctx context.Context, etag string, v interface{}) (boo // v has the same requirements as the json package. Values like functions and channels cannot be // saved into Realtime Database. func (r *Ref) Set(ctx context.Context, v interface{}) error { - resp, err := r.sendWithBody(ctx, "PUT", v, internal.WithQueryParam("print", "silent")) - if err != nil { - return err + req := &internal.Request{ + Method: http.MethodPut, + Body: internal.NewJSONEntity(v), + Opts: []internal.HTTPOption{ + internal.WithQueryParam("print", "silent"), + }, } - return resp.CheckStatus(http.StatusNoContent) + _, err := r.sendAndUnmarshal(ctx, req, nil) + return err } // SetIfUnchanged conditionally sets the data at this location to the given value. @@ -143,16 +165,23 @@ func (r *Ref) Set(ctx context.Context, v interface{}) error { // Sets the data at this location to v only if the specified ETag matches. Returns true if the // value is written. Returns false if no changes are made to the database. func (r *Ref) SetIfUnchanged(ctx context.Context, etag string, v interface{}) (bool, error) { - resp, err := r.sendWithBody(ctx, "PUT", v, internal.WithHeader("If-Match", etag)) + req := &internal.Request{ + Method: http.MethodPut, + Body: internal.NewJSONEntity(v), + Opts: []internal.HTTPOption{ + internal.WithHeader("If-Match", etag), + }, + SuccessFn: successOrPreconditionFailed, + } + resp, err := r.sendAndUnmarshal(ctx, req, nil) if err != nil { return false, err } + if resp.Status == http.StatusPreconditionFailed { return false, nil } - if err := resp.CheckStatus(http.StatusOK); err != nil { - return false, err - } + return true, nil } @@ -164,16 +193,18 @@ func (r *Ref) Push(ctx context.Context, v interface{}) (*Ref, error) { if v == nil { v = "" } - resp, err := r.sendWithBody(ctx, "POST", v) - if err != nil { - return nil, err + + req := &internal.Request{ + Method: http.MethodPost, + Body: internal.NewJSONEntity(v), } var d struct { Name string `json:"name"` } - if err := resp.Unmarshal(http.StatusOK, &d); err != nil { + if _, err := r.sendAndUnmarshal(ctx, req, &d); err != nil { return nil, err } + return r.Child(d.Name), nil } @@ -182,11 +213,16 @@ func (r *Ref) Update(ctx context.Context, v map[string]interface{}) error { if len(v) == 0 { return fmt.Errorf("value argument must be a non-empty map") } - resp, err := r.sendWithBody(ctx, "PATCH", v, internal.WithQueryParam("print", "silent")) - if err != nil { - return err + + req := &internal.Request{ + Method: http.MethodPatch, + Body: internal.NewJSONEntity(v), + Opts: []internal.HTTPOption{ + internal.WithQueryParam("print", "silent"), + }, } - return resp.CheckStatus(http.StatusNoContent) + _, err := r.sendAndUnmarshal(ctx, req, nil) + return err } // UpdateFn represents a function type that can be passed into Transaction(). @@ -207,28 +243,41 @@ type UpdateFn func(TransactionNode) (interface{}, error) // The update function may also force an early abort by returning an error instead of returning a // value. func (r *Ref) Transaction(ctx context.Context, fn UpdateFn) error { - resp, err := r.send(ctx, "GET", internal.WithHeader("X-Firebase-ETag", "true")) + req := &internal.Request{ + Method: http.MethodGet, + Opts: []internal.HTTPOption{ + internal.WithHeader("X-Firebase-ETag", "true"), + }, + } + resp, err := r.sendAndUnmarshal(ctx, req, nil) if err != nil { return err - } else if err := resp.CheckStatus(http.StatusOK); err != nil { - return err } - etag := resp.Header.Get("Etag") + etag := resp.Header.Get("Etag") for i := 0; i < txnRetries; i++ { new, err := fn(&transactionNodeImpl{resp.Body}) if err != nil { return err } - resp, err = r.sendWithBody(ctx, "PUT", new, internal.WithHeader("If-Match", etag)) + + req := &internal.Request{ + Method: http.MethodPut, + Body: internal.NewJSONEntity(new), + Opts: []internal.HTTPOption{ + internal.WithHeader("If-Match", etag), + }, + SuccessFn: successOrPreconditionFailed, + } + resp, err = r.sendAndUnmarshal(ctx, req, nil) if err != nil { return err } + if resp.Status == http.StatusOK { return nil - } else if err := resp.CheckStatus(http.StatusPreconditionFailed); err != nil { - return err } + etag = resp.Header.Get("ETag") } return fmt.Errorf("transaction aborted after failed retries") @@ -236,26 +285,23 @@ func (r *Ref) Transaction(ctx context.Context, fn UpdateFn) error { // Delete removes this node from the database. func (r *Ref) Delete(ctx context.Context) error { - resp, err := r.send(ctx, "DELETE") - if err != nil { - return err + req := &internal.Request{ + Method: http.MethodDelete, } - return resp.CheckStatus(http.StatusOK) + _, err := r.sendAndUnmarshal(ctx, req, nil) + return err } -func (r *Ref) send( - ctx context.Context, - method string, - opts ...internal.HTTPOption) (*internal.Response, error) { - - return r.client.send(ctx, method, r.Path, nil, opts...) +func (r *Ref) sendAndUnmarshal( + ctx context.Context, req *internal.Request, v interface{}) (*internal.Response, error) { + req.URL = r.Path + return r.client.sendAndUnmarshal(ctx, req, v) } -func (r *Ref) sendWithBody( - ctx context.Context, - method string, - body interface{}, - opts ...internal.HTTPOption) (*internal.Response, error) { +func successOrNotModified(resp *internal.Response) bool { + return internal.HasSuccessStatus(resp) || resp.Status == http.StatusNotModified +} - return r.client.send(ctx, method, r.Path, internal.NewJSONEntity(body), opts...) +func successOrPreconditionFailed(resp *internal.Response) bool { + return internal.HasSuccessStatus(resp) || resp.Status == http.StatusPreconditionFailed } diff --git a/db/ref_test.go b/db/ref_test.go index be832b19..56956902 100644 --- a/db/ref_test.go +++ b/db/ref_test.go @@ -20,6 +20,8 @@ import ( "net/http" "reflect" "testing" + + "firebase.google.com/go/v4/errorutils" ) type refOp func(r *Ref) error @@ -289,6 +291,15 @@ func TestWelformedHttpError(t *testing.T) { if err == nil || err.Error() != want { t.Errorf("%s = %v; want = %v", tc.name, err, want) } + + if !errorutils.IsInternal(err) { + t.Errorf("IsInternal(err) = false; want = true") + } + + resp := errorutils.HTTPResponse(err) + if resp == nil || resp.StatusCode != http.StatusInternalServerError { + t.Errorf("HTTPResponse(err) = %v; want = {StatusCode: %d}", resp, http.StatusInternalServerError) + } }) } @@ -303,13 +314,22 @@ func TestUnexpectedHttpError(t *testing.T) { srv := mock.Start(client) defer srv.Close() - want := "http error status: 500; reason: \"unexpected error\"" + want := "unexpected http response with status: 500\n\"unexpected error\"" for _, tc := range testOps { t.Run(tc.name, func(t *testing.T) { err := tc.op(testref) if err == nil || err.Error() != want { t.Errorf("%s = %v; want = %v", tc.name, err, want) } + + if !errorutils.IsInternal(err) { + t.Errorf("IsInternal(err) = false; want = true") + } + + resp := errorutils.HTTPResponse(err) + if resp == nil || resp.StatusCode != http.StatusInternalServerError { + t.Errorf("HTTPResponse(err) = %v; want = {StatusCode: %d}", resp, http.StatusInternalServerError) + } }) } @@ -319,6 +339,59 @@ func TestUnexpectedHttpError(t *testing.T) { } } +func TestPlatformErrorCodes(t *testing.T) { + mock := &mockServer{Resp: map[string]string{"error": "test error"}} + srv := mock.Start(client) + defer srv.Close() + + cases := []struct { + name string + status int + check func(err error) bool + }{ + { + name: "InvalidArgument", + status: http.StatusBadRequest, + check: errorutils.IsInvalidArgument, + }, + { + name: "Unauthenticated", + status: http.StatusUnauthorized, + check: errorutils.IsUnauthenticated, + }, + { + name: "NotFound", + status: http.StatusNotFound, + check: errorutils.IsNotFound, + }, + { + name: "Internal", + status: http.StatusInternalServerError, + check: errorutils.IsInternal, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + mock.Status = tc.status + want := fmt.Sprintf("http error status: %d; reason: test error", tc.status) + err := testref.Delete(context.Background()) + if err == nil || err.Error() != want { + t.Errorf("Delete() = %v; want = %v", err, want) + } + + if !tc.check(err) { + t.Errorf("Is%s(err) = false; want = true", tc.name) + } + + resp := errorutils.HTTPResponse(err) + if resp == nil || resp.StatusCode != tc.status { + t.Errorf("HTTPResponse(err) = %v; want = {StatusCode: %d}", resp, tc.status) + } + }) + } +} + func TestInvalidPath(t *testing.T) { mock := &mockServer{Resp: "test"} srv := mock.Start(client) @@ -719,6 +792,42 @@ func TestTransactionAbort(t *testing.T) { checkAllRequests(t, mock.Reqs, wanted) } +func TestTransactionFailure(t *testing.T) { + mock := &mockServer{ + Resp: &person{"Peter Parker", 17}, + Header: map[string]string{"ETag": "mock-etag1"}, + } + srv := mock.Start(client) + defer srv.Close() + + cnt := 0 + var fn UpdateFn = func(t TransactionNode) (interface{}, error) { + if cnt == 0 { + mock.Status = http.StatusInternalServerError + mock.Resp = map[string]string{"error": "test error"} + } + + cnt++ + var p person + if err := t.Unmarshal(&p); err != nil { + return nil, err + } + + p.Age++ + return &p, nil + } + + want := "http error status: 500; reason: test error" + err := testref.Transaction(context.Background(), fn) + if err == nil || err.Error() != want { + t.Errorf("Transaction() = %v; want = %v", err, want) + } + + if !errorutils.IsInternal(err) { + t.Errorf("IsInternal() = false; want = true") + } +} + func TestDelete(t *testing.T) { mock := &mockServer{Resp: "null"} srv := mock.Start(client) diff --git a/errorutils/errorutils.go b/errorutils/errorutils.go new file mode 100644 index 00000000..93a6b07f --- /dev/null +++ b/errorutils/errorutils.go @@ -0,0 +1,143 @@ +// Copyright 2020 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package errorutils provides functions for checking and handling error conditions. +package errorutils // import "firebase.google.com/go/v4/errorutils" + +import "firebase.google.com/go/v4/internal" + +import "net/http" + +// IsInvalidArgument checks if the given error was due to an invalid client argument. +func IsInvalidArgument(err error) bool { + return internal.HasPlatformErrorCode(err, internal.InvalidArgument) +} + +// IsFailedPrecondition checks if the given error was because a request could not be executed +// in the current system state, such as deleting a non-empty directory. +func IsFailedPrecondition(err error) bool { + return internal.HasPlatformErrorCode(err, internal.FailedPrecondition) +} + +// IsOutOfRange checks if the given error due to an invalid range specified by the client. +func IsOutOfRange(err error) bool { + return internal.HasPlatformErrorCode(err, internal.OutOfRange) +} + +// IsUnauthenticated checks if the given error was caused by an unauthenticated request. +// +// Unauthenticated requests are due to missing, invalid, or expired OAuth token. +func IsUnauthenticated(err error) bool { + return internal.HasPlatformErrorCode(err, internal.Unauthenticated) +} + +// IsPermissionDenied checks if the given error was due to a client not having suffificient +// permissions. +// +// This can happen because the OAuth token does not have the right scopes, the client doesn't have +// permission, or the API has not been enabled for the client project. +func IsPermissionDenied(err error) bool { + return internal.HasPlatformErrorCode(err, internal.PermissionDenied) +} + +// IsNotFound checks if the given error was due to a specified resource being not found. +// +// This may also occur when the request is rejected by undisclosed reasons, such as whitelisting. +func IsNotFound(err error) bool { + return internal.HasPlatformErrorCode(err, internal.NotFound) +} + +// IsConflict checks if the given error was due to a concurrency conflict, such as a +// read-modify-write conflict. +// +// This represents an HTTP 409 Conflict status code, without additional information to distinguish +// between ABORTED or ALREADY_EXISTS error conditions. +func IsConflict(err error) bool { + return internal.HasPlatformErrorCode(err, internal.Conflict) +} + +// IsAborted checks if the given error was due to a concurrency conflict, such as a +// read-modify-write conflict. +func IsAborted(err error) bool { + return internal.HasPlatformErrorCode(err, internal.Aborted) +} + +// IsAlreadyExists checks if the given error was because a resource that a client tried to create +// already exists. +func IsAlreadyExists(err error) bool { + return internal.HasPlatformErrorCode(err, internal.AlreadyExists) +} + +// IsResourceExhausted checks if the given error was caused by either running out of a quota or +// reaching a rate limit. +func IsResourceExhausted(err error) bool { + return internal.HasPlatformErrorCode(err, internal.ResourceExhausted) +} + +// IsCancelled checks if the given error was due to the client cancelling a request. +func IsCancelled(err error) bool { + return internal.HasPlatformErrorCode(err, internal.Cancelled) +} + +// IsDataLoss checks if the given error was due to an unrecoverable data loss or corruption. +// +// The client should report such errors to the end user. +func IsDataLoss(err error) bool { + return internal.HasPlatformErrorCode(err, internal.DataLoss) +} + +// IsUnknown checks if the given error was cuased by an unknown server error. +// +// This typically indicates a server bug. +func IsUnknown(err error) bool { + return internal.HasPlatformErrorCode(err, internal.Unknown) +} + +// IsInternal checks if the given error was due to an internal server error. +// +// This typically indicates a server bug. +func IsInternal(err error) bool { + return internal.HasPlatformErrorCode(err, internal.Internal) +} + +// IsUnavailable checks if the given error was caused by an unavailable service. +// +// This typically indicates that the target service is temporarily down. +func IsUnavailable(err error) bool { + return internal.HasPlatformErrorCode(err, internal.Unavailable) +} + +// IsDeadlineExceeded checks if the given error was due a request exceeding a deadline. +// +// This will happen only if the caller sets a deadline that is shorter than the method's default +// deadline (i.e. requested deadline is not enough for the server to process the request) and the +// request did not finish within the deadline. +func IsDeadlineExceeded(err error) bool { + return internal.HasPlatformErrorCode(err, internal.DeadlineExceeded) +} + +// HTTPResponse returns the http.Response instance that caused the given error. +// +// If the error was not caused by an HTTP error response, returns nil. +// +// Returns a buffered copy of the original response received from the network stack. It is safe to +// read the response content from the returned http.Response. +func HTTPResponse(err error) *http.Response { + fe, ok := err.(*internal.FirebaseError) + if ok { + return fe.Response + } + + return nil +} diff --git a/firebase.go b/firebase.go index f7cfaeec..0a03c249 100644 --- a/firebase.go +++ b/firebase.go @@ -15,7 +15,7 @@ // Package firebase is the entry point to the Firebase Admin SDK. It provides functionality for initializing App // instances, which serve as the central entities that provide access to various other Firebase services exposed // from the SDK. -package firebase // import "firebase.google.com/go" +package firebase // import "firebase.google.com/go/v4" import ( "context" @@ -25,12 +25,12 @@ import ( "os" "cloud.google.com/go/firestore" - "firebase.google.com/go/auth" - "firebase.google.com/go/db" - "firebase.google.com/go/iid" - "firebase.google.com/go/internal" - "firebase.google.com/go/messaging" - "firebase.google.com/go/storage" + "firebase.google.com/go/v4/auth" + "firebase.google.com/go/v4/db" + "firebase.google.com/go/v4/iid" + "firebase.google.com/go/v4/internal" + "firebase.google.com/go/v4/messaging" + "firebase.google.com/go/v4/storage" "google.golang.org/api/option" "google.golang.org/api/transport" ) diff --git a/firebase_test.go b/firebase_test.go index f29bb73b..7830225e 100644 --- a/firebase_test.go +++ b/firebase_test.go @@ -29,7 +29,7 @@ import ( "testing" "time" - "firebase.google.com/go/messaging" + "firebase.google.com/go/v4/messaging" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/option" @@ -376,8 +376,7 @@ func TestMessagingSendWithCustomEndpoint(t *testing.T) { tokenSource := &testTokenSource{AccessToken: "mock-token-from-custom"} app, err := NewApp( ctx, - nil, - option.WithCredentialsFile("testdata/service_account.json"), + &Config{ProjectID: "test-project-id"}, option.WithTokenSource(tokenSource), option.WithEndpoint(ts.URL), ) @@ -391,7 +390,7 @@ func TestMessagingSendWithCustomEndpoint(t *testing.T) { } msg := &messaging.Message{ - Token: "...", + Token: "token", } n, err := c.Send(ctx, msg) if n != name || err != nil { diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..c50a801a --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module firebase.google.com/go/v4 + +go 1.11 + +require ( + cloud.google.com/go/firestore v1.1.1 + cloud.google.com/go/storage v1.0.0 + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + google.golang.org/api v0.17.0 + google.golang.org/appengine v1.6.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..63bcc71d --- /dev/null +++ b/go.sum @@ -0,0 +1,179 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1 h1:hL+ycaJpVE9M7nLoiXb/Pn10ENE2u+oddxbD8uu0ZVU= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.1 h1:vFLWT9tT+SQnfY20DgeNmwh56CSB3kc+Jt16o6Wy8IE= +cloud.google.com/go/firestore v1.1.1/go.mod h1:ADXYdzUfnr5T2SaB0Of9UXDIjgcRIZ221HQOikRONfE= +cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0 h1:VV2nUM3wwLLGh9lSABFgZMjInyUbJeaRSE64WuAIQ+4= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587 h1:5Uz0rkjCFu9BC9gCRN7EkwVvhNyQgGWb8KNJrPwBoHY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd h1:Zc7EU2PqpsNeIfOoVA7hvQX4cS3YDJEs5KlfatT3hLo= +golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0 h1:0q95w+VuFtv4PAx4PZVQdBMmYbaCHbnfKaEiDIcVyag= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191206224255-0243a4be9c8f h1:naitw5DILWPQvG0oG04mR9jF8fmKpRdW3E3zzKA4D0Y= +google.golang.org/genproto v0.0.0-20191206224255-0243a4be9c8f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/iid/iid.go b/iid/iid.go index 19362dcf..4527d6fb 100644 --- a/iid/iid.go +++ b/iid/iid.go @@ -13,90 +13,89 @@ // limitations under the License. // Package iid contains functions for deleting instance IDs from Firebase projects. -package iid // import "firebase.google.com/go/iid" +package iid // import "firebase.google.com/go/v4/iid" import ( "context" "errors" "fmt" "net/http" + "strings" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/errorutils" + "firebase.google.com/go/v4/internal" ) const iidEndpoint = "https://console.firebase.google.com/v1" -const ( - invalidArgument = "invalid-argument" - unauthorized = "unauthorized" - insufficientPermission = "insufficient-permission" - notFound = "instance-id-not-found" - alreadyDeleted = "instance-id-already-deleted" - tooManyRequests = "too-many-requests" - internalError = "internal-error" - serverUnavailable = "server-unavailable" - unknown = "unknown-error" -) - -var errorCodes = map[int]struct { - code, message string -}{ - http.StatusBadRequest: {invalidArgument, "malformed instance id argument"}, - http.StatusUnauthorized: {insufficientPermission, "request not authorized"}, - http.StatusForbidden: { - insufficientPermission, - "project does not match instance ID or the client does not have sufficient privileges", - }, - http.StatusNotFound: {notFound, "failed to find the instance id"}, - http.StatusConflict: {alreadyDeleted, "already deleted"}, - http.StatusTooManyRequests: {tooManyRequests, "request throttled out by the backend server"}, - http.StatusInternalServerError: {internalError, "internal server error"}, - http.StatusServiceUnavailable: {serverUnavailable, "backend servers are over capacity"}, +var errorMessages = map[int]string{ + http.StatusBadRequest: "malformed instance id argument", + http.StatusUnauthorized: "request not authorized", + http.StatusForbidden: "project does not match instance ID or the client does not have sufficient privileges", + http.StatusNotFound: "failed to find the instance id", + http.StatusConflict: "already deleted", + http.StatusTooManyRequests: "request throttled out by the backend server", + http.StatusInternalServerError: "internal server error", + http.StatusServiceUnavailable: "backend servers are over capacity", } // IsInvalidArgument checks if the given error was due to an invalid instance ID argument. +// +// Deprecated. Use errorutils.IsInvalidArgument() function instead. func IsInvalidArgument(err error) bool { - return internal.HasErrorCode(err, invalidArgument) + return errorutils.IsInvalidArgument(err) } // IsInsufficientPermission checks if the given error was due to the request not having the // required authorization. This could be due to the client not having the required permission // or the specified instance ID not matching the target Firebase project. +// +// Deprecated. Use errorutils.IsUnauthenticated() or errorutils.IsPermissionDenied() instead. func IsInsufficientPermission(err error) bool { - return internal.HasErrorCode(err, insufficientPermission) + return errorutils.IsUnauthenticated(err) || errorutils.IsPermissionDenied(err) } // IsNotFound checks if the given error was due to a non existing instance ID. func IsNotFound(err error) bool { - return internal.HasErrorCode(err, notFound) + return errorutils.IsNotFound(err) } // IsAlreadyDeleted checks if the given error was due to the instance ID being already deleted from // the project. +// +// Deprecated. Use errorutils.IsConflict() function instead. func IsAlreadyDeleted(err error) bool { - return internal.HasErrorCode(err, alreadyDeleted) + return errorutils.IsConflict(err) } // IsTooManyRequests checks if the given error was due to the client sending too many requests // causing a server quota to exceed. +// +// Deprecated. Use errorutils.IsResourceExhausted() function instead. func IsTooManyRequests(err error) bool { - return internal.HasErrorCode(err, tooManyRequests) + return errorutils.IsResourceExhausted(err) } // IsInternal checks if the given error was due to an internal server error. +// +// Deprecated. Use errorutils.IsInternal() function instead. func IsInternal(err error) bool { - return internal.HasErrorCode(err, internalError) + return errorutils.IsInternal(err) } // IsServerUnavailable checks if the given error was due to the backend server being temporarily // unavailable. +// +// Deprecated. Use errorutils.IsUnavailable() function instead. func IsServerUnavailable(err error) bool { - return internal.HasErrorCode(err, serverUnavailable) + return errorutils.IsUnavailable(err) } // IsUnknown checks if the given error was due to unknown error returned by the backend server. +// +// Deprecated. Use errorutils.IsUnknown() function instead. func IsUnknown(err error) bool { - return internal.HasErrorCode(err, unknown) + return errorutils.IsUnknown(err) } // Client is the interface for the Firebase Instance ID service. @@ -121,6 +120,7 @@ func NewClient(ctx context.Context, c *internal.InstanceIDConfig) (*Client, erro return nil, err } + hc.CreateErrFn = createError return &Client{ endpoint: iidEndpoint, client: hc, @@ -140,16 +140,17 @@ 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: http.MethodDelete, URL: url}) - if err != nil { - return err - } + _, err := c.client.Do(ctx, &internal.Request{Method: http.MethodDelete, URL: url}) + return err +} - if info, ok := errorCodes[resp.Status]; ok { - return internal.Errorf(info.code, "instance id %q: %s", iid, info.message) - } - if err := resp.CheckStatus(http.StatusOK); err != nil { - return internal.Error(unknown, err.Error()) +func createError(resp *internal.Response) error { + err := internal.NewFirebaseError(resp) + if msg, ok := errorMessages[resp.Status]; ok { + requestPath := resp.LowLevelResponse().Request.URL.Path + idx := strings.LastIndex(requestPath, "/") + err.String = fmt.Sprintf("instance id %q: %s", requestPath[idx+1:], msg) } - return nil + + return err } diff --git a/iid/iid_test.go b/iid/iid_test.go index 3b785c1e..c9134207 100644 --- a/iid/iid_test.go +++ b/iid/iid_test.go @@ -21,7 +21,8 @@ import ( "net/http/httptest" "testing" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/errorutils" + "firebase.google.com/go/v4/internal" "google.golang.org/api/option" ) @@ -104,6 +105,17 @@ func TestDeleteInstanceIDError(t *testing.T) { client.client.RetryConfig = nil errorHandlers := map[int]func(error) bool{ + http.StatusBadRequest: errorutils.IsInvalidArgument, + http.StatusUnauthorized: errorutils.IsUnauthenticated, + http.StatusForbidden: errorutils.IsPermissionDenied, + http.StatusNotFound: errorutils.IsNotFound, + http.StatusConflict: errorutils.IsConflict, + http.StatusTooManyRequests: errorutils.IsResourceExhausted, + http.StatusInternalServerError: errorutils.IsInternal, + http.StatusServiceUnavailable: errorutils.IsUnavailable, + } + + deprecatedErrorHandlers := map[int]func(error) bool{ http.StatusBadRequest: IsInvalidArgument, http.StatusUnauthorized: IsInsufficientPermission, http.StatusForbidden: IsInsufficientPermission, @@ -116,12 +128,22 @@ func TestDeleteInstanceIDError(t *testing.T) { for code, check := range errorHandlers { status = code - want := fmt.Sprintf("instance id %q: %s", "test-iid", errorCodes[code].message) + want := fmt.Sprintf("instance id %q: %s", "test-iid", errorMessages[code]) err := client.DeleteInstanceID(ctx, "test-iid") if err == nil || !check(err) || err.Error() != want { t.Errorf("DeleteInstanceID() = %v; want = %v", err, want) } + resp := errorutils.HTTPResponse(err) + if resp.StatusCode != code { + t.Errorf("HTTPResponse().StatusCode = %d; want = %d", resp.StatusCode, code) + } + + deprecatedCheck := deprecatedErrorHandlers[code] + if !deprecatedCheck(err) { + t.Errorf("DeleteInstanceID() = %v; want = %v", err, want) + } + if tr == nil { t.Fatalf("Request = nil; want non-nil") } @@ -155,11 +177,17 @@ func TestDeleteInstanceIDUnexpectedError(t *testing.T) { } client.endpoint = ts.URL - want := "http error status: 511; reason: {}" + want := "unexpected http response with status: 511\n{}" err = client.DeleteInstanceID(ctx, "test-iid") - if err == nil || !IsUnknown(err) || err.Error() != want { + if err == nil || err.Error() != want { t.Errorf("DeleteInstanceID() = %v; want = %v", err, want) } + if !IsUnknown(err) { + t.Errorf("IsUnknown() = false; want = true") + } + if !errorutils.IsUnknown(err) { + t.Errorf("errorutils.IsUnknown() = false; want = true") + } if tr == nil { t.Fatalf("Request = nil; want non-nil") @@ -190,7 +218,6 @@ func TestDeleteInstanceIDConnectionError(t *testing.T) { client.client.RetryConfig = nil if err := client.DeleteInstanceID(ctx, "test-iid"); err == nil { - t.Errorf("DeleteInstanceID() = nil; want = error") - return + t.Fatalf("DeleteInstanceID() = nil; want = error") } } diff --git a/integration/auth/auth_test.go b/integration/auth/auth_test.go index a9fa7b3a..668b45be 100644 --- a/integration/auth/auth_test.go +++ b/integration/auth/auth_test.go @@ -29,9 +29,9 @@ import ( "testing" "time" - firebase "firebase.google.com/go" - "firebase.google.com/go/auth" - "firebase.google.com/go/integration/internal" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/auth" + "firebase.google.com/go/v4/integration/internal" "golang.org/x/oauth2/google" "google.golang.org/api/option" ) diff --git a/integration/auth/provider_config_test.go b/integration/auth/provider_config_test.go index ae7ad147..4d830de3 100644 --- a/integration/auth/provider_config_test.go +++ b/integration/auth/provider_config_test.go @@ -21,7 +21,7 @@ import ( "reflect" "testing" - "firebase.google.com/go/auth" + "firebase.google.com/go/v4/auth" "google.golang.org/api/iterator" ) diff --git a/integration/auth/tenant_mgt_test.go b/integration/auth/tenant_mgt_test.go index e803443b..381bb61c 100644 --- a/integration/auth/tenant_mgt_test.go +++ b/integration/auth/tenant_mgt_test.go @@ -22,7 +22,7 @@ import ( "testing" "time" - "firebase.google.com/go/auth" + "firebase.google.com/go/v4/auth" "google.golang.org/api/iterator" ) diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index 63419a87..9e64559d 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -28,8 +28,8 @@ import ( "testing" "time" - "firebase.google.com/go/auth" - "firebase.google.com/go/auth/hash" + "firebase.google.com/go/v4/auth" + "firebase.google.com/go/v4/auth/hash" "google.golang.org/api/iterator" ) diff --git a/integration/db/db_test.go b/integration/db/db_test.go index d3b3197d..fc34b5ff 100644 --- a/integration/db/db_test.go +++ b/integration/db/db_test.go @@ -28,9 +28,10 @@ import ( "reflect" "testing" - "firebase.google.com/go" - "firebase.google.com/go/db" - "firebase.google.com/go/integration/internal" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/db" + "firebase.google.com/go/v4/errorutils" + "firebase.google.com/go/v4/integration/internal" ) var client *db.Client @@ -584,13 +585,24 @@ func TestNoAccess(t *testing.T) { var got string if err := r.Get(context.Background(), &got); err == nil || got != "" { t.Errorf("Get() = (%q, %v); want = (empty, error)", got, err) - } else if err.Error() != permDenied { - t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } else { + if err.Error() != permDenied { + t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } + if !errorutils.IsUnauthenticated(err) { + t.Errorf("IsUnauthenticated() = false; want = true") + } } + if err := r.Set(context.Background(), "update"); err == nil { t.Errorf("Set() = nil; want = error") - } else if err.Error() != permDenied { - t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } else { + if err.Error() != permDenied { + t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } + if !errorutils.IsUnauthenticated(err) { + t.Errorf("IsUnauthenticated() = false; want = true") + } } } @@ -600,11 +612,17 @@ func TestReadAccess(t *testing.T) { if err := r.Get(context.Background(), &got); err != nil || got != "test" { t.Errorf("Get() = (%q, %v); want = (%q, nil)", got, err, "test") } - if err := r.Set(context.Background(), "update"); err == nil { - t.Errorf("Set() = nil; want = error") - } else if err.Error() != permDenied { + + err := r.Set(context.Background(), "update") + if err == nil { + t.Fatalf("Set() = nil; want = error") + } + if err.Error() != permDenied { t.Errorf("Error = %q; want = %q", err.Error(), permDenied) } + if !errorutils.IsUnauthenticated(err) { + t.Errorf("IsUnauthenticated() = false; want = true") + } } func TestReadWriteAccess(t *testing.T) { @@ -621,11 +639,16 @@ func TestReadWriteAccess(t *testing.T) { func TestQueryAccess(t *testing.T) { r := aoClient.NewRef("_adminsdk/go/protected") got := make(map[string]interface{}) - if err := r.OrderByKey().LimitToFirst(2).Get(context.Background(), &got); err == nil { - t.Errorf("OrderByQuery() = nil; want = error") - } else if err.Error() != permDenied { + err := r.OrderByKey().LimitToFirst(2).Get(context.Background(), &got) + if err == nil { + t.Fatalf("OrderByQuery() = nil; want = error") + } + if err.Error() != permDenied { t.Errorf("Error = %q; want = %q", err.Error(), permDenied) } + if !errorutils.IsUnauthenticated(err) { + t.Errorf("IsUnauthenticated() = false; want = true") + } } func TestGuestAccess(t *testing.T) { @@ -634,32 +657,53 @@ func TestGuestAccess(t *testing.T) { if err := r.Get(context.Background(), &got); err != nil || got != "test" { t.Errorf("Get() = (%q, %v); want = (%q, nil)", got, err, "test") } + if err := r.Set(context.Background(), "update"); err == nil { t.Errorf("Set() = nil; want = error") - } else if err.Error() != permDenied { - t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } else { + if err.Error() != permDenied { + t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } + if !errorutils.IsUnauthenticated(err) { + t.Errorf("IsUnauthenticated() = false; want = true") + } } got = "" r = guestClient.NewRef("_adminsdk/go") if err := r.Get(context.Background(), &got); err == nil || got != "" { t.Errorf("Get() = (%q, %v); want = (empty, error)", got, err) - } else if err.Error() != permDenied { - t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } else { + if err.Error() != permDenied { + t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } + if !errorutils.IsUnauthenticated(err) { + t.Errorf("IsUnauthenticated() = false; want = true") + } } c := r.Child("protected/user2") if err := c.Get(context.Background(), &got); err == nil || got != "" { t.Errorf("Get() = (%q, %v); want = (empty, error)", got, err) - } else if err.Error() != permDenied { - t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } else { + if err.Error() != permDenied { + t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } + if !errorutils.IsUnauthenticated(err) { + t.Errorf("IsUnauthenticated() = false; want = true") + } } c = r.Child("admin") if err := c.Get(context.Background(), &got); err == nil || got != "" { t.Errorf("Get() = (%q, %v); want = (empty, error)", got, err) - } else if err.Error() != permDenied { - t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } else { + if err.Error() != permDenied { + t.Errorf("Error = %q; want = %q", err.Error(), permDenied) + } + if !errorutils.IsUnauthenticated(err) { + t.Errorf("IsUnauthenticated() = false; want = true") + } } } diff --git a/integration/db/query_test.go b/integration/db/query_test.go index b473d4c3..6bb48cf0 100644 --- a/integration/db/query_test.go +++ b/integration/db/query_test.go @@ -19,7 +19,7 @@ import ( "reflect" "testing" - "firebase.google.com/go/db" + "firebase.google.com/go/v4/db" ) var heightSorted = []string{ diff --git a/integration/firestore/firestore_test.go b/integration/firestore/firestore_test.go index 1b861d92..8e3dd60f 100644 --- a/integration/firestore/firestore_test.go +++ b/integration/firestore/firestore_test.go @@ -20,7 +20,7 @@ import ( "reflect" "testing" - "firebase.google.com/go/integration/internal" + "firebase.google.com/go/v4/integration/internal" ) func TestFirestore(t *testing.T) { diff --git a/integration/iid/iid_test.go b/integration/iid/iid_test.go index edf4cac0..a14b2fdf 100644 --- a/integration/iid/iid_test.go +++ b/integration/iid/iid_test.go @@ -22,8 +22,9 @@ import ( "os" "testing" - "firebase.google.com/go/iid" - "firebase.google.com/go/integration/internal" + "firebase.google.com/go/v4/errorutils" + "firebase.google.com/go/v4/iid" + "firebase.google.com/go/v4/integration/internal" ) var client *iid.Client @@ -57,7 +58,7 @@ func TestNonExisting(t *testing.T) { t.Errorf("DeleteInstanceID(non-existing) = nil; want error") } want := `instance id "fictive-ID0": failed to find the instance id` - if !iid.IsNotFound(err) || err.Error() != want { + if !errorutils.IsNotFound(err) || err.Error() != want { t.Errorf("DeleteInstanceID(non-existing) = %v; want = %v", err, want) } } diff --git a/integration/internal/internal.go b/integration/internal/internal.go index a5cd7af6..3a7948cc 100644 --- a/integration/internal/internal.go +++ b/integration/internal/internal.go @@ -18,14 +18,13 @@ package internal import ( "context" "encoding/json" - "go/build" "io/ioutil" "net/http" "path/filepath" "strings" - firebase "firebase.google.com/go" - "firebase.google.com/go/internal" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/internal" "google.golang.org/api/option" "google.golang.org/api/transport" ) @@ -35,7 +34,7 @@ const apiKeyPath = "integration_apikey.txt" // Resource returns the absolute path to the specified test resource file. func Resource(name string) string { - p := []string{build.Default.GOPATH, "src", "firebase.google.com", "go", "testdata", name} + p := []string{"..", "..", "testdata", name} return filepath.Join(p...) } diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index f62aa72a..b86aa2ae 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -24,8 +24,8 @@ import ( "regexp" "testing" - "firebase.google.com/go/integration/internal" - "firebase.google.com/go/messaging" + "firebase.google.com/go/v4/integration/internal" + "firebase.google.com/go/v4/messaging" ) // The registration token has the proper format, but is not valid (i.e. expired). The intention of diff --git a/integration/storage/storage_test.go b/integration/storage/storage_test.go index 6865af6f..2c2706bf 100644 --- a/integration/storage/storage_test.go +++ b/integration/storage/storage_test.go @@ -24,9 +24,9 @@ import ( "testing" gcs "cloud.google.com/go/storage" - "firebase.google.com/go" - "firebase.google.com/go/integration/internal" - "firebase.google.com/go/storage" + "firebase.google.com/go/v4" + "firebase.google.com/go/v4/integration/internal" + "firebase.google.com/go/v4/storage" ) var ctx context.Context diff --git a/internal/errors.go b/internal/errors.go new file mode 100644 index 00000000..e209d158 --- /dev/null +++ b/internal/errors.go @@ -0,0 +1,197 @@ +// Copyright 2020 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "os" + "syscall" +) + +// ErrorCode represents the platform-wide error codes that can be raised by +// Admin SDK APIs. +type ErrorCode string + +const ( + // InvalidArgument is a OnePlatform error code. + InvalidArgument ErrorCode = "INVALID_ARGUMENT" + + // FailedPrecondition is a OnePlatform error code. + FailedPrecondition ErrorCode = "FAILED_PRECONDITION" + + // OutOfRange is a OnePlatform error code. + OutOfRange ErrorCode = "OUT_OF_RANGE" + + // Unauthenticated is a OnePlatform error code. + Unauthenticated ErrorCode = "UNAUTHENTICATED" + + // PermissionDenied is a OnePlatform error code. + PermissionDenied ErrorCode = "PERMISSION_DENIED" + + // NotFound is a OnePlatform error code. + NotFound ErrorCode = "NOT_FOUND" + + // Conflict is a custom error code that represents HTTP 409 responses. + // + // OnePlatform APIs typically respond with ABORTED or ALREADY_EXISTS explicitly. But a few + // old APIs send HTTP 409 Conflict without any additional details to distinguish between the two + // cases. For these we currently use this error code. As more APIs adopt OnePlatform conventions + // this will become less important. + Conflict ErrorCode = "CONFLICT" + + // Aborted is a OnePlatform error code. + Aborted ErrorCode = "ABORTED" + + // AlreadyExists is a OnePlatform error code. + AlreadyExists ErrorCode = "ALREADY_EXISTS" + + // ResourceExhausted is a OnePlatform error code. + ResourceExhausted ErrorCode = "RESOURCE_EXHAUSTED" + + // Cancelled is a OnePlatform error code. + Cancelled ErrorCode = "CANCELLED" + + // DataLoss is a OnePlatform error code. + DataLoss ErrorCode = "DATA_LOSS" + + // Unknown is a OnePlatform error code. + Unknown ErrorCode = "UNKNOWN" + + // Internal is a OnePlatform error code. + Internal ErrorCode = "INTERNAL" + + // Unavailable is a OnePlatform error code. + Unavailable ErrorCode = "UNAVAILABLE" + + // DeadlineExceeded is a OnePlatform error code. + DeadlineExceeded ErrorCode = "DEADLINE_EXCEEDED" +) + +// FirebaseError is an error type containing an error code string. +type FirebaseError struct { + ErrorCode ErrorCode + String string + Response *http.Response + Ext map[string]interface{} +} + +func (fe *FirebaseError) Error() string { + return fe.String +} + +// HasPlatformErrorCode checks if the given error contains a specific error code. +func HasPlatformErrorCode(err error, code ErrorCode) bool { + fe, ok := err.(*FirebaseError) + return ok && fe.ErrorCode == code +} + +var httpStatusToErrorCodes = map[int]ErrorCode{ + http.StatusBadRequest: InvalidArgument, + http.StatusUnauthorized: Unauthenticated, + http.StatusForbidden: PermissionDenied, + http.StatusNotFound: NotFound, + http.StatusConflict: Conflict, + http.StatusTooManyRequests: ResourceExhausted, + http.StatusInternalServerError: Internal, + http.StatusServiceUnavailable: Unavailable, +} + +// NewFirebaseError creates a new error from the given HTTP response. +func NewFirebaseError(resp *Response) *FirebaseError { + code, ok := httpStatusToErrorCodes[resp.Status] + if !ok { + code = Unknown + } + + return &FirebaseError{ + ErrorCode: code, + String: fmt.Sprintf("unexpected http response with status: %d\n%s", resp.Status, string(resp.Body)), + Response: resp.LowLevelResponse(), + Ext: make(map[string]interface{}), + } +} + +// NewFirebaseErrorOnePlatform parses the response payload as a GCP error response +// and create an error from the details extracted. +// +// If the response failes to parse, or otherwise doesn't provide any useful details +// NewFirebaseErrorOnePlatform creates an error with some sensible defaults. +func NewFirebaseErrorOnePlatform(resp *Response) *FirebaseError { + base := NewFirebaseError(resp) + + var gcpError struct { + Error struct { + Status string `json:"status"` + Message string `json:"message"` + } `json:"error"` + } + json.Unmarshal(resp.Body, &gcpError) // ignore any json parse errors at this level + if gcpError.Error.Status != "" { + base.ErrorCode = ErrorCode(gcpError.Error.Status) + } + + if gcpError.Error.Message != "" { + base.String = gcpError.Error.Message + } + + return base +} + +func newFirebaseErrorTransport(err error) *FirebaseError { + var code ErrorCode + var msg string + if os.IsTimeout(err) { + code = DeadlineExceeded + msg = fmt.Sprintf("timed out while making an http call: %v", err) + } else if isConnectionRefused(err) { + code = Unavailable + msg = fmt.Sprintf("failed to establish a connection: %v", err) + } else { + code = Unknown + msg = fmt.Sprintf("unknown error while making an http call: %v", err) + } + + return &FirebaseError{ + ErrorCode: code, + String: msg, + Ext: make(map[string]interface{}), + } +} + +// isConnectionRefused attempts to determine if the given error was caused by a failure to establish a +// connection. +// +// A net.OpError where the Op field is set to "dial" or "read" is considered a connection refused +// error. Similarly an ECONNREFUSED error code (Linux-specific) is also considered a connection +// refused error. +func isConnectionRefused(err error) bool { + switch t := err.(type) { + case *url.Error: + return isConnectionRefused(t.Err) + case *net.OpError: + if t.Op == "dial" || t.Op == "read" { + return true + } + return isConnectionRefused(t.Err) + case syscall.Errno: + return t == syscall.ECONNREFUSED + } + + return false +} diff --git a/internal/errors_test.go b/internal/errors_test.go new file mode 100644 index 00000000..3733429e --- /dev/null +++ b/internal/errors_test.go @@ -0,0 +1,337 @@ +// Copyright 2020 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "strings" + "syscall" + "testing" +) + +var platformErrorCodes = []ErrorCode{ + InvalidArgument, + Unauthenticated, + NotFound, + Aborted, + AlreadyExists, + Internal, + Unavailable, + Unknown, +} + +func TestPlatformError(t *testing.T) { + var body string + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(body)) + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &HTTPClient{ + Client: http.DefaultClient, + } + get := &Request{ + Method: http.MethodGet, + URL: server.URL, + } + want := "Test error message" + + for _, code := range platformErrorCodes { + body = fmt.Sprintf(`{ + "error": { + "status": %q, + "message": "Test error message" + } + }`, code) + + resp, err := client.Do(context.Background(), get) + if resp != nil || err == nil || err.Error() != want { + t.Fatalf("[%s]: Do() = (%v, %v); want = (nil, %q)", code, resp, err, want) + } + if !HasPlatformErrorCode(err, code) { + t.Errorf("[%s]: HasPlatformErrorCode() = false; want = true", code) + } + + fe, ok := err.(*FirebaseError) + if !ok { + t.Fatalf("[%s]: Do() err = %v; want = FirebaseError", code, err) + } + + if fe.ErrorCode != code { + t.Errorf("[%s]: Do() err.ErrorCode = %q; want = %q", code, fe.ErrorCode, code) + } + if fe.Response == nil { + t.Fatalf("[%s]: Do() err.Response = nil; want = non-nil", code) + } + if fe.Response.StatusCode != http.StatusNotFound { + t.Errorf("[%s]: Do() err.Response.StatusCode = %d; want = %d", code, fe.Response.StatusCode, http.StatusNotFound) + } + if fe.Ext == nil || len(fe.Ext) > 0 { + t.Errorf("[%s]: Do() err.Ext = %v; want = empty-map", code, fe.Ext) + } + } +} + +func TestPlatformErrorWithoutDetails(t *testing.T) { + var status int + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(status) + w.Write([]byte("{}")) + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &HTTPClient{ + Client: http.DefaultClient, + } + get := &Request{ + Method: http.MethodGet, + URL: server.URL, + } + + httpStatusMappings := map[int]ErrorCode{ + http.StatusNotImplemented: Unknown, + } + + // Add known error code mappings + for k, v := range httpStatusToErrorCodes { + httpStatusMappings[k] = v + } + + for httpStatus, platformCode := range httpStatusMappings { + status = httpStatus + want := fmt.Sprintf("unexpected http response with status: %d\n{}", httpStatus) + + resp, err := client.Do(context.Background(), get) + if resp != nil || err == nil || err.Error() != want { + t.Fatalf("[%d]: Do() = (%v, %v); want = (nil, %q)", httpStatus, resp, err, want) + } + if !HasPlatformErrorCode(err, platformCode) { + t.Errorf("[%d]: HasPlatformErrorCode(%q) = false; want = true", httpStatus, platformCode) + } + + fe, ok := err.(*FirebaseError) + if !ok { + t.Fatalf("[%d]: Do() err = %v; want = FirebaseError", httpStatus, err) + } + + if fe.ErrorCode != platformCode { + t.Errorf("[%d]: Do() err.ErrorCode = %q; want = %q", httpStatus, fe.ErrorCode, platformCode) + } + if fe.Response == nil { + t.Fatalf("[%d]: Do() err.Response = nil; want = non-nil", httpStatus) + } + if fe.Response.StatusCode != httpStatus { + t.Errorf("[%d]: Do() err.Response.StatusCode = %d; want = %d", httpStatus, fe.Response.StatusCode, httpStatus) + } + if fe.Ext == nil || len(fe.Ext) > 0 { + t.Errorf("[%d]: Do() err.Ext = %v; want = empty-map", httpStatus, fe.Ext) + } + } +} + +func TestTimeoutError(t *testing.T) { + client := &HTTPClient{ + Client: &http.Client{ + Transport: &faultyTransport{ + Err: &timeoutError{}, + }, + }, + } + get := &Request{ + Method: http.MethodGet, + URL: "http://test.url", + } + want := "timed out while making an http call" + + resp, err := client.Do(context.Background(), get) + if resp != nil || err == nil || !strings.HasPrefix(err.Error(), want) { + t.Fatalf("Do() = (%v, %v); want = (nil, %q)", resp, err, want) + } + + fe, ok := err.(*FirebaseError) + if !ok { + t.Fatalf("Do() err = %v; want = FirebaseError", err) + } + + if fe.ErrorCode != DeadlineExceeded { + t.Errorf("Do() err.ErrorCode = %q; want = %q", fe.ErrorCode, DeadlineExceeded) + } + if fe.Response != nil { + t.Errorf("Do() err.Response = %v; want = nil", fe.Response) + } + if fe.Ext == nil || len(fe.Ext) > 0 { + t.Errorf("Do() err.Ext = %v; want = empty-map", fe.Ext) + } +} + +type timeoutError struct{} + +func (t *timeoutError) Error() string { + return "test timeout error" +} + +func (t *timeoutError) Timeout() bool { + return true +} + +func TestNetworkOutageError(t *testing.T) { + errors := []struct { + name string + err error + }{ + {"NetDialError", &net.OpError{Op: "dial", Err: errors.New("test error")}}, + {"NetReadError", &net.OpError{Op: "read", Err: errors.New("test error")}}, + { + "WrappedNetReadError", + &net.OpError{ + Op: "test", + Err: &net.OpError{Op: "read", Err: errors.New("test error")}, + }, + }, + {"ECONNREFUSED", syscall.ECONNREFUSED}, + } + + get := &Request{ + Method: http.MethodGet, + URL: "http://test.url", + } + want := "failed to establish a connection" + + for _, tc := range errors { + t.Run(tc.name, func(t *testing.T) { + client := &HTTPClient{ + Client: &http.Client{ + Transport: &faultyTransport{ + Err: tc.err, + }, + }, + } + + resp, err := client.Do(context.Background(), get) + if resp != nil || err == nil || !strings.HasPrefix(err.Error(), want) { + t.Fatalf("Do() = (%v, %v); want = (nil, %q)", resp, err, want) + } + + fe, ok := err.(*FirebaseError) + if !ok { + t.Fatalf("Do() err = %v; want = FirebaseError", err) + } + + if fe.ErrorCode != Unavailable { + t.Errorf("Do() err.ErrorCode = %q; want = %q", fe.ErrorCode, Unavailable) + } + if fe.Response != nil { + t.Errorf("Do() err.Response = %v; want = nil", fe.Response) + } + if fe.Ext == nil || len(fe.Ext) > 0 { + t.Errorf("Do() err.Ext = %v; want = empty-map", fe.Ext) + } + }) + } +} + +func TestUnknownNetworkError(t *testing.T) { + client := &HTTPClient{ + Client: &http.Client{ + Transport: &faultyTransport{ + Err: errors.New("unknown error"), + }, + }, + } + get := &Request{ + Method: http.MethodGet, + URL: "http://test.url", + } + want := "unknown error while making an http call" + + resp, err := client.Do(context.Background(), get) + if resp != nil || err == nil || !strings.HasPrefix(err.Error(), want) { + t.Fatalf("Do() = (%v, %v); want = (nil, %q)", resp, err, want) + } + + fe, ok := err.(*FirebaseError) + if !ok { + t.Fatalf("Do() err = %v; want = FirebaseError", err) + } + + if fe.ErrorCode != Unknown { + t.Errorf("Do() err.ErrorCode = %q; want = %q", fe.ErrorCode, Unknown) + } + if fe.Response != nil { + t.Errorf("Do() err.Response = %v; want = nil", fe.Response) + } + if fe.Ext == nil || len(fe.Ext) > 0 { + t.Errorf("Do() err.Ext = %v; want = empty-map", fe.Ext) + } +} + +func TestErrorHTTPResponse(t *testing.T) { + body := `{"key": "value"}` + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(body)) + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &HTTPClient{ + Client: http.DefaultClient, + } + get := &Request{ + Method: http.MethodGet, + URL: server.URL, + } + want := fmt.Sprintf("unexpected http response with status: 500\n%s", body) + + resp, err := client.Do(context.Background(), get) + if resp != nil || err == nil || err.Error() != want { + t.Fatalf("Do() = (%v, %v); want = (nil, %q)", resp, err, want) + } + + fe, ok := err.(*FirebaseError) + if !ok { + t.Fatalf("Do() err = %v; want = FirebaseError", err) + } + + hr := fe.Response + defer hr.Body.Close() + if hr.StatusCode != http.StatusInternalServerError { + t.Errorf("Do() Response.StatusCode = %d; want = %d", hr.StatusCode, http.StatusInternalServerError) + } + + b, err := ioutil.ReadAll(hr.Body) + if err != nil { + t.Fatalf("ReadAll(Response.Body) = %v", err) + } + + var m map[string]string + if err := json.Unmarshal(b, &m); err != nil { + t.Fatalf("Unmarshal(Response.Body) = %v", err) + } + + if len(m) != 1 || m["key"] != "value" { + t.Errorf("Unmarshal(Response.Body) = %v; want = {key: value}", m) + } +} diff --git a/internal/http_client.go b/internal/http_client.go index de03a8e7..000cba36 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -41,7 +41,6 @@ import ( type HTTPClient struct { Client *http.Client RetryConfig *RetryConfig - ErrParser ErrorParser // Deprecated. Use CreateErrFn instead. CreateErrFn CreateErrFn SuccessFn SuccessFn Opts []HTTPOption @@ -101,10 +100,28 @@ type Request struct { // Response contains information extracted from an HTTP response. type Response struct { - Status int - Header http.Header - Body []byte - errParser ErrorParser + Status int + Header http.Header + Body []byte + resp *http.Response +} + +// LowLevelResponse returns an http.Response that represents the underlying low-level HTTP +// response. +// +// This always returns a buffered copy of the original HTTP response. Body can be read from the +// returned response with no impact on the underlying HTTP connection. Closing the Body on the +// returned response is a No-op. +func (r *Response) LowLevelResponse() *http.Response { + // If the Response instance was initialized manually (as is the case when parsing batch + // responses) the resp field may be nil. + if r.resp == nil { + return nil + } + + resp := *r.resp + resp.Body = ioutil.NopCloser(bytes.NewBuffer(r.Body)) + return &resp } // Do executes the given Request, and returns a Response. @@ -117,20 +134,23 @@ type Response struct { // used as the default error function. func (c *HTTPClient) Do(ctx context.Context, req *Request) (*Response, error) { var result *attemptResult - var err error for retries := 0; ; retries++ { - result, err = c.attempt(ctx, req, retries) + hr, err := req.buildHTTPRequest(c.Opts) if err != nil { return nil, err } + + result = c.attempt(ctx, hr, retries) if !result.Retry { break } + if err = result.waitForRetry(ctx); err != nil { return nil, err } } + return c.handleResult(req, result) } @@ -155,12 +175,7 @@ func (c *HTTPClient) DoAndUnmarshal(ctx context.Context, req *Request, v interfa return resp, nil } -func (c *HTTPClient) attempt(ctx context.Context, req *Request, retries int) (*attemptResult, error) { - hr, err := req.buildHTTPRequest(c.Opts) - if err != nil { - return nil, err - } - +func (c *HTTPClient) attempt(ctx context.Context, hr *http.Request, retries int) *attemptResult { resp, err := c.Client.Do(hr.WithContext(ctx)) result := &attemptResult{} if err != nil { @@ -168,7 +183,7 @@ func (c *HTTPClient) attempt(ctx context.Context, req *Request, retries int) (*a } else { // Read the response body here forcing any I/O errors to occur so that retry logic will // cover them as well. - ir, err := newResponse(resp, c.ErrParser) + ir, err := newResponse(resp) result.Resp = ir result.Err = err } @@ -181,12 +196,13 @@ func (c *HTTPClient) attempt(ctx context.Context, req *Request, retries int) (*a result.RetryAfter = delay result.Retry = retry } - return result, nil + + return result } func (c *HTTPClient) handleResult(req *Request, result *attemptResult) (*Response, error) { if result.Err != nil { - return nil, fmt.Errorf("error while making http call: %v", result.Err) + return nil, newFirebaseErrorTransport(result.Err) } if !c.success(req, result.Resp) { @@ -202,18 +218,18 @@ func (c *HTTPClient) success(req *Request, resp *Response) bool { successFn = req.SuccessFn } else if c.SuccessFn != nil { successFn = c.SuccessFn + } else { + successFn = HasSuccessStatus } - if successFn != nil { - return successFn(resp) - } - - // TODO: Default to HasSuccessStatusCode - return true + return successFn(resp) } func (c *HTTPClient) newError(req *Request, resp *Response) error { - createErr := CreatePlatformError + createErr := func(r *Response) error { + return NewFirebaseErrorOnePlatform(r) + } + if req.CreateErrFn != nil { createErr = req.CreateErrFn } else if c.CreateErrFn != nil { @@ -286,60 +302,21 @@ func (e *jsonEntity) Mime() string { return "application/json" } -func newResponse(resp *http.Response, errParser ErrorParser) (*Response, error) { +func newResponse(resp *http.Response) (*Response, error) { defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } + return &Response{ - Status: resp.StatusCode, - Body: b, - Header: resp.Header, - errParser: errParser, + Status: resp.StatusCode, + Body: b, + Header: resp.Header, + resp: resp, }, nil } -// CheckStatus checks whether the Response status code has the given HTTP status code. -// -// Returns an error if the status code does not match. If an ErrorParser is specified, uses that to -// construct the returned error message. Otherwise includes the full response body in the error. -// -// Deprecated. Directly verify the Status field on the Response instead. -func (r *Response) CheckStatus(want int) error { - if r.Status == want { - return nil - } - - var msg string - if r.errParser != nil { - msg = r.errParser(r.Body) - } - if msg == "" { - msg = string(r.Body) - } - return fmt.Errorf("http error status: %d; reason: %s", r.Status, msg) -} - -// Unmarshal checks if the Response has the given HTTP status code, and if so unmarshals the -// response body into the variable pointed by v. -// -// Unmarshal uses https://golang.org/pkg/encoding/json/#Unmarshal internally, and hence v has the -// same requirements as the json package. -// -// Deprecated. Use DoAndUnmarshal function instead. -func (r *Response) Unmarshal(want int, v interface{}) error { - if err := r.CheckStatus(want); err != nil { - return err - } - return json.Unmarshal(r.Body, v) -} - -// ErrorParser is a function that is used to construct custom error messages. -// -// Deprecated. Use SuccessFn and CreateErrFn instead. -type ErrorParser func([]byte) string - // HTTPOption is an additional parameter that can be specified to customize an outgoing request. type HTTPOption func(*http.Request) @@ -376,33 +353,6 @@ func HasSuccessStatus(r *Response) bool { return r.Status >= http.StatusOK && r.Status < http.StatusNotModified } -// CreatePlatformError parses the response payload as a GCP error response -// and create an error from the details extracted. -// -// If the response failes to parse, or otherwise doesn't provide any useful details -// CreatePlatformError creates an error with some sensible defaults. -func CreatePlatformError(resp *Response) error { - var gcpError struct { - Error struct { - Status string `json:"status"` - Message string `json:"message"` - } `json:"error"` - } - json.Unmarshal(resp.Body, &gcpError) // ignore any json parse errors at this level - code := gcpError.Error.Status - if code == "" { - code = "UNKNOWN" - } - - message := gcpError.Error.Message - if message == "" { - message = fmt.Sprintf( - "unexpected http response with status: %d; body: %s", resp.Status, string(resp.Body)) - } - - return Error(code, message) -} - // RetryConfig specifies how the HTTPClient should retry failing HTTP requests. // // A request is never retried more than MaxRetries times. If CheckForRetry is nil, all network diff --git a/internal/http_client_test.go b/internal/http_client_test.go index 4d5cfd56..fbb4afa0 100644 --- a/internal/http_client_test.go +++ b/internal/http_client_test.go @@ -22,7 +22,6 @@ import ( "net/http" "net/http/httptest" "reflect" - "strings" "testing" "time" @@ -167,20 +166,13 @@ func TestHTTPClient(t *testing.T) { client := &HTTPClient{Client: http.DefaultClient} for _, tc := range testRequests { tc.req.URL = server.URL - resp, err := client.Do(context.Background(), tc.req) + var got map[string]interface{} + resp, err := client.DoAndUnmarshal(context.Background(), tc.req, &got) if err != nil { t.Fatal(err) } - if err := resp.CheckStatus(http.StatusOK); err != nil { - t.Errorf("CheckStatus() = %v; want nil", err) - } - if err := resp.CheckStatus(http.StatusCreated); err == nil { - t.Errorf("CheckStatus() = nil; want error") - } - - var got map[string]interface{} - if err := resp.Unmarshal(http.StatusOK, &got); err != nil { - t.Errorf("Unmarshal() = %v; want nil", err) + if resp.Status != http.StatusOK { + t.Errorf("Status = %d; want = %d", resp.Status, http.StatusOK) } if !reflect.DeepEqual(got, want) { t.Errorf("Body = %v; want = %v", got, want) @@ -238,108 +230,55 @@ func TestSuccessFn(t *testing.T) { Method: http.MethodGet, URL: server.URL, } - want := "unexpected http response with status: 200; body: {}" + want := "unexpected http response with status: 200\n{}" resp, err := client.Do(context.Background(), get) if resp != nil || err == nil || err.Error() != want { t.Fatalf("Do() = (%v, %v); want = (nil, %q)", resp, err, want) } - if !HasErrorCode(err, "UNKNOWN") { - t.Errorf("ErrorCode = %q; want = %q", err.(*FirebaseError).Code, "UNKNOWN") + fe, ok := err.(*FirebaseError) + if !ok { + t.Fatalf("Do() err = %v; want = FirebaseError", err) } -} - -func TestSuccessFnOnRequest(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("{}")) - }) - server := httptest.NewServer(handler) - defer server.Close() - client := &HTTPClient{ - Client: http.DefaultClient, - SuccessFn: HasSuccessStatus, + if fe.ErrorCode != Unknown { + t.Errorf("Do() err.ErrorCode = %q; want = %q", fe.ErrorCode, Unknown) } - get := &Request{ - Method: http.MethodGet, - URL: server.URL, - SuccessFn: func(r *Response) bool { - return false - }, + if fe.Response == nil { + t.Fatalf("Do() err.Response = nil; want = non-nil") } - want := "unexpected http response with status: 200; body: {}" - - resp, err := client.Do(context.Background(), get) - if resp != nil || err == nil || err.Error() != want { - t.Fatalf("Do() = (%v, %v); want = (nil, %q)", resp, err, want) - } - - if !HasErrorCode(err, "UNKNOWN") { - t.Errorf("ErrorCode = %q; want = %q", err.(*FirebaseError).Code, "UNKNOWN") + if fe.Response.StatusCode != http.StatusOK { + t.Errorf("Do() err.Response.StatusCode = %d; want = %d", fe.Response.StatusCode, http.StatusOK) } } -func TestPlatformError(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - resp := `{ - "error": { - "status": "NOT_FOUND", - "message": "Requested entity not found" - } - }` - - w.WriteHeader(http.StatusNotFound) - w.Write([]byte(resp)) - }) - server := httptest.NewServer(handler) - defer server.Close() - - client := &HTTPClient{ - Client: http.DefaultClient, - SuccessFn: HasSuccessStatus, - } - get := &Request{ - Method: http.MethodGet, - URL: server.URL, - } - want := "Requested entity not found" - - resp, err := client.Do(context.Background(), get) - if resp != nil || err == nil || err.Error() != want { - t.Fatalf("Do() = (%v, %v); want = (nil, %q)", resp, err, want) - } - - if !HasErrorCode(err, "NOT_FOUND") { - t.Errorf("ErrorCode = %q; want = %q", err.(*FirebaseError).Code, "NOT_FOUND") - } -} - -func TestPlatformErrorWithoutDetails(t *testing.T) { +func TestSuccessFnOnRequest(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) w.Write([]byte("{}")) }) server := httptest.NewServer(handler) defer server.Close() client := &HTTPClient{ - Client: http.DefaultClient, - SuccessFn: HasSuccessStatus, + Client: http.DefaultClient, } get := &Request{ Method: http.MethodGet, URL: server.URL, + SuccessFn: func(r *Response) bool { + return false + }, } - want := "unexpected http response with status: 404; body: {}" + want := "unexpected http response with status: 200\n{}" resp, err := client.Do(context.Background(), get) if resp != nil || err == nil || err.Error() != want { t.Fatalf("Do() = (%v, %v); want = (nil, %q)", resp, err, want) } - if !HasErrorCode(err, "UNKNOWN") { - t.Errorf("ErrorCode = %q; want = %q", err.(*FirebaseError).Code, "UNKNOWN") + if !HasPlatformErrorCode(err, Unknown) { + t.Errorf("ErrorCode = %q; want = %q", err.(*FirebaseError).ErrorCode, Unknown) } } @@ -356,7 +295,6 @@ func TestCreateErrFn(t *testing.T) { CreateErrFn: func(r *Response) error { return fmt.Errorf("custom error with status: %d", r.Status) }, - SuccessFn: HasSuccessStatus, } get := &Request{ Method: http.MethodGet, @@ -383,7 +321,6 @@ func TestCreateErrFnOnRequest(t *testing.T) { CreateErrFn: func(r *Response) error { return fmt.Errorf("custom error with status: %d", r.Status) }, - SuccessFn: HasSuccessStatus, } get := &Request{ Method: http.MethodGet, @@ -417,9 +354,6 @@ func TestContext(t *testing.T) { if err != nil { t.Fatal(err) } - if err := resp.CheckStatus(http.StatusOK); err != nil { - t.Fatal(err) - } cancel() resp, err = client.Do(ctx, &Request{ @@ -431,55 +365,6 @@ func TestContext(t *testing.T) { } } -func TestErrorParser(t *testing.T) { - data := map[string]interface{}{ - "error": "test error", - } - b, err := json.Marshal(data) - if err != nil { - t.Fatal(err) - } - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - w.Header().Set("Content-Type", "application/json") - w.Write(b) - }) - server := httptest.NewServer(handler) - defer server.Close() - - ep := func(b []byte) string { - var p struct { - Error string `json:"error"` - } - if err := json.Unmarshal(b, &p); err != nil { - return "" - } - return p.Error - } - client := &HTTPClient{ - Client: http.DefaultClient, - ErrParser: ep, - } - req := &Request{Method: http.MethodGet, URL: server.URL} - resp, err := client.Do(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - want := "http error status: 500; reason: test error" - if err := resp.CheckStatus(http.StatusOK); err.Error() != want { - t.Errorf("CheckStatus() = %q; want = %q", err.Error(), want) - } - var got map[string]interface{} - if err := resp.Unmarshal(http.StatusOK, &got); err.Error() != want { - t.Errorf("CheckStatus() = %q; want = %q", err.Error(), want) - } - if got != nil { - t.Errorf("Body = %v; want = nil", got) - } -} - func TestInvalidURL(t *testing.T) { req := &Request{ Method: http.MethodGet, @@ -509,14 +394,10 @@ func TestUnmarshalError(t *testing.T) { req := &Request{Method: http.MethodGet, URL: server.URL} client := &HTTPClient{Client: http.DefaultClient} - resp, err := client.Do(context.Background(), req) - if err != nil { - t.Fatal(err) - } - var got func() - if err := resp.Unmarshal(http.StatusOK, &got); err == nil { - t.Errorf("Unmarshal() = nil; want error") + _, err = client.DoAndUnmarshal(context.Background(), req, &got) + if err == nil { + t.Errorf("DoAndUnmarshal() = nil; want error") } } @@ -534,14 +415,15 @@ func TestRetryDisabled(t *testing.T) { client := &HTTPClient{ Client: http.DefaultClient, RetryConfig: nil, + SuccessFn: acceptAll, } req := &Request{Method: http.MethodGet, URL: server.URL} resp, err := client.Do(context.Background(), req) if err != nil { t.Fatal(err) } - if err := resp.CheckStatus(http.StatusServiceUnavailable); err != nil { - t.Errorf("CheckStatus() = %q; want = nil", err.Error()) + if resp.Status != http.StatusServiceUnavailable { + t.Errorf("Status = %d; want = %d", resp.Status, http.StatusServiceUnavailable) } if requests != 1 { t.Errorf("Total requests = %d; want = 1", requests) @@ -848,7 +730,9 @@ func TestNewHTTPClientRetryOnHTTPErrors(t *testing.T) { if err != nil { t.Fatal(err) } + client.RetryConfig.ExpBackoffFactor = 0 + client.SuccessFn = acceptAll for _, status = range []int{http.StatusInternalServerError, http.StatusServiceUnavailable} { requests = 0 req := &Request{Method: http.MethodGet, URL: server.URL} @@ -856,8 +740,8 @@ func TestNewHTTPClientRetryOnHTTPErrors(t *testing.T) { if err != nil { t.Fatal(err) } - if err := resp.CheckStatus(status); err != nil { - t.Errorf("CheckStatus(%d) = %q; want = nil", status, err.Error()) + if resp.Status != status { + t.Errorf("Status = %d; want = %d", resp.Status, status) } wantRequests := 1 + defaultMaxRetries if requests != wantRequests { @@ -881,13 +765,16 @@ func TestNewHttpClientNoRetryOnNotFound(t *testing.T) { if err != nil { t.Fatal(err) } + + client.SuccessFn = acceptAll req := &Request{Method: http.MethodGet, URL: server.URL} resp, err := client.Do(context.Background(), req) if err != nil { t.Fatal(err) } - if err := resp.CheckStatus(http.StatusNotFound); err != nil { - t.Errorf("CheckStatus() = %q; want = nil", err.Error()) + + if resp.Status != http.StatusNotFound { + t.Errorf("Status = %d; want = %d", resp.Status, http.StatusNotFound) } if requests != 1 { t.Errorf("Total requests = %d; want = 1", requests) @@ -909,12 +796,11 @@ func TestNewHttpClientRetryOnResponseReadError(t *testing.T) { t.Fatal(err) } client.RetryConfig.ExpBackoffFactor = 0 - wantPrefix := "error while making http call: " req := &Request{Method: http.MethodGet, URL: server.URL} resp, err := client.Do(context.Background(), req) - if resp != nil || err == nil || !strings.HasPrefix(err.Error(), wantPrefix) { - t.Errorf("Do() = (%v, %v); want = (nil, %q)", resp, err, wantPrefix) + if resp != nil || err == nil { + t.Errorf("Do() = (%v, %v); want = (nil, error)", resp, err) } wantRequests := 1 + defaultMaxRetries @@ -923,6 +809,16 @@ func TestNewHttpClientRetryOnResponseReadError(t *testing.T) { } } +func TestNilLowLevelResponse(t *testing.T) { + r := &Response{ + resp: nil, + } + + if ll := r.LowLevelResponse(); ll != nil { + t.Errorf("LowLevelResponse() = %v; want = nil", ll) + } +} + type faultyEntity struct { RequestAttempts int } @@ -938,9 +834,18 @@ func (e *faultyEntity) Mime() string { type faultyTransport struct { RequestAttempts int + Err error } func (e *faultyTransport) RoundTrip(req *http.Request) (*http.Response, error) { e.RequestAttempts++ + if e.Err != nil { + return nil, e.Err + } + return nil, errors.New("test error") } + +func acceptAll(resp *Response) bool { + return true +} diff --git a/internal/internal.go b/internal/internal.go index 8edc7e4d..b6670014 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -13,10 +13,9 @@ // limitations under the License. // Package internal contains functionality that is only accessible from within the Admin SDK. -package internal // import "firebase.google.com/go/internal" +package internal // import "firebase.google.com/go/v4/internal" import ( - "fmt" "time" "golang.org/x/oauth2" @@ -74,35 +73,6 @@ type MessagingConfig struct { Version string } -// FirebaseError is an error type containing an error code string. -type FirebaseError struct { - Code string - String string -} - -func (fe *FirebaseError) Error() string { - return fe.String -} - -// HasErrorCode checks if the given error contain a specific error code. -func HasErrorCode(err error, code string) bool { - fe, ok := err.(*FirebaseError) - return ok && fe.Code == code -} - -// Error creates a new FirebaseError from the specified error code and message. -func Error(code string, msg string) *FirebaseError { - return &FirebaseError{ - Code: code, - String: msg, - } -} - -// Errorf creates a new FirebaseError from the specified error code and message. -func Errorf(code string, msg string, args ...interface{}) *FirebaseError { - return Error(code, fmt.Sprintf(msg, args...)) -} - // MockTokenSource is a TokenSource implementation that can be used for testing. type MockTokenSource struct { AccessToken string diff --git a/internal/json_http_client_test.go b/internal/json_http_client_test.go index 53c9da72..15ebed73 100644 --- a/internal/json_http_client_test.go +++ b/internal/json_http_client_test.go @@ -185,11 +185,10 @@ func TestDoAndUnmarshalTransportError(t *testing.T) { URL: server.URL, } var data interface{} - wantPrefix := "error while making http call: " resp, err := client.DoAndUnmarshal(context.Background(), get, &data) - if resp != nil || err == nil || !strings.HasPrefix(err.Error(), wantPrefix) { - t.Errorf("DoAndUnmarshal() = (%v, %v); want = (nil, %q)", resp, err, wantPrefix) + if resp != nil || err == nil { + t.Errorf("DoAndUnmarshal() = (%v, %v); want = (nil, error)", resp, err) } if data != nil { diff --git a/messaging/messaging.go b/messaging/messaging.go index aabfce3d..cd0dd52e 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -14,7 +14,7 @@ // Package messaging contains functions for sending messages and managing // device subscriptions with Firebase Cloud Messaging (FCM). -package messaging // import "firebase.google.com/go/messaging" +package messaging // import "firebase.google.com/go/v4/messaging" import ( "context" @@ -27,7 +27,7 @@ import ( "strings" "time" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "google.golang.org/api/transport" ) @@ -39,71 +39,20 @@ const ( apiFormatVersionHeader = "X-GOOG-API-FORMAT-VERSION" apiFormatVersion = "2" - internalError = "internal-error" - invalidAPNSCredentials = "invalid-apns-credentials" - invalidArgument = "invalid-argument" - messageRateExceeded = "message-rate-exceeded" - mismatchedCredential = "mismatched-credential" - registrationTokenNotRegistered = "registration-token-not-registered" - serverUnavailable = "server-unavailable" - tooManyTopics = "too-many-topics" - unknownError = "unknown-error" + apnsAuthError = "APNS_AUTH_ERROR" + internalError = "INTERNAL" + thirdPartyAuthError = "THIRD_PARTY_AUTH_ERROR" + invalidArgument = "INVALID_ARGUMENT" + quotaExceeded = "QUOTA_EXCEEDED" + senderIDMismatch = "SENDER_ID_MISMATCH" + unregistered = "UNREGISTERED" + unavailable = "UNAVAILABLE" rfc3339Zulu = "2006-01-02T15:04:05.000000000Z" ) var ( topicNamePattern = regexp.MustCompile("^(/topics/)?(private/)?[a-zA-Z0-9-_.~%]+$") - - fcmErrorCodes = map[string]struct{ Code, Msg string }{ - // FCM v1 canonical error codes - "NOT_FOUND": { - registrationTokenNotRegistered, - "app instance has been unregistered; code: " + registrationTokenNotRegistered, - }, - "PERMISSION_DENIED": { - mismatchedCredential, - "sender id does not match registration token; code: " + mismatchedCredential, - }, - "RESOURCE_EXHAUSTED": { - messageRateExceeded, - "messaging service quota exceeded; code: " + messageRateExceeded, - }, - "UNAUTHENTICATED": { - invalidAPNSCredentials, - "apns certificate or auth key was invalid; code: " + invalidAPNSCredentials, - }, - - // FCM v1 new error codes - "APNS_AUTH_ERROR": { - invalidAPNSCredentials, - "apns certificate or auth key was invalid; code: " + invalidAPNSCredentials, - }, - "INTERNAL": { - internalError, - "backend servers encountered an unknown internl error; code: " + internalError, - }, - "INVALID_ARGUMENT": { - invalidArgument, - "request contains an invalid argument; code: " + invalidArgument, - }, - "SENDER_ID_MISMATCH": { - mismatchedCredential, - "sender id does not match registration token; code: " + mismatchedCredential, - }, - "QUOTA_EXCEEDED": { - messageRateExceeded, - "messaging service quota exceeded; code: " + messageRateExceeded, - }, - "UNAVAILABLE": { - serverUnavailable, - "backend servers are temporarily unavailable; code: " + serverUnavailable, - }, - "UNREGISTERED": { - registrationTokenNotRegistered, - "app instance has been unregistered; code: " + registrationTokenNotRegistered, - }, - } ) // Message to be sent via Firebase Cloud Messaging. @@ -554,7 +503,7 @@ type WebpushConfig struct { Headers map[string]string `json:"headers,omitempty"` Data map[string]string `json:"data,omitempty"` Notification *WebpushNotification `json:"notification,omitempty"` - FcmOptions *WebpushFcmOptions `json:"fcm_options,omitempty"` + FCMOptions *WebpushFCMOptions `json:"fcm_options,omitempty"` } // WebpushNotificationAction represents an action that can be performed upon receiving a WebPush notification. @@ -660,8 +609,8 @@ func (n *WebpushNotification) UnmarshalJSON(b []byte) error { return nil } -// WebpushFcmOptions contains additional options for features provided by the FCM web SDK. -type WebpushFcmOptions struct { +// WebpushFCMOptions contains additional options for features provided by the FCM web SDK. +type WebpushFCMOptions struct { Link string `json:"link,omitempty"` } @@ -939,7 +888,6 @@ type fcmClient struct { func newFCMClient(hc *http.Client, conf *internal.MessagingConfig, endpoint string) *fcmClient { client := internal.WithDefaultRetryConfig(hc) client.CreateErrFn = handleFCMError - client.SuccessFn = internal.HasSuccessStatus version := fmt.Sprintf("fire-admin-go/%s", conf.Version) client.Opts = []internal.HTTPOption{ @@ -998,52 +946,95 @@ func (c *fcmClient) makeSendRequest(ctx context.Context, req *fcmRequest) (strin // IsInternal checks if the given error was due to an internal server error. func IsInternal(err error) bool { - return internal.HasErrorCode(err, internalError) + return hasMessagingErrorCode(err, internalError) } // IsInvalidAPNSCredentials checks if the given error was due to invalid APNS certificate or auth // key. +// +// Deprecated. Use IsThirdPartyAuthError(). func IsInvalidAPNSCredentials(err error) bool { - return internal.HasErrorCode(err, invalidAPNSCredentials) + return IsThirdPartyAuthError(err) +} + +// IsThirdPartyAuthError checks if the given error was due to invalid APNS certificate or auth +// key. +func IsThirdPartyAuthError(err error) bool { + return hasMessagingErrorCode(err, thirdPartyAuthError) || hasMessagingErrorCode(err, apnsAuthError) } // IsInvalidArgument checks if the given error was due to an invalid argument in the request. func IsInvalidArgument(err error) bool { - return internal.HasErrorCode(err, invalidArgument) + return hasMessagingErrorCode(err, invalidArgument) } // IsMessageRateExceeded checks if the given error was due to the client exceeding a quota. +// +// Deprecated. Use IsQuotaExceeded(). func IsMessageRateExceeded(err error) bool { - return internal.HasErrorCode(err, messageRateExceeded) + return IsQuotaExceeded(err) +} + +// IsQuotaExceeded checks if the given error was due to the client exceeding a quota. +func IsQuotaExceeded(err error) bool { + return hasMessagingErrorCode(err, quotaExceeded) } // IsMismatchedCredential checks if the given error was due to an invalid credential or permission // error. +// +// Deprecated. Use IsSenderIDMismatch(). func IsMismatchedCredential(err error) bool { - return internal.HasErrorCode(err, mismatchedCredential) + return IsSenderIDMismatch(err) +} + +// IsSenderIDMismatch checks if the given error was due to an invalid credential or permission +// error. +func IsSenderIDMismatch(err error) bool { + return hasMessagingErrorCode(err, senderIDMismatch) } // IsRegistrationTokenNotRegistered checks if the given error was due to a registration token that // became invalid. +// +// Deprecated. Use IsUnregistered(). func IsRegistrationTokenNotRegistered(err error) bool { - return internal.HasErrorCode(err, registrationTokenNotRegistered) + return IsUnregistered(err) +} + +// IsUnregistered checks if the given error was due to a registration token that +// became invalid. +func IsUnregistered(err error) bool { + return hasMessagingErrorCode(err, unregistered) } // IsServerUnavailable checks if the given error was due to the backend server being temporarily // unavailable. +// +// Deprecated. Use IsUnavailable(). func IsServerUnavailable(err error) bool { - return internal.HasErrorCode(err, serverUnavailable) + return IsUnavailable(err) +} + +// IsUnavailable checks if the given error was due to the backend server being temporarily +// unavailable. +func IsUnavailable(err error) bool { + return hasMessagingErrorCode(err, unavailable) } // IsTooManyTopics checks if the given error was due to the client exceeding the allowed number // of topics. +// +// Deprecated. Always returns false. func IsTooManyTopics(err error) bool { - return internal.HasErrorCode(err, tooManyTopics) + return false } // IsUnknown checks if the given error was due to unknown error returned by the backend server. +// +// Deprecated. Always returns false. func IsUnknown(err error) bool { - return internal.HasErrorCode(err, unknownError) + return false } type fcmRequest struct { @@ -1055,10 +1046,8 @@ type fcmResponse struct { Name string `json:"name"` } -type fcmError struct { +type fcmErrorResponse struct { Error struct { - Status string `json:"status"` - Message string `json:"message"` Details []struct { Type string `json:"@type"` ErrorCode string `json:"errorCode"` @@ -1067,29 +1056,25 @@ type fcmError struct { } func handleFCMError(resp *internal.Response) error { - var fe fcmError + base := internal.NewFirebaseErrorOnePlatform(resp) + var fe fcmErrorResponse json.Unmarshal(resp.Body, &fe) // ignore any json parse errors at this level - var serverCode string for _, d := range fe.Error.Details { if d.Type == "type.googleapis.com/google.firebase.fcm.v1.FcmError" { - serverCode = d.ErrorCode + base.Ext["messagingErrorCode"] = d.ErrorCode break } } - if serverCode == "" { - serverCode = fe.Error.Status - } - var clientCode, msg string - info, ok := fcmErrorCodes[serverCode] - if ok { - clientCode, msg = info.Code, info.Msg - } else { - clientCode = unknownError - msg = fmt.Sprintf("server responded with an unknown error; response: %s", string(resp.Body)) - } - if fe.Error.Message != "" { - msg += "; details: " + fe.Error.Message + return base +} + +func hasMessagingErrorCode(err error, code string) bool { + fe, ok := err.(*internal.FirebaseError) + if !ok { + return false } - return internal.Errorf(clientCode, "http error status: %d; reason: %s", resp.Status, msg) + + got, ok := fe.Ext["messagingErrorCode"] + return ok && got == code } diff --git a/messaging/messaging_batch.go b/messaging/messaging_batch.go index cb4e848a..b291dde1 100644 --- a/messaging/messaging_batch.go +++ b/messaging/messaging_batch.go @@ -28,7 +28,7 @@ import ( "net/http" "net/textproto" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" ) const maxMessages = 500 diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go index be966513..477db6cb 100644 --- a/messaging/messaging_test.go +++ b/messaging/messaging_test.go @@ -24,7 +24,8 @@ import ( "testing" "time" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/errorutils" + "firebase.google.com/go/v4/internal" "google.golang.org/api/option" ) @@ -311,7 +312,7 @@ var validMessages = []struct { Vibrate: []int{100, 200, 100}, CustomData: map[string]interface{}{"k1": "v1", "k2": "v2"}, }, - FcmOptions: &WebpushFcmOptions{ + FCMOptions: &WebpushFCMOptions{ Link: "https://link.com", }, }, @@ -959,7 +960,7 @@ var invalidMessages = []struct { req: &Message{ Webpush: &WebpushConfig{ Notification: &WebpushNotification{}, - FcmOptions: &WebpushFcmOptions{ + FCMOptions: &WebpushFCMOptions{ Link: "link", }, }, @@ -972,7 +973,7 @@ var invalidMessages = []struct { req: &Message{ Webpush: &WebpushConfig{ Notification: &WebpushNotification{}, - FcmOptions: &WebpushFcmOptions{ + FCMOptions: &WebpushFCMOptions{ Link: "http://link.com", }, }, @@ -1209,11 +1210,11 @@ func TestSendError(t *testing.T) { client.fcmEndpoint = ts.URL client.fcmClient.httpClient.RetryConfig = nil - for _, tc := range httpErrors { + for idx, tc := range httpErrors { resp = tc.resp name, err := client.Send(ctx, &Message{Topic: "topic"}) if err == nil || err.Error() != tc.want || !tc.check(err) { - t.Errorf("Send() = (%q, %v); want = (%q, %q)", name, err, "", tc.want) + t.Errorf("Send(%d) = (%q, %v); want = (%q, %q)", idx, name, err, "", tc.want) } } } @@ -1277,60 +1278,89 @@ var httpErrors = []struct { }{ { resp: "{}", - want: "http error status: 500; reason: server responded with an unknown error; response: {}", - check: IsUnknown, + want: "unexpected http response with status: 500\n{}", + check: errorutils.IsInternal, }, { resp: "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}", - want: "http error status: 500; reason: request contains an invalid argument; code: invalid-argument; details: test error", - check: IsInvalidArgument, + want: "test error", + check: errorutils.IsInvalidArgument, }, { - resp: "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}", - want: "http error status: 500; reason: app instance has been unregistered; code: registration-token-not-registered; " + - "details: test error", - check: IsRegistrationTokenNotRegistered, + resp: "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}", + want: "test error", + check: errorutils.IsNotFound, }, { - resp: "{\"error\": {\"status\": \"QUOTA_EXCEEDED\", \"message\": \"test error\"}}", - want: "http error status: 500; reason: messaging service quota exceeded; code: message-rate-exceeded; " + - "details: test error", - check: IsMessageRateExceeded, + resp: "{\"error\": {\"status\": \"RESOURCE_EXHAUSTED\", \"message\": \"test error\"}}", + want: "test error", + check: errorutils.IsResourceExhausted, }, { - resp: "{\"error\": {\"status\": \"UNAVAILABLE\", \"message\": \"test error\"}}", - want: "http error status: 500; reason: backend servers are temporarily unavailable; code: server-unavailable; " + - "details: test error", - check: IsServerUnavailable, + resp: "{\"error\": {\"status\": \"UNAVAILABLE\", \"message\": \"test error\"}}", + want: "test error", + check: errorutils.IsUnavailable, }, { - resp: "{\"error\": {\"status\": \"INTERNAL\", \"message\": \"test error\"}}", - want: "http error status: 500; reason: backend servers encountered an unknown internl error; code: internal-error; " + - "details: test error", - check: IsInternal, + resp: "{\"error\": {\"status\": \"INTERNAL\", \"message\": \"test error\"}}", + want: "test error", + check: errorutils.IsInternal, + }, + { + resp: `{"error": {"status": "INVALID_ARGUMENT", "message": "test error", "details": [` + + `{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "UNREGISTERED"}]}}`, + want: "test error", + check: func(err error) bool { + return IsRegistrationTokenNotRegistered(err) && IsUnregistered(err) + }, + }, + { + resp: `{"error": {"status": "INVALID_ARGUMENT", "message": "test error", "details": [` + + `{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "SENDER_ID_MISMATCH"}]}}`, + want: "test error", + check: func(err error) bool { + return IsMismatchedCredential(err) && IsSenderIDMismatch(err) + }, }, { - resp: "{\"error\": {\"status\": \"APNS_AUTH_ERROR\", \"message\": \"test error\"}}", - want: "http error status: 500; reason: apns certificate or auth key was invalid; code: invalid-apns-credentials; " + - "details: test error", - check: IsInvalidAPNSCredentials, + resp: `{"error": {"status": "RESOURCE_EXHAUSTED", "message": "test error", "details": [` + + `{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "QUOTA_EXCEEDED"}]}}`, + want: "test error", + check: func(err error) bool { + return IsMessageRateExceeded(err) && IsQuotaExceeded(err) + }, + }, + { + resp: `{"error": {"status": "UNAVAILABLE", "message": "test error", "details": [` + + `{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "UNAVAILABLE"}]}}`, + want: "test error", + check: func(err error) bool { + return IsServerUnavailable(err) && IsUnavailable(err) + }, }, { - resp: "{\"error\": {\"status\": \"SENDER_ID_MISMATCH\", \"message\": \"test error\"}}", - want: "http error status: 500; reason: sender id does not match registration token; code: mismatched-credential; " + - "details: test error", - check: IsMismatchedCredential, + resp: `{"error": {"status": "INTERNAL", "message": "test error", "details": [` + + `{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "INTERNAL"}]}}`, + want: "test error", + check: IsInternal, }, { resp: `{"error": {"status": "INVALID_ARGUMENT", "message": "test error", "details": [` + - `{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "UNREGISTERED"}]}}`, - want: "http error status: 500; reason: app instance has been unregistered; code: registration-token-not-registered; " + - "details: test error", - check: IsRegistrationTokenNotRegistered, + `{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "INVALID_ARGUMENT"}]}}`, + want: "test error", + check: IsInvalidArgument, + }, + { + resp: `{"error": {"status": "INVALID_ARGUMENT", "message": "test error", "details": [` + + `{"@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "THIRD_PARTY_AUTH_ERROR"}]}}`, + want: "test error", + check: func(err error) bool { + return IsInvalidAPNSCredentials(err) && IsThirdPartyAuthError(err) + }, }, { resp: "not json", - want: "http error status: 500; reason: server responded with an unknown error; response: not json", - check: IsUnknown, + want: "unexpected http response with status: 500\nnot json", + check: errorutils.IsInternal, }, } diff --git a/messaging/messaging_utils.go b/messaging/messaging_utils.go index f25c4370..e69dd652 100644 --- a/messaging/messaging_utils.go +++ b/messaging/messaging_utils.go @@ -222,8 +222,8 @@ func validateWebpushConfig(webpush *WebpushConfig) error { return fmt.Errorf("multiple specifications for the key %q", k) } } - if webpush.FcmOptions != nil { - link := webpush.FcmOptions.Link + if webpush.FCMOptions != nil { + link := webpush.FCMOptions.Link p, err := url.ParseRequestURI(link) if err != nil { return fmt.Errorf("invalid link URL: %q", link) diff --git a/messaging/topic_mgt.go b/messaging/topic_mgt.go index 6252cdb5..15a29787 100644 --- a/messaging/topic_mgt.go +++ b/messaging/topic_mgt.go @@ -21,7 +21,7 @@ import ( "net/http" "strings" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" ) const ( @@ -30,25 +30,6 @@ const ( iidUnsubscribe = "batchRemove" ) -var iidErrorCodes = map[string]struct{ Code, Msg string }{ - "INVALID_ARGUMENT": { - invalidArgument, - "request contains an invalid argument; code: " + invalidArgument, - }, - "NOT_FOUND": { - registrationTokenNotRegistered, - "request contains an invalid argument; code: " + registrationTokenNotRegistered, - }, - "INTERNAL": { - internalError, - "server encountered an internal error; code: " + internalError, - }, - "TOO_MANY_TOPICS": { - tooManyTopics, - "client exceeded the number of allowed topics; code: " + tooManyTopics, - }, -} - // TopicManagementResponse is the result produced by topic management operations. // // TopicManagementResponse provides an overview of how many input tokens were successfully handled, @@ -67,14 +48,7 @@ func newTopicManagementResponse(resp *iidResponse) *TopicManagementResponse { tmr.SuccessCount++ } else { tmr.FailureCount++ - code := res["error"].(string) - info, ok := iidErrorCodes[code] - var reason string - if ok { - reason = info.Msg - } else { - reason = unknownError - } + reason := res["error"].(string) tmr.Errors = append(tmr.Errors, &ErrorInfo{ Index: idx, Reason: reason, @@ -92,7 +66,6 @@ type iidClient struct { func newIIDClient(hc *http.Client) *iidClient { client := internal.WithDefaultRetryConfig(hc) client.CreateErrFn = handleIIDError - client.SuccessFn = internal.HasSuccessStatus client.Opts = []internal.HTTPOption{internal.WithHeader("access_token_auth", "true")} return &iidClient{ iidEndpoint: iidEndpoint, @@ -134,7 +107,7 @@ type iidResponse struct { Results []map[string]interface{} `json:"results"` } -type iidError struct { +type iidErrorResponse struct { Error string `json:"error"` } @@ -176,15 +149,12 @@ func (c *iidClient) makeTopicManagementRequest(ctx context.Context, req *iidRequ } func handleIIDError(resp *internal.Response) error { - var ie iidError + base := internal.NewFirebaseError(resp) + var ie iidErrorResponse json.Unmarshal(resp.Body, &ie) // ignore any json parse errors at this level - var clientCode, msg string - info, ok := iidErrorCodes[ie.Error] - if ok { - clientCode, msg = info.Code, info.Msg - } else { - clientCode = unknownError - msg = fmt.Sprintf("client encountered an unknown error; response: %s", string(resp.Body)) + if ie.Error != "" { + base.String = fmt.Sprintf("error while calling the iid service: %s", ie.Error) } - return internal.Errorf(clientCode, "http error status: %d; reason: %s", resp.Status, msg) + + return base } diff --git a/messaging/topic_mgt_test.go b/messaging/topic_mgt_test.go index 68a82a2d..15ea45d1 100644 --- a/messaging/topic_mgt_test.go +++ b/messaging/topic_mgt_test.go @@ -23,6 +23,8 @@ import ( "reflect" "strings" "testing" + + "firebase.google.com/go/v4/errorutils" ) func TestSubscribe(t *testing.T) { @@ -113,8 +115,9 @@ func TestInvalidUnsubscribe(t *testing.T) { func TestTopicManagementError(t *testing.T) { var resp string + var status int ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(status) w.Header().Set("Content-Type", "application/json") w.Write([]byte(resp)) })) @@ -129,42 +132,45 @@ func TestTopicManagementError(t *testing.T) { client.iidClient.httpClient.RetryConfig = nil cases := []struct { - resp, want string - check func(error) bool + name, resp, want string + status int + check func(err error) bool }{ { - resp: "{}", - want: "http error status: 500; reason: client encountered an unknown error; response: {}", - check: IsUnknown, - }, - { - resp: "{\"error\": \"INVALID_ARGUMENT\"}", - want: "http error status: 500; reason: request contains an invalid argument; code: invalid-argument", - check: IsInvalidArgument, + name: "EmptyResponse", + resp: "{}", + want: "unexpected http response with status: 500\n{}", + status: http.StatusInternalServerError, + check: errorutils.IsInternal, }, { - resp: "{\"error\": \"TOO_MANY_TOPICS\"}", - want: "http error status: 500; reason: client exceeded the number of allowed topics; code: too-many-topics", - check: IsTooManyTopics, + name: "ErrorCode", + resp: "{\"error\": \"INVALID_ARGUMENT\"}", + want: "error while calling the iid service: INVALID_ARGUMENT", + status: http.StatusBadRequest, + check: errorutils.IsInvalidArgument, }, { - resp: "not json", - want: "http error status: 500; reason: client encountered an unknown error; response: not json", - check: IsUnknown, + name: "NotJson", + resp: "not json", + want: "unexpected http response with status: 500\nnot json", + status: http.StatusInternalServerError, + check: errorutils.IsInternal, }, } + for _, tc := range cases { resp = tc.resp + status = tc.status + tmr, err := client.SubscribeToTopic(ctx, []string{"id1"}, "topic") if err == nil || err.Error() != tc.want || !tc.check(err) { - t.Errorf("SubscribeToTopic() = (%#v, %v); want = (nil, %q)", tmr, err, tc.want) + t.Errorf("SubscribeToTopic(%s) = (%#v, %v); want = (nil, %q)", tc.name, tmr, err, tc.want) } - } - for _, tc := range cases { - resp = tc.resp - tmr, err := client.UnsubscribeFromTopic(ctx, []string{"id1"}, "topic") - if err == nil || err.Error() != tc.want { - t.Errorf("UnsubscribeFromTopic() = (%#v, %v); want = (nil, %q)", tmr, err, tc.want) + + tmr, err = client.UnsubscribeFromTopic(ctx, []string{"id1"}, "topic") + if err == nil || err.Error() != tc.want || !tc.check(err) { + t.Errorf("UnsubscribeFromTopic(%s) = (%#v, %v); want = (nil, %q)", tc.name, tmr, err, tc.want) } } } @@ -208,8 +214,8 @@ func checkTopicMgtResponse(t *testing.T, resp *TopicManagementResponse) { if e.Index != 1 { t.Errorf("ErrorInfo.Index = %d; want = %d", e.Index, 1) } - if e.Reason != "unknown-error" { - t.Errorf("ErrorInfo.Reason = %s; want = %s", e.Reason, "unknown-error") + if e.Reason != "error_reason" { + t.Errorf("ErrorInfo.Reason = %s; want = %s", e.Reason, "error_reason") } } diff --git a/snippets/auth.go b/snippets/auth.go index 259a5334..229e0f9d 100644 --- a/snippets/auth.go +++ b/snippets/auth.go @@ -23,9 +23,9 @@ import ( "net/http" "time" - firebase "firebase.google.com/go" - "firebase.google.com/go/auth" - "firebase.google.com/go/auth/hash" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/auth" + "firebase.google.com/go/v4/auth/hash" "google.golang.org/api/iterator" ) diff --git a/snippets/db.go b/snippets/db.go index 9e4a1434..51af7507 100644 --- a/snippets/db.go +++ b/snippets/db.go @@ -20,8 +20,8 @@ import ( "fmt" "log" - "firebase.google.com/go" - "firebase.google.com/go/db" + "firebase.google.com/go/v4" + "firebase.google.com/go/v4/db" "google.golang.org/api/option" ) diff --git a/snippets/init.go b/snippets/init.go index aba34c51..3125a702 100644 --- a/snippets/init.go +++ b/snippets/init.go @@ -19,8 +19,8 @@ import ( "context" "log" - firebase "firebase.google.com/go" - "firebase.google.com/go/auth" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/auth" "google.golang.org/api/option" ) diff --git a/snippets/messaging.go b/snippets/messaging.go index 8b191129..129502ef 100644 --- a/snippets/messaging.go +++ b/snippets/messaging.go @@ -20,8 +20,8 @@ import ( "log" "time" - firebase "firebase.google.com/go" - "firebase.google.com/go/messaging" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/messaging" ) func sendToToken(app *firebase.App) { diff --git a/snippets/storage.go b/snippets/storage.go index a39538a3..5ef1b1f2 100644 --- a/snippets/storage.go +++ b/snippets/storage.go @@ -18,7 +18,7 @@ import ( "context" "log" - firebase "firebase.google.com/go" + firebase "firebase.google.com/go/v4" "google.golang.org/api/option" ) diff --git a/storage/storage.go b/storage/storage.go index 3403bf40..25fc5853 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -20,7 +20,7 @@ import ( "errors" "cloud.google.com/go/storage" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" ) // Client is the interface for the Firebase Storage service. diff --git a/storage/storage_test.go b/storage/storage_test.go index 52a64f4a..ab57f1dd 100644 --- a/storage/storage_test.go +++ b/storage/storage_test.go @@ -18,7 +18,7 @@ import ( "context" "testing" - "firebase.google.com/go/internal" + "firebase.google.com/go/v4/internal" "google.golang.org/api/option" ) From 38e9443e62575a970034129c2ab5c3b2c5cd9f3c Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 12 Jun 2020 12:10:21 -0700 Subject: [PATCH 4/7] fix: Removed import path comments (#380) * fix: Checked and updated import path comments * Removed import paths altogether * Removed other lingering import paths --- README.md | 4 ++-- auth/auth_std.go | 2 +- auth/hash/hash.go | 2 +- db/query.go | 2 +- errorutils/errorutils.go | 8 +++++--- firebase.go | 2 +- iid/iid.go | 2 +- internal/internal.go | 2 +- messaging/messaging.go | 2 +- storage/storage.go | 2 +- 10 files changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6a8fb4f0..9eb67b52 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ requests, code review feedback, and also pull requests. ## Supported Go Versions -We support Go v1.11 and higher. +We support Go v1.12 and higher. [Continuous integration](https://github.com/firebase/firebase-admin-go/actions) system -tests the code on Go v1.11 through v1.13. +tests the code on Go v1.12 through v1.14. ## Documentation diff --git a/auth/auth_std.go b/auth/auth_std.go index 46a3b4b5..d4981919 100644 --- a/auth/auth_std.go +++ b/auth/auth_std.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package auth // import "firebase.google.com/go/auth" +package auth import ( "context" diff --git a/auth/hash/hash.go b/auth/hash/hash.go index 67aa51af..e93d7dfa 100644 --- a/auth/hash/hash.go +++ b/auth/hash/hash.go @@ -15,7 +15,7 @@ // Package hash contains a collection of password hash algorithms that can be used with the // auth.ImportUsers() API. Refer to https://firebase.google.com/docs/auth/admin/import-users for // more details about supported hash algorithms. -package hash // import "firebase.google.com/go/auth/hash" +package hash import ( "encoding/base64" diff --git a/db/query.go b/db/query.go index ff6a394e..424e00a8 100644 --- a/db/query.go +++ b/db/query.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package db // import "firebase.google.com/go/v4/db" +package db import ( "context" diff --git a/errorutils/errorutils.go b/errorutils/errorutils.go index 93a6b07f..fe81b756 100644 --- a/errorutils/errorutils.go +++ b/errorutils/errorutils.go @@ -13,11 +13,13 @@ // limitations under the License. // Package errorutils provides functions for checking and handling error conditions. -package errorutils // import "firebase.google.com/go/v4/errorutils" +package errorutils -import "firebase.google.com/go/v4/internal" +import ( + "net/http" -import "net/http" + "firebase.google.com/go/v4/internal" +) // IsInvalidArgument checks if the given error was due to an invalid client argument. func IsInvalidArgument(err error) bool { diff --git a/firebase.go b/firebase.go index 0a03c249..05aba60c 100644 --- a/firebase.go +++ b/firebase.go @@ -15,7 +15,7 @@ // Package firebase is the entry point to the Firebase Admin SDK. It provides functionality for initializing App // instances, which serve as the central entities that provide access to various other Firebase services exposed // from the SDK. -package firebase // import "firebase.google.com/go/v4" +package firebase import ( "context" diff --git a/iid/iid.go b/iid/iid.go index 4527d6fb..2149d85e 100644 --- a/iid/iid.go +++ b/iid/iid.go @@ -13,7 +13,7 @@ // limitations under the License. // Package iid contains functions for deleting instance IDs from Firebase projects. -package iid // import "firebase.google.com/go/v4/iid" +package iid import ( "context" diff --git a/internal/internal.go b/internal/internal.go index b6670014..cb525147 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -13,7 +13,7 @@ // limitations under the License. // Package internal contains functionality that is only accessible from within the Admin SDK. -package internal // import "firebase.google.com/go/v4/internal" +package internal import ( "time" diff --git a/messaging/messaging.go b/messaging/messaging.go index cd0dd52e..08335330 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -14,7 +14,7 @@ // Package messaging contains functions for sending messages and managing // device subscriptions with Firebase Cloud Messaging (FCM). -package messaging // import "firebase.google.com/go/v4/messaging" +package messaging import ( "context" diff --git a/storage/storage.go b/storage/storage.go index 25fc5853..3963a81f 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -13,7 +13,7 @@ // limitations under the License. // Package storage provides functions for accessing Google Cloud Storge buckets. -package storage // import "firebase.google.com/go/storage" +package storage import ( "context" From 1f197e802f1cfac812f3bef551448abb03e0db63 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 12 Jun 2020 12:53:41 -0700 Subject: [PATCH 5/7] chore: Adding a CI job to build in non-module (gopath) mode (#381) * chore: Adding a CI job to build in non-module (gopath) mode * Fixed a syntax error in yaml * Removed unconditional requirement on tests --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eca8b98c..9e96b5a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,8 @@ name: Continuous Integration on: pull_request jobs: - build: - name: Build + module: + name: Module build runs-on: ubuntu-latest strategy: matrix: @@ -38,3 +38,26 @@ jobs: - name: Run Static Analyzer run: go vet -v ./... + + gopath: + name: Gopath build + runs-on: ubuntu-latest + env: + GOPATH: ${{ github.workspace }}/go + + steps: + - name: Set up Go 1.12 + uses: actions/setup-go@v1 + with: + go-version: 1.12 + + - name: Check out code into GOPATH + 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) + + - name: Run Unit Tests + run: go test -v -race -test.short firebase.google.com/go/... From 921f33da849acc6f569ae3050f0219634ce5331a Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 16 Jun 2020 10:02:07 -0700 Subject: [PATCH 6/7] [chore] Release 4.0.0 (#383) --- firebase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase.go b/firebase.go index 05aba60c..4762eacb 100644 --- a/firebase.go +++ b/firebase.go @@ -38,7 +38,7 @@ import ( var defaultAuthOverrides = make(map[string]interface{}) // Version of the Firebase Go Admin SDK. -const Version = "3.13.0" +const Version = "4.0.0" // firebaseEnvName is the name of the environment variable with the Config. const firebaseEnvName = "FIREBASE_CONFIG" From e921fe9f884c5d8438f80c0efd881d238744d2b4 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 16 Jun 2020 11:09:03 -0700 Subject: [PATCH 7/7] [chore] Release 4.0.0 - take 2 (#384) --- integration/auth/user_mgt_test.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index 9e64559d..c80d5e30 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -249,9 +249,22 @@ func TestLastRefreshTime(t *testing.T) { 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) + // Attempt to retrieve the user 3 times (with a small delay between each attempt.) Occasionally, + // this call retrieves the user data without the lastLoginTime/lastRefreshTime fields; possibly + // because it's hitting a different server than what the login request used. + var getUsersResult *auth.UserRecord + for i := 0; i < 3; i++ { + var err error + getUsersResult, err = client.GetUser(context.Background(), userRecord.UID) + if err != nil { + t.Fatalf("GetUser(...) failed with error: %v", err) + } + + if getUsersResult.UserMetadata.LastRefreshTimestamp != 0 { + break + } + + time.Sleep(time.Second * time.Duration(2^i)) } // Ensure last refresh time is approx now (with tollerance of 10m)