github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/blob/azureblob/azureblob_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 azureblob 16 17 import ( 18 "context" 19 "encoding/base64" 20 "errors" 21 "fmt" 22 "net/http" 23 "net/url" 24 "os" 25 "strings" 26 "testing" 27 28 "github.com/Azure/azure-sdk-for-go/sdk/azcore" 29 "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" 30 31 "github.com/google/go-cmp/cmp" 32 "gocloud.dev/blob" 33 "gocloud.dev/blob/driver" 34 "gocloud.dev/blob/drivertest" 35 "gocloud.dev/internal/testing/setup" 36 ) 37 38 // Prerequisites for -record mode 39 // 1. Sign-in to your Azure Subscription at http://portal.azure.com. 40 // 41 // 2. Create a Storage Account. 42 // 43 // 3. Locate the Access Key (Primary or Secondary) under your Storage Account > Settings > Access Keys. 44 // 45 // 4. Set the environment variables AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY to 46 // the storage account name and your access key. 47 // 48 // 5. Create a container in your Storage Account > Blob. Update the bucketName 49 // constant to your container name. 50 // 51 // Here is a step-by-step walkthrough using the Azure Portal 52 // https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-portal 53 // 54 // 5. Run the tests with -record. 55 56 const ( 57 bucketName = "go-cloud-bucket" 58 accountName = "gocloudblobtests" 59 ) 60 61 type harness struct { 62 svcClient *azblob.ServiceClient 63 closer func() 64 httpClient *http.Client 65 } 66 67 func newHarness(ctx context.Context, t *testing.T) (drivertest.Harness, error) { 68 var key string 69 if *setup.Record { 70 name := os.Getenv("AZURE_STORAGE_ACCOUNT") 71 if name != accountName { 72 t.Fatalf("Please update the accountName constant to match your settings file so future records work (%q vs %q)", name, accountName) 73 } 74 key = os.Getenv("AZURE_STORAGE_KEY") 75 } else { 76 // In replay mode, we use fake credentials. 77 key = base64.StdEncoding.EncodeToString([]byte("FAKECREDS")) 78 } 79 credential, err := azblob.NewSharedKeyCredential(accountName, key) 80 if err != nil { 81 return nil, err 82 } 83 httpClient, done := setup.NewAzureTestBlobClient(ctx, t) 84 // Hack to work around the fact that SignedURLs for PUTs are not fully 85 // portable; they require a "x-ms-blob-type" header. Intercept all 86 // requests, and insert that header where needed. 87 httpClient.Transport = &requestInterceptor{httpClient.Transport} 88 clientOptions := azblob.ClientOptions{ 89 Transport: httpClient, 90 } 91 serviceURL := fmt.Sprintf("https://%s.blob.core.windows.net", accountName) 92 svcClient, err := azblob.NewServiceClientWithSharedKey(serviceURL, credential, &clientOptions) 93 if err != nil { 94 return nil, err 95 } 96 return &harness{svcClient: svcClient, closer: done, httpClient: httpClient}, nil 97 } 98 99 // requestInterceptor implements a hack for the lack of portability for 100 // SignedURLs for PUT. It adds the required "x-ms-blob-type" header where 101 // Azure requires it. 102 type requestInterceptor struct { 103 base http.RoundTripper 104 } 105 106 func (ri *requestInterceptor) RoundTrip(req *http.Request) (*http.Response, error) { 107 if req.Method == http.MethodPut && strings.Contains(req.URL.Path, "blob-for-signing") { 108 reqClone := *req 109 reqClone.Header.Add("x-ms-blob-type", "BlockBlob") 110 req = &reqClone 111 } 112 return ri.base.RoundTrip(req) 113 } 114 115 func (h *harness) HTTPClient() *http.Client { 116 return h.httpClient 117 } 118 119 func (h *harness) MakeDriver(ctx context.Context) (driver.Bucket, error) { 120 return openBucket(ctx, h.svcClient, bucketName, nil) 121 } 122 123 func (h *harness) MakeDriverForNonexistentBucket(ctx context.Context) (driver.Bucket, error) { 124 return openBucket(ctx, h.svcClient, "bucket-does-not-exist", nil) 125 } 126 127 func (h *harness) Close() { 128 h.closer() 129 } 130 131 func TestConformance(t *testing.T) { 132 // See setup instructions above for more details. 133 drivertest.RunConformanceTests(t, newHarness, []drivertest.AsTest{verifyContentLanguage{}}) 134 } 135 136 func BenchmarkAzureblob(b *testing.B) { 137 name := os.Getenv("AZURE_STORAGE_ACCOUNT") 138 key := os.Getenv("AZURE_STORAGE_KEY") 139 credential, err := azblob.NewSharedKeyCredential(name, key) 140 if err != nil { 141 b.Fatal(err) 142 } 143 serviceURL := fmt.Sprintf("https://%s.blob.core.windows.net/", accountName) 144 svcClient, err := azblob.NewServiceClientWithSharedKey(serviceURL, credential, nil) 145 if err != nil { 146 b.Fatal(err) 147 } 148 bkt, err := OpenBucket(context.Background(), svcClient, bucketName, nil) 149 if err != nil { 150 b.Fatal(err) 151 } 152 drivertest.RunBenchmarks(b, bkt) 153 } 154 155 var language = "nl" 156 157 // verifyContentLanguage uses As to access the underlying Azure types and 158 // read/write the ContentLanguage field. 159 type verifyContentLanguage struct{} 160 161 func (verifyContentLanguage) Name() string { 162 return "verify ContentLanguage can be written and read through As" 163 } 164 165 func (verifyContentLanguage) BucketCheck(b *blob.Bucket) error { 166 var u *azblob.ContainerClient 167 if !b.As(&u) { 168 return errors.New("Bucket.As failed") 169 } 170 return nil 171 } 172 173 func (verifyContentLanguage) ErrorCheck(b *blob.Bucket, err error) error { 174 var to1 *azblob.StorageError 175 var to2 *azcore.ResponseError 176 var to3 *azblob.InternalError 177 if !b.ErrorAs(err, &to1) && !b.ErrorAs(err, &to2) && !b.ErrorAs(err, &to3) { 178 return errors.New("Bucket.ErrorAs failed") 179 } 180 return nil 181 } 182 183 func (verifyContentLanguage) BeforeRead(as func(interface{}) bool) error { 184 var u *azblob.BlobDownloadOptions 185 if !as(&u) { 186 return fmt.Errorf("BeforeRead As failed to get %T", u) 187 } 188 return nil 189 } 190 191 func (verifyContentLanguage) BeforeWrite(as func(interface{}) bool) error { 192 var azOpts *azblob.UploadStreamOptions 193 if !as(&azOpts) { 194 return errors.New("Writer.As failed") 195 } 196 azOpts.HTTPHeaders.BlobContentLanguage = &language 197 return nil 198 } 199 200 func (verifyContentLanguage) BeforeCopy(as func(interface{}) bool) error { 201 var co *azblob.BlobStartCopyOptions 202 if !as(&co) { 203 return errors.New("BeforeCopy.As failed") 204 } 205 return nil 206 } 207 208 func (verifyContentLanguage) BeforeList(as func(interface{}) bool) error { 209 var azOpts *azblob.ContainerListBlobsHierarchyOptions 210 if !as(&azOpts) { 211 return errors.New("BeforeList.As failed") 212 } 213 return nil 214 } 215 216 func (verifyContentLanguage) BeforeSign(as func(interface{}) bool) error { 217 var azOpts *azblob.BlobSASPermissions 218 if !as(&azOpts) { 219 return errors.New("BeforeSign.As failed") 220 } 221 return nil 222 } 223 224 func (verifyContentLanguage) AttributesCheck(attrs *blob.Attributes) error { 225 var resp azblob.BlobGetPropertiesResponse 226 if !attrs.As(&resp) { 227 return errors.New("Attributes.As returned false") 228 } 229 if got := *resp.ContentLanguage; got != language { 230 return fmt.Errorf("got %q want %q", got, language) 231 } 232 return nil 233 } 234 235 func (verifyContentLanguage) ReaderCheck(r *blob.Reader) error { 236 var resp azblob.BlobDownloadResponse 237 if !r.As(&resp) { 238 return errors.New("Reader.As returned false") 239 } 240 if got := *resp.ContentLanguage; got != language { 241 return fmt.Errorf("got %q want %q", got, language) 242 } 243 return nil 244 } 245 246 func (verifyContentLanguage) ListObjectCheck(o *blob.ListObject) error { 247 if o.IsDir { 248 var prefix azblob.BlobPrefix 249 if !o.As(&prefix) { 250 return errors.New("ListObject.As for directory returned false") 251 } 252 return nil 253 } 254 var item azblob.BlobItemInternal 255 if !o.As(&item) { 256 return errors.New("ListObject.As for object returned false") 257 } 258 if got := *item.Properties.ContentLanguage; got != language { 259 return fmt.Errorf("got %q want %q", got, language) 260 } 261 return nil 262 } 263 264 func TestOpenBucket(t *testing.T) { 265 tests := []struct { 266 description string 267 nilClient bool 268 accountName string 269 containerName string 270 want string 271 wantErr bool 272 }{ 273 { 274 description: "nil client results in error", 275 nilClient: true, 276 accountName: "myaccount", 277 containerName: "foo", 278 wantErr: true, 279 }, 280 { 281 description: "empty container name results in error", 282 accountName: "myaccount", 283 wantErr: true, 284 }, 285 { 286 description: "success", 287 accountName: "myaccount", 288 containerName: "foo", 289 want: "foo", 290 }, 291 } 292 293 ctx := context.Background() 294 for _, test := range tests { 295 t.Run(test.description, func(t *testing.T) { 296 var svcClient *azblob.ServiceClient 297 var err error 298 if !test.nilClient { 299 svcClient, err = azblob.NewServiceClientWithNoCredential("", nil) 300 if err != nil { 301 t.Fatal(err) 302 } 303 } 304 // Create portable type. 305 b, err := OpenBucket(ctx, svcClient, test.containerName, nil) 306 if b != nil { 307 defer b.Close() 308 } 309 if (err != nil) != test.wantErr { 310 t.Errorf("got err %v want error %v", err, test.wantErr) 311 } 312 }) 313 } 314 } 315 316 func TestOpenerFromEnv(t *testing.T) { 317 tests := []struct { 318 accountName string 319 accountKey string 320 sasToken string 321 connectionString string 322 domain string 323 protocol string 324 isCDN bool 325 isLocalEmulator bool 326 327 want *credInfoT 328 wantOpts *ServiceURLOptions 329 }{ 330 { 331 // Shared key. 332 accountName: "myaccount", 333 accountKey: "fakecreds", 334 want: &credInfoT{ 335 CredType: credTypeSharedKey, 336 AccountName: "myaccount", 337 AccountKey: "fakecreds", 338 }, 339 wantOpts: &ServiceURLOptions{ 340 AccountName: "myaccount", 341 }, 342 }, 343 { 344 // SAS Token. 345 accountName: "myaccount", 346 sasToken: "a-sas-token", 347 want: &credInfoT{ 348 CredType: credTypeSASViaNone, 349 AccountName: "myaccount", 350 }, 351 wantOpts: &ServiceURLOptions{ 352 AccountName: "myaccount", 353 SASToken: "a-sas-token", 354 }, 355 }, 356 { 357 // Connection string. 358 accountName: "myaccount", 359 connectionString: "a-connection-string", 360 want: &credInfoT{ 361 CredType: credTypeConnectionString, 362 AccountName: "myaccount", 363 ConnectionString: "a-connection-string", 364 }, 365 wantOpts: &ServiceURLOptions{ 366 AccountName: "myaccount", 367 }, 368 }, 369 { 370 // Default. 371 accountName: "anotheraccount", 372 want: &credInfoT{ 373 CredType: credTypeDefault, 374 AccountName: "anotheraccount", 375 }, 376 wantOpts: &ServiceURLOptions{ 377 AccountName: "anotheraccount", 378 }, 379 }, 380 { 381 // Setting protocol and domain. 382 accountName: "myaccount", 383 protocol: "http", 384 domain: "foo.bar.com", 385 want: &credInfoT{ 386 CredType: credTypeDefault, 387 AccountName: "myaccount", 388 }, 389 wantOpts: &ServiceURLOptions{ 390 AccountName: "myaccount", 391 Protocol: "http", 392 StorageDomain: "foo.bar.com", 393 }, 394 }, 395 { 396 // Local emulator. 397 accountName: "myaccount", 398 isLocalEmulator: true, 399 want: &credInfoT{ 400 CredType: credTypeDefault, 401 AccountName: "myaccount", 402 }, 403 wantOpts: &ServiceURLOptions{ 404 AccountName: "myaccount", 405 IsLocalEmulator: true, 406 }, 407 }, 408 } 409 for _, test := range tests { 410 os.Setenv("AZURE_STORAGE_ACCOUNT", test.accountName) 411 os.Setenv("AZURE_STORAGE_KEY", test.accountKey) 412 os.Setenv("AZURE_STORAGE_SAS_TOKEN", test.sasToken) 413 os.Setenv("AZURE_STORAGE_CONNECTION_STRING", test.connectionString) 414 os.Setenv("AZURE_STORAGE_DOMAIN", test.domain) 415 os.Setenv("AZURE_STORAGE_PROTOCOL", test.protocol) 416 if test.isCDN { 417 os.Setenv("AZURE_STORAGE_IS_CDN", "true") 418 } else { 419 os.Setenv("AZURE_STORAGE_IS_CDN", "") 420 } 421 if test.isLocalEmulator { 422 os.Setenv("AZURE_STORAGE_IS_LOCAL_EMULATOR", "true") 423 } else { 424 os.Setenv("AZURE_STORAGE_IS_LOCAL_EMULATOR", "") 425 } 426 427 got := newCredInfoFromEnv() 428 if diff := cmp.Diff(got, test.want); diff != "" { 429 t.Errorf("unexpected diff in credInfo: %s", diff) 430 } 431 gotOpts := NewDefaultServiceURLOptions() 432 if diff := cmp.Diff(gotOpts, test.wantOpts); diff != "" { 433 t.Errorf("unexpected diff in Options: %s", diff) 434 } 435 436 } 437 } 438 439 func TestNewServiceURL(t *testing.T) { 440 tests := []struct { 441 opts ServiceURLOptions 442 query url.Values 443 want ServiceURL 444 wantErrOverrides bool 445 wantErrURL bool 446 }{ 447 { 448 // Unknown query parameter. 449 opts: ServiceURLOptions{ 450 AccountName: "myaccount", 451 }, 452 query: url.Values{ 453 "foo": {"bar"}, 454 }, 455 wantErrOverrides: true, 456 }, 457 { 458 // Duplicate query parameter. 459 opts: ServiceURLOptions{ 460 AccountName: "myaccount", 461 }, 462 query: url.Values{ 463 "domain": {"blob.core.usgovcloudapi.net", "blob.core.windows.net"}, 464 }, 465 wantErrOverrides: true, 466 }, 467 { 468 // Missing account name. 469 opts: ServiceURLOptions{}, 470 wantErrURL: true, 471 }, 472 { 473 // Account name set in the query 474 opts: ServiceURLOptions{}, 475 query: url.Values{ 476 "storage_account": {"testaccount"}, 477 }, 478 want: "https://testaccount.blob.core.windows.net", 479 }, 480 { 481 // Basic working case. 482 opts: ServiceURLOptions{ 483 AccountName: "myaccount", 484 }, 485 want: "https://myaccount.blob.core.windows.net", 486 }, 487 { 488 // SASToken. 489 opts: ServiceURLOptions{ 490 AccountName: "myaccount", 491 SASToken: "my-sas-token", 492 }, 493 want: "https://myaccount.blob.core.windows.net?my-sas-token", 494 }, 495 { 496 // Setting domain from ServiceURLOptions. 497 opts: ServiceURLOptions{ 498 AccountName: "myaccount", 499 StorageDomain: "blob.core.usgovcloudapi.net", 500 }, 501 want: "https://myaccount.blob.core.usgovcloudapi.net", 502 }, 503 { 504 // Setting domain from the URL. 505 opts: ServiceURLOptions{ 506 AccountName: "myaccount", 507 StorageDomain: "overridden", 508 }, 509 query: url.Values{ 510 "domain": {"blob.core.usgovcloudapi.net"}, 511 }, 512 want: "https://myaccount.blob.core.usgovcloudapi.net", 513 }, 514 { 515 // Setting protocol from ServiceURLOptions. 516 opts: ServiceURLOptions{ 517 AccountName: "myaccount", 518 Protocol: "http", 519 }, 520 want: "http://myaccount.blob.core.windows.net", 521 }, 522 { 523 // Setting protocol from the URL. 524 opts: ServiceURLOptions{ 525 AccountName: "myaccount", 526 Protocol: "https", 527 }, 528 query: url.Values{ 529 "protocol": {"http"}, 530 }, 531 want: "http://myaccount.blob.core.windows.net", 532 }, 533 { 534 // Setting IsCDN from ServiceURLOptions. 535 opts: ServiceURLOptions{ 536 AccountName: "myaccount", 537 IsCDN: true, 538 }, 539 want: "https://blob.core.windows.net", 540 }, 541 { 542 // Setting IsCDN from the URL. 543 opts: ServiceURLOptions{ 544 AccountName: "myaccount", 545 }, 546 query: url.Values{ 547 "cdn": {"true"}, 548 }, 549 want: "https://blob.core.windows.net", 550 }, 551 { 552 // Local emulator, implicit from domain. 553 opts: ServiceURLOptions{ 554 AccountName: "myaccount", 555 Protocol: "http", 556 StorageDomain: "localhost:10001", 557 }, 558 want: "http://localhost:10001/myaccount", 559 }, 560 { 561 // Local emulator, implicit from domain through URL parameter. 562 opts: ServiceURLOptions{ 563 AccountName: "myaccount", 564 }, 565 query: url.Values{ 566 "protocol": {"http"}, 567 "domain": {"127.0.0.1:10001"}, 568 }, 569 want: "http://127.0.0.1:10001/myaccount", 570 }, 571 { 572 // Local emulator, explicit through ServiceURLOptions. 573 opts: ServiceURLOptions{ 574 AccountName: "myaccount", 575 StorageDomain: "mylocalemulator", 576 IsLocalEmulator: true, 577 }, 578 want: "https://mylocalemulator/myaccount", 579 }, 580 { 581 // Local emulator, explicit through URL parameter. 582 opts: ServiceURLOptions{ 583 AccountName: "myaccount", 584 StorageDomain: "mylocalemulator", 585 }, 586 query: url.Values{ 587 "localemu": {"true"}, 588 }, 589 want: "https://mylocalemulator/myaccount", 590 }, 591 } 592 593 for _, test := range tests { 594 opts, err := test.opts.withOverrides(test.query) 595 if (err != nil) != test.wantErrOverrides { 596 t.Fatalf("withOverrides got err %v want error %v", err, test.wantErrOverrides) 597 } 598 if err != nil { 599 continue 600 } 601 got, err := NewServiceURL(opts) 602 if (err != nil) != test.wantErrURL { 603 t.Errorf("NewServiceURL got err %v want error %v", err, test.wantErrURL) 604 } 605 if got != test.want { 606 t.Errorf("got %q want %q", got, test.want) 607 } 608 } 609 } 610 611 func TestOpenBucketFromURL(t *testing.T) { 612 prevAccount := os.Getenv("AZURE_STORAGE_ACCOUNT") 613 prevKey := os.Getenv("AZURE_STORAGE_KEY") 614 os.Setenv("AZURE_STORAGE_ACCOUNT", "my-account") 615 os.Setenv("AZURE_STORAGE_KEY", "bXlrZXk=") // mykey base64 encoded 616 defer func() { 617 os.Setenv("AZURE_STORAGE_ACCOUNT", prevAccount) 618 os.Setenv("AZURE_STORAGE_KEY", prevKey) 619 }() 620 621 tests := []struct { 622 URL string 623 WantErr bool 624 }{ 625 // OK. 626 {"azblob://mybucket", false}, 627 // With storage domain. 628 {"azblob://mybucket?domain=blob.core.usgovcloudapi.net", false}, 629 // With duplicate storage domain. 630 {"azblob://mybucket?domain=blob.core.usgovcloudapi.net&domain=blob.core.windows.net", true}, 631 // With protocol. 632 {"azblob://mybucket?protocol=http", false}, 633 // With invalid protocol. 634 {"azblob://mybucket?protocol=ftp", true}, 635 // With Account. 636 {"azblob://mybucket?storage_account=test", false}, 637 // With CDN. 638 {"azblob://mybucket?cdn=true", false}, 639 // With invalid CDN. 640 {"azblob://mybucket?cdn=42", true}, 641 // With local emulator. 642 {"azblob://mybucket?localemu=true", false}, 643 // With invalid local emulator. 644 {"azblob://mybucket?localemu=42", true}, 645 // Invalid parameter. 646 {"azblob://mybucket?param=value", true}, 647 } 648 649 ctx := context.Background() 650 for _, test := range tests { 651 b, err := blob.OpenBucket(ctx, test.URL) 652 if b != nil { 653 defer b.Close() 654 } 655 if (err != nil) != test.WantErr { 656 t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr) 657 } 658 } 659 }