// Copyright 2023 Versity Software // This file is licensed under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package controllers import ( "bufio" "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "reflect" "strings" "testing" "time" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp" "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" "github.com/versity/versitygw/s3err" "github.com/versity/versitygw/s3response" ) var ( acl auth.ACL acldata []byte ) func init() { var err error acldata, err = json.Marshal(acl) if err != nil { panic(err) } } func TestNew(t *testing.T) { type args struct { be backend.Backend iam auth.IAMService } be := backend.BackendUnsupported{} tests := []struct { name string args args want S3ApiController }{ { name: "Initialize S3 api controller", args: args{ be: be, iam: &auth.IAMServiceInternal{}, }, want: S3ApiController{ be: be, iam: &auth.IAMServiceInternal{}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := New(tt.args.be, tt.args.iam, nil, nil); !reflect.DeepEqual(got, tt.want) { t.Errorf("New() = %v, want %v", got, tt.want) } }) } } func TestS3ApiController_ListBuckets(t *testing.T) { type args struct { req *http.Request } app := fiber.New() s3ApiController := S3ApiController{ be: &BackendMock{ ListBucketsFunc: func(context.Context, string, bool) (s3response.ListAllMyBucketsResult, error) { return s3response.ListAllMyBucketsResult{}, nil }, }, } app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access", Role: "admin:"}) ctx.Locals("isDebug", false) return ctx.Next() }) app.Get("/", s3ApiController.ListBuckets) // Error case appErr := fiber.New() s3ApiControllerErr := S3ApiController{ be: &BackendMock{ ListBucketsFunc: func(context.Context, string, bool) (s3response.ListAllMyBucketsResult, error) { return s3response.ListAllMyBucketsResult{}, s3err.GetAPIError(s3err.ErrMethodNotAllowed) }, }, } appErr.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access", Role: "admin:"}) ctx.Locals("isDebug", false) return ctx.Next() }) appErr.Get("/", s3ApiControllerErr.ListBuckets) tests := []struct { name string args args app *fiber.App wantErr bool statusCode int }{ { name: "List-bucket-method-not-allowed", args: args{ req: httptest.NewRequest(http.MethodGet, "/", nil), }, app: appErr, wantErr: false, statusCode: 405, }, { name: "list-bucket-success", args: args{ req: httptest.NewRequest(http.MethodGet, "/", nil), }, app: app, wantErr: false, statusCode: 200, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.ListBuckets() error = %v, wantErr %v", err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.ListBuckets() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } }) } } func getPtr(val string) *string { return &val } func TestS3ApiController_GetActions(t *testing.T) { type args struct { req *http.Request } now := time.Now() app := fiber.New() contentLength := int64(1000) s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, ListPartsFunc: func(context.Context, *s3.ListPartsInput) (s3response.ListPartsResult, error) { return s3response.ListPartsResult{}, nil }, GetObjectAclFunc: func(context.Context, *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) { return &s3.GetObjectAclOutput{}, nil }, GetObjectAttributesFunc: func(context.Context, *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) { return &s3.GetObjectAttributesOutput{}, nil }, GetObjectFunc: func(context.Context, *s3.GetObjectInput, io.Writer) (*s3.GetObjectOutput, error) { return &s3.GetObjectOutput{ Metadata: map[string]string{"hello": "world"}, ContentType: getPtr("application/xml"), ContentEncoding: getPtr("gzip"), ETag: getPtr("98sda7f97sa9df798sd79f8as9df"), ContentLength: &contentLength, LastModified: &now, StorageClass: "storage class", }, nil }, GetObjectTaggingFunc: func(_ context.Context, bucket, object string) (map[string]string, error) { return map[string]string{"hello": "world"}, nil }, }, } app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) app.Get("/:bucket/:key/*", s3ApiController.GetActions) // GetObjectAttributes success case getObjAttrs := httptest.NewRequest(http.MethodGet, "/my-bucket/key", nil) getObjAttrs.Header.Set("X-Amz-Object-Attributes", "hello") tests := []struct { name string app *fiber.App args args wantErr bool statusCode int }{ { name: "Get-actions-get-tags-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket/key/key.json?tagging", nil), }, wantErr: false, statusCode: 200, }, { name: "Get-actions-invalid-max-parts-string", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=invalid", nil), }, wantErr: false, statusCode: 400, }, { name: "Get-actions-invalid-max-parts-negative", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=-8", nil), }, wantErr: false, statusCode: 400, }, { name: "Get-actions-invalid-part-number-marker-string", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=200&part-number-marker=invalid", nil), }, wantErr: false, statusCode: 400, }, { name: "Get-actions-invalid-part-number-marker-negative", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=200&part-number-marker=-8", nil), }, wantErr: false, statusCode: 400, }, { name: "Get-actions-list-object-parts-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?uploadId=hello&max-parts=200&part-number-marker=23", nil), }, wantErr: false, statusCode: 200, }, { name: "Get-actions-get-object-acl-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket/key?acl", nil), }, wantErr: false, statusCode: 200, }, { name: "Get-actions-get-object-attributes-success", app: app, args: args{ req: getObjAttrs, }, wantErr: false, statusCode: 200, }, { name: "Get-actions-get-object-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket/key", nil), }, wantErr: false, statusCode: 200, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.GetActions() error = %v, wantErr %v", err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.GetActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } }) } } func TestS3ApiController_ListActions(t *testing.T) { type args struct { req *http.Request } app := fiber.New() s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, ListMultipartUploadsFunc: func(_ context.Context, output *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) { return s3response.ListMultipartUploadsResult{}, nil }, ListObjectsV2Func: func(context.Context, *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) { return &s3.ListObjectsV2Output{}, nil }, ListObjectsFunc: func(context.Context, *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) { return &s3.ListObjectsOutput{}, nil }, GetBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) (map[string]string, error) { return map[string]string{}, nil }, GetBucketVersioningFunc: func(contextMoqParam context.Context, bucket string) (*s3.GetBucketVersioningOutput, error) { return &s3.GetBucketVersioningOutput{}, nil }, ListObjectVersionsFunc: func(contextMoqParam context.Context, listObjectVersionsInput *s3.ListObjectVersionsInput) (*s3.ListObjectVersionsOutput, error) { return &s3.ListObjectVersionsOutput{}, nil }, GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { return []byte{}, nil }, }, } app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) app.Get("/:bucket", s3ApiController.ListActions) //Error case s3ApiControllerError := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, ListObjectsFunc: func(context.Context, *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) }, GetBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) (map[string]string, error) { return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) }, }, } appError := fiber.New() appError.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) appError.Get("/:bucket", s3ApiControllerError.ListActions) tests := []struct { name string app *fiber.App args args wantErr bool statusCode int }{ { name: "Get-bucket-tagging-non-existing-bucket", app: appError, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket?tagging", nil), }, wantErr: false, statusCode: 404, }, { name: "Get-bucket-tagging-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket?tagging", nil), }, wantErr: false, statusCode: 200, }, { name: "Get-bucket-acl-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket?acl=acl", nil), }, wantErr: false, statusCode: 200, }, { name: "List-Multipart-Upload-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket?uploads=uploads", nil), }, wantErr: false, statusCode: 200, }, { name: "List-Objects-V2-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket?list-type=2", nil), }, wantErr: false, statusCode: 200, }, { name: "List-Objects-V1-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket", nil), }, wantErr: false, statusCode: 200, }, { name: "List-actions-error-case", app: appError, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket", nil), }, wantErr: false, statusCode: 501, }, { name: "List-actions-get-bucket-versioning-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket?versioning", nil), }, wantErr: false, statusCode: 200, }, { name: "List-actions-get-bucket-policy-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket?policy", nil), }, wantErr: false, statusCode: 200, }, { name: "List-actions-list-object-versions-success", app: app, args: args{ req: httptest.NewRequest(http.MethodGet, "/my-bucket?versions", nil), }, wantErr: false, statusCode: 200, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.ListActions() error = %v, wantErr %v", err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.ListActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } }) } } func TestS3ApiController_PutBucketActions(t *testing.T) { type args struct { req *http.Request } app := fiber.New() // Mock valid acl acl := auth.ACL{Owner: "valid access", ACL: "public-read-write"} acldata, err := json.Marshal(acl) if err != nil { t.Errorf("Failed to parse the params: %v", err.Error()) return } body := ` hell string hello ` invOwnerBody := ` hello ` succBody := ` valid access ` tagBody := ` organization marketing ` versioningBody := ` Enabled Enabled ` s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, PutBucketAclFunc: func(context.Context, string, []byte) error { return nil }, CreateBucketFunc: func(context.Context, *s3.CreateBucketInput, []byte) error { return nil }, PutBucketTaggingFunc: func(contextMoqParam context.Context, bucket string, tags map[string]string) error { return nil }, PutBucketVersioningFunc: func(contextMoqParam context.Context, putBucketVersioningInput *s3.PutBucketVersioningInput) error { return nil }, PutBucketPolicyFunc: func(contextMoqParam context.Context, bucket string, policy []byte) error { return nil }, }, } // Mock ctx.Locals app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{Owner: "valid access"}) return ctx.Next() }) app.Put("/:bucket", s3ApiController.PutBucketActions) // invalid acl case invAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", nil) invAclReq.Header.Set("X-Amz-Acl", "invalid") // invalid acl case 2 errAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", nil) errAclReq.Header.Set("X-Amz-Acl", "private") errAclReq.Header.Set("X-Amz-Grant-Read", "hello") // PutBucketAcl incorrect bucket owner case incorrectBucketOwner := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", strings.NewReader(invOwnerBody)) incorrectBucketOwner.Header.Set("X-Amz-Acl", "private") // PutBucketAcl acl success aclSuccReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", strings.NewReader(succBody)) aclSuccReq.Header.Set("X-Amz-Acl", "private") // Invalid acl body case errAclBodyReq := httptest.NewRequest(http.MethodPut, "/my-bucket?acl", strings.NewReader(body)) errAclBodyReq.Header.Set("X-Amz-Grant-Read", "hello") tests := []struct { name string app *fiber.App args args wantErr bool statusCode int }{ { name: "Put-bucket-tagging-invalid-body", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket?tagging", nil), }, wantErr: false, statusCode: 400, }, { name: "Put-bucket-tagging-success", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket?tagging", strings.NewReader(tagBody)), }, wantErr: false, statusCode: 200, }, { name: "Put-bucket-versioning-invalid-body", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket?versioning", nil), }, wantErr: false, statusCode: 400, }, { name: "Put-bucket-versioning-success", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket?versioning", strings.NewReader(versioningBody)), }, wantErr: false, statusCode: 200, }, { name: "Put-bucket-policy-success", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket?policy", nil), }, wantErr: false, statusCode: 200, }, { name: "Put-bucket-acl-invalid-acl", app: app, args: args{ req: invAclReq, }, wantErr: false, statusCode: 400, }, { name: "Put-bucket-acl-incorrect-acl", app: app, args: args{ req: errAclReq, }, wantErr: false, statusCode: 400, }, { name: "Put-bucket-acl-incorrect-acl-body", app: app, args: args{ req: errAclBodyReq, }, wantErr: false, statusCode: 400, }, { name: "Put-bucket-acl-incorrect-bucket-owner", app: app, args: args{ req: incorrectBucketOwner, }, wantErr: false, statusCode: 403, }, { name: "Put-bucket-acl-success", app: app, args: args{ req: aclSuccReq, }, wantErr: false, statusCode: 200, }, { name: "Put-bucket-invalid-bucket-name", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/aa", nil), }, wantErr: false, statusCode: 400, }, { name: "Put-bucket-success", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket", nil), }, wantErr: false, statusCode: 200, }, } for _, tt := range tests { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.PutBucketActions() error = %v, wantErr %v", err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.PutBucketActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } } } func TestS3ApiController_PutActions(t *testing.T) { type args struct { req *http.Request } body := ` hell string hello ` tagBody := ` string string ` app := fiber.New() s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, PutObjectAclFunc: func(context.Context, *s3.PutObjectAclInput) error { return nil }, CopyObjectFunc: func(context.Context, *s3.CopyObjectInput) (*s3.CopyObjectOutput, error) { return &s3.CopyObjectOutput{ CopyObjectResult: &types.CopyObjectResult{}, }, nil }, PutObjectFunc: func(context.Context, *s3.PutObjectInput) (string, error) { return "ETag", nil }, UploadPartFunc: func(context.Context, *s3.UploadPartInput) (string, error) { return "hello", nil }, PutObjectTaggingFunc: func(_ context.Context, bucket, object string, tags map[string]string) error { return nil }, UploadPartCopyFunc: func(context.Context, *s3.UploadPartCopyInput) (s3response.CopyObjectResult, error) { return s3response.CopyObjectResult{}, nil }, }, } app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) app.Put("/:bucket/:key/*", s3ApiController.PutActions) // UploadPartCopy success uploadPartCpyReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=12asd32&partNumber=3", nil) uploadPartCpyReq.Header.Set("X-Amz-Copy-Source", "srcBucket/srcObject") // UploadPartCopy error case uploadPartCpyErrReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=12asd32&partNumber=invalid", nil) uploadPartCpyErrReq.Header.Set("X-Amz-Copy-Source", "srcBucket/srcObject") // CopyObject success cpySrcReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil) cpySrcReq.Header.Set("X-Amz-Copy-Source", "srcBucket/srcObject") // PutObjectAcl success aclReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil) aclReq.Header.Set("X-Amz-Acl", "private") // PutObjectAcl success grt case aclGrtReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key", nil) aclGrtReq.Header.Set("X-Amz-Grant-Read", "private") // invalid acl case 1 invAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", nil) invAclReq.Header.Set("X-Amz-Acl", "invalid") // invalid acl case 2 errAclReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", nil) errAclReq.Header.Set("X-Amz-Acl", "private") errAclReq.Header.Set("X-Amz-Grant-Read", "hello") // invalid body & grt case invAclBodyGrtReq := httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", strings.NewReader(body)) invAclBodyGrtReq.Header.Set("X-Amz-Grant-Read", "hello") tests := []struct { name string app *fiber.App args args wantErr bool statusCode int }{ { name: "Put-object-part-error-case", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=abc&partNumber=invalid", nil), }, wantErr: false, statusCode: 400, }, { name: "Put-object-part-success", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?uploadId=4&partNumber=3", nil), }, wantErr: false, statusCode: 200, }, { name: "Set-tags-success", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?tagging", strings.NewReader(tagBody)), }, wantErr: false, statusCode: 200, }, { name: "Put-object-acl-invalid-acl", app: app, args: args{ req: invAclReq, }, wantErr: false, statusCode: 400, }, { name: "Put-object-acl-incorrect-acl", app: app, args: args{ req: errAclReq, }, wantErr: false, statusCode: 400, }, { name: "Put-object-acl-incorrect-acl-body-case", app: app, args: args{ req: invAclBodyGrtReq, }, wantErr: false, statusCode: 400, }, { name: "Put-object-acl-success", app: app, args: args{ req: aclReq, }, wantErr: false, statusCode: 200, }, { name: "Put-object-acl-success-body-case", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key?acl", strings.NewReader(body)), }, wantErr: false, statusCode: 200, }, { name: "Put-object-acl-success-grt-case", app: app, args: args{ req: aclGrtReq, }, wantErr: false, statusCode: 200, }, { name: "Upload-part-copy-invalid-part-number", app: app, args: args{ req: uploadPartCpyErrReq, }, wantErr: false, statusCode: 400, }, { name: "Upload-part-copy-success", app: app, args: args{ req: uploadPartCpyReq, }, wantErr: false, statusCode: 200, }, { name: "Copy-object-success", app: app, args: args{ req: cpySrcReq, }, wantErr: false, statusCode: 200, }, { name: "Put-object-success", app: app, args: args{ req: httptest.NewRequest(http.MethodPut, "/my-bucket/my-key/key2", nil), }, wantErr: false, statusCode: 200, }, } for _, tt := range tests { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.PutActions() %v error = %v, wantErr %v", tt.name, err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.PutActions() %v statusCode = %v, wantStatusCode = %v", tt.name, resp.StatusCode, tt.statusCode) } } } func TestS3ApiController_DeleteBucket(t *testing.T) { type args struct { req *http.Request } app := fiber.New() s3ApiController := S3ApiController{ be: &BackendMock{ DeleteBucketFunc: func(context.Context, *s3.DeleteBucketInput) error { return nil }, DeleteBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) error { return nil }, }, } app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) app.Delete("/:bucket", s3ApiController.DeleteBucket) tests := []struct { name string app *fiber.App args args wantErr bool statusCode int }{ { name: "Delete-bucket-success", app: app, args: args{ req: httptest.NewRequest(http.MethodDelete, "/my-bucket", nil), }, wantErr: false, statusCode: 204, }, { name: "Delete-bucket-tagging-success", app: app, args: args{ req: httptest.NewRequest(http.MethodDelete, "/my-bucket?tagging", nil), }, wantErr: false, statusCode: 204, }, } for _, tt := range tests { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.DeleteBucket() error = %v, wantErr %v", err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.DeleteBucket() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } } } func TestS3ApiController_DeleteObjects(t *testing.T) { type args struct { req *http.Request } app := fiber.New() s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, DeleteObjectsFunc: func(context.Context, *s3.DeleteObjectsInput) (s3response.DeleteResult, error) { return s3response.DeleteResult{}, nil }, }, } app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) app.Post("/:bucket", s3ApiController.DeleteObjects) // Valid request body xmlBody := `body` request := httptest.NewRequest(http.MethodPost, "/my-bucket", strings.NewReader(xmlBody)) request.Header.Set("Content-Type", "application/xml") tests := []struct { name string app *fiber.App args args wantErr bool statusCode int }{ { name: "Delete-Objects-success", app: app, args: args{ req: request, }, wantErr: false, statusCode: 200, }, { name: "Delete-Objects-error", app: app, args: args{ req: httptest.NewRequest(http.MethodPost, "/my-bucket", nil), }, wantErr: false, statusCode: 400, }, } for _, tt := range tests { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.DeleteObjects() error = %v, wantErr %v", err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.DeleteObjects() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } } } func TestS3ApiController_DeleteActions(t *testing.T) { type args struct { req *http.Request } app := fiber.New() s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, DeleteObjectFunc: func(context.Context, *s3.DeleteObjectInput) error { return nil }, AbortMultipartUploadFunc: func(context.Context, *s3.AbortMultipartUploadInput) error { return nil }, DeleteObjectTaggingFunc: func(_ context.Context, bucket, object string) error { return nil }, }, } app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions) // Error case appErr := fiber.New() s3ApiControllerErr := S3ApiController{be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, DeleteObjectFunc: func(context.Context, *s3.DeleteObjectInput) error { return s3err.GetAPIError(7) }, }} appErr.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) appErr.Delete("/:bucket/:key/*", s3ApiControllerErr.DeleteActions) tests := []struct { name string app *fiber.App args args wantErr bool statusCode int }{ { name: "Abort-multipart-upload-success", app: app, args: args{ req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key?uploadId=324234", nil), }, wantErr: false, statusCode: 204, }, { name: "Remove-object-tagging-success", app: app, args: args{ req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key/key2?tagging", nil), }, wantErr: false, statusCode: 204, }, { name: "Delete-object-success", app: app, args: args{ req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key", nil), }, wantErr: false, statusCode: 204, }, { name: "Delete-object-error", app: appErr, args: args{ req: httptest.NewRequest(http.MethodDelete, "/my-bucket/invalid-key", nil), }, wantErr: false, statusCode: 404, }, } for _, tt := range tests { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.DeleteActions() error = %v, wantErr %v", err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.DeleteActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } } } func TestS3ApiController_HeadBucket(t *testing.T) { type args struct { req *http.Request } app := fiber.New() s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, HeadBucketFunc: func(context.Context, *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) { return &s3.HeadBucketOutput{}, nil }, }, } app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) app.Head("/:bucket", s3ApiController.HeadBucket) // Error case appErr := fiber.New() s3ApiControllerErr := S3ApiController{be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, HeadBucketFunc: func(context.Context, *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) { return nil, s3err.GetAPIError(3) }, }, } appErr.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) appErr.Head("/:bucket", s3ApiControllerErr.HeadBucket) tests := []struct { name string app *fiber.App args args wantErr bool statusCode int }{ { name: "Head-bucket-success", app: app, args: args{ req: httptest.NewRequest(http.MethodHead, "/my-bucket", nil), }, wantErr: false, statusCode: 200, }, { name: "Head-bucket-error", app: appErr, args: args{ req: httptest.NewRequest(http.MethodHead, "/my-bucket", nil), }, wantErr: false, statusCode: 409, }, } for _, tt := range tests { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.HeadBucket() error = %v, wantErr %v", err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.HeadBucket() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } } } func TestS3ApiController_HeadObject(t *testing.T) { type args struct { req *http.Request } app := fiber.New() // Mock values contentEncoding := "gzip" contentType := "application/xml" eTag := "Valid etag" lastModifie := time.Now() contentLength := int64(64) s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, HeadObjectFunc: func(context.Context, *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) { return &s3.HeadObjectOutput{ ContentEncoding: &contentEncoding, ContentLength: &contentLength, ContentType: &contentType, LastModified: &lastModifie, ETag: &eTag, }, nil }, }, } app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) app.Head("/:bucket/:key/*", s3ApiController.HeadObject) //Error case appErr := fiber.New() s3ApiControllerErr := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, HeadObjectFunc: func(context.Context, *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) { return nil, s3err.GetAPIError(42) }, }, } appErr.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) appErr.Head("/:bucket/:key/*", s3ApiControllerErr.HeadObject) tests := []struct { name string app *fiber.App args args wantErr bool statusCode int }{ { name: "Head-object-success", app: app, args: args{ req: httptest.NewRequest(http.MethodHead, "/my-bucket/my-key", nil), }, wantErr: false, statusCode: 200, }, { name: "Head-object-error", app: appErr, args: args{ req: httptest.NewRequest(http.MethodHead, "/my-bucket/my-key", nil), }, wantErr: false, statusCode: 400, }, } for _, tt := range tests { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.HeadObject() error = %v, wantErr %v", err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.HeadObject() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } } } func TestS3ApiController_CreateActions(t *testing.T) { type args struct { req *http.Request } app := fiber.New() s3ApiController := S3ApiController{ be: &BackendMock{ GetBucketAclFunc: func(context.Context, *s3.GetBucketAclInput) ([]byte, error) { return acldata, nil }, RestoreObjectFunc: func(context.Context, *s3.RestoreObjectInput) error { return nil }, CompleteMultipartUploadFunc: func(context.Context, *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) { return &s3.CompleteMultipartUploadOutput{}, nil }, CreateMultipartUploadFunc: func(context.Context, *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) { return &s3.CreateMultipartUploadOutput{}, nil }, SelectObjectContentFunc: func(context.Context, *s3.SelectObjectContentInput) func(w *bufio.Writer) { return func(w *bufio.Writer) {} }, }, } bdy := ` string string ` app.Use(func(ctx *fiber.Ctx) error { ctx.Locals("account", auth.Account{Access: "valid access"}) ctx.Locals("isRoot", true) ctx.Locals("isDebug", false) ctx.Locals("parsedAcl", auth.ACL{}) return ctx.Next() }) app.Post("/:bucket/:key/*", s3ApiController.CreateActions) tests := []struct { name string app *fiber.App args args wantErr bool statusCode int }{ { name: "Restore-object-success", app: app, args: args{ req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?restore", strings.NewReader(`body`)), }, wantErr: false, statusCode: 200, }, { name: "Restore-object-error", app: app, args: args{ req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?restore", nil), }, wantErr: false, statusCode: 500, }, { name: "Select-object-content-invalid-body", app: app, args: args{ req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", nil), }, wantErr: false, statusCode: 400, }, { name: "Select-object-content-invalid-body", app: app, args: args{ req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?select&select-type=2", strings.NewReader(bdy)), }, wantErr: false, statusCode: 200, }, { name: "Complete-multipart-upload-error", app: app, args: args{ req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", nil), }, wantErr: false, statusCode: 400, }, { name: "Complete-multipart-upload-success", app: app, args: args{ req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", strings.NewReader(`body`)), }, wantErr: false, statusCode: 200, }, { name: "Create-multipart-upload-success", app: app, args: args{ req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key", nil), }, wantErr: false, statusCode: 200, }, } for _, tt := range tests { resp, err := tt.app.Test(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("S3ApiController.CreateActions() error = %v, wantErr %v", err, tt.wantErr) } if resp.StatusCode != tt.statusCode { t.Errorf("S3ApiController.CreateActions() statusCode = %v, wantStatusCode = %v", resp.StatusCode, tt.statusCode) } } } func Test_XMLresponse(t *testing.T) { type args struct { ctx *fiber.Ctx resp any err error } app := fiber.New() ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) tests := []struct { name string args args wantErr bool statusCode int }{ { name: "Internal-server-error", args: args{ ctx: ctx, resp: nil, err: s3err.GetAPIError(s3err.ErrInternalError), }, wantErr: false, statusCode: 500, }, { name: "Error-not-implemented", args: args{ ctx: ctx, resp: nil, err: s3err.GetAPIError(s3err.ErrNotImplemented), }, wantErr: false, statusCode: 501, }, { name: "Invalid-request-body", args: args{ ctx: ctx, resp: make(chan int), err: nil, }, wantErr: true, statusCode: 200, }, { name: "Successful-response", args: args{ ctx: ctx, resp: "Valid response", err: nil, }, wantErr: false, statusCode: 200, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := SendXMLResponse(tt.args.ctx, tt.args.resp, tt.args.err, &MetaOpts{}); (err != nil) != tt.wantErr { t.Errorf("response() %v error = %v, wantErr %v", tt.name, err, tt.wantErr) } statusCode := tt.args.ctx.Response().StatusCode() if statusCode != tt.statusCode { t.Errorf("response() %v code = %v, wantErr %v", tt.name, statusCode, tt.wantErr) } tt.args.ctx.Status(http.StatusOK) }) } } func Test_response(t *testing.T) { type args struct { ctx *fiber.Ctx resp any err error opts *MetaOpts } app := fiber.New() ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) tests := []struct { name string args args wantErr bool statusCode int }{ { name: "Internal-server-error", args: args{ ctx: ctx, resp: nil, err: s3err.GetAPIError(s3err.ErrInternalError), opts: &MetaOpts{}, }, wantErr: false, statusCode: 500, }, { name: "Internal-server-error-not-api", args: args{ ctx: ctx, resp: nil, err: fmt.Errorf("custom error"), opts: &MetaOpts{}, }, wantErr: false, statusCode: 500, }, { name: "Error-not-implemented", args: args{ ctx: ctx, resp: nil, err: s3err.GetAPIError(s3err.ErrNotImplemented), opts: &MetaOpts{}, }, wantErr: false, statusCode: 501, }, { name: "Successful-response", args: args{ ctx: ctx, resp: "Valid response", err: nil, opts: &MetaOpts{}, }, wantErr: false, statusCode: 200, }, { name: "Successful-response-status-204", args: args{ ctx: ctx, resp: "Valid response", err: nil, opts: &MetaOpts{ Status: 204, }, }, wantErr: false, statusCode: 204, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := SendResponse(tt.args.ctx, tt.args.err, tt.args.opts); (err != nil) != tt.wantErr { t.Errorf("response() %v error = %v, wantErr %v", tt.name, err, tt.wantErr) } statusCode := tt.args.ctx.Response().StatusCode() if statusCode != tt.statusCode { t.Errorf("response() %v code = %v, wantErr %v", tt.name, statusCode, tt.wantErr) } }) } }