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