diff --git a/datastore/integration_test.go b/datastore/integration_test.go index 193c3977780..8f005af952f 100644 --- a/datastore/integration_test.go +++ b/datastore/integration_test.go @@ -31,7 +31,6 @@ import ( "cloud.google.com/go/internal/testutil" "cloud.google.com/go/rpcreplay" - "golang.org/x/oauth2" "google.golang.org/api/iterator" "google.golang.org/api/option" "google.golang.org/grpc" @@ -1265,7 +1264,6 @@ func TestDetectProjectID(t *testing.T) { ctx := context.Background() creds := testutil.Credentials(ctx, ScopeDatastore) - ts := fakets{} if creds == nil { t.Skip("Integration tests skipped. See CONTRIBUTING.md for details") } @@ -1275,15 +1273,10 @@ func TestDetectProjectID(t *testing.T) { t.Errorf("NewClient: %v", err) } + ts := testutil.ErroringTokenSource{} // Try to use creds without project ID. _, err := NewClient(ctx, DetectProjectID, option.WithTokenSource(ts)) - if err == nil && err.Error() != "datastore: see the docs on DetectProjectID" { + if err == nil || err.Error() != "datastore: see the docs on DetectProjectID" { t.Errorf("expected an error while using TokenSource that does not have a project ID") } } - -type fakets struct{} - -func (f fakets) Token() (*oauth2.Token, error) { - return nil, errors.New("shouldn't see this") -} diff --git a/firestore/client.go b/firestore/client.go index a049a33e757..a3e12433d5f 100644 --- a/firestore/client.go +++ b/firestore/client.go @@ -30,6 +30,7 @@ import ( gax "github.com/googleapis/gax-go/v2" "google.golang.org/api/iterator" "google.golang.org/api/option" + "google.golang.org/api/transport" pb "google.golang.org/genproto/googleapis/firestore/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -41,6 +42,15 @@ import ( // the resource being operated on. const resourcePrefixHeader = "google-cloud-resource-prefix" +// DetectProjectID is a sentinel value that instructs NewClient to detect the +// project ID. It is given in place of the projectID argument. NewClient will +// use the project ID from the given credentials or the default credentials +// (https://developers.google.com/accounts/docs/application-default-credentials) +// if no credentials were provided. When providing credentials, not all +// options will allow NewClient to extract the project ID. Specifically a JWT +// does not have the project ID encoded. +const DetectProjectID = "*detect-project-id*" + // A Client provides access to the Firestore service. type Client struct { c *vkit.Client @@ -61,6 +71,18 @@ func NewClient(ctx context.Context, projectID string, opts ...option.ClientOptio o = []option.ClientOption{option.WithGRPCConn(conn)} } o = append(o, opts...) + + if projectID == DetectProjectID { + creds, err := transport.Creds(ctx, o...) + if err != nil { + return nil, fmt.Errorf("fetching creds: %v", err) + } + if creds.ProjectID == "" { + return nil, errors.New("firestore: see the docs on DetectProjectID") + } + projectID = creds.ProjectID + } + vc, err := vkit.NewClient(ctx, o...) if err != nil { return nil, err diff --git a/firestore/integration_test.go b/firestore/integration_test.go index bf9c9ffab7d..3cba654508e 100644 --- a/firestore/integration_test.go +++ b/firestore/integration_test.go @@ -1337,3 +1337,27 @@ func (h testHelper) mustSet(doc *DocumentRef, data interface{}, opts ...SetOptio } return wr } + +func TestDetectProjectID(t *testing.T) { + if testing.Short() { + t.Skip("Integration tests skipped in short mode") + } + ctx := context.Background() + + creds := testutil.Credentials(ctx) + if creds == nil { + t.Skip("Integration tests skipped. See CONTRIBUTING.md for details") + } + + // Use creds with project ID. + if _, err := NewClient(ctx, DetectProjectID, option.WithCredentials(creds)); err != nil { + t.Errorf("NewClient: %v", err) + } + + ts := testutil.ErroringTokenSource{} + // Try to use creds without project ID. + _, err := NewClient(ctx, DetectProjectID, option.WithTokenSource(ts)) + if err == nil || err.Error() != "firestore: see the docs on DetectProjectID" { + t.Errorf("expected an error while using TokenSource that does not have a project ID") + } +} diff --git a/internal/testutil/context.go b/internal/testutil/context.go index 39346dc8a6f..edada10464b 100644 --- a/internal/testutil/context.go +++ b/internal/testutil/context.go @@ -17,6 +17,7 @@ package testutil import ( "context" + "errors" "fmt" "io/ioutil" "log" @@ -139,3 +140,13 @@ func CanReplay(replayFilename string) bool { _, err := os.Stat(replayFilename) return err == nil } + +// ErroringTokenSource is a token source for testing purposes, +// to always return a non-nil error to its caller. It is useful +// when testing error responses with bad oauth2 credentials. +type ErroringTokenSource struct{} + +// Token implements oauth2.TokenSource, returning a nil oauth2.Token and a non-nil error. +func (fts ErroringTokenSource) Token() (*oauth2.Token, error) { + return nil, errors.New("intentional error") +}