From 3cfee3a03281be869b72dc61c38105dc1d560759 Mon Sep 17 00:00:00 2001 From: Jon Austin <62040422+jonaustin09@users.noreply.github.com> Date: Tue, 6 Jun 2023 08:38:12 -0700 Subject: [PATCH] Utils unit tests (#54) * fix: Fixed error cases of primitive values * feat: Added unit test for: DeleteBucket, DeleteObjects, DeleteActions, HeadBucket, HeadObject, CreateActions controllers * feat: Added unit tests for GetUserMetaData, CreateHttpRequestFromCtx, MarshalStructToXML utility functions * fix: fixed CreateHttpRequestFromCtx unit test case --- s3api/controllers/base.go | 6 +- s3api/controllers/base_test.go | 420 +++++++++++++++++++++++++++------ s3api/utils/utils_test.go | 122 ++++++++++ 3 files changed, 481 insertions(+), 67 deletions(-) create mode 100644 s3api/utils/utils_test.go diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index 222b7ad1..a6138b08 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -325,7 +325,11 @@ func (c S3ApiController) CreateActions(ctx *fiber.Ctx) error { key = strings.Join([]string{key, keyEnd}, "/") } - if err := xml.Unmarshal(ctx.Body(), &restoreRequest); err == nil { + if ctx.Request().URI().QueryArgs().Has("restore") { + xmlErr := xml.Unmarshal(ctx.Body(), &restoreRequest) + if xmlErr != nil { + return errors.New("wrong api call") + } err := c.be.RestoreObject(bucket, key, &restoreRequest) return Responce[any](ctx, nil, err) } diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index 310ffdbe..d19166a6 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -19,9 +19,11 @@ import ( "net/http" "net/http/httptest" "reflect" + "strings" "testing" "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/backend" @@ -534,127 +536,413 @@ func TestS3ApiController_PutActions(t *testing.T) { func TestS3ApiController_DeleteBucket(t *testing.T) { type args struct { - ctx *fiber.Ctx + req *http.Request } + + app := fiber.New() + s3ApiController := S3ApiController{be: &BackendMock{ + DeleteBucketFunc: func(bucket string) error { + return nil + }, + }} + + app.Delete("/:bucket", s3ApiController.DeleteBucket) + + // error case + appErr := fiber.New() + + s3ApiControllerErr := S3ApiController{be: &BackendMock{ + DeleteBucketFunc: func(bucket string) error { + return s3err.GetAPIError(48) + }, + }} + + appErr.Delete("/:bucket", s3ApiControllerErr.DeleteBucket) + tests := []struct { - name string - c S3ApiController - args args - wantErr bool + name string + app *fiber.App + args args + wantErr bool + statusCode int }{ - // TODO: Add test cases. + { + name: "Delete-bucket-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodDelete, "/my-bucket", nil), + }, + wantErr: false, + statusCode: 200, + }, + { + name: "Delete-bucket-error", + app: appErr, + args: args{ + req: httptest.NewRequest(http.MethodDelete, "/my-bucket", nil), + }, + wantErr: false, + statusCode: 400, + }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.c.DeleteBucket(tt.args.ctx); (err != nil) != tt.wantErr { - t.Errorf("S3ApiController.DeleteBucket() error = %v, wantErr %v", err, tt.wantErr) - } - }) + 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 { - ctx *fiber.Ctx + req *http.Request } + + app := fiber.New() + s3ApiController := S3ApiController{be: &BackendMock{ + DeleteObjectsFunc: func(bucket string, objects *s3.DeleteObjectsInput) error { + return nil + }, + }} + + 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 - c S3ApiController - args args - wantErr bool + name string + app *fiber.App + args args + wantErr bool + statusCode int }{ - // TODO: Add test cases. + { + 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: 500, + }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := tt.c.DeleteObjects(tt.args.ctx); (err != nil) != tt.wantErr { - t.Errorf("S3ApiController.DeleteObjects() error = %v, wantErr %v", err, tt.wantErr) - } - }) + 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 { - ctx *fiber.Ctx + req *http.Request } + + app := fiber.New() + s3ApiController := S3ApiController{be: &BackendMock{ + DeleteObjectFunc: func(bucket, object string) error { + return nil + }, + AbortMultipartUploadFunc: func(*s3.AbortMultipartUploadInput) error { + return nil + }, + }} + + app.Delete("/:bucket/:key/*", s3ApiController.DeleteActions) + + //Error case + appErr := fiber.New() + + s3ApiControllerErr := S3ApiController{be: &BackendMock{ + DeleteObjectFunc: func(bucket, object string) error { + return s3err.GetAPIError(7) + }, + }} + + appErr.Delete("/:bucket", s3ApiControllerErr.DeleteBucket) + tests := []struct { - name string - c S3ApiController - args args - wantErr bool + name string + app *fiber.App + args args + wantErr bool + statusCode int }{ - // TODO: Add test cases. + { + name: "Abort-multipart-upload-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key?uploadId=324234", nil), + }, + wantErr: false, + statusCode: 200, + }, + { + name: "Delete-object-success", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodDelete, "/my-bucket/my-key", nil), + }, + wantErr: false, + statusCode: 200, + }, + { + 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 { - t.Run(tt.name, func(t *testing.T) { - if err := tt.c.DeleteActions(tt.args.ctx); (err != nil) != tt.wantErr { - t.Errorf("S3ApiController.DeleteActions() error = %v, wantErr %v", err, tt.wantErr) - } - }) + 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 { - ctx *fiber.Ctx + req *http.Request } + + app := fiber.New() + s3ApiController := S3ApiController{be: &BackendMock{ + HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) { + return &s3.HeadBucketOutput{}, nil + }, + }} + + app.Head("/:bucket", s3ApiController.HeadBucket) + + //Error case + appErr := fiber.New() + + s3ApiControllerErr := S3ApiController{be: &BackendMock{ + HeadBucketFunc: func(bucket string) (*s3.HeadBucketOutput, error) { + return nil, s3err.GetAPIError(3) + }, + }} + + appErr.Head("/:bucket", s3ApiControllerErr.HeadBucket) + tests := []struct { - name string - c S3ApiController - args args - wantErr bool + name string + app *fiber.App + args args + wantErr bool + statusCode int }{ - // TODO: Add test cases. + { + 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 { - t.Run(tt.name, func(t *testing.T) { - if err := tt.c.HeadBucket(tt.args.ctx); (err != nil) != tt.wantErr { - t.Errorf("S3ApiController.HeadBucket() error = %v, wantErr %v", err, tt.wantErr) - } - }) + 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 { - ctx *fiber.Ctx + req *http.Request } + + app := fiber.New() + s3ApiController := S3ApiController{be: &BackendMock{ + HeadObjectFunc: func(bucket, object string) (*s3.HeadObjectOutput, error) { + return nil, nil + }, + }} + + app.Head("/:bucket/:key/*", s3ApiController.HeadObject) + + //Error case + appErr := fiber.New() + + s3ApiControllerErr := S3ApiController{be: &BackendMock{ + HeadObjectFunc: func(bucket, object string) (*s3.HeadObjectOutput, error) { + return nil, s3err.GetAPIError(42) + }, + }} + + appErr.Head("/:bucket/:key/*", s3ApiControllerErr.HeadObject) + tests := []struct { - name string - c S3ApiController - args args - wantErr bool + name string + app *fiber.App + args args + wantErr bool + statusCode int }{ - // TODO: Add test cases. + { + 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 { - t.Run(tt.name, func(t *testing.T) { - if err := tt.c.HeadObject(tt.args.ctx); (err != nil) != tt.wantErr { - t.Errorf("S3ApiController.HeadObject() error = %v, wantErr %v", err, tt.wantErr) - } - }) + 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 { - ctx *fiber.Ctx + req *http.Request } + app := fiber.New() + s3ApiController := S3ApiController{be: &BackendMock{ + RestoreObjectFunc: func(bucket, object string, restoreRequest *s3.RestoreObjectInput) error { + return nil + }, + CompleteMultipartUploadFunc: func(bucket, object, uploadID string, parts []types.Part) (*s3.CompleteMultipartUploadOutput, error) { + return &s3.CompleteMultipartUploadOutput{}, nil + }, + CreateMultipartUploadFunc: func(*s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) { + return &s3.CreateMultipartUploadOutput{}, nil + }, + }} + + app.Post("/:bucket/:key/*", s3ApiController.CreateActions) + tests := []struct { - name string - c S3ApiController - args args - wantErr bool + name string + app *fiber.App + args args + wantErr bool + statusCode int }{ - // TODO: Add test cases. + { + 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: "Complete-multipart-upload-error", + app: app, + args: args{ + req: httptest.NewRequest(http.MethodPost, "/my-bucket/my-key?uploadId=23423", nil), + }, + wantErr: false, + statusCode: 500, + }, + { + 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 { - t.Run(tt.name, func(t *testing.T) { - if err := tt.c.CreateActions(tt.args.ctx); (err != nil) != tt.wantErr { - t.Errorf("S3ApiController.CreateActions() error = %v, wantErr %v", err, tt.wantErr) - } - }) + 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) + } } } diff --git a/s3api/utils/utils_test.go b/s3api/utils/utils_test.go new file mode 100644 index 00000000..c5b11524 --- /dev/null +++ b/s3api/utils/utils_test.go @@ -0,0 +1,122 @@ +package utils + +import ( + "bytes" + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func TestCreateHttpRequestFromCtx(t *testing.T) { + type args struct { + ctx *fiber.Ctx + } + + app := fiber.New() + + // Expected output, Case 1 + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + req := ctx.Request() + request, _ := http.NewRequest(string(req.Header.Method()), req.URI().String(), bytes.NewReader(req.Body())) + + // Case 2 + ctx2 := app.AcquireCtx(&fasthttp.RequestCtx{}) + req2 := ctx2.Request() + req2.Header.Add("X-Amz-Mfa", "Some valid Mfa") + + request2, _ := http.NewRequest(string(req2.Header.Method()), req2.URI().String(), bytes.NewReader(req2.Body())) + request2.Header.Add("X-Amz-Mfa", "Some valid Mfa") + + tests := []struct { + name string + args args + want *http.Request + wantErr bool + }{ + { + name: "Success-response", + args: args{ + ctx: ctx, + }, + want: request, + wantErr: false, + }, + { + name: "Success-response-With-Headers", + args: args{ + ctx: ctx2, + }, + want: request2, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CreateHttpRequestFromCtx(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("CreateHttpRequestFromCtx() error = %v, wantErr %v", err, tt.wantErr) + return + } + + fmt.Println(got.Header, tt.want.Header) + + if !reflect.DeepEqual(got.Header, tt.want.Header) { + t.Errorf("CreateHttpRequestFromCtx() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetUserMetaData(t *testing.T) { + type args struct { + headers *fasthttp.RequestHeader + } + + app := fiber.New() + + // Case 1 + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + req := ctx.Request() + + // Case 2 + ctx2 := app.AcquireCtx(&fasthttp.RequestCtx{}) + req2 := ctx2.Request() + + req2.Header.Add("X-Amz-Meta-Name", "Nick") + req2.Header.Add("X-Amz-Meta-Age", "27") + + tests := []struct { + name string + args args + wantMetadata map[string]string + }{ + { + name: "Success-empty-response", + args: args{ + headers: &req.Header, + }, + wantMetadata: map[string]string{}, + }, + { + name: "Success-non-empty-response", + args: args{ + headers: &req2.Header, + }, + wantMetadata: map[string]string{ + "Age": "27", + "Name": "Nick", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotMetadata := GetUserMetaData(tt.args.headers); !reflect.DeepEqual(gotMetadata, tt.wantMetadata) { + t.Errorf("GetUserMetaData() = %v, want %v", gotMetadata, tt.wantMetadata) + } + }) + } +}