package s3_test import ( "bytes" "crypto/md5" "fmt" "io/ioutil" "net" "net/http" "sort" "strings" "time" "github.com/goamz/goamz/aws" "github.com/goamz/goamz/s3" "github.com/goamz/goamz/testutil" . "gopkg.in/check.v1" ) // AmazonServer represents an Amazon S3 server. type AmazonServer struct { auth aws.Auth } func (s *AmazonServer) SetUp(c *C) { auth, err := aws.EnvAuth() if err != nil { c.Fatal(err.Error()) } s.auth = auth } var _ = Suite(&AmazonClientSuite{Region: aws.USEast}) var _ = Suite(&AmazonClientSuite{Region: aws.EUWest}) var _ = Suite(&AmazonDomainClientSuite{Region: aws.USEast}) // AmazonClientSuite tests the client against a live S3 server. type AmazonClientSuite struct { aws.Region srv AmazonServer ClientTests } func (s *AmazonClientSuite) SetUpSuite(c *C) { if !testutil.Amazon { c.Skip("live tests against AWS disabled (no -amazon)") } s.srv.SetUp(c) s.s3 = s3.New(s.srv.auth, s.Region) // In case tests were interrupted in the middle before. s.ClientTests.Cleanup() } func (s *AmazonClientSuite) TearDownTest(c *C) { s.ClientTests.Cleanup() } // AmazonDomainClientSuite tests the client against a live S3 // server using bucket names in the endpoint domain name rather // than the request path. type AmazonDomainClientSuite struct { aws.Region srv AmazonServer ClientTests } func (s *AmazonDomainClientSuite) SetUpSuite(c *C) { if !testutil.Amazon { c.Skip("live tests against AWS disabled (no -amazon)") } s.srv.SetUp(c) region := s.Region region.S3BucketEndpoint = "https://${bucket}.s3.amazonaws.com" s.s3 = s3.New(s.srv.auth, region) s.ClientTests.Cleanup() } func (s *AmazonDomainClientSuite) TearDownTest(c *C) { s.ClientTests.Cleanup() } // ClientTests defines integration tests designed to test the client. // It is not used as a test suite in itself, but embedded within // another type. type ClientTests struct { s3 *s3.S3 authIsBroken bool } func (s *ClientTests) Cleanup() { killBucket(testBucket(s.s3)) } func testBucket(s *s3.S3) *s3.Bucket { // Watch out! If this function is corrupted and made to match with something // people own, killBucket will happily remove *everything* inside the bucket. key := s.Auth.AccessKey if len(key) >= 8 { key = s.Auth.AccessKey[:8] } return s.Bucket(fmt.Sprintf("goamz-%s-%s", s.Region.Name, key)) } var attempts = aws.AttemptStrategy{ Min: 5, Total: 20 * time.Second, Delay: 100 * time.Millisecond, } func killBucket(b *s3.Bucket) { var err error for attempt := attempts.Start(); attempt.Next(); { err = b.DelBucket() if err == nil { return } if _, ok := err.(*net.DNSError); ok { return } e, ok := err.(*s3.Error) if ok && e.Code == "NoSuchBucket" { return } if ok && e.Code == "BucketNotEmpty" { // Errors are ignored here. Just retry. resp, err := b.List("", "", "", 1000) if err == nil { for _, key := range resp.Contents { _ = b.Del(key.Key) } } multis, _, _ := b.ListMulti("", "") for _, m := range multis { _ = m.Abort() } } } message := "cannot delete test bucket" if err != nil { message += ": " + err.Error() } panic(message) } func get(url string) ([]byte, error) { for attempt := attempts.Start(); attempt.Next(); { resp, err := http.Get(url) if err != nil { if attempt.HasNext() { continue } return nil, err } data, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { if attempt.HasNext() { continue } return nil, err } return data, err } panic("unreachable") } func (s *ClientTests) TestBasicFunctionality(c *C) { b := testBucket(s.s3) err := b.PutBucket(s3.PublicRead) c.Assert(err, IsNil) err = b.Put("name", []byte("yo!"), "text/plain", s3.PublicRead, s3.Options{}) c.Assert(err, IsNil) defer b.Del("name") data, err := b.Get("name") c.Assert(err, IsNil) c.Assert(string(data), Equals, "yo!") data, err = get(b.URL("name")) c.Assert(err, IsNil) c.Assert(string(data), Equals, "yo!") buf := bytes.NewBufferString("hey!") err = b.PutReader("name2", buf, int64(buf.Len()), "text/plain", s3.Private, s3.Options{}) c.Assert(err, IsNil) defer b.Del("name2") rc, err := b.GetReader("name2") c.Assert(err, IsNil) data, err = ioutil.ReadAll(rc) c.Check(err, IsNil) c.Check(string(data), Equals, "hey!") rc.Close() data, err = get(b.SignedURL("name2", time.Now().Add(time.Hour))) c.Assert(err, IsNil) c.Assert(string(data), Equals, "hey!") if !s.authIsBroken { data, err = get(b.SignedURL("name2", time.Now().Add(-time.Hour))) c.Assert(err, IsNil) c.Assert(string(data), Matches, "(?s).*AccessDenied.*") } err = b.DelBucket() c.Assert(err, NotNil) s3err, ok := err.(*s3.Error) c.Assert(ok, Equals, true) c.Assert(s3err.Code, Equals, "BucketNotEmpty") c.Assert(s3err.BucketName, Equals, b.Name) c.Assert(s3err.Message, Equals, "The bucket you tried to delete is not empty") err = b.Del("name") c.Assert(err, IsNil) err = b.Del("name2") c.Assert(err, IsNil) err = b.DelBucket() c.Assert(err, IsNil) } func (s *ClientTests) TestGetNotFound(c *C) { b := s.s3.Bucket("goamz-" + s.s3.Auth.AccessKey) data, err := b.Get("non-existent") s3err, _ := err.(*s3.Error) c.Assert(s3err, NotNil) c.Assert(s3err.StatusCode, Equals, 404) c.Assert(s3err.Code, Equals, "NoSuchBucket") c.Assert(s3err.Message, Equals, "The specified bucket does not exist") c.Assert(data, IsNil) } // Communicate with all endpoints to see if they are alive. func (s *ClientTests) TestRegions(c *C) { errs := make(chan error, len(aws.Regions)) for _, region := range aws.Regions { go func(r aws.Region) { s := s3.New(s.s3.Auth, r) b := s.Bucket("goamz-" + s.Auth.AccessKey) _, err := b.Get("non-existent") errs <- err }(region) } for _ = range aws.Regions { err := <-errs if err != nil { s3_err, ok := err.(*s3.Error) if ok { c.Check(s3_err.Code, Matches, "NoSuchBucket") } else if _, ok = err.(*net.DNSError); ok { // Okay as well. } else { c.Errorf("Non-S3 error: %s", err) } } else { c.Errorf("Test should have errored but it seems to have succeeded") } } } var objectNames = []string{ "index.html", "index2.html", "photos/2006/February/sample2.jpg", "photos/2006/February/sample3.jpg", "photos/2006/February/sample4.jpg", "photos/2006/January/sample.jpg", "test/bar", "test/foo", } func keys(names ...string) []s3.Key { ks := make([]s3.Key, len(names)) for i, name := range names { ks[i].Key = name } return ks } // As the ListResp specifies all the parameters to the // request too, we use it to specify request parameters // and expected results. The Contents field is // used only for the key names inside it. var listTests = []s3.ListResp{ // normal list. { Contents: keys(objectNames...), }, { Marker: objectNames[0], Contents: keys(objectNames[1:]...), }, { Marker: objectNames[0] + "a", Contents: keys(objectNames[1:]...), }, { Marker: "z", }, // limited results. { MaxKeys: 2, Contents: keys(objectNames[0:2]...), IsTruncated: true, }, { MaxKeys: 2, Marker: objectNames[0], Contents: keys(objectNames[1:3]...), IsTruncated: true, }, { MaxKeys: 2, Marker: objectNames[len(objectNames)-2], Contents: keys(objectNames[len(objectNames)-1:]...), }, // with delimiter { Delimiter: "/", CommonPrefixes: []string{"photos/", "test/"}, Contents: keys("index.html", "index2.html"), }, { Delimiter: "/", Prefix: "photos/2006/", CommonPrefixes: []string{"photos/2006/February/", "photos/2006/January/"}, }, { Delimiter: "/", Prefix: "t", CommonPrefixes: []string{"test/"}, }, { Delimiter: "/", MaxKeys: 1, Contents: keys("index.html"), IsTruncated: true, }, { Delimiter: "/", MaxKeys: 1, Marker: "index2.html", CommonPrefixes: []string{"photos/"}, IsTruncated: true, }, { Delimiter: "/", MaxKeys: 1, Marker: "photos/", CommonPrefixes: []string{"test/"}, IsTruncated: false, }, { Delimiter: "Feb", CommonPrefixes: []string{"photos/2006/Feb"}, Contents: keys("index.html", "index2.html", "photos/2006/January/sample.jpg", "test/bar", "test/foo"), }, } func (s *ClientTests) TestDoublePutBucket(c *C) { b := testBucket(s.s3) err := b.PutBucket(s3.PublicRead) c.Assert(err, IsNil) err = b.PutBucket(s3.PublicRead) if err != nil { c.Assert(err, FitsTypeOf, new(s3.Error)) c.Assert(err.(*s3.Error).Code, Equals, "BucketAlreadyOwnedByYou") } } func (s *ClientTests) TestBucketList(c *C) { b := testBucket(s.s3) err := b.PutBucket(s3.Private) c.Assert(err, IsNil) objData := make(map[string][]byte) for i, path := range objectNames { data := []byte(strings.Repeat("a", i)) err := b.Put(path, data, "text/plain", s3.Private, s3.Options{}) c.Assert(err, IsNil) defer b.Del(path) objData[path] = data } for i, t := range listTests { c.Logf("test %d", i) resp, err := b.List(t.Prefix, t.Delimiter, t.Marker, t.MaxKeys) c.Assert(err, IsNil) c.Check(resp.Name, Equals, b.Name) c.Check(resp.Delimiter, Equals, t.Delimiter) c.Check(resp.IsTruncated, Equals, t.IsTruncated) c.Check(resp.CommonPrefixes, DeepEquals, t.CommonPrefixes) checkContents(c, resp.Contents, objData, t.Contents) } } func etag(data []byte) string { sum := md5.New() sum.Write(data) return fmt.Sprintf(`"%x"`, sum.Sum(nil)) } func checkContents(c *C, contents []s3.Key, data map[string][]byte, expected []s3.Key) { c.Assert(contents, HasLen, len(expected)) for i, k := range contents { c.Check(k.Key, Equals, expected[i].Key) // TODO mtime c.Check(k.Size, Equals, int64(len(data[k.Key]))) c.Check(k.ETag, Equals, etag(data[k.Key])) } } func (s *ClientTests) TestMultiInitPutList(c *C) { b := testBucket(s.s3) err := b.PutBucket(s3.Private) c.Assert(err, IsNil) multi, err := b.InitMulti("multi", "text/plain", s3.Private) c.Assert(err, IsNil) c.Assert(multi.UploadId, Matches, ".+") defer multi.Abort() var sent []s3.Part for i := 0; i < 5; i++ { p, err := multi.PutPart(i+1, strings.NewReader(fmt.Sprintf("", i+1))) c.Assert(err, IsNil) c.Assert(p.N, Equals, i+1) c.Assert(p.Size, Equals, int64(8)) c.Assert(p.ETag, Matches, ".+") sent = append(sent, p) } s3.SetListPartsMax(2) parts, err := multi.ListParts() c.Assert(err, IsNil) c.Assert(parts, HasLen, len(sent)) for i := range parts { c.Assert(parts[i].N, Equals, sent[i].N) c.Assert(parts[i].Size, Equals, sent[i].Size) c.Assert(parts[i].ETag, Equals, sent[i].ETag) } err = multi.Complete(parts) s3err, failed := err.(*s3.Error) c.Assert(failed, Equals, true) c.Assert(s3err.Code, Equals, "EntityTooSmall") err = multi.Abort() c.Assert(err, IsNil) _, err = multi.ListParts() s3err, ok := err.(*s3.Error) c.Assert(ok, Equals, true) c.Assert(s3err.Code, Equals, "NoSuchUpload") } // This may take a minute or more due to the minimum size accepted S3 // on multipart upload parts. func (s *ClientTests) TestMultiComplete(c *C) { b := testBucket(s.s3) err := b.PutBucket(s3.Private) c.Assert(err, IsNil) multi, err := b.InitMulti("multi", "text/plain", s3.Private) c.Assert(err, IsNil) c.Assert(multi.UploadId, Matches, ".+") defer multi.Abort() // Minimum size S3 accepts for all but the last part is 5MB. data1 := make([]byte, 5*1024*1024) data2 := []byte("") part1, err := multi.PutPart(1, bytes.NewReader(data1)) c.Assert(err, IsNil) part2, err := multi.PutPart(2, bytes.NewReader(data2)) c.Assert(err, IsNil) // Purposefully reversed. The order requirement must be handled. err = multi.Complete([]s3.Part{part2, part1}) c.Assert(err, IsNil) data, err := b.Get("multi") c.Assert(err, IsNil) c.Assert(len(data), Equals, len(data1)+len(data2)) for i := range data1 { if data[i] != data1[i] { c.Fatalf("uploaded object at byte %d: want %d, got %d", data1[i], data[i]) } } c.Assert(string(data[len(data1):]), Equals, string(data2)) } type multiList []*s3.Multi func (l multiList) Len() int { return len(l) } func (l multiList) Less(i, j int) bool { return l[i].Key < l[j].Key } func (l multiList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func (s *ClientTests) TestListMulti(c *C) { b := testBucket(s.s3) err := b.PutBucket(s3.Private) c.Assert(err, IsNil) // Ensure an empty state before testing its behavior. multis, _, err := b.ListMulti("", "") for _, m := range multis { err := m.Abort() c.Assert(err, IsNil) } keys := []string{ "a/multi2", "a/multi3", "b/multi4", "multi1", } for _, key := range keys { m, err := b.InitMulti(key, "", s3.Private) c.Assert(err, IsNil) defer m.Abort() } // Amazon's implementation of the multiple-request listing for // multipart uploads in progress seems broken in multiple ways. // (next tokens are not provided, etc). //s3.SetListMultiMax(2) multis, prefixes, err := b.ListMulti("", "") c.Assert(err, IsNil) for attempt := attempts.Start(); attempt.Next() && len(multis) < len(keys); { multis, prefixes, err = b.ListMulti("", "") c.Assert(err, IsNil) } sort.Sort(multiList(multis)) c.Assert(prefixes, IsNil) var gotKeys []string for _, m := range multis { gotKeys = append(gotKeys, m.Key) } c.Assert(gotKeys, DeepEquals, keys) for _, m := range multis { c.Assert(m.Bucket, Equals, b) c.Assert(m.UploadId, Matches, ".+") } multis, prefixes, err = b.ListMulti("", "/") for attempt := attempts.Start(); attempt.Next() && len(prefixes) < 2; { multis, prefixes, err = b.ListMulti("", "") c.Assert(err, IsNil) } c.Assert(err, IsNil) c.Assert(prefixes, DeepEquals, []string{"a/", "b/"}) c.Assert(multis, HasLen, 1) c.Assert(multis[0].Bucket, Equals, b) c.Assert(multis[0].Key, Equals, "multi1") c.Assert(multis[0].UploadId, Matches, ".+") for attempt := attempts.Start(); attempt.Next() && len(multis) < 2; { multis, prefixes, err = b.ListMulti("", "") c.Assert(err, IsNil) } multis, prefixes, err = b.ListMulti("a/", "/") c.Assert(err, IsNil) c.Assert(prefixes, IsNil) c.Assert(multis, HasLen, 2) c.Assert(multis[0].Bucket, Equals, b) c.Assert(multis[0].Key, Equals, "a/multi2") c.Assert(multis[0].UploadId, Matches, ".+") c.Assert(multis[1].Bucket, Equals, b) c.Assert(multis[1].Key, Equals, "a/multi3") c.Assert(multis[1].UploadId, Matches, ".+") } func (s *ClientTests) TestMultiPutAllZeroLength(c *C) { b := testBucket(s.s3) err := b.PutBucket(s3.Private) c.Assert(err, IsNil) multi, err := b.InitMulti("multi", "text/plain", s3.Private) c.Assert(err, IsNil) defer multi.Abort() // This tests an edge case. Amazon requires at least one // part for multiprat uploads to work, even the part is empty. parts, err := multi.PutAll(strings.NewReader(""), 5*1024*1024) c.Assert(err, IsNil) c.Assert(parts, HasLen, 1) c.Assert(parts[0].Size, Equals, int64(0)) c.Assert(parts[0].ETag, Equals, `"d41d8cd98f00b204e9800998ecf8427e"`) err = multi.Complete(parts) c.Assert(err, IsNil) }