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®ion=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¶m=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 }