1package azblob 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io/ioutil" 10 "math/rand" 11 "net/url" 12 "os" 13 "reflect" 14 "runtime" 15 "strings" 16 "testing" 17 "time" 18 19 chk "gopkg.in/check.v1" 20 21 "github.com/Azure/azure-pipeline-go/pipeline" 22 "github.com/Azure/go-autorest/autorest/adal" 23) 24 25// For testing docs, see: https://labix.org/gocheck 26// To test a specific test: go test -check.f MyTestSuite 27 28// Hookup to the testing framework 29func Test(t *testing.T) { chk.TestingT(t) } 30 31type aztestsSuite struct{} 32 33var _ = chk.Suite(&aztestsSuite{}) 34 35func (s *aztestsSuite) TestRetryPolicyRetryReadsFromSecondaryHostField(c *chk.C) { 36 _, found := reflect.TypeOf(RetryOptions{}).FieldByName("RetryReadsFromSecondaryHost") 37 if !found { 38 // Make sure the RetryOption was not erroneously overwritten 39 c.Fatal("RetryOption's RetryReadsFromSecondaryHost field must exist in the Blob SDK - uncomment it and make sure the field is returned from the retryReadsFromSecondaryHost() method too!") 40 } 41} 42 43const ( 44 containerPrefix = "go" 45 blobPrefix = "gotestblob" 46 blockBlobDefaultData = "GoBlockBlobData" 47 validationErrorSubstring = "validation failed" 48 invalidHeaderErrorSubstring = "invalid header field" // error thrown by the http client 49) 50 51var ctx = context.Background() 52var basicHeaders = BlobHTTPHeaders{ 53 ContentType: "my_type", 54 ContentDisposition: "my_disposition", 55 CacheControl: "control", 56 ContentMD5: nil, 57 ContentLanguage: "my_language", 58 ContentEncoding: "my_encoding", 59} 60 61var basicMetadata = Metadata{"foo": "bar"} 62 63type testPipeline struct{} 64 65const testPipelineMessage string = "Test factory invoked" 66 67func (tm testPipeline) Do(ctx context.Context, methodFactory pipeline.Factory, request pipeline.Request) (pipeline.Response, error) { 68 return nil, errors.New(testPipelineMessage) 69} 70 71// This function generates an entity name by concatenating the passed prefix, 72// the name of the test requesting the entity name, and the minute, second, and nanoseconds of the call. 73// This should make it easy to associate the entities with their test, uniquely identify 74// them, and determine the order in which they were created. 75// Note that this imposes a restriction on the length of test names 76func generateName(prefix string) string { 77 // These next lines up through the for loop are obtaining and walking up the stack 78 // trace to extrat the test name, which is stored in name 79 pc := make([]uintptr, 10) 80 runtime.Callers(0, pc) 81 frames := runtime.CallersFrames(pc) 82 name := "" 83 for f, next := frames.Next(); next; f, next = frames.Next() { 84 name = f.Function 85 if strings.Contains(name, "Suite") { 86 break 87 } 88 } 89 funcNameStart := strings.Index(name, "Test") 90 name = name[funcNameStart+len("Test"):] // Just get the name of the test and not any of the garbage at the beginning 91 name = strings.ToLower(name) // Ensure it is a valid resource name 92 currentTime := time.Now() 93 name = fmt.Sprintf("%s%s%d%d%d", prefix, strings.ToLower(name), currentTime.Minute(), currentTime.Second(), currentTime.Nanosecond()) 94 return name 95} 96 97func generateContainerName() string { 98 return generateName(containerPrefix) 99} 100 101func generateBlobName() string { 102 return generateName(blobPrefix) 103} 104 105func getContainerURL(c *chk.C, bsu ServiceURL) (container ContainerURL, name string) { 106 name = generateContainerName() 107 container = bsu.NewContainerURL(name) 108 109 return container, name 110} 111 112func getBlockBlobURL(c *chk.C, container ContainerURL) (blob BlockBlobURL, name string) { 113 name = generateBlobName() 114 blob = container.NewBlockBlobURL(name) 115 116 return blob, name 117} 118 119func getAppendBlobURL(c *chk.C, container ContainerURL) (blob AppendBlobURL, name string) { 120 name = generateBlobName() 121 blob = container.NewAppendBlobURL(name) 122 123 return blob, name 124} 125 126func getPageBlobURL(c *chk.C, container ContainerURL) (blob PageBlobURL, name string) { 127 name = generateBlobName() 128 blob = container.NewPageBlobURL(name) 129 130 return 131} 132 133func getReaderToRandomBytes(n int) *bytes.Reader { 134 r, _ := getRandomDataAndReader(n) 135 return r 136} 137 138func getRandomDataAndReader(n int) (*bytes.Reader, []byte) { 139 data := make([]byte, n, n) 140 rand.Read(data) 141 return bytes.NewReader(data), data 142} 143 144func createNewContainer(c *chk.C, bsu ServiceURL) (container ContainerURL, name string) { 145 container, name = getContainerURL(c, bsu) 146 147 cResp, err := container.Create(ctx, nil, PublicAccessNone) 148 c.Assert(err, chk.IsNil) 149 c.Assert(cResp.StatusCode(), chk.Equals, 201) 150 return container, name 151} 152 153func createNewContainerWithSuffix(c *chk.C, bsu ServiceURL, suffix string) (container ContainerURL, name string) { 154 // The goal of adding the suffix is to be able to predetermine what order the containers will be in when listed. 155 // We still need the container prefix to come first, though, to ensure only containers as a part of this test 156 // are listed at all. 157 name = generateName(containerPrefix + suffix) 158 container = bsu.NewContainerURL(name) 159 160 cResp, err := container.Create(ctx, nil, PublicAccessNone) 161 c.Assert(err, chk.IsNil) 162 c.Assert(cResp.StatusCode(), chk.Equals, 201) 163 return container, name 164} 165 166func createNewBlockBlob(c *chk.C, container ContainerURL) (blob BlockBlobURL, name string) { 167 blob, name = getBlockBlobURL(c, container) 168 169 cResp, err := blob.Upload(ctx, strings.NewReader(blockBlobDefaultData), BlobHTTPHeaders{}, nil, BlobAccessConditions{}, DefaultAccessTier, nil, ClientProvidedKeyOptions{}) 170 171 c.Assert(err, chk.IsNil) 172 c.Assert(cResp.StatusCode(), chk.Equals, 201) 173 174 return 175} 176 177func createNewBlockBlobWithCPK(c *chk.C, container ContainerURL, cpk ClientProvidedKeyOptions) (blob BlockBlobURL, name string) { 178 blob, name = getBlockBlobURL(c, container) 179 180 cResp, err := blob.Upload(ctx, strings.NewReader(blockBlobDefaultData), BlobHTTPHeaders{}, 181 nil, BlobAccessConditions{}, DefaultAccessTier, nil, cpk) 182 c.Assert(err, chk.IsNil) 183 c.Assert(cResp.StatusCode(), chk.Equals, 201) 184 return 185} 186 187func createNewAppendBlob(c *chk.C, container ContainerURL) (blob AppendBlobURL, name string) { 188 blob, name = getAppendBlobURL(c, container) 189 190 resp, err := blob.Create(ctx, BlobHTTPHeaders{}, nil, BlobAccessConditions{}, nil, ClientProvidedKeyOptions{}) 191 192 c.Assert(err, chk.IsNil) 193 c.Assert(resp.StatusCode(), chk.Equals, 201) 194 return 195} 196 197func createNewAppendBlobWithCPK(c *chk.C, container ContainerURL, cpk ClientProvidedKeyOptions) (blob AppendBlobURL, name string) { 198 blob, name = getAppendBlobURL(c, container) 199 200 resp, err := blob.Create(ctx, BlobHTTPHeaders{}, nil, BlobAccessConditions{}, nil, cpk) 201 c.Assert(err, chk.IsNil) 202 c.Assert(resp.StatusCode(), chk.Equals, 201) 203 return 204} 205 206func createNewPageBlob(c *chk.C, container ContainerURL) (blob PageBlobURL, name string) { 207 blob, name = getPageBlobURL(c, container) 208 209 resp, err := blob.Create(ctx, PageBlobPageBytes*10, 0, BlobHTTPHeaders{}, nil, BlobAccessConditions{}, DefaultPremiumBlobAccessTier, nil, ClientProvidedKeyOptions{}) 210 c.Assert(err, chk.IsNil) 211 c.Assert(resp.StatusCode(), chk.Equals, 201) 212 return 213} 214 215func createNewPageBlobWithSize(c *chk.C, container ContainerURL, sizeInBytes int64) (blob PageBlobURL, name string) { 216 blob, name = getPageBlobURL(c, container) 217 218 resp, err := blob.Create(ctx, sizeInBytes, 0, BlobHTTPHeaders{}, nil, BlobAccessConditions{}, DefaultPremiumBlobAccessTier, nil, ClientProvidedKeyOptions{}) 219 c.Assert(err, chk.IsNil) 220 c.Assert(resp.StatusCode(), chk.Equals, 201) 221 return 222} 223 224func createNewPageBlobWithCPK(c *chk.C, container ContainerURL, sizeInBytes int64, cpk ClientProvidedKeyOptions) (blob PageBlobURL, name string) { 225 blob, name = getPageBlobURL(c, container) 226 227 resp, err := blob.Create(ctx, sizeInBytes, 0, BlobHTTPHeaders{}, nil, BlobAccessConditions{}, DefaultPremiumBlobAccessTier, nil, cpk) 228 c.Assert(err, chk.IsNil) 229 c.Assert(resp.StatusCode(), chk.Equals, 201) 230 return 231} 232 233func createBlockBlobWithPrefix(c *chk.C, container ContainerURL, prefix string) (blob BlockBlobURL, name string) { 234 name = prefix + generateName(blobPrefix) 235 blob = container.NewBlockBlobURL(name) 236 237 cResp, err := blob.Upload(ctx, strings.NewReader(blockBlobDefaultData), BlobHTTPHeaders{}, nil, BlobAccessConditions{}, DefaultAccessTier, nil, ClientProvidedKeyOptions{}) 238 239 c.Assert(err, chk.IsNil) 240 c.Assert(cResp.StatusCode(), chk.Equals, 201) 241 return 242} 243 244func deleteContainer(c *chk.C, container ContainerURL) { 245 resp, err := container.Delete(ctx, ContainerAccessConditions{}) 246 c.Assert(err, chk.IsNil) 247 c.Assert(resp.StatusCode(), chk.Equals, 202) 248} 249 250func getGenericCredential(accountType string) (*SharedKeyCredential, error) { 251 accountNameEnvVar := accountType + "ACCOUNT_NAME" 252 accountKeyEnvVar := accountType + "ACCOUNT_KEY" 253 accountName, accountKey := os.Getenv(accountNameEnvVar), os.Getenv(accountKeyEnvVar) 254 if accountName == "" || accountKey == "" { 255 return nil, errors.New(accountNameEnvVar + " and/or " + accountKeyEnvVar + " environment variables not specified.") 256 } 257 return NewSharedKeyCredential(accountName, accountKey) 258} 259 260//getOAuthCredential can intake a OAuth credential from environment variables in one of the following ways: 261//Direct: Supply a ADAL OAuth token in OAUTH_TOKEN and application ID in APPLICATION_ID to refresh the supplied token. 262//Client secret: Supply a client secret in CLIENT_SECRET and application ID in APPLICATION_ID for SPN auth. 263//TENANT_ID is optional and will be inferred as common if it is not explicitly defined. 264func getOAuthCredential(accountType string) (*TokenCredential, error) { 265 oauthTokenEnvVar := accountType + "OAUTH_TOKEN" 266 clientSecretEnvVar := accountType + "CLIENT_SECRET" 267 applicationIdEnvVar := accountType + "APPLICATION_ID" 268 tenantIdEnvVar := accountType + "TENANT_ID" 269 oauthToken, appId, tenantId, clientSecret := []byte(os.Getenv(oauthTokenEnvVar)), os.Getenv(applicationIdEnvVar), os.Getenv(tenantIdEnvVar), os.Getenv(clientSecretEnvVar) 270 if (len(oauthToken) == 0 && clientSecret == "") || appId == "" { 271 return nil, errors.New("(" + oauthTokenEnvVar + " OR " + clientSecretEnvVar + ") and/or " + applicationIdEnvVar + " environment variables not specified.") 272 } 273 if tenantId == "" { 274 tenantId = "common" 275 } 276 277 var Token adal.Token 278 if len(oauthToken) != 0 { 279 if err := json.Unmarshal(oauthToken, &Token); err != nil { 280 return nil, err 281 } 282 } 283 284 var spt *adal.ServicePrincipalToken 285 286 oauthConfig, err := adal.NewOAuthConfig("https://login.microsoftonline.com", tenantId) 287 if err != nil { 288 return nil, err 289 } 290 291 if len(oauthToken) == 0 { 292 spt, err = adal.NewServicePrincipalToken( 293 *oauthConfig, 294 appId, 295 clientSecret, 296 "https://storage.azure.com") 297 if err != nil { 298 return nil, err 299 } 300 } else { 301 spt, err = adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, 302 appId, 303 "https://storage.azure.com", 304 Token, 305 ) 306 if err != nil { 307 return nil, err 308 } 309 } 310 311 err = spt.Refresh() 312 if err != nil { 313 return nil, err 314 } 315 316 tc := NewTokenCredential(spt.Token().AccessToken, func(tc TokenCredential) time.Duration { 317 _ = spt.Refresh() 318 return time.Until(spt.Token().Expires()) 319 }) 320 321 return &tc, nil 322} 323 324func getGenericBSU(accountType string) (ServiceURL, error) { 325 credential, err := getGenericCredential(accountType) 326 if err != nil { 327 return ServiceURL{}, err 328 } 329 330 pipeline := NewPipeline(credential, PipelineOptions{}) 331 blobPrimaryURL, _ := url.Parse("https://" + credential.AccountName() + ".blob.core.windows.net/") 332 return NewServiceURL(*blobPrimaryURL, pipeline), nil 333} 334 335func getBSU() ServiceURL { 336 bsu, _ := getGenericBSU("") 337 return bsu 338} 339 340func getAlternateBSU() (ServiceURL, error) { 341 return getGenericBSU("SECONDARY_") 342} 343 344func getPremiumBSU() (ServiceURL, error) { 345 return getGenericBSU("PREMIUM_") 346} 347 348func getBlobStorageBSU() (ServiceURL, error) { 349 return getGenericBSU("BLOB_STORAGE_") 350} 351 352func validateStorageError(c *chk.C, err error, code ServiceCodeType) { 353 serr, _ := err.(StorageError) 354 c.Assert(serr.ServiceCode(), chk.Equals, code) 355} 356 357func getRelativeTimeGMT(amount time.Duration) time.Time { 358 currentTime := time.Now().In(time.FixedZone("GMT", 0)) 359 currentTime = currentTime.Add(amount * time.Second) 360 return currentTime 361} 362 363func generateCurrentTimeWithModerateResolution() time.Time { 364 highResolutionTime := time.Now().UTC() 365 return time.Date(highResolutionTime.Year(), highResolutionTime.Month(), highResolutionTime.Day(), highResolutionTime.Hour(), highResolutionTime.Minute(), 366 highResolutionTime.Second(), 0, highResolutionTime.Location()) 367} 368 369// Some tests require setting service properties. It can take up to 30 seconds for the new properties to be reflected across all FEs. 370// We will enable the necessary property and try to run the test implementation. If it fails with an error that should be due to 371// those changes not being reflected yet, we will wait 30 seconds and try the test again. If it fails this time for any reason, 372// we fail the test. It is the responsibility of the the testImplFunc to determine which error string indicates the test should be retried. 373// There can only be one such string. All errors that cannot be due to this detail should be asserted and not returned as an error string. 374func runTestRequiringServiceProperties(c *chk.C, bsu ServiceURL, code string, 375 enableServicePropertyFunc func(*chk.C, ServiceURL), 376 testImplFunc func(*chk.C, ServiceURL) error, 377 disableServicePropertyFunc func(*chk.C, ServiceURL)) { 378 enableServicePropertyFunc(c, bsu) 379 defer disableServicePropertyFunc(c, bsu) 380 err := testImplFunc(c, bsu) 381 // We cannot assume that the error indicative of slow update will necessarily be a StorageError. As in ListBlobs. 382 if err != nil && err.Error() == code { 383 time.Sleep(time.Second * 30) 384 err = testImplFunc(c, bsu) 385 c.Assert(err, chk.IsNil) 386 } 387} 388 389func enableSoftDelete(c *chk.C, bsu ServiceURL) { 390 days := int32(1) 391 _, err := bsu.SetProperties(ctx, StorageServiceProperties{DeleteRetentionPolicy: &RetentionPolicy{Enabled: true, Days: &days}}) 392 c.Assert(err, chk.IsNil) 393} 394 395func disableSoftDelete(c *chk.C, bsu ServiceURL) { 396 _, err := bsu.SetProperties(ctx, StorageServiceProperties{DeleteRetentionPolicy: &RetentionPolicy{Enabled: false}}) 397 c.Assert(err, chk.IsNil) 398} 399 400func validateUpload(c *chk.C, blobURL BlockBlobURL) { 401 resp, err := blobURL.Download(ctx, 0, 0, BlobAccessConditions{}, false, ClientProvidedKeyOptions{}) 402 c.Assert(err, chk.IsNil) 403 data, _ := ioutil.ReadAll(resp.Response().Body) 404 c.Assert(data, chk.HasLen, 0) 405} 406