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  }