github.com/cornelk/go-cloud@v0.17.1/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 30 "cloud.google.com/go/storage" 31 "github.com/cornelk/go-cloud/blob" 32 "github.com/cornelk/go-cloud/blob/driver" 33 "github.com/cornelk/go-cloud/blob/drivertest" 34 "github.com/cornelk/go-cloud/gcerrors" 35 "github.com/cornelk/go-cloud/gcp" 36 "github.com/cornelk/go-cloud/internal/testing/setup" 37 "github.com/google/go-cmp/cmp" 38 "google.golang.org/api/googleapi" 39 ) 40 41 const ( 42 // These constants capture values that were used during the last -record. 43 // 44 // If you want to use --record mode, 45 // 1. Create a bucket in your GCP project: 46 // https://console.cloud.google.com/storage/browser, then "Create Bucket". 47 // 2. Update the bucketName constant to your bucket name. 48 // 3. Create a service account in your GCP project and update the 49 // serviceAccountID constant to it. 50 // 4. Download a private key to a .pem file as described here: 51 // https://godoc.org/cloud.google.com/go/storage#SignedURLOptions 52 // and pass a path to it via the --privatekey flag. 53 // TODO(issue #300): Use Terraform to provision a bucket, and get the bucket 54 // name from the Terraform output instead (saving a copy of it for replay). 55 bucketName = "go-cloud-blob-test-bucket" 56 serviceAccountID = "storage-updater@go-cloud-test-216917.iam.gserviceaccount.com" 57 ) 58 59 var pathToPrivateKey = flag.String("privatekey", "", "path to .pem file containing private key (required for --record); defaults to ~/Downloads/gcs-private-key.pem") 60 61 type harness struct { 62 client *gcp.HTTPClient 63 opts *Options 64 rt http.RoundTripper 65 closer func() 66 } 67 68 func newHarness(ctx context.Context, t *testing.T) (drivertest.Harness, error) { 69 opts := &Options{GoogleAccessID: serviceAccountID} 70 if *setup.Record { 71 if *pathToPrivateKey == "" { 72 usr, _ := user.Current() 73 *pathToPrivateKey = filepath.Join(usr.HomeDir, "Downloads", "gcs-private-key.pem") 74 } 75 // Use a real private key for signing URLs during -record. 76 pk, err := ioutil.ReadFile(*pathToPrivateKey) 77 if err != nil { 78 t.Fatalf("Couldn't find private key at %v: %v", *pathToPrivateKey, err) 79 } 80 opts.PrivateKey = pk 81 } else { 82 // Use a dummy signer in replay mode. 83 opts.SignBytes = func(b []byte) ([]byte, error) { return []byte("signed!"), nil } 84 } 85 client, rt, done := setup.NewGCPClient(ctx, t) 86 return &harness{client: client, opts: opts, rt: rt, closer: done}, nil 87 } 88 89 func (h *harness) HTTPClient() *http.Client { 90 return &http.Client{Transport: h.rt} 91 } 92 93 func (h *harness) MakeDriver(ctx context.Context) (driver.Bucket, error) { 94 return openBucket(ctx, h.client, bucketName, h.opts) 95 } 96 97 func (h *harness) Close() { 98 h.closer() 99 } 100 101 func TestConformance(t *testing.T) { 102 drivertest.RunConformanceTests(t, newHarness, []drivertest.AsTest{verifyContentLanguage{}}) 103 } 104 105 func BenchmarkGcsblob(b *testing.B) { 106 ctx := context.Background() 107 creds, err := gcp.DefaultCredentials(ctx) 108 if err != nil { 109 b.Fatal(err) 110 } 111 client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), gcp.CredentialsTokenSource(creds)) 112 if err != nil { 113 b.Fatal(err) 114 } 115 bkt, err := OpenBucket(context.Background(), client, bucketName, nil) 116 if err != nil { 117 b.Fatal(err) 118 } 119 drivertest.RunBenchmarks(b, bkt) 120 } 121 122 const language = "nl" 123 124 // verifyContentLanguage uses As to access the underlying GCS types and 125 // read/write the ContentLanguage field. 126 type verifyContentLanguage struct{} 127 128 func (verifyContentLanguage) Name() string { 129 return "verify ContentLanguage can be written and read through As" 130 } 131 132 func (verifyContentLanguage) BucketCheck(b *blob.Bucket) error { 133 var client *storage.Client 134 if !b.As(&client) { 135 return errors.New("Bucket.As failed") 136 } 137 return nil 138 } 139 140 func (verifyContentLanguage) ErrorCheck(b *blob.Bucket, err error) error { 141 // Can't really verify this one because the storage library returns 142 // a sentinel error, storage.ErrObjectNotExist, for "not exists" 143 // instead of the supported As type googleapi.Error. 144 // Call ErrorAs anyway, and expect it to fail. 145 var to *googleapi.Error 146 if b.ErrorAs(err, &to) { 147 return errors.New("expected ErrorAs to fail") 148 } 149 return nil 150 } 151 152 func (verifyContentLanguage) BeforeRead(as func(interface{}) bool) error { 153 var objp **storage.ObjectHandle 154 if !as(&objp) { 155 return errors.New("BeforeRead.As failed to get ObjectHandle") 156 } 157 var sr *storage.Reader 158 if !as(&sr) { 159 return errors.New("BeforeRead.As failed to get Reader") 160 } 161 return nil 162 } 163 164 func (verifyContentLanguage) BeforeWrite(as func(interface{}) bool) error { 165 var objp **storage.ObjectHandle 166 if !as(&objp) { 167 return errors.New("Writer.As failed to get ObjectHandle") 168 } 169 var sw *storage.Writer 170 if !as(&sw) { 171 return errors.New("Writer.As failed to get Writer") 172 } 173 sw.ContentLanguage = language 174 return nil 175 } 176 177 func (verifyContentLanguage) BeforeCopy(as func(interface{}) bool) error { 178 var coh *CopyObjectHandles 179 if !as(&coh) { 180 return errors.New("BeforeCopy.As failed to get CopyObjectHandles") 181 } 182 var copier *storage.Copier 183 if !as(&copier) { 184 return errors.New("BeforeCopy.As failed") 185 } 186 return nil 187 } 188 189 func (verifyContentLanguage) BeforeList(as func(interface{}) bool) error { 190 var q *storage.Query 191 if !as(&q) { 192 return errors.New("List.As failed") 193 } 194 // Nothing to do. 195 return nil 196 } 197 198 func (verifyContentLanguage) AttributesCheck(attrs *blob.Attributes) error { 199 var oa storage.ObjectAttrs 200 if !attrs.As(&oa) { 201 return errors.New("Attributes.As returned false") 202 } 203 if got := oa.ContentLanguage; got != language { 204 return fmt.Errorf("got %q want %q", got, language) 205 } 206 return nil 207 } 208 209 func (verifyContentLanguage) ReaderCheck(r *blob.Reader) error { 210 var rr *storage.Reader 211 if !r.As(&rr) { 212 return errors.New("Reader.As returned false") 213 } 214 // GCS doesn't return Content-Language via storage.Reader. 215 return nil 216 } 217 218 func (verifyContentLanguage) ListObjectCheck(o *blob.ListObject) error { 219 var oa storage.ObjectAttrs 220 if !o.As(&oa) { 221 return errors.New("ListObject.As returned false") 222 } 223 if o.IsDir { 224 return nil 225 } 226 if got := oa.ContentLanguage; got != language { 227 return fmt.Errorf("got %q want %q", got, language) 228 } 229 return nil 230 } 231 232 // GCS-specific unit tests. 233 func TestBufferSize(t *testing.T) { 234 tests := []struct { 235 size int 236 want int 237 }{ 238 { 239 size: 5 * 1024 * 1024, 240 want: 5 * 1024 * 1024, 241 }, 242 { 243 size: 0, 244 want: googleapi.DefaultUploadChunkSize, 245 }, 246 { 247 size: -1024, 248 want: 0, 249 }, 250 } 251 for i, test := range tests { 252 got := bufferSize(test.size) 253 if got != test.want { 254 t.Errorf("%d) got buffer size %d, want %d", i, got, test.want) 255 } 256 } 257 } 258 259 func TestOpenBucket(t *testing.T) { 260 tests := []struct { 261 description string 262 bucketName string 263 nilClient bool 264 want string 265 wantErr bool 266 }{ 267 { 268 description: "empty bucket name results in error", 269 wantErr: true, 270 }, 271 { 272 description: "nil client results in error", 273 bucketName: "foo", 274 nilClient: true, 275 wantErr: true, 276 }, 277 { 278 description: "success", 279 bucketName: "foo", 280 want: "foo", 281 }, 282 } 283 284 ctx := context.Background() 285 for _, test := range tests { 286 t.Run(test.description, func(t *testing.T) { 287 var client *gcp.HTTPClient 288 if !test.nilClient { 289 var done func() 290 client, _, done = setup.NewGCPClient(ctx, t) 291 defer done() 292 } 293 294 // Create driver impl. 295 drv, err := openBucket(ctx, client, test.bucketName, nil) 296 if (err != nil) != test.wantErr { 297 t.Errorf("got err %v want error %v", err, test.wantErr) 298 } 299 if err == nil && drv != nil && drv.name != test.want { 300 t.Errorf("got %q want %q", drv.name, test.want) 301 } 302 303 // Create portable type. 304 b, err := OpenBucket(ctx, client, test.bucketName, nil) 305 if b != nil { 306 defer b.Close() 307 } 308 if (err != nil) != test.wantErr { 309 t.Errorf("got err %v want error %v", err, test.wantErr) 310 } 311 }) 312 } 313 } 314 315 // TestPreconditions tests setting of ObjectHandle preconditions via As. 316 func TestPreconditions(t *testing.T) { 317 const ( 318 key = "precondition-key" 319 key2 = "precondition-key2" 320 content = "hello world" 321 ) 322 323 ctx := context.Background() 324 h, err := newHarness(ctx, t) 325 if err != nil { 326 t.Fatal(err) 327 } 328 defer h.Close() 329 330 drv, err := h.MakeDriver(ctx) 331 if err != nil { 332 t.Fatal(err) 333 } 334 bucket := blob.NewBucket(drv) 335 defer bucket.Close() 336 337 // Try writing with a failing precondition. 338 if err := bucket.WriteAll(ctx, key, []byte(content), &blob.WriterOptions{ 339 BeforeWrite: func(asFunc func(interface{}) bool) error { 340 var objp **storage.ObjectHandle 341 if !asFunc(&objp) { 342 return errors.New("Writer.As failed to get ObjectHandle") 343 } 344 // Replace the ObjectHandle with a new one that adds Conditions. 345 *objp = (*objp).If(storage.Conditions{GenerationMatch: -999}) 346 return nil 347 }, 348 }); err == nil || gcerrors.Code(err) != gcerrors.FailedPrecondition { 349 t.Errorf("got error %v, wanted FailedPrecondition for Write", err) 350 } 351 352 // Repeat with a precondition that will pass. 353 if err := bucket.WriteAll(ctx, key, []byte(content), &blob.WriterOptions{ 354 BeforeWrite: func(asFunc func(interface{}) bool) error { 355 var objp **storage.ObjectHandle 356 if !asFunc(&objp) { 357 return errors.New("Writer.As failed to get ObjectHandle") 358 } 359 // Replace the ObjectHandle with a new one that adds Conditions. 360 *objp = (*objp).If(storage.Conditions{DoesNotExist: true}) 361 return nil 362 }, 363 }); err != nil { 364 t.Errorf("got error %v, wanted nil", err) 365 } 366 defer bucket.Delete(ctx, key) 367 368 // Try reading with a failing precondition. 369 _, err = bucket.NewReader(ctx, key, &blob.ReaderOptions{ 370 BeforeRead: func(asFunc func(interface{}) bool) error { 371 var objp **storage.ObjectHandle 372 if !asFunc(&objp) { 373 return errors.New("Reader.As failed to get ObjectHandle") 374 } 375 // Replace the ObjectHandle with a new one. 376 *objp = (*objp).Generation(999999) 377 return nil 378 }, 379 }) 380 if err == nil || gcerrors.Code(err) != gcerrors.NotFound { 381 t.Errorf("got error %v, wanted NotFound for Read", err) 382 } 383 384 attrs, err := bucket.Attributes(ctx, key) 385 if err != nil { 386 t.Fatal(err) 387 } 388 var oa storage.ObjectAttrs 389 if !attrs.As(&oa) { 390 t.Fatal("Attributes.As failed") 391 } 392 generation := oa.Generation 393 394 // Repeat with a precondition that will pass. 395 reader, err := bucket.NewReader(ctx, key, &blob.ReaderOptions{ 396 BeforeRead: func(asFunc func(interface{}) bool) error { 397 var objp **storage.ObjectHandle 398 if !asFunc(&objp) { 399 return errors.New("Reader.As failed to get ObjectHandle") 400 } 401 // Replace the ObjectHandle with a new one. 402 *objp = (*objp).Generation(generation) 403 return nil 404 }, 405 }) 406 if err != nil { 407 t.Fatal(err) 408 } 409 defer reader.Close() 410 gotBytes, err := ioutil.ReadAll(reader) 411 if err != nil { 412 t.Fatal(err) 413 } 414 if got := string(gotBytes); got != content { 415 t.Errorf("got %q want %q", got, content) 416 } 417 418 // Try copying with a failing precondition on Dst. 419 err = bucket.Copy(ctx, key2, key, &blob.CopyOptions{ 420 BeforeCopy: func(asFunc func(interface{}) bool) error { 421 var coh *CopyObjectHandles 422 if !asFunc(&coh) { 423 return errors.New("Copy.As failed to get CopyObjectHandles") 424 } 425 // Replace the dst ObjectHandle with a new one. 426 coh.Dst = coh.Dst.If(storage.Conditions{GenerationMatch: -999}) 427 return nil 428 }, 429 }) 430 if err == nil || gcerrors.Code(err) != gcerrors.FailedPrecondition { 431 t.Errorf("got error %v, wanted FailedPrecondition for Copy", err) 432 } 433 434 // Try copying with a failing precondition on Src. 435 err = bucket.Copy(ctx, key2, key, &blob.CopyOptions{ 436 BeforeCopy: func(asFunc func(interface{}) bool) error { 437 var coh *CopyObjectHandles 438 if !asFunc(&coh) { 439 return errors.New("Copy.As failed to get CopyObjectHandles") 440 } 441 // Replace the src ObjectHandle with a new one. 442 coh.Src = coh.Src.Generation(9999999) 443 return nil 444 }, 445 }) 446 if err == nil || gcerrors.Code(err) != gcerrors.NotFound { 447 t.Errorf("got error %v, wanted NotFound for Copy", err) 448 } 449 450 // Repeat with preconditions on Dst and Src that will succeed. 451 err = bucket.Copy(ctx, key2, key, &blob.CopyOptions{ 452 BeforeCopy: func(asFunc func(interface{}) bool) error { 453 var coh *CopyObjectHandles 454 if !asFunc(&coh) { 455 return errors.New("Reader.As failed to get CopyObjectHandles") 456 } 457 coh.Dst = coh.Dst.If(storage.Conditions{DoesNotExist: true}) 458 coh.Src = coh.Src.Generation(generation) 459 return nil 460 }, 461 }) 462 if err != nil { 463 t.Error(err) 464 } 465 defer bucket.Delete(ctx, key2) 466 } 467 468 func TestURLOpenerForParams(t *testing.T) { 469 ctx := context.Background() 470 471 // Create a file for use as a dummy private key file. 472 privateKey := []byte("some content") 473 pkFile, err := ioutil.TempFile("", "my-private-key") 474 if err != nil { 475 t.Fatal(err) 476 } 477 defer os.Remove(pkFile.Name()) 478 if _, err := pkFile.Write(privateKey); err != nil { 479 t.Fatal(err) 480 } 481 if err := pkFile.Close(); err != nil { 482 t.Fatal(err) 483 } 484 485 tests := []struct { 486 name string 487 currOpts Options 488 query url.Values 489 wantOpts Options 490 wantErr bool 491 }{ 492 { 493 name: "InvalidParam", 494 query: url.Values{ 495 "foo": {"bar"}, 496 }, 497 wantErr: true, 498 }, 499 { 500 name: "AccessID", 501 query: url.Values{ 502 "access_id": {"bar"}, 503 }, 504 wantOpts: Options{GoogleAccessID: "bar"}, 505 }, 506 { 507 name: "AccessID override", 508 currOpts: Options{GoogleAccessID: "foo"}, 509 query: url.Values{ 510 "access_id": {"bar"}, 511 }, 512 wantOpts: Options{GoogleAccessID: "bar"}, 513 }, 514 { 515 name: "AccessID not overridden", 516 currOpts: Options{GoogleAccessID: "bar"}, 517 wantOpts: Options{GoogleAccessID: "bar"}, 518 }, 519 { 520 name: "BadPrivateKeyPath", 521 query: url.Values{ 522 "private_key_path": {"/path/does/not/exist"}, 523 }, 524 wantErr: true, 525 }, 526 { 527 name: "PrivateKeyPath", 528 query: url.Values{ 529 "private_key_path": {pkFile.Name()}, 530 }, 531 wantOpts: Options{PrivateKey: privateKey}, 532 }, 533 } 534 535 for _, test := range tests { 536 t.Run(test.name, func(t *testing.T) { 537 o := &URLOpener{Options: test.currOpts} 538 got, err := o.forParams(ctx, test.query) 539 if (err != nil) != test.wantErr { 540 t.Errorf("got err %v want error %v", err, test.wantErr) 541 } 542 if err != nil { 543 return 544 } 545 if diff := cmp.Diff(got, &test.wantOpts); diff != "" { 546 t.Errorf("opener.forParams(...) diff (-want +got):\n%s", diff) 547 } 548 }) 549 } 550 } 551 552 func TestOpenBucketFromURL(t *testing.T) { 553 cleanup := setup.FakeGCPDefaultCredentials(t) 554 defer cleanup() 555 556 pkFile, err := ioutil.TempFile("", "my-private-key") 557 if err != nil { 558 t.Fatal(err) 559 } 560 defer os.Remove(pkFile.Name()) 561 if err := ioutil.WriteFile(pkFile.Name(), []byte("key"), 0666); err != nil { 562 t.Fatal(err) 563 } 564 565 tests := []struct { 566 URL string 567 WantErr bool 568 }{ 569 // OK. 570 {"gs://mybucket", false}, 571 // OK, setting access_id. 572 {"gs://mybucket?access_id=foo", false}, 573 // OK, setting private_key_path. 574 {"gs://mybucket?private_key_path=" + pkFile.Name(), false}, 575 // Invalid private_key_path. 576 {"gs://mybucket?private_key_path=invalid-path", true}, 577 // Invalid parameter. 578 {"gs://mybucket?param=value", true}, 579 } 580 581 ctx := context.Background() 582 for _, test := range tests { 583 b, err := blob.OpenBucket(ctx, test.URL) 584 if b != nil { 585 defer b.Close() 586 } 587 if (err != nil) != test.WantErr { 588 t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr) 589 } 590 } 591 }