package s3 import ( "context" "errors" "io" "net/http" "strings" "testing" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/s3/types" ) // This file replicates the tests in https://github.com/smithy-lang/smithy/blob/main/smithy-aws-protocol-tests/model/restXml/services/s3.smithy, // which we cannot generate through normal protocoltest codegen due to // requirement on handwritten source in S3. type capturedRequest struct { r *http.Request } func (cr *capturedRequest) Do(r *http.Request) (*http.Response, error) { cr.r = r return &http.Response{ // returns are moot, for request tests only StatusCode: 400, Body: http.NoBody, }, nil } func TestS3Protocol_ListObjectsV2_Request(t *testing.T) { for name, tt := range map[string]struct { Options func(*Options) OperationOptions func(*Options) Input *ListObjectsV2Input ExpectMethod string ExpectHost string ExpectPath string ExpectQuery []string }{ "S3DefaultAddressing": { Options: func(o *Options) { o.Region = "us-west-2" }, OperationOptions: func(o *Options) {}, Input: &ListObjectsV2Input{ Bucket: aws.String("mybucket"), }, ExpectMethod: "GET", ExpectHost: "mybucket.s3.us-west-2.amazonaws.com", ExpectPath: "/", ExpectQuery: []string{"list-type=2"}, }, "S3VirtualHostAddressing": { Options: func(o *Options) { o.Region = "us-west-2" o.UsePathStyle = false }, OperationOptions: func(o *Options) {}, Input: &ListObjectsV2Input{ Bucket: aws.String("mybucket"), }, ExpectMethod: "GET", ExpectHost: "mybucket.s3.us-west-2.amazonaws.com", ExpectPath: "/", ExpectQuery: []string{"list-type=2"}, }, "S3PathAddressing": { Options: func(o *Options) { o.Region = "us-west-2" o.UsePathStyle = true }, OperationOptions: func(o *Options) {}, Input: &ListObjectsV2Input{ Bucket: aws.String("mybucket"), }, ExpectMethod: "GET", ExpectHost: "s3.us-west-2.amazonaws.com", ExpectPath: "/mybucket", ExpectQuery: []string{"list-type=2"}, }, "S3VirtualHostDualstackAddressing": { Options: func(o *Options) { o.Region = "us-west-2" o.UsePathStyle = false o.EndpointOptions.UseDualStackEndpoint = aws.DualStackEndpointStateEnabled }, OperationOptions: func(o *Options) {}, Input: &ListObjectsV2Input{ Bucket: aws.String("mybucket"), }, ExpectMethod: "GET", ExpectHost: "mybucket.s3.dualstack.us-west-2.amazonaws.com", ExpectPath: "/", ExpectQuery: []string{"list-type=2"}, }, "S3VirtualHostAccelerateAddressing": { Options: func(o *Options) { o.Region = "us-west-2" o.UsePathStyle = false o.UseAccelerate = true }, OperationOptions: func(o *Options) {}, Input: &ListObjectsV2Input{ Bucket: aws.String("mybucket"), }, ExpectMethod: "GET", ExpectHost: "mybucket.s3-accelerate.amazonaws.com", ExpectPath: "/", ExpectQuery: []string{"list-type=2"}, }, "S3VirtualHostDualstackAccelerateAddressing": { Options: func(o *Options) { o.Region = "us-west-2" o.UsePathStyle = false o.EndpointOptions.UseDualStackEndpoint = aws.DualStackEndpointStateEnabled o.UseAccelerate = true }, OperationOptions: func(o *Options) {}, Input: &ListObjectsV2Input{ Bucket: aws.String("mybucket"), }, ExpectMethod: "GET", ExpectHost: "mybucket.s3-accelerate.dualstack.amazonaws.com", ExpectPath: "/", ExpectQuery: []string{"list-type=2"}, }, "S3OperationAddressingPreferred": { Options: func(o *Options) { o.Region = "us-west-2" o.UsePathStyle = true }, OperationOptions: func(o *Options) { o.UsePathStyle = false }, Input: &ListObjectsV2Input{ Bucket: aws.String("mybucket"), }, ExpectMethod: "GET", ExpectHost: "mybucket.s3.us-west-2.amazonaws.com", ExpectPath: "/", ExpectQuery: []string{"list-type=2"}, }, } { t.Run(name, func(t *testing.T) { var r capturedRequest svc := New(Options{HTTPClient: &r}, tt.Options) svc.ListObjectsV2(context.Background(), tt.Input, tt.OperationOptions) if r.r == nil { t.Fatal("captured request is nil") } req := r.r if tt.ExpectMethod != req.Method { t.Errorf("expect method: %v != %v", tt.ExpectMethod, req.Method) } if tt.ExpectHost != req.URL.Host { t.Errorf("expect host: %v != %v", tt.ExpectHost, req.URL.Host) } if tt.ExpectPath != req.URL.RawPath { t.Errorf("expect path: %v != %v", tt.ExpectPath, req.URL.RawPath) } for _, q := range tt.ExpectQuery { if !strings.Contains(req.URL.RawQuery, q) { t.Errorf("query %v is missing %v", req.URL.RawQuery, q) } } }) } } func TestS3Protocol_DeleteObjectTagging_Request(t *testing.T) { for name, tt := range map[string]struct { ClientOptions func(*Options) OperationOptions func(*Options) Input *DeleteObjectTaggingInput ExpectMethod string ExpectHost string ExpectPath string ExpectQuery []string }{ "S3EscapeObjectKeyInUriLabel": { ClientOptions: func(o *Options) { o.Region = "us-west-2" }, OperationOptions: func(o *Options) {}, Input: &DeleteObjectTaggingInput{ Bucket: aws.String("mybucket"), Key: aws.String("my key.txt"), }, ExpectMethod: "DELETE", ExpectHost: "mybucket.s3.us-west-2.amazonaws.com", ExpectPath: "/my%20key.txt", ExpectQuery: []string{"tagging"}, }, "S3EscapePathObjectKeyInUriLabel": { ClientOptions: func(o *Options) { o.Region = "us-west-2" }, OperationOptions: func(o *Options) {}, Input: &DeleteObjectTaggingInput{ Bucket: aws.String("mybucket"), Key: aws.String("foo/bar/my key.txt"), }, ExpectMethod: "DELETE", ExpectHost: "mybucket.s3.us-west-2.amazonaws.com", ExpectPath: "/foo/bar/my%20key.txt", ExpectQuery: []string{"tagging"}, }, } { t.Run(name, func(t *testing.T) { var r capturedRequest svc := New(Options{HTTPClient: &r}, tt.ClientOptions) svc.DeleteObjectTagging(context.Background(), tt.Input, tt.OperationOptions) if r.r == nil { t.Fatal("captured request is nil") } req := r.r if tt.ExpectMethod != req.Method { t.Errorf("expect method: %v != %v", tt.ExpectMethod, req.Method) } if tt.ExpectHost != req.URL.Host { t.Errorf("expect host: %v != %v", tt.ExpectHost, req.URL.Host) } if tt.ExpectPath != req.URL.RawPath { t.Errorf("expect path: %v != %v", tt.ExpectPath, req.URL.RawPath) } for _, q := range tt.ExpectQuery { if !strings.Contains(req.URL.RawQuery, q) { t.Errorf("query %v is missing %v", req.URL.RawQuery, q) } } }) } } func TestS3Protocol_GetObject_Request(t *testing.T) { for name, tt := range map[string]struct { ClientOptions func(*Options) OperationOptions func(*Options) Input *GetObjectInput ExpectMethod string ExpectHost string ExpectPath string ExpectQuery []string }{ "S3PreservesLeadingDotSegmentInUriLabel": { ClientOptions: func(o *Options) { o.Region = "us-west-2" o.UsePathStyle = false }, OperationOptions: func(o *Options) {}, Input: &GetObjectInput{ Bucket: aws.String("mybucket"), Key: aws.String("../key.txt"), }, ExpectMethod: "GET", ExpectHost: "mybucket.s3.us-west-2.amazonaws.com", ExpectPath: "/../key.txt", }, "S3PreservesEmbeddedDotSegmentInUriLabel": { ClientOptions: func(o *Options) { o.Region = "us-west-2" o.UsePathStyle = false }, OperationOptions: func(o *Options) {}, Input: &GetObjectInput{ Bucket: aws.String("mybucket"), Key: aws.String("foo/../key.txt"), }, ExpectMethod: "GET", ExpectHost: "mybucket.s3.us-west-2.amazonaws.com", ExpectPath: "/foo/../key.txt", }, } { t.Run(name, func(t *testing.T) { var r capturedRequest svc := New(Options{HTTPClient: &r}, tt.ClientOptions) svc.GetObject(context.Background(), tt.Input, tt.OperationOptions) if r.r == nil { t.Fatal("captured request is nil") } req := r.r if tt.ExpectMethod != req.Method { t.Errorf("expect method: %v != %v", tt.ExpectMethod, req.Method) } if tt.ExpectHost != req.URL.Host { t.Errorf("expect host: %v != %v", tt.ExpectHost, req.URL.Host) } if tt.ExpectPath != req.URL.RawPath { t.Errorf("expect path: %v != %v", tt.ExpectPath, req.URL.RawPath) } for _, q := range tt.ExpectQuery { if !strings.Contains(req.URL.RawQuery, q) { t.Errorf("query %v is missing %v", req.URL.RawQuery, q) } } }) } } type mockHTTPResponse struct { resp *http.Response } func (m *mockHTTPResponse) Do(r *http.Request) (*http.Response, error) { return m.resp, nil } func TestS3Protocol_GetBucketLocation_Response(t *testing.T) { for name, tt := range map[string]struct { Response *http.Response Expect *GetBucketLocationOutput }{ "GetBucketLocationUnwrappedOutput": { Response: &http.Response{ StatusCode: 200, Body: io.NopCloser(strings.NewReader("\nus-west-2")), }, Expect: &GetBucketLocationOutput{ LocationConstraint: types.BucketLocationConstraintUsWest2, }, }, } { t.Run(name, func(t *testing.T) { svc := New(Options{ Region: "us-west-2", HTTPClient: &mockHTTPResponse{tt.Response}, }) out, err := svc.GetBucketLocation(context.Background(), &GetBucketLocationInput{ Bucket: aws.String("bucket"), }) if err != nil { t.Fatalf("get bucket location: %v", err) } if tt.Expect.LocationConstraint != out.LocationConstraint { t.Errorf("LocationConstraint %v != %v", tt.Expect.LocationConstraint, out.LocationConstraint) } }) } } func TestS3Protocol_Error_NoSuchBucket(t *testing.T) { for name, tt := range map[string]struct { Response *http.Response }{ "GetBucketLocationUnwrappedOutput": { Response: &http.Response{ StatusCode: 400, Body: io.NopCloser(strings.NewReader("\n\n\tSender\n\tNoSuchBucket\n")), }, }, } { t.Run(name, func(t *testing.T) { svc := New(Options{ Region: "us-west-2", HTTPClient: &mockHTTPResponse{tt.Response}, }) _, err := svc.GetObject(context.Background(), &GetObjectInput{ Bucket: aws.String("bucket"), Key: aws.String("key"), }) if err == nil { t.Fatal("call operation: expected error, got none") } // of note: we don't actually return a *types.NoSuchBucket in this // case, but we DO capture the right error code var terr interface { ErrorCode() string } if !errors.As(err, &terr) { t.Errorf("error does not implement ErrorCode(), was %v", err) } if actual := terr.ErrorCode(); actual != "NoSuchBucket" { t.Errorf("error code, expected NoSuchBucket, was %v", actual) } }) } }