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