github.com/thiagoyeds/go-cloud@v0.26.0/blob/s3blob/s3blob_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 s3blob
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"net/http"
    22  	"testing"
    23  
    24  	s3managerv2 "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
    25  	s3v2 "github.com/aws/aws-sdk-go-v2/service/s3"
    26  	typesv2 "github.com/aws/aws-sdk-go-v2/service/s3/types"
    27  	"github.com/aws/aws-sdk-go/aws"
    28  	"github.com/aws/aws-sdk-go/aws/awserr"
    29  	"github.com/aws/aws-sdk-go/aws/client"
    30  	"github.com/aws/aws-sdk-go/aws/session"
    31  	"github.com/aws/aws-sdk-go/service/s3"
    32  	"github.com/aws/aws-sdk-go/service/s3/s3manager"
    33  	"github.com/aws/smithy-go"
    34  	"gocloud.dev/blob"
    35  	"gocloud.dev/blob/driver"
    36  	"gocloud.dev/blob/drivertest"
    37  	"gocloud.dev/internal/testing/setup"
    38  )
    39  
    40  // These constants record the region & bucket used for the last --record.
    41  // If you want to use --record mode,
    42  // 1. Create a bucket in your AWS project from the S3 management console.
    43  //    https://s3.console.aws.amazon.com/s3/home.
    44  // 2. Update this constant to your bucket name.
    45  // TODO(issue #300): Use Terraform to provision a bucket, and get the bucket
    46  //    name from the Terraform output instead (saving a copy of it for replay).
    47  const (
    48  	bucketName = "go-cloud-testing"
    49  	region     = "us-west-1"
    50  )
    51  
    52  type harness struct {
    53  	useV2    bool
    54  	session  *session.Session
    55  	clientV2 *s3v2.Client
    56  	opts     *Options
    57  	rt       http.RoundTripper
    58  	closer   func()
    59  }
    60  
    61  func newHarness(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
    62  	sess, rt, done, _ := setup.NewAWSSession(ctx, t, region)
    63  	return &harness{useV2: false, session: sess, opts: nil, rt: rt, closer: done}, nil
    64  }
    65  
    66  func newHarnessUsingLegacyList(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
    67  	sess, rt, done, _ := setup.NewAWSSession(ctx, t, region)
    68  	return &harness{useV2: false, session: sess, opts: &Options{UseLegacyList: true}, rt: rt, closer: done}, nil
    69  }
    70  
    71  func newHarnessV2(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
    72  	cfg, rt, done, _ := setup.NewAWSv2Config(ctx, t, region)
    73  	return &harness{useV2: true, clientV2: s3v2.NewFromConfig(cfg), opts: nil, rt: rt, closer: done}, nil
    74  }
    75  
    76  func newHarnessUsingLegacyListV2(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
    77  	cfg, rt, done, _ := setup.NewAWSv2Config(ctx, t, region)
    78  	return &harness{useV2: true, clientV2: s3v2.NewFromConfig(cfg), opts: &Options{UseLegacyList: true}, rt: rt, closer: done}, nil
    79  }
    80  
    81  func (h *harness) HTTPClient() *http.Client {
    82  	return &http.Client{Transport: h.rt}
    83  }
    84  
    85  func (h *harness) MakeDriver(ctx context.Context) (driver.Bucket, error) {
    86  	return openBucket(ctx, h.useV2, h.session, h.clientV2, bucketName, h.opts)
    87  }
    88  
    89  func (h *harness) MakeDriverForNonexistentBucket(ctx context.Context) (driver.Bucket, error) {
    90  	return openBucket(ctx, h.useV2, h.session, h.clientV2, "bucket-does-not-exist", h.opts)
    91  }
    92  
    93  func (h *harness) Close() {
    94  	h.closer()
    95  }
    96  
    97  func TestConformance(t *testing.T) {
    98  	drivertest.RunConformanceTests(t, newHarness, []drivertest.AsTest{verifyContentLanguage{useV2: false, usingLegacyList: false}})
    99  }
   100  
   101  func TestConformanceUsingLegacyList(t *testing.T) {
   102  	drivertest.RunConformanceTests(t, newHarnessUsingLegacyList, []drivertest.AsTest{verifyContentLanguage{useV2: false, usingLegacyList: true}})
   103  }
   104  
   105  func TestConformanceV2(t *testing.T) {
   106  	drivertest.RunConformanceTests(t, newHarnessV2, []drivertest.AsTest{verifyContentLanguage{useV2: true, usingLegacyList: false}})
   107  }
   108  
   109  func TestConformanceUsingLegacyListV2(t *testing.T) {
   110  	drivertest.RunConformanceTests(t, newHarnessUsingLegacyListV2, []drivertest.AsTest{verifyContentLanguage{useV2: true, usingLegacyList: true}})
   111  }
   112  
   113  func BenchmarkS3blob(b *testing.B) {
   114  	sess, err := session.NewSession(&aws.Config{
   115  		Region: aws.String(region),
   116  	})
   117  	if err != nil {
   118  		b.Fatal(err)
   119  	}
   120  	bkt, err := OpenBucket(context.Background(), sess, bucketName, nil)
   121  	if err != nil {
   122  		b.Fatal(err)
   123  	}
   124  	drivertest.RunBenchmarks(b, bkt)
   125  }
   126  
   127  const language = "nl"
   128  
   129  // verifyContentLanguage uses As to access the underlying GCS types and
   130  // read/write the ContentLanguage field.
   131  type verifyContentLanguage struct {
   132  	useV2           bool
   133  	usingLegacyList bool
   134  }
   135  
   136  func (verifyContentLanguage) Name() string {
   137  	return "verify ContentLanguage can be written and read through As"
   138  }
   139  
   140  func (v verifyContentLanguage) BucketCheck(b *blob.Bucket) error {
   141  	if v.useV2 {
   142  		var client *s3v2.Client
   143  		if !b.As(&client) {
   144  			return errors.New("Bucket.As failed")
   145  		}
   146  		return nil
   147  	}
   148  	var client *s3.S3
   149  	if !b.As(&client) {
   150  		return errors.New("Bucket.As failed")
   151  	}
   152  	return nil
   153  }
   154  
   155  func (v verifyContentLanguage) ErrorCheck(b *blob.Bucket, err error) error {
   156  	if v.useV2 {
   157  		var e smithy.APIError
   158  		if !b.ErrorAs(err, &e) {
   159  			return errors.New("blob.ErrorAs failed")
   160  		}
   161  	} else {
   162  		var e awserr.Error
   163  		if !b.ErrorAs(err, &e) {
   164  			return errors.New("blob.ErrorAs failed")
   165  		}
   166  	}
   167  	return nil
   168  }
   169  
   170  func (v verifyContentLanguage) BeforeRead(as func(interface{}) bool) error {
   171  	if v.useV2 {
   172  		var req *s3v2.GetObjectInput
   173  		if !as(&req) {
   174  			return errors.New("BeforeRead As failed")
   175  		}
   176  		return nil
   177  	}
   178  	var req *s3.GetObjectInput
   179  	if !as(&req) {
   180  		return errors.New("BeforeRead As failed")
   181  	}
   182  	return nil
   183  }
   184  
   185  func (v verifyContentLanguage) BeforeWrite(as func(interface{}) bool) error {
   186  	if v.useV2 {
   187  		var req *s3v2.PutObjectInput
   188  		if !as(&req) {
   189  			return errors.New("Writer.As failed for PutObjectInput")
   190  		}
   191  		req.ContentLanguage = aws.String(language)
   192  		var u *s3managerv2.Uploader
   193  		if !as(&u) {
   194  			return errors.New("Writer.As failed for Uploader")
   195  		}
   196  		return nil
   197  	}
   198  	var req *s3manager.UploadInput
   199  	if !as(&req) {
   200  		return errors.New("Writer.As failed for UploadInput")
   201  	}
   202  	req.ContentLanguage = aws.String(language)
   203  	var u *s3manager.Uploader
   204  	if !as(&u) {
   205  		return errors.New("Writer.As failed for Uploader")
   206  	}
   207  	return nil
   208  }
   209  
   210  func (v verifyContentLanguage) BeforeCopy(as func(interface{}) bool) error {
   211  	if v.useV2 {
   212  		var in *s3v2.CopyObjectInput
   213  		if !as(&in) {
   214  			return errors.New("BeforeCopy.As failed")
   215  		}
   216  		return nil
   217  	}
   218  	var in *s3.CopyObjectInput
   219  	if !as(&in) {
   220  		return errors.New("BeforeCopy.As failed")
   221  	}
   222  	return nil
   223  }
   224  
   225  func (v verifyContentLanguage) BeforeList(as func(interface{}) bool) error {
   226  	if v.useV2 {
   227  		if v.usingLegacyList {
   228  			var req *s3v2.ListObjectsInput
   229  			if !as(&req) {
   230  				return errors.New("List.As failed")
   231  			}
   232  		} else {
   233  			var req *s3v2.ListObjectsV2Input
   234  			if !as(&req) {
   235  				return errors.New("List.As failed")
   236  			}
   237  		}
   238  		return nil
   239  	}
   240  	if v.usingLegacyList {
   241  		var req *s3.ListObjectsInput
   242  		if !as(&req) {
   243  			return errors.New("List.As failed")
   244  		}
   245  	} else {
   246  		var req *s3.ListObjectsV2Input
   247  		if !as(&req) {
   248  			return errors.New("List.As failed")
   249  		}
   250  	}
   251  	return nil
   252  }
   253  
   254  func (v verifyContentLanguage) BeforeSign(as func(interface{}) bool) error {
   255  	if v.useV2 {
   256  		var (
   257  			get *s3v2.GetObjectInput
   258  			put *s3v2.PutObjectInput
   259  			del *s3v2.DeleteObjectInput
   260  		)
   261  		if as(&get) || as(&put) || as(&del) {
   262  			return nil
   263  		}
   264  		return errors.New("BeforeSign.As failed")
   265  	}
   266  	var (
   267  		get *s3.GetObjectInput
   268  		put *s3.PutObjectInput
   269  		del *s3.DeleteObjectInput
   270  	)
   271  	if as(&get) || as(&put) || as(&del) {
   272  		return nil
   273  	}
   274  	return errors.New("BeforeSign.As failed")
   275  }
   276  
   277  func (v verifyContentLanguage) AttributesCheck(attrs *blob.Attributes) error {
   278  	if v.useV2 {
   279  		var hoo s3v2.HeadObjectOutput
   280  		if !attrs.As(&hoo) {
   281  			return errors.New("Attributes.As returned false")
   282  		}
   283  		if got := *hoo.ContentLanguage; got != language {
   284  			return fmt.Errorf("got %q want %q", got, language)
   285  		}
   286  		return nil
   287  	}
   288  	var hoo s3.HeadObjectOutput
   289  	if !attrs.As(&hoo) {
   290  		return errors.New("Attributes.As returned false")
   291  	}
   292  	if got := *hoo.ContentLanguage; got != language {
   293  		return fmt.Errorf("got %q want %q", got, language)
   294  	}
   295  	return nil
   296  }
   297  
   298  func (v verifyContentLanguage) ReaderCheck(r *blob.Reader) error {
   299  	if v.useV2 {
   300  		var goo s3v2.GetObjectOutput
   301  		if !r.As(&goo) {
   302  			return errors.New("Reader.As returned false")
   303  		}
   304  		if got := *goo.ContentLanguage; got != language {
   305  			return fmt.Errorf("got %q want %q", got, language)
   306  		}
   307  		return nil
   308  	}
   309  	var goo s3.GetObjectOutput
   310  	if !r.As(&goo) {
   311  		return errors.New("Reader.As returned false")
   312  	}
   313  	if got := *goo.ContentLanguage; got != language {
   314  		return fmt.Errorf("got %q want %q", got, language)
   315  	}
   316  	return nil
   317  }
   318  
   319  func (v verifyContentLanguage) ListObjectCheck(o *blob.ListObject) error {
   320  	if v.useV2 {
   321  		if o.IsDir {
   322  			var commonPrefix typesv2.CommonPrefix
   323  			if !o.As(&commonPrefix) {
   324  				return errors.New("ListObject.As for directory returned false")
   325  			}
   326  			return nil
   327  		}
   328  		var obj typesv2.Object
   329  		if !o.As(&obj) {
   330  			return errors.New("ListObject.As for object returned false")
   331  		}
   332  		if obj.Key == nil || o.Key != *obj.Key {
   333  			return errors.New("ListObject.As for object returned a different item")
   334  		}
   335  		return nil
   336  	}
   337  	if o.IsDir {
   338  		var commonPrefix s3.CommonPrefix
   339  		if !o.As(&commonPrefix) {
   340  			return errors.New("ListObject.As for directory returned false")
   341  		}
   342  		return nil
   343  	}
   344  	var obj s3.Object
   345  	if !o.As(&obj) {
   346  		return errors.New("ListObject.As for object returned false")
   347  	}
   348  	if obj.Key == nil || o.Key != *obj.Key {
   349  		return errors.New("ListObject.As for object returned a different item")
   350  	}
   351  	return nil
   352  }
   353  
   354  func TestOpenBucket(t *testing.T) {
   355  	tests := []struct {
   356  		description string
   357  		useV2       bool
   358  		bucketName  string
   359  		nilClient   bool
   360  		want        string
   361  		wantErr     bool
   362  	}{
   363  		{
   364  			description: "empty bucket name results in error",
   365  			wantErr:     true,
   366  		},
   367  		{
   368  			description: "empty bucket name results in error V2",
   369  			useV2:       true,
   370  			wantErr:     true,
   371  		},
   372  		{
   373  			description: "nil client results in error",
   374  			bucketName:  "foo",
   375  			nilClient:   true,
   376  			wantErr:     true,
   377  		},
   378  		{
   379  			description: "nil client results in error V2",
   380  			bucketName:  "foo",
   381  			useV2:       true,
   382  			nilClient:   true,
   383  			wantErr:     true,
   384  		},
   385  		{
   386  			description: "success",
   387  			bucketName:  "foo",
   388  			want:        "foo",
   389  		},
   390  		{
   391  			description: "success V2",
   392  			bucketName:  "foo",
   393  			useV2:       true,
   394  			want:        "foo",
   395  		},
   396  	}
   397  
   398  	ctx := context.Background()
   399  	for _, test := range tests {
   400  		t.Run(test.description, func(t *testing.T) {
   401  			var sess client.ConfigProvider
   402  			var clientV2 *s3v2.Client
   403  			if !test.nilClient {
   404  				if test.useV2 {
   405  					cfg, _, done, _ := setup.NewAWSv2Config(ctx, t, region)
   406  					defer done()
   407  					clientV2 = s3v2.NewFromConfig(cfg)
   408  				} else {
   409  					s, _, done, _ := setup.NewAWSSession(ctx, t, region)
   410  					defer done()
   411  					sess = s
   412  				}
   413  			}
   414  
   415  			// Create driver impl.
   416  			drv, err := openBucket(ctx, test.useV2, sess, clientV2, test.bucketName, nil)
   417  			if (err != nil) != test.wantErr {
   418  				t.Errorf("got err %v want error %v", err, test.wantErr)
   419  			}
   420  			if err == nil && drv != nil && drv.name != test.want {
   421  				t.Errorf("got %q want %q", drv.name, test.want)
   422  			}
   423  
   424  			// Create portable type.
   425  			var b *blob.Bucket
   426  			if test.useV2 {
   427  				b, err = OpenBucketV2(ctx, clientV2, test.bucketName, nil)
   428  			} else {
   429  				b, err = OpenBucket(ctx, sess, test.bucketName, nil)
   430  			}
   431  			if b != nil {
   432  				defer b.Close()
   433  			}
   434  			if (err != nil) != test.wantErr {
   435  				t.Errorf("got err %v want error %v", err, test.wantErr)
   436  			}
   437  		})
   438  	}
   439  }
   440  
   441  func TestOpenBucketFromURL(t *testing.T) {
   442  	tests := []struct {
   443  		URL     string
   444  		WantErr bool
   445  	}{
   446  		// OK.
   447  		{"s3://mybucket", false},
   448  		// OK, setting region.
   449  		{"s3://mybucket?region=us-west1", false},
   450  		// OK, setting profile.
   451  		{"s3://mybucket?profile=main", false},
   452  		// OK, setting both profile and region.
   453  		{"s3://mybucket?profile=main&region=us-west-1", false},
   454  		// OK, use V2.
   455  		{"s3://mybucket?awssdk=2", false},
   456  		// Invalid parameter together with a valid one.
   457  		{"s3://mybucket?profile=main&param=value", true},
   458  		// Invalid parameter.
   459  		{"s3://mybucket?param=value", true},
   460  	}
   461  
   462  	ctx := context.Background()
   463  	for _, test := range tests {
   464  		b, err := blob.OpenBucket(ctx, test.URL)
   465  		if b != nil {
   466  			defer b.Close()
   467  		}
   468  		if (err != nil) != test.WantErr {
   469  			t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr)
   470  		}
   471  	}
   472  }