diff --git a/auth/auth.go b/auth/auth.go index 09b1d4fb..aa72c6c5 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -22,7 +22,6 @@ import ( "encoding/pem" "errors" "fmt" - "net/http" "strings" "firebase.google.com/go/internal" @@ -102,6 +101,7 @@ func NewClient(ctx context.Context, c *internal.AuthConfig) (*Client, error) { } email = svcAcct.ClientEmail } + var snr signer if email != "" && pk != nil { snr = serviceAcctSigner{email: email, pk: pk} @@ -111,21 +111,15 @@ func NewClient(ctx context.Context, c *internal.AuthConfig) (*Client, error) { return nil, err } } - hc := http.DefaultClient - if len(c.Opts) > 0 { // TODO: fix the default when len = 0 - hc, _, err = transport.NewHTTPClient(ctx, c.Opts...) - if err != nil { - return nil, err - } - } - ks, err := newHTTPKeySource(googleCertURL, hc) + + hc, _, err := transport.NewHTTPClient(ctx, c.Opts...) if err != nil { return nil, err } return &Client{ hc: &internal.HTTPClient{Client: hc}, - ks: ks, + ks: newHTTPKeySource(googleCertURL, hc), projectID: c.ProjectID, snr: snr, url: idToolKitURL, diff --git a/auth/auth_test.go b/auth/auth_test.go index 61e48a57..79e957d8 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -41,12 +41,17 @@ var testIDToken string var testGetUserResponse []byte var testListUsersResponse []byte +var defaultTestOpts = []option.ClientOption{ + option.WithCredentialsFile("../testdata/service_account.json"), +} + func TestMain(m *testing.M) { var ( err error ks keySource ctx context.Context creds *google.DefaultCredentials + opts []option.ClientOption ) if appengine.IsDevAppServer() { aectx, aedone, err := aetest.NewContext() @@ -62,8 +67,8 @@ func TestMain(m *testing.M) { } } else { ctx = context.Background() - opt := option.WithCredentialsFile("../testdata/service_account.json") - creds, err = transport.Creds(ctx, opt) + opts = defaultTestOpts + creds, err = transport.Creds(ctx, opts...) if err != nil { log.Fatalln(err) } @@ -72,7 +77,7 @@ func TestMain(m *testing.M) { } client, err = NewClient(ctx, &internal.AuthConfig{ Creds: creds, - Opts: []option.ClientOption{option.WithCredentialsFile("../testdata/service_account.json")}, + Opts: opts, ProjectID: "mock-project-id", }) if err != nil { @@ -170,7 +175,9 @@ func TestCustomTokenError(t *testing.T) { } func TestCustomTokenInvalidCredential(t *testing.T) { - s, err := NewClient(context.Background(), &internal.AuthConfig{}) + // AuthConfig with nil Creds + conf := &internal.AuthConfig{Opts: defaultTestOpts} + s, err := NewClient(context.Background(), conf) if err != nil { t.Fatal(err) } @@ -237,7 +244,9 @@ func TestVerifyIDTokenError(t *testing.T) { } func TestNoProjectID(t *testing.T) { - c, err := NewClient(context.Background(), &internal.AuthConfig{}) + // AuthConfig with empty ProjectID + conf := &internal.AuthConfig{Opts: defaultTestOpts} + c, err := NewClient(context.Background(), conf) if err != nil { t.Fatal(err) } diff --git a/auth/crypto.go b/auth/crypto.go index 72ef46a8..3de074cd 100644 --- a/auth/crypto.go +++ b/auth/crypto.go @@ -75,13 +75,13 @@ type httpKeySource struct { Mutex *sync.Mutex } -func newHTTPKeySource(uri string, hc *http.Client) (*httpKeySource, error) { +func newHTTPKeySource(uri string, hc *http.Client) *httpKeySource { return &httpKeySource{ KeyURI: uri, HTTPClient: hc, Clock: systemClock{}, Mutex: &sync.Mutex{}, - }, nil + } } // Keys returns the RSA Public Keys hosted at this key source's URI. Refreshes the data if diff --git a/auth/crypto_test.go b/auth/crypto_test.go index 821aab06..6c3306ef 100644 --- a/auth/crypto_test.go +++ b/auth/crypto_test.go @@ -83,11 +83,8 @@ func TestHTTPKeySource(t *testing.T) { if err != nil { t.Fatal(err) } - ks, err := newHTTPKeySource("http://mock.url", http.DefaultClient) - if err != nil { - t.Fatal(err) - } + ks := newHTTPKeySource("http://mock.url", http.DefaultClient) if ks.HTTPClient == nil { t.Errorf("HTTPClient = nil; want non-nil") } @@ -105,11 +102,7 @@ func TestHTTPKeySourceWithClient(t *testing.T) { } hc, rc := newTestHTTPClient(data) - ks, err := newHTTPKeySource("http://mock.url", hc) - if err != nil { - t.Fatal(err) - } - + ks := newHTTPKeySource("http://mock.url", hc) if ks.HTTPClient != hc { t.Errorf("HTTPClient = %v; want %v", ks.HTTPClient, hc) } @@ -120,11 +113,7 @@ func TestHTTPKeySourceWithClient(t *testing.T) { func TestHTTPKeySourceEmptyResponse(t *testing.T) { hc, _ := newTestHTTPClient([]byte("")) - ks, err := newHTTPKeySource("http://mock.url", hc) - if err != nil { - t.Fatal(err) - } - + ks := newHTTPKeySource("http://mock.url", hc) if keys, err := ks.Keys(); keys != nil || err == nil { t.Errorf("Keys() = (%v, %v); want = (nil, error)", keys, err) } @@ -132,11 +121,7 @@ func TestHTTPKeySourceEmptyResponse(t *testing.T) { func TestHTTPKeySourceIncorrectResponse(t *testing.T) { hc, _ := newTestHTTPClient([]byte("{\"foo\": 1}")) - ks, err := newHTTPKeySource("http://mock.url", hc) - if err != nil { - t.Fatal(err) - } - + ks := newHTTPKeySource("http://mock.url", hc) if keys, err := ks.Keys(); keys != nil || err == nil { t.Errorf("Keys() = (%v, %v); want = (nil, error)", keys, err) } @@ -148,11 +133,7 @@ func TestHTTPKeySourceTransportError(t *testing.T) { Err: errors.New("transport error"), }, } - ks, err := newHTTPKeySource("http://mock.url", hc) - if err != nil { - t.Fatal(err) - } - + ks := newHTTPKeySource("http://mock.url", hc) if keys, err := ks.Keys(); keys != nil || err == nil { t.Errorf("Keys() = (%v, %v); want = (nil, error)", keys, err) } diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index 810490fd..edf5db0f 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -25,8 +25,11 @@ import ( "testing" "firebase.google.com/go/internal" + "golang.org/x/net/context" + "golang.org/x/oauth2" "google.golang.org/api/iterator" + "google.golang.org/api/option" ) var testUser = &UserRecord{ @@ -621,24 +624,17 @@ func TestMakeExportedUser(t *testing.T) { } func TestHTTPError(t *testing.T) { - s := mockAuthServer{} - s.Srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - s.Req = append(s.Req, r) - w.WriteHeader(500) - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"error":"test"}`)) - })) + s := echoServer([]byte(`{"error":"test"}`), t) defer s.Close() + s.Status = 500 - client, err := NewClient(context.Background(), &internal.AuthConfig{}) - if err != nil { - t.Fatal() + u, err := s.Client.GetUser(context.Background(), "some uid") + if u != nil || err == nil { + t.Fatalf("GetUser() = (%v, %v); want = (nil, error)", u, err) } - client.url = s.Srv.URL + "/" want := `http error status: 500; reason: {"error":"test"}` - _, err = client.GetUser(context.Background(), "some uid") - if err == nil || err.Error() != want { + if err.Error() != want { t.Errorf("GetUser() = %v; want = %q", err, want) } } @@ -664,7 +660,6 @@ type mockAuthServer struct { func echoServer(resp interface{}, t *testing.T) *mockAuthServer { var b []byte var err error - testVersion := "test.version" switch v := resp.(type) { case nil: b = []byte("") @@ -678,19 +673,30 @@ func echoServer(resp interface{}, t *testing.T) *mockAuthServer { } s := mockAuthServer{Resp: b} + const testToken = "test.token" + const testVersion = "test.version" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() reqBody, err := ioutil.ReadAll(r.Body) if err != nil { t.Fatal(err) } + s.Rbody = reqBody s.Req = append(s.Req, r) - vh := r.Header.Get("X-Client-Version") - wantvh := "Go/Admin/" + testVersion - if vh != wantvh { - t.Errorf("version header = %s; want: %s", vh, wantvh) + + gh := r.Header.Get("Authorization") + wh := "Bearer " + testToken + if gh != wh { + t.Errorf("Authorization header = %q; want = %q", gh, wh) } - s.Rbody = reqBody + + gh = r.Header.Get("X-Client-Version") + wh = "Go/Admin/" + testVersion + if gh != wh { + t.Errorf("X-Client-Version header = %q; want: %q", gh, wh) + } + for k, v := range s.Header { w.Header().Set(k, v) } @@ -702,7 +708,14 @@ func echoServer(resp interface{}, t *testing.T) *mockAuthServer { }) s.Srv = httptest.NewServer(handler) - authClient, err := NewClient(context.Background(), &internal.AuthConfig{Version: testVersion}) + + conf := &internal.AuthConfig{ + Opts: []option.ClientOption{ + option.WithTokenSource(&mockTokenSource{testToken}), + }, + Version: testVersion, + } + authClient, err := NewClient(context.Background(), conf) if err != nil { t.Fatal(err) } @@ -714,3 +727,11 @@ func echoServer(resp interface{}, t *testing.T) *mockAuthServer { func (s *mockAuthServer) Close() { s.Srv.Close() } + +type mockTokenSource struct { + AccessToken string +} + +func (m *mockTokenSource) Token() (*oauth2.Token, error) { + return &oauth2.Token{AccessToken: m.AccessToken}, nil +} diff --git a/firebase.go b/firebase.go index fdbd8485..c0ed3569 100644 --- a/firebase.go +++ b/firebase.go @@ -23,6 +23,7 @@ import ( "cloud.google.com/go/firestore" "firebase.google.com/go/auth" + "firebase.google.com/go/iid" "firebase.google.com/go/internal" "firebase.google.com/go/storage" @@ -44,7 +45,7 @@ var firebaseScopes = []string{ } // Version of the Firebase Go Admin SDK. -const Version = "2.2.1" +const Version = "2.3.0" // An App holds configuration and state common to all Firebase services that are exposed from the SDK. type App struct { @@ -89,6 +90,15 @@ func (a *App) Firestore(ctx context.Context) (*firestore.Client, error) { return firestore.NewClient(ctx, a.projectID, a.opts...) } +// InstanceID returns an instance of iid.Client. +func (a *App) InstanceID(ctx context.Context) (*iid.Client, error) { + conf := &internal.InstanceIDConfig{ + ProjectID: a.projectID, + Opts: a.opts, + } + return iid.NewClient(ctx, conf) +} + // NewApp creates a new App from the provided config and client options. // // If the client options contain a valid credential (a service account file, a refresh token file or an diff --git a/firebase_test.go b/firebase_test.go index f8a15b08..9e2388ce 100644 --- a/firebase_test.go +++ b/firebase_test.go @@ -281,6 +281,18 @@ func TestFirestoreWithNoProjectID(t *testing.T) { } } +func TestInstanceID(t *testing.T) { + ctx := context.Background() + app, err := NewApp(ctx, nil, option.WithCredentialsFile("testdata/service_account.json")) + if err != nil { + t.Fatal(err) + } + + if c, err := app.InstanceID(ctx); c == nil || err != nil { + t.Errorf("InstanceID() = (%v, %v); want (iid, nil)", c, err) + } +} + func TestCustomTokenSource(t *testing.T) { ctx := context.Background() ts := &testTokenSource{AccessToken: "mock-token-from-custom"} diff --git a/iid/iid.go b/iid/iid.go new file mode 100644 index 00000000..d7798e1a --- /dev/null +++ b/iid/iid.go @@ -0,0 +1,91 @@ +// Copyright 2017 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 iid contains functions for deleting instance IDs from Firebase projects. +package iid + +import ( + "errors" + "fmt" + "net/http" + + "google.golang.org/api/transport" + + "firebase.google.com/go/internal" + + "golang.org/x/net/context" +) + +const iidEndpoint = "https://console.firebase.google.com/v1" + +var errorCodes = map[int]string{ + 400: "malformed instance id argument", + 401: "request not authorized", + 403: "project does not match instance ID or the client does not have sufficient privileges", + 404: "failed to find the instance id", + 409: "already deleted", + 429: "request throttled out by the backend server", + 500: "internal server error", + 503: "backend servers are over capacity", +} + +// Client is the interface for the Firebase Instance ID service. +type Client struct { + // To enable testing against arbitrary endpoints. + endpoint string + client *internal.HTTPClient + project string +} + +// NewClient creates a new instance of the Firebase instance ID Client. +// +// This function can only be invoked from within the SDK. Client applications should access the +// the instance ID service through firebase.App. +func NewClient(ctx context.Context, c *internal.InstanceIDConfig) (*Client, error) { + if c.ProjectID == "" { + return nil, errors.New("project id is required to access instance id client") + } + + hc, _, err := transport.NewHTTPClient(ctx, c.Opts...) + if err != nil { + return nil, err + } + + return &Client{ + endpoint: iidEndpoint, + client: &internal.HTTPClient{Client: hc}, + project: c.ProjectID, + }, nil +} + +// DeleteInstanceID deletes an instance ID from Firebase. +// +// This can be used to delete an instance ID and associated user data from a Firebase project, +// pursuant to the General Data protection Regulation (GDPR). +func (c *Client) DeleteInstanceID(ctx context.Context, iid string) error { + if iid == "" { + return errors.New("instance id must not be empty") + } + + url := fmt.Sprintf("%s/project/%s/instanceId/%s", c.endpoint, c.project, iid) + resp, err := c.client.Do(ctx, &internal.Request{Method: "DELETE", URL: url}) + if err != nil { + return err + } + + if msg, ok := errorCodes[resp.Status]; ok { + return fmt.Errorf("instance id %q: %s", iid, msg) + } + return resp.CheckStatus(http.StatusOK) +} diff --git a/iid/iid_test.go b/iid/iid_test.go new file mode 100644 index 00000000..9a7c6d15 --- /dev/null +++ b/iid/iid_test.go @@ -0,0 +1,192 @@ +// Copyright 2017 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 iid + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "google.golang.org/api/option" + + "firebase.google.com/go/internal" + + "golang.org/x/net/context" +) + +var testIIDConfig = &internal.InstanceIDConfig{ + ProjectID: "test-project", + Opts: []option.ClientOption{ + option.WithTokenSource(&internal.MockTokenSource{AccessToken: "test-token"}), + }, +} + +func TestNoProjectID(t *testing.T) { + client, err := NewClient(context.Background(), &internal.InstanceIDConfig{}) + if client != nil || err == nil { + t.Errorf("NewClient() = (%v, %v); want = (nil, error)", client, err) + } +} + +func TestInvalidInstanceID(t *testing.T) { + ctx := context.Background() + client, err := NewClient(ctx, testIIDConfig) + if err != nil { + t.Fatal(err) + } + + if err := client.DeleteInstanceID(ctx, ""); err == nil { + t.Errorf("DeleteInstanceID(empty) = nil; want error") + } +} + +func TestDeleteInstanceID(t *testing.T) { + var tr *http.Request + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tr = r + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{}")) + })) + defer ts.Close() + + ctx := context.Background() + client, err := NewClient(ctx, testIIDConfig) + if err != nil { + t.Fatal(err) + } + client.endpoint = ts.URL + if err := client.DeleteInstanceID(ctx, "test-iid"); err != nil { + t.Errorf("DeleteInstanceID() = %v; want nil", err) + } + + if tr == nil { + t.Fatalf("Request = nil; want non-nil") + } + if tr.Method != "DELETE" { + t.Errorf("Method = %q; want = %q", tr.Method, "DELETE") + } + if tr.URL.Path != "/project/test-project/instanceId/test-iid" { + t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/instanceId/test-iid") + } + if h := tr.Header.Get("Authorization"); h != "Bearer test-token" { + t.Errorf("Authorization = %q; want = %q", h, "Bearer test-token") + } +} + +func TestDeleteInstanceIDError(t *testing.T) { + status := 200 + var tr *http.Request + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tr = r + w.WriteHeader(status) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{}")) + })) + defer ts.Close() + + ctx := context.Background() + client, err := NewClient(ctx, testIIDConfig) + if err != nil { + t.Fatal(err) + } + client.endpoint = ts.URL + + for k, v := range errorCodes { + status = k + err := client.DeleteInstanceID(ctx, "test-iid") + if err == nil { + t.Fatal("DeleteInstanceID() = nil; want = error") + } + + want := fmt.Sprintf("instance id %q: %s", "test-iid", v) + if err.Error() != want { + t.Errorf("DeleteInstanceID() = %v; want = %v", err, want) + } + + if tr == nil { + t.Fatalf("Request = nil; want non-nil") + } + if tr.Method != "DELETE" { + t.Errorf("Method = %q; want = %q", tr.Method, "DELETE") + } + if tr.URL.Path != "/project/test-project/instanceId/test-iid" { + t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/instanceId/test-iid") + } + if h := tr.Header.Get("Authorization"); h != "Bearer test-token" { + t.Errorf("Authorization = %q; want = %q", h, "Bearer test-token") + } + tr = nil + } +} + +func TestDeleteInstanceIDUnexpectedError(t *testing.T) { + var tr *http.Request + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tr = r + w.WriteHeader(511) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{}")) + })) + defer ts.Close() + + ctx := context.Background() + client, err := NewClient(ctx, testIIDConfig) + if err != nil { + t.Fatal(err) + } + client.endpoint = ts.URL + + err = client.DeleteInstanceID(ctx, "test-iid") + if err == nil { + t.Fatal("DeleteInstanceID() = nil; want = error") + } + + want := "http error status: 511; reason: {}" + if err.Error() != want { + t.Errorf("DeleteInstanceID() = %v; want = %v", err, want) + } + + if tr == nil { + t.Fatalf("Request = nil; want non-nil") + } + if tr.Method != "DELETE" { + t.Errorf("Method = %q; want = %q", tr.Method, "DELETE") + } + if tr.URL.Path != "/project/test-project/instanceId/test-iid" { + t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/instanceId/test-iid") + } + if h := tr.Header.Get("Authorization"); h != "Bearer test-token" { + t.Errorf("Authorization = %q; want = %q", h, "Bearer test-token") + } +} + +func TestDeleteInstanceIDConnectionError(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do nothing + })) + ts.Close() + + ctx := context.Background() + client, err := NewClient(ctx, testIIDConfig) + if err != nil { + t.Fatal(err) + } + client.endpoint = ts.URL + if err := client.DeleteInstanceID(ctx, "test-iid"); err == nil { + t.Errorf("DeleteInstanceID() = nil; want = error") + return + } +} diff --git a/integration/iid/iid_test.go b/integration/iid/iid_test.go new file mode 100644 index 00000000..8b7b316c --- /dev/null +++ b/integration/iid/iid_test.go @@ -0,0 +1,61 @@ +// Copyright 2017 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 iid contains integration tests for the firebase.google.com/go/iid package. +package iid + +import ( + "context" + "flag" + "log" + "os" + "testing" + + "firebase.google.com/go/iid" + "firebase.google.com/go/integration/internal" +) + +var client *iid.Client + +func TestMain(m *testing.M) { + flag.Parse() + if testing.Short() { + log.Println("skipping instance ID integration tests in short mode.") + os.Exit(0) + } + + ctx := context.Background() + app, err := internal.NewTestApp(ctx) + if err != nil { + log.Fatalln(err) + } + + client, err = app.InstanceID(ctx) + if err != nil { + log.Fatalln(err) + } + + os.Exit(m.Run()) +} + +func TestNonExisting(t *testing.T) { + err := client.DeleteInstanceID(context.Background(), "non-existing") + if err == nil { + t.Errorf("DeleteInstanceID(non-existing) = nil; want error") + } + want := `instance id "non-existing": failed to find the instance id` + if err.Error() != want { + t.Errorf("DeleteInstanceID(non-existing) = %v; want = %v", err, want) + } +} diff --git a/internal/http_client_test.go b/internal/http_client_test.go index 8df2b10c..67b1c35b 100644 --- a/internal/http_client_test.go +++ b/internal/http_client_test.go @@ -173,6 +173,37 @@ func TestHTTPClient(t *testing.T) { } } +func TestContext(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{}")) + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &HTTPClient{Client: http.DefaultClient} + ctx, cancel := context.WithCancel(context.Background()) + resp, err := client.Do(ctx, &Request{ + Method: "GET", + URL: server.URL, + }) + if err != nil { + t.Fatal(err) + } + if err := resp.CheckStatus(http.StatusOK); err != nil { + t.Fatal(err) + } + + cancel() + resp, err = client.Do(ctx, &Request{ + Method: "GET", + URL: server.URL, + }) + if resp != nil || err == nil { + t.Errorf("Do() = (%v; %v); want = (nil, error)", resp, err) + } +} + func TestErrorParser(t *testing.T) { data := map[string]interface{}{ "error": "test error", diff --git a/internal/internal.go b/internal/internal.go index 7ceddc97..34c4f32d 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -16,6 +16,7 @@ package internal import ( + "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/option" ) @@ -28,8 +29,24 @@ type AuthConfig struct { Version string } +// InstanceIDConfig represents the configuration of Firebase Instance ID service. +type InstanceIDConfig struct { + Opts []option.ClientOption + ProjectID string +} + // StorageConfig represents the configuration of Google Cloud Storage service. type StorageConfig struct { Opts []option.ClientOption Bucket string } + +// MockTokenSource is a TokenSource implementation that can be used for testing. +type MockTokenSource struct { + AccessToken string +} + +// Token returns the test token associated with the TokenSource. +func (ts *MockTokenSource) Token() (*oauth2.Token, error) { + return &oauth2.Token{AccessToken: ts.AccessToken}, nil +}