github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/blob/gcsblob/gcsblob_test.go (about)

     1  // Copyright 2018 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gcsblob
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"flag"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"os/user"
    27  	"path/filepath"
    28  	"testing"
    29  	"time"
    30  
    31  	"cloud.google.com/go/storage"
    32  	"github.com/google/go-cmp/cmp"
    33  	"gocloud.dev/blob"
    34  	"gocloud.dev/blob/driver"
    35  	"gocloud.dev/blob/drivertest"
    36  	"gocloud.dev/gcerrors"
    37  	"gocloud.dev/gcp"
    38  	"gocloud.dev/internal/testing/setup"
    39  	"google.golang.org/api/googleapi"
    40  )
    41  
    42  const (
    43  	// These constants capture values that were used during the last -record.
    44  	//
    45  	// If you want to use --record mode,
    46  	// 1. Create a bucket in your GCP project:
    47  	//    https://console.cloud.google.com/storage/browser, then "Create Bucket".
    48  	// 2. Update the bucketName constant to your bucket name.
    49  	// 3. Create a service account in your GCP project and update the
    50  	//    serviceAccountID constant to it.
    51  	// 4. Download a private key to a .pem file as described here:
    52  	//    https://godoc.org/cloud.google.com/go/storage#SignedURLOptions
    53  	//    and pass a path to it via the --privatekey flag.
    54  	// TODO(issue #300): Use Terraform to provision a bucket, and get the bucket
    55  	//    name from the Terraform output instead (saving a copy of it for replay).
    56  	bucketName       = "go-cloud-blob-test-bucket"
    57  	serviceAccountID = "storage-updater@go-cloud-test-216917.iam.gserviceaccount.com"
    58  )
    59  
    60  var pathToPrivateKey = flag.String("privatekey", "", "path to .pem file containing private key (required for --record); defaults to ~/Downloads/gcs-private-key.pem")
    61  
    62  type harness struct {
    63  	client *gcp.HTTPClient
    64  	opts   *Options
    65  	rt     http.RoundTripper
    66  	closer func()
    67  }
    68  
    69  func newHarness(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
    70  	opts := &Options{GoogleAccessID: serviceAccountID}
    71  	if *setup.Record {
    72  		if *pathToPrivateKey == "" {
    73  			usr, _ := user.Current()
    74  			*pathToPrivateKey = filepath.Join(usr.HomeDir, "Downloads", "gcs-private-key.pem")
    75  		}
    76  		// Use a real private key for signing URLs during -record.
    77  		pk, err := ioutil.ReadFile(*pathToPrivateKey)
    78  		if err != nil {
    79  			t.Fatalf("Couldn't find private key at %v: %v", *pathToPrivateKey, err)
    80  		}
    81  		opts.PrivateKey = pk
    82  	} else {
    83  		// Use a dummy signer in replay mode.
    84  		opts.SignBytes = func(b []byte) ([]byte, error) { return []byte("signed!"), nil }
    85  	}
    86  	client, rt, done := setup.NewGCPClient(ctx, t)
    87  	return &harness{client: client, opts: opts, rt: rt, closer: done}, nil
    88  }
    89  
    90  func (h *harness) HTTPClient() *http.Client {
    91  	return &http.Client{Transport: h.rt}
    92  }
    93  
    94  func (h *harness) MakeDriver(ctx context.Context) (driver.Bucket, error) {
    95  	return openBucket(ctx, h.client, bucketName, h.opts)
    96  }
    97  
    98  func (h *harness) MakeDriverForNonexistentBucket(ctx context.Context) (driver.Bucket, error) {
    99  	return openBucket(ctx, h.client, "bucket-does-not-exist", h.opts)
   100  }
   101  
   102  func (h *harness) Close() {
   103  	h.closer()
   104  }
   105  
   106  func TestConformance(t *testing.T) {
   107  	drivertest.RunConformanceTests(t, newHarness, []drivertest.AsTest{verifyContentLanguage{}})
   108  }
   109  
   110  func BenchmarkGcsblob(b *testing.B) {
   111  	ctx := context.Background()
   112  	creds, err := gcp.DefaultCredentials(ctx)
   113  	if err != nil {
   114  		b.Fatal(err)
   115  	}
   116  	client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), gcp.CredentialsTokenSource(creds))
   117  	if err != nil {
   118  		b.Fatal(err)
   119  	}
   120  	bkt, err := OpenBucket(context.Background(), client, bucketName, nil)
   121  	if err != nil {
   122  		b.Fatal(err)
   123  	}
   124  	drivertest.RunBenchmarks(b, bkt)
   125  }
   126  
   127  const language = "nl"
   128  
   129  // verifyContentLanguage uses As to access the underlying GCS types and
   130  // read/write the ContentLanguage field.
   131  type verifyContentLanguage struct{}
   132  
   133  func (verifyContentLanguage) Name() string {
   134  	return "verify ContentLanguage can be written and read through As"
   135  }
   136  
   137  func (verifyContentLanguage) BucketCheck(b *blob.Bucket) error {
   138  	var client *storage.Client
   139  	if !b.As(&client) {
   140  		return errors.New("Bucket.As failed")
   141  	}
   142  	return nil
   143  }
   144  
   145  func (verifyContentLanguage) ErrorCheck(b *blob.Bucket, err error) error {
   146  	// Can't really verify this one because the storage library returns
   147  	// a sentinel error, storage.ErrObjectNotExist, for "not exists"
   148  	// instead of the supported As type googleapi.Error.
   149  	// Call ErrorAs anyway, and expect it to fail.
   150  	var to *googleapi.Error
   151  	if b.ErrorAs(err, &to) {
   152  		return errors.New("expected ErrorAs to fail")
   153  	}
   154  	return nil
   155  }
   156  
   157  func (verifyContentLanguage) BeforeRead(as func(interface{}) bool) error {
   158  	var objp **storage.ObjectHandle
   159  	if !as(&objp) {
   160  		return errors.New("BeforeRead.As failed to get ObjectHandle")
   161  	}
   162  	var sr *storage.Reader
   163  	if !as(&sr) {
   164  		return errors.New("BeforeRead.As failed to get Reader")
   165  	}
   166  	return nil
   167  }
   168  
   169  func (verifyContentLanguage) BeforeWrite(as func(interface{}) bool) error {
   170  	var objp **storage.ObjectHandle
   171  	if !as(&objp) {
   172  		return errors.New("Writer.As failed to get ObjectHandle")
   173  	}
   174  	var sw *storage.Writer
   175  	if !as(&sw) {
   176  		return errors.New("Writer.As failed to get Writer")
   177  	}
   178  	sw.ContentLanguage = language
   179  	return nil
   180  }
   181  
   182  func (verifyContentLanguage) BeforeCopy(as func(interface{}) bool) error {
   183  	var coh *CopyObjectHandles
   184  	if !as(&coh) {
   185  		return errors.New("BeforeCopy.As failed to get CopyObjectHandles")
   186  	}
   187  	var copier *storage.Copier
   188  	if !as(&copier) {
   189  		return errors.New("BeforeCopy.As failed")
   190  	}
   191  	return nil
   192  }
   193  
   194  func (verifyContentLanguage) BeforeList(as func(interface{}) bool) error {
   195  	var q *storage.Query
   196  	if !as(&q) {
   197  		return errors.New("List.As failed")
   198  	}
   199  	// Nothing to do.
   200  	return nil
   201  }
   202  
   203  func (verifyContentLanguage) BeforeSign(as func(interface{}) bool) error {
   204  	var opts *storage.SignedURLOptions
   205  	if !as(&opts) {
   206  		return errors.New("BeforeSign.As failed")
   207  	}
   208  	// Nothing to do.
   209  	return nil
   210  }
   211  
   212  func (verifyContentLanguage) AttributesCheck(attrs *blob.Attributes) error {
   213  	var oa storage.ObjectAttrs
   214  	if !attrs.As(&oa) {
   215  		return errors.New("Attributes.As returned false")
   216  	}
   217  	if got := oa.ContentLanguage; got != language {
   218  		return fmt.Errorf("got %q want %q", got, language)
   219  	}
   220  	return nil
   221  }
   222  
   223  func (verifyContentLanguage) ReaderCheck(r *blob.Reader) error {
   224  	var rr *storage.Reader
   225  	if !r.As(&rr) {
   226  		return errors.New("Reader.As returned false")
   227  	}
   228  	// GCS doesn't return Content-Language via storage.Reader.
   229  	return nil
   230  }
   231  
   232  func (verifyContentLanguage) ListObjectCheck(o *blob.ListObject) error {
   233  	var oa storage.ObjectAttrs
   234  	if !o.As(&oa) {
   235  		return errors.New("ListObject.As returned false")
   236  	}
   237  	if o.IsDir {
   238  		return nil
   239  	}
   240  	if got := oa.ContentLanguage; got != language {
   241  		return fmt.Errorf("got %q want %q", got, language)
   242  	}
   243  	return nil
   244  }
   245  
   246  // GCS-specific unit tests.
   247  func TestBufferSize(t *testing.T) {
   248  	tests := []struct {
   249  		size int
   250  		want int
   251  	}{
   252  		{
   253  			size: 5 * 1024 * 1024,
   254  			want: 5 * 1024 * 1024,
   255  		},
   256  		{
   257  			size: 0,
   258  			want: googleapi.DefaultUploadChunkSize,
   259  		},
   260  		{
   261  			size: -1024,
   262  			want: 0,
   263  		},
   264  	}
   265  	for i, test := range tests {
   266  		got := bufferSize(test.size)
   267  		if got != test.want {
   268  			t.Errorf("%d) got buffer size %d, want %d", i, got, test.want)
   269  		}
   270  	}
   271  }
   272  
   273  func TestOpenBucket(t *testing.T) {
   274  	tests := []struct {
   275  		description string
   276  		bucketName  string
   277  		nilClient   bool
   278  		want        string
   279  		wantErr     bool
   280  	}{
   281  		{
   282  			description: "empty bucket name results in error",
   283  			wantErr:     true,
   284  		},
   285  		{
   286  			description: "nil client results in error",
   287  			bucketName:  "foo",
   288  			nilClient:   true,
   289  			wantErr:     true,
   290  		},
   291  		{
   292  			description: "success",
   293  			bucketName:  "foo",
   294  			want:        "foo",
   295  		},
   296  	}
   297  
   298  	ctx := context.Background()
   299  	for _, test := range tests {
   300  		t.Run(test.description, func(t *testing.T) {
   301  			var client *gcp.HTTPClient
   302  			if !test.nilClient {
   303  				var done func()
   304  				client, _, done = setup.NewGCPClient(ctx, t)
   305  				defer done()
   306  			}
   307  
   308  			// Create driver impl.
   309  			drv, err := openBucket(ctx, client, test.bucketName, nil)
   310  			if (err != nil) != test.wantErr {
   311  				t.Errorf("got err %v want error %v", err, test.wantErr)
   312  			}
   313  			if err == nil && drv != nil && drv.name != test.want {
   314  				t.Errorf("got %q want %q", drv.name, test.want)
   315  			}
   316  
   317  			// Create portable type.
   318  			b, err := OpenBucket(ctx, client, test.bucketName, nil)
   319  			if b != nil {
   320  				defer b.Close()
   321  			}
   322  			if (err != nil) != test.wantErr {
   323  				t.Errorf("got err %v want error %v", err, test.wantErr)
   324  			}
   325  		})
   326  	}
   327  }
   328  
   329  // TestBeforeReadNonExistentKey tests using BeforeRead on a nonexistent key.
   330  func TestBeforeReadNonExistentKey(t *testing.T) {
   331  	ctx := context.Background()
   332  	h, err := newHarness(ctx, t)
   333  	if err != nil {
   334  		t.Fatal(err)
   335  	}
   336  	defer h.Close()
   337  
   338  	drv, err := h.MakeDriver(ctx)
   339  	if err != nil {
   340  		t.Fatal(err)
   341  	}
   342  	bucket := blob.NewBucket(drv)
   343  	defer bucket.Close()
   344  
   345  	// Try reading a nonexistent key.
   346  	_, err = bucket.NewReader(ctx, "nonexistent-key", &blob.ReaderOptions{
   347  		BeforeRead: func(asFunc func(interface{}) bool) error {
   348  			var objp **storage.ObjectHandle
   349  			if !asFunc(&objp) {
   350  				return errors.New("Reader.As failed to get ObjectHandle")
   351  			}
   352  			var rp *storage.Reader
   353  			if asFunc(&rp) {
   354  				return errors.New("Reader.As unexpectedly got storage.Reader")
   355  			}
   356  			return nil
   357  		},
   358  	})
   359  	if err == nil || gcerrors.Code(err) != gcerrors.NotFound {
   360  		t.Errorf("got error %v, wanted NotFound for Read", err)
   361  	}
   362  }
   363  
   364  // TestPreconditions tests setting of ObjectHandle preconditions via As.
   365  func TestPreconditions(t *testing.T) {
   366  	const (
   367  		key     = "precondition-key"
   368  		key2    = "precondition-key2"
   369  		content = "hello world"
   370  	)
   371  
   372  	ctx := context.Background()
   373  	h, err := newHarness(ctx, t)
   374  	if err != nil {
   375  		t.Fatal(err)
   376  	}
   377  	defer h.Close()
   378  
   379  	drv, err := h.MakeDriver(ctx)
   380  	if err != nil {
   381  		t.Fatal(err)
   382  	}
   383  	bucket := blob.NewBucket(drv)
   384  	defer bucket.Close()
   385  
   386  	// Try writing with a failing precondition.
   387  	if err := bucket.WriteAll(ctx, key, []byte(content), &blob.WriterOptions{
   388  		BeforeWrite: func(asFunc func(interface{}) bool) error {
   389  			var objp **storage.ObjectHandle
   390  			if !asFunc(&objp) {
   391  				return errors.New("Writer.As failed to get ObjectHandle")
   392  			}
   393  			// Replace the ObjectHandle with a new one that adds Conditions.
   394  			*objp = (*objp).If(storage.Conditions{GenerationMatch: -999})
   395  			return nil
   396  		},
   397  	}); err == nil || gcerrors.Code(err) != gcerrors.FailedPrecondition {
   398  		t.Errorf("got error %v, wanted FailedPrecondition for Write", err)
   399  	}
   400  
   401  	// Repeat with a precondition that will pass.
   402  	if err := bucket.WriteAll(ctx, key, []byte(content), &blob.WriterOptions{
   403  		BeforeWrite: func(asFunc func(interface{}) bool) error {
   404  			var objp **storage.ObjectHandle
   405  			if !asFunc(&objp) {
   406  				return errors.New("Writer.As failed to get ObjectHandle")
   407  			}
   408  			// Replace the ObjectHandle with a new one that adds Conditions.
   409  			*objp = (*objp).If(storage.Conditions{DoesNotExist: true})
   410  			return nil
   411  		},
   412  	}); err != nil {
   413  		t.Errorf("got error %v, wanted nil", err)
   414  	}
   415  	defer bucket.Delete(ctx, key)
   416  
   417  	// Try reading with a failing precondition.
   418  	_, err = bucket.NewReader(ctx, key, &blob.ReaderOptions{
   419  		BeforeRead: func(asFunc func(interface{}) bool) error {
   420  			var objp **storage.ObjectHandle
   421  			if !asFunc(&objp) {
   422  				return errors.New("Reader.As failed to get ObjectHandle")
   423  			}
   424  			// Replace the ObjectHandle with a new one.
   425  			*objp = (*objp).Generation(999999)
   426  			return nil
   427  		},
   428  	})
   429  	if err == nil || gcerrors.Code(err) != gcerrors.NotFound {
   430  		t.Errorf("got error %v, wanted NotFound for Read", err)
   431  	}
   432  
   433  	attrs, err := bucket.Attributes(ctx, key)
   434  	if err != nil {
   435  		t.Fatal(err)
   436  	}
   437  	var oa storage.ObjectAttrs
   438  	if !attrs.As(&oa) {
   439  		t.Fatal("Attributes.As failed")
   440  	}
   441  	generation := oa.Generation
   442  
   443  	// Repeat with a precondition that will pass.
   444  	reader, err := bucket.NewReader(ctx, key, &blob.ReaderOptions{
   445  		BeforeRead: func(asFunc func(interface{}) bool) error {
   446  			var objp **storage.ObjectHandle
   447  			if !asFunc(&objp) {
   448  				return errors.New("Reader.As failed to get ObjectHandle")
   449  			}
   450  			// Replace the ObjectHandle with a new one.
   451  			*objp = (*objp).Generation(generation)
   452  			return nil
   453  		},
   454  	})
   455  	if err != nil {
   456  		t.Fatal(err)
   457  	}
   458  	defer reader.Close()
   459  	gotBytes, err := ioutil.ReadAll(reader)
   460  	if err != nil {
   461  		t.Fatal(err)
   462  	}
   463  	if got := string(gotBytes); got != content {
   464  		t.Errorf("got %q want %q", got, content)
   465  	}
   466  
   467  	// Try copying with a failing precondition on Dst.
   468  	err = bucket.Copy(ctx, key2, key, &blob.CopyOptions{
   469  		BeforeCopy: func(asFunc func(interface{}) bool) error {
   470  			var coh *CopyObjectHandles
   471  			if !asFunc(&coh) {
   472  				return errors.New("Copy.As failed to get CopyObjectHandles")
   473  			}
   474  			// Replace the dst ObjectHandle with a new one.
   475  			coh.Dst = coh.Dst.If(storage.Conditions{GenerationMatch: -999})
   476  			return nil
   477  		},
   478  	})
   479  	if err == nil || gcerrors.Code(err) != gcerrors.FailedPrecondition {
   480  		t.Errorf("got error %v, wanted FailedPrecondition for Copy", err)
   481  	}
   482  
   483  	// Try copying with a failing precondition on Src.
   484  	err = bucket.Copy(ctx, key2, key, &blob.CopyOptions{
   485  		BeforeCopy: func(asFunc func(interface{}) bool) error {
   486  			var coh *CopyObjectHandles
   487  			if !asFunc(&coh) {
   488  				return errors.New("Copy.As failed to get CopyObjectHandles")
   489  			}
   490  			// Replace the src ObjectHandle with a new one.
   491  			coh.Src = coh.Src.Generation(9999999)
   492  			return nil
   493  		},
   494  	})
   495  	if err == nil || gcerrors.Code(err) != gcerrors.NotFound {
   496  		t.Errorf("got error %v, wanted NotFound for Copy", err)
   497  	}
   498  
   499  	// Repeat with preconditions on Dst and Src that will succeed.
   500  	err = bucket.Copy(ctx, key2, key, &blob.CopyOptions{
   501  		BeforeCopy: func(asFunc func(interface{}) bool) error {
   502  			var coh *CopyObjectHandles
   503  			if !asFunc(&coh) {
   504  				return errors.New("Reader.As failed to get CopyObjectHandles")
   505  			}
   506  			coh.Dst = coh.Dst.If(storage.Conditions{DoesNotExist: true})
   507  			coh.Src = coh.Src.Generation(generation)
   508  			return nil
   509  		},
   510  	})
   511  	if err != nil {
   512  		t.Error(err)
   513  	}
   514  	defer bucket.Delete(ctx, key2)
   515  }
   516  
   517  func TestURLOpenerForParams(t *testing.T) {
   518  	ctx := context.Background()
   519  
   520  	// Create a file for use as a dummy private key file.
   521  	privateKey := []byte("some content")
   522  	pkFile, err := ioutil.TempFile("", "my-private-key")
   523  	if err != nil {
   524  		t.Fatal(err)
   525  	}
   526  	defer os.Remove(pkFile.Name())
   527  	if _, err := pkFile.Write(privateKey); err != nil {
   528  		t.Fatal(err)
   529  	}
   530  	if err := pkFile.Close(); err != nil {
   531  		t.Fatal(err)
   532  	}
   533  
   534  	tests := []struct {
   535  		name     string
   536  		currOpts Options
   537  		query    url.Values
   538  		wantOpts Options
   539  		wantErr  bool
   540  	}{
   541  		{
   542  			name: "InvalidParam",
   543  			query: url.Values{
   544  				"foo": {"bar"},
   545  			},
   546  			wantErr: true,
   547  		},
   548  		{
   549  			name: "AccessID",
   550  			query: url.Values{
   551  				"access_id": {"bar"},
   552  			},
   553  			wantOpts: Options{GoogleAccessID: "bar"},
   554  		},
   555  		{
   556  			name:     "AccessID override",
   557  			currOpts: Options{GoogleAccessID: "foo"},
   558  			query: url.Values{
   559  				"access_id": {"bar"},
   560  			},
   561  			wantOpts: Options{GoogleAccessID: "bar"},
   562  		},
   563  		{
   564  			name:     "AccessID not overridden",
   565  			currOpts: Options{GoogleAccessID: "bar"},
   566  			wantOpts: Options{GoogleAccessID: "bar"},
   567  		},
   568  		{
   569  			name: "BadPrivateKeyPath",
   570  			query: url.Values{
   571  				"private_key_path": {"/path/does/not/exist"},
   572  			},
   573  			wantErr: true,
   574  		},
   575  		{
   576  			name: "PrivateKeyPath",
   577  			query: url.Values{
   578  				"private_key_path": {pkFile.Name()},
   579  			},
   580  			wantOpts: Options{PrivateKey: privateKey},
   581  		},
   582  		{
   583  			name:     "PrivateKey cleared",
   584  			currOpts: Options{PrivateKey: privateKey},
   585  			query: url.Values{
   586  				"private_key_path": {""},
   587  			},
   588  			wantOpts: Options{},
   589  		},
   590  		{
   591  			name: "AccessID change clears PrivateKey and MakeSignBytes",
   592  			currOpts: Options{
   593  				GoogleAccessID: "foo",
   594  				PrivateKey:     privateKey,
   595  				MakeSignBytes: func(context.Context) SignBytesFunc {
   596  					return func([]byte) ([]byte, error) {
   597  						return nil, context.DeadlineExceeded
   598  					}
   599  				},
   600  			},
   601  			query: url.Values{
   602  				"access_id": {"bar"},
   603  			},
   604  			wantOpts: Options{GoogleAccessID: "bar"},
   605  		},
   606  	}
   607  
   608  	for _, test := range tests {
   609  		t.Run(test.name, func(t *testing.T) {
   610  			o := &URLOpener{Options: test.currOpts}
   611  			got, err := o.forParams(ctx, test.query)
   612  			if (err != nil) != test.wantErr {
   613  				t.Errorf("got err %v want error %v", err, test.wantErr)
   614  			}
   615  			if err != nil {
   616  				return
   617  			}
   618  			if diff := cmp.Diff(got, &test.wantOpts); diff != "" {
   619  				t.Errorf("opener.forParams(...) diff (-want +got):\n%s", diff)
   620  			}
   621  		})
   622  	}
   623  }
   624  
   625  func TestOpenBucketFromURL(t *testing.T) {
   626  	cleanup := setup.FakeGCPDefaultCredentials(t)
   627  	defer cleanup()
   628  
   629  	pkFile, err := ioutil.TempFile("", "my-private-key")
   630  	if err != nil {
   631  		t.Fatal(err)
   632  	}
   633  	defer os.Remove(pkFile.Name())
   634  	if err := ioutil.WriteFile(pkFile.Name(), []byte("key"), 0666); err != nil {
   635  		t.Fatal(err)
   636  	}
   637  
   638  	tests := []struct {
   639  		URL     string
   640  		WantErr bool
   641  	}{
   642  		// OK.
   643  		{"gs://mybucket", false},
   644  		// OK, setting access_id.
   645  		{"gs://mybucket?access_id=foo", false},
   646  		// OK, setting private_key_path.
   647  		{"gs://mybucket?private_key_path=" + pkFile.Name(), false},
   648  		// OK, clearing any pre-existing private key.
   649  		{"gs://mybucket?private_key_path=", false},
   650  		// Invalid private_key_path.
   651  		{"gs://mybucket?private_key_path=invalid-path", true},
   652  		// Invalid parameter.
   653  		{"gs://mybucket?param=value", true},
   654  	}
   655  
   656  	ctx := context.Background()
   657  	for _, test := range tests {
   658  		b, err := blob.OpenBucket(ctx, test.URL)
   659  		if b != nil {
   660  			defer b.Close()
   661  		}
   662  		if (err != nil) != test.WantErr {
   663  			t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr)
   664  		}
   665  	}
   666  }
   667  
   668  func TestReadDefaultCredentials(t *testing.T) {
   669  	tests := []struct {
   670  		givenJSON      string
   671  		WantAccessID   string
   672  		WantPrivateKey []byte
   673  	}{
   674  		// Variant A: service account file
   675  		{`{
   676  			"type": "service_account",
   677  			"project_id": "project-id",
   678  			"private_key_id": "key-id",
   679  			"private_key": "-----BEGIN PRIVATE KEY-----\nprivate-key\n-----END PRIVATE KEY-----\n",
   680  			"client_email": "service-account-email",
   681  			"client_id": "client-id",
   682  			"auth_uri": "https://accounts.google.com/o/oauth2/auth",
   683  			"token_uri": "https://accounts.google.com/o/oauth2/token",
   684  			"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
   685  			"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service-account-email"
   686  		  }`,
   687  			"service-account-email",
   688  			[]byte("-----BEGIN PRIVATE KEY-----\nprivate-key\n-----END PRIVATE KEY-----\n"),
   689  		},
   690  		// Variant A: credentials file absent a private key (stripped)
   691  		{`{
   692  			"google": {},
   693  			"client_email": "service-account-email",
   694  			"client_id": "client-id"
   695  		  }`,
   696  			"service-account-email",
   697  			[]byte(""),
   698  		},
   699  		// Variant B: obtained through the REST API
   700  		{`{
   701  			"name": "projects/project-id/serviceAccounts/service-account-email/keys/key-id",
   702  			"privateKeyType": "TYPE_GOOGLE_CREDENTIALS_FILE",
   703  			"privateKeyData": "private-key",
   704  			"validAfterTime": "date",
   705  			"validBeforeTime": "date",
   706  			"keyAlgorithm": "KEY_ALG_RSA_2048"
   707  		  }`,
   708  			"service-account-email",
   709  			[]byte("private-key"),
   710  		},
   711  		// An empty input shall not throw an exception
   712  		{"", "", nil},
   713  	}
   714  
   715  	for i, test := range tests {
   716  		inJSON := []byte(test.givenJSON)
   717  		if len(test.givenJSON) == 0 {
   718  			inJSON = nil
   719  		}
   720  
   721  		gotAccessID, gotPrivateKey := readDefaultCredentials(inJSON)
   722  		if gotAccessID != test.WantAccessID || string(gotPrivateKey) != string(test.WantPrivateKey) {
   723  			t.Errorf("Mismatched field values in case %d:\n -- got:  %v, %v\n -- want: %v, %v", i,
   724  				gotAccessID, gotPrivateKey,
   725  				test.WantAccessID, test.WantPrivateKey,
   726  			)
   727  		}
   728  	}
   729  }
   730  
   731  func TestRemainingSignedURLSchemes(t *testing.T) {
   732  	tests := []struct {
   733  		name          string
   734  		currOpts      Options
   735  		wantSignedURL string // Not the actual URL, which is subject to change, but a mimickry.
   736  		wantErr       bool
   737  	}{
   738  		{
   739  			name:    "no scheme available, error",
   740  			wantErr: true,
   741  		},
   742  		{
   743  			name: "too many schemes configured",
   744  			currOpts: Options{
   745  				GoogleAccessID: "foo",
   746  				PrivateKey:     []byte("private-key"),
   747  				SignBytes: func([]byte) ([]byte, error) {
   748  					return []byte("signed"), nil
   749  				},
   750  			},
   751  			wantErr: true,
   752  		},
   753  		{
   754  			name: "SignBytes",
   755  			currOpts: Options{
   756  				GoogleAccessID: "foo",
   757  				SignBytes: func([]byte) ([]byte, error) {
   758  					return []byte("signed"), nil
   759  				},
   760  			},
   761  			wantSignedURL: "https://host/go-cloud-blob-test-bucket/some-key?GoogleAccessId=foo&Signature=c2lnbmVk",
   762  		},
   763  		{
   764  			name: "MakeSignBytes is being used",
   765  			currOpts: Options{
   766  				GoogleAccessID: "foo",
   767  				MakeSignBytes: func(context.Context) SignBytesFunc {
   768  					return func([]byte) ([]byte, error) {
   769  						return []byte("signed"), nil
   770  					}
   771  				},
   772  			},
   773  			wantSignedURL: "https://host/go-cloud-blob-test-bucket/some-key?GoogleAccessId=foo&Signature=c2lnbmVk",
   774  		},
   775  	}
   776  
   777  	ctx := context.Background()
   778  	signOpts := &driver.SignedURLOptions{
   779  		Expiry: 30 * time.Second,
   780  		Method: http.MethodGet,
   781  	}
   782  
   783  	for _, test := range tests {
   784  		t.Run(test.name, func(t *testing.T) {
   785  			bucket := bucket{name: bucketName, opts: &test.currOpts}
   786  
   787  			// SignedURL doesn't check whether a key exists.
   788  			gotURL, gotErr := bucket.SignedURL(ctx, "some-key", signOpts)
   789  			if (gotErr != nil) != test.wantErr {
   790  				t.Errorf("Got unexpected error %v", gotErr)
   791  			}
   792  			if test.wantSignedURL == "" {
   793  				return
   794  			}
   795  
   796  			got, _ := url.Parse(gotURL)
   797  			want, _ := url.Parse(test.wantSignedURL)
   798  			gotParams, wantParams := got.Query(), want.Query()
   799  			for _, param := range []string{"GoogleAccessId", "Signature"} {
   800  				if gotParams.Get(param) != wantParams.Get(param) {
   801  					// Print the full URL because the parameter might've not been set at all.
   802  					t.Errorf("Query parameter in SignedURL differs: %v\n -- got URL:  %v\n -- want URL: %v",
   803  						param, got, want)
   804  				}
   805  			}
   806  		})
   807  	}
   808  }