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
15package gcsblob
16
17import (
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
42const (
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
60var pathToPrivateKey = flag.String("privatekey", "", "path to .pem file containing private key (required for --record); defaults to ~/Downloads/gcs-private-key.pem")
61
62type harness struct {
63	client *gcp.HTTPClient
64	opts   *Options
65	rt     http.RoundTripper
66	closer func()
67}
68
69func 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
90func (h *harness) HTTPClient() *http.Client {
91	return &http.Client{Transport: h.rt}
92}
93
94func (h *harness) MakeDriver(ctx context.Context) (driver.Bucket, error) {
95	return openBucket(ctx, h.client, bucketName, h.opts)
96}
97
98func (h *harness) MakeDriverForNonexistentBucket(ctx context.Context) (driver.Bucket, error) {
99	return openBucket(ctx, h.client, "bucket-does-not-exist", h.opts)
100}
101
102func (h *harness) Close() {
103	h.closer()
104}
105
106func TestConformance(t *testing.T) {
107	drivertest.RunConformanceTests(t, newHarness, []drivertest.AsTest{verifyContentLanguage{}})
108}
109
110func 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
127const language = "nl"
128
129// verifyContentLanguage uses As to access the underlying GCS types and
130// read/write the ContentLanguage field.
131type verifyContentLanguage struct{}
132
133func (verifyContentLanguage) Name() string {
134	return "verify ContentLanguage can be written and read through As"
135}
136
137func (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
145func (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
157func (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
169func (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
182func (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
194func (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
203func (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
212func (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
223func (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
232func (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.
247func 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
273func 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.
330func 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.
365func 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
517func 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
625func 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
668func 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
731func 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}
809