From 67d0750ee08de65dfd1d5420454a20a1446f49ae Mon Sep 17 00:00:00 2001 From: niksis02 Date: Tue, 8 Jul 2025 22:20:00 +0400 Subject: [PATCH] feat: adds unit tests for object DELETE and POST operations --- s3api/controllers/object-delete_test.go | 296 +++++++++++++ s3api/controllers/object-post.go | 55 ++- s3api/controllers/object-post_test.go | 561 ++++++++++++++++++++++++ s3response/s3response.go | 4 + 4 files changed, 887 insertions(+), 29 deletions(-) create mode 100644 s3api/controllers/object-delete_test.go create mode 100644 s3api/controllers/object-post_test.go diff --git a/s3api/controllers/object-delete_test.go b/s3api/controllers/object-delete_test.go new file mode 100644 index 0000000..d905435 --- /dev/null +++ b/s3api/controllers/object-delete_test.go @@ -0,0 +1,296 @@ +// 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 ( + "context" + "net/http" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/versity/versitygw/s3api/utils" + "github.com/versity/versitygw/s3err" + "github.com/versity/versitygw/s3event" +) + +func TestS3ApiController_DeleteObjectTagging(t *testing.T) { + tests := []struct { + name string + input testInput + output testOutput + }{ + { + name: "verify access fails", + input: testInput{ + locals: accessDeniedLocals, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + }, + { + name: "backend returns error", + input: testInput{ + locals: defaultLocals, + beErr: s3err.GetAPIError(s3err.ErrInvalidRequest), + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + Status: http.StatusNoContent, + EventName: s3event.EventObjectTaggingDelete, + }, + }, + err: s3err.GetAPIError(s3err.ErrInvalidRequest), + }, + }, + { + name: "successful response", + input: testInput{ + locals: defaultLocals, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + Status: http.StatusNoContent, + EventName: s3event.EventObjectTaggingDelete, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BackendMock{ + DeleteObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object string) error { + return tt.input.beErr + }, + GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + }, + } + + ctrl := S3ApiController{ + be: be, + } + + testController( + t, + ctrl.DeleteObjectTagging, + tt.output.response, + tt.output.err, + ctxInputs{ + locals: tt.input.locals, + }) + }) + } +} + +func TestS3ApiController_AbortMultipartUpload(t *testing.T) { + tests := []struct { + name string + input testInput + output testOutput + }{ + { + name: "verify access fails", + input: testInput{ + locals: accessDeniedLocals, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + }, + { + name: "backend returns error", + input: testInput{ + locals: defaultLocals, + beErr: s3err.GetAPIError(s3err.ErrInvalidRequest), + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + Status: http.StatusNoContent, + }, + }, + err: s3err.GetAPIError(s3err.ErrInvalidRequest), + }, + }, + { + name: "successful response", + input: testInput{ + locals: defaultLocals, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + Status: http.StatusNoContent, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BackendMock{ + AbortMultipartUploadFunc: func(contextMoqParam context.Context, abortMultipartUploadInput *s3.AbortMultipartUploadInput) error { + return tt.input.beErr + }, + GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + }, + } + + ctrl := S3ApiController{ + be: be, + } + + testController( + t, + ctrl.AbortMultipartUpload, + tt.output.response, + tt.output.err, + ctxInputs{ + locals: tt.input.locals, + }) + }) + } +} + +func TestS3ApiController_DeleteObject(t *testing.T) { + delMarker, versionId := true, "versionId" + var emptyRes *s3.DeleteObjectOutput + + tests := []struct { + name string + input testInput + output testOutput + }{ + { + name: "verify access fails", + input: testInput{ + locals: accessDeniedLocals, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + }, + { + name: "object locked", + input: testInput{ + locals: defaultLocals, + extraMockErr: s3err.GetAPIError(s3err.ErrObjectLocked), + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrObjectLocked), + }, + }, + { + name: "backend returns error", + input: testInput{ + locals: defaultLocals, + beErr: s3err.GetAPIError(s3err.ErrInvalidRequest), + extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound), + beRes: emptyRes, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + Status: http.StatusNoContent, + EventName: s3event.EventObjectRemovedDelete, + }, + }, + err: s3err.GetAPIError(s3err.ErrInvalidRequest), + }, + }, + { + name: "successful response", + input: testInput{ + locals: defaultLocals, + extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound), + beRes: &s3.DeleteObjectOutput{ + DeleteMarker: &delMarker, + VersionId: &versionId, + }, + }, + output: testOutput{ + response: &Response{ + Headers: map[string]*string{ + "x-amz-delete-marker": utils.GetStringPtr("true"), + "x-amz-version-id": &versionId, + }, + MetaOpts: &MetaOptions{ + BucketOwner: "root", + Status: http.StatusNoContent, + EventName: s3event.EventObjectRemovedDelete, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BackendMock{ + DeleteObjectFunc: func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) { + return tt.input.beRes.(*s3.DeleteObjectOutput), tt.input.beErr + }, + GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + }, + GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, tt.input.extraMockErr + }, + } + + ctrl := S3ApiController{ + be: be, + } + + testController( + t, + ctrl.DeleteObject, + tt.output.response, + tt.output.err, + ctxInputs{ + locals: tt.input.locals, + }) + }) + } +} diff --git a/s3api/controllers/object-post.go b/s3api/controllers/object-post.go index 0d6fe8c..658320a 100644 --- a/s3api/controllers/object-post.go +++ b/s3api/controllers/object-post.go @@ -39,15 +39,6 @@ func (c S3ApiController) RestoreObject(ctx *fiber.Ctx) (*Response, error) { IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) - var restoreRequest types.RestoreRequest - if err := xml.Unmarshal(ctx.Body(), &restoreRequest); err != nil { - debuglogger.Logf("failed to parse the request body: %v", err) - return &Response{ - MetaOpts: &MetaOptions{ - BucketOwner: parsedAcl.Owner, - }, - }, s3err.GetAPIError(s3err.ErrMalformedXML) - } err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ Readonly: c.readonly, @@ -68,6 +59,16 @@ func (c S3ApiController) RestoreObject(ctx *fiber.Ctx) (*Response, error) { }, err } + var restoreRequest types.RestoreRequest + if err := xml.Unmarshal(ctx.Body(), &restoreRequest); err != nil { + debuglogger.Logf("failed to parse the request body: %v", err) + return &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrMalformedXML) + } + err = c.be.RestoreObject(ctx.Context(), &s3.RestoreObjectInput{ Bucket: &bucket, Key: &key, @@ -89,19 +90,7 @@ func (c S3ApiController) SelectObjectContent(ctx *fiber.Ctx) (*Response, error) IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) - var payload s3response.SelectObjectContentPayload - - err := xml.Unmarshal(ctx.Body(), &payload) - if err != nil { - debuglogger.Logf("error unmarshalling select object content: %v", err) - return &Response{ - MetaOpts: &MetaOptions{ - BucketOwner: parsedAcl.Owner, - }, - }, s3err.GetAPIError(s3err.ErrMalformedXML) - } - - err = auth.VerifyAccess(ctx.Context(), c.be, + err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ Readonly: c.readonly, Acl: parsedAcl, @@ -121,6 +110,17 @@ func (c S3ApiController) SelectObjectContent(ctx *fiber.Ctx) (*Response, error) }, err } + var payload s3response.SelectObjectContentPayload + err = xml.Unmarshal(ctx.Body(), &payload) + if err != nil { + debuglogger.Logf("error unmarshalling select object content: %v", err) + return &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrMalformedXML) + } + sw := c.be.SelectObjectContent(ctx.Context(), &s3.SelectObjectContentInput{ Bucket: &bucket, @@ -260,11 +260,8 @@ func (c S3ApiController) CompleteMultipartUpload(ctx *fiber.Ctx) (*Response, err }, err } - data := struct { - Parts []types.CompletedPart `xml:"Part"` - }{} - - err = xml.Unmarshal(ctx.Body(), &data) + var body s3response.CompleteMultipartUploadRequestBody + err = xml.Unmarshal(ctx.Body(), &body) if err != nil { debuglogger.Logf("error unmarshalling complete multipart upload: %v", err) return &Response{ @@ -274,7 +271,7 @@ func (c S3ApiController) CompleteMultipartUpload(ctx *fiber.Ctx) (*Response, err }, s3err.GetAPIError(s3err.ErrMalformedXML) } - if len(data.Parts) == 0 { + if len(body.Parts) == 0 { debuglogger.Logf("empty parts provided for complete multipart upload") return &Response{ MetaOpts: &MetaOptions{ @@ -332,7 +329,7 @@ func (c S3ApiController) CompleteMultipartUpload(ctx *fiber.Ctx) (*Response, err Key: &key, UploadId: &uploadId, MultipartUpload: &types.CompletedMultipartUpload{ - Parts: data.Parts, + Parts: body.Parts, }, MpuObjectSize: mpuObjectSize, ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]), diff --git a/s3api/controllers/object-post_test.go b/s3api/controllers/object-post_test.go new file mode 100644 index 0000000..baca9f3 --- /dev/null +++ b/s3api/controllers/object-post_test.go @@ -0,0 +1,561 @@ +// 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/xml" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/stretchr/testify/assert" + "github.com/versity/versitygw/s3api/utils" + "github.com/versity/versitygw/s3err" + "github.com/versity/versitygw/s3event" + "github.com/versity/versitygw/s3response" +) + +func TestS3ApiController_RestoreObject(t *testing.T) { + validRestoreBody, err := xml.Marshal(types.RestoreRequest{ + Description: utils.GetStringPtr("description"), + Type: types.RestoreRequestTypeSelect, + }) + assert.NoError(t, err) + + tests := []struct { + name string + input testInput + output testOutput + }{ + { + name: "verify access fails", + input: testInput{ + locals: accessDeniedLocals, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + }, + { + name: "invalid request body", + input: testInput{ + locals: defaultLocals, + body: []byte("invalid_body"), + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrMalformedXML), + }, + }, + { + name: "backend returns error", + input: testInput{ + locals: defaultLocals, + beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket), + body: validRestoreBody, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + EventName: s3event.EventObjectRestoreCompleted, + }, + }, + err: s3err.GetAPIError(s3err.ErrNoSuchBucket), + }, + }, + { + name: "successful response", + input: testInput{ + locals: defaultLocals, + body: validRestoreBody, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + EventName: s3event.EventObjectRestoreCompleted, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BackendMock{ + RestoreObjectFunc: func(contextMoqParam context.Context, restoreObjectInput *s3.RestoreObjectInput) error { + return tt.input.beErr + }, + GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + }, + } + + ctrl := S3ApiController{ + be: be, + } + + testController( + t, + ctrl.RestoreObject, + tt.output.response, + tt.output.err, + ctxInputs{ + locals: tt.input.locals, + body: tt.input.body, + }) + }) + } +} + +func TestS3ApiController_SelectObjectContent(t *testing.T) { + validSelectBody, err := xml.Marshal(s3response.SelectObjectContentPayload{ + Expression: utils.GetStringPtr("expression"), + ExpressionType: types.ExpressionTypeSql, + }) + assert.NoError(t, err) + + tests := []struct { + name string + input testInput + output testOutput + }{ + { + name: "verify access fails", + input: testInput{ + locals: accessDeniedLocals, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + }, + { + name: "invalid request body", + input: testInput{ + locals: defaultLocals, + body: []byte("invalid_body"), + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrMalformedXML), + }, + }, + { + name: "successful response", + input: testInput{ + locals: defaultLocals, + body: validSelectBody, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BackendMock{ + SelectObjectContentFunc: func(ctx context.Context, input *s3.SelectObjectContentInput) func(w *bufio.Writer) { + return func(w *bufio.Writer) {} + }, + GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + }, + } + + ctrl := S3ApiController{ + be: be, + } + + testController( + t, + ctrl.SelectObjectContent, + tt.output.response, + tt.output.err, + ctxInputs{ + locals: tt.input.locals, + body: tt.input.body, + }) + }) + } +} + +func TestS3ApiController_CreateMultipartUpload(t *testing.T) { + tests := []struct { + name string + input testInput + output testOutput + }{ + { + name: "verify access fails", + input: testInput{ + locals: accessDeniedLocals, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + }, + { + name: "invalid object lock headers", + input: testInput{ + locals: defaultLocals, + headers: map[string]string{ + "X-Amz-Object-Lock-Mode": string(types.ObjectLockModeGovernance), + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders), + }, + }, + { + name: "invalid checksum headers", + input: testInput{ + locals: defaultLocals, + headers: map[string]string{ + "X-Amz-Checksum-Algorithm": "invalid_checksum_algo", + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrInvalidChecksumAlgorithm), + }, + }, + { + name: "backend returns error", + input: testInput{ + locals: defaultLocals, + beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket), + beRes: s3response.InitiateMultipartUploadResult{}, + }, + output: testOutput{ + response: &Response{ + Data: s3response.InitiateMultipartUploadResult{}, + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrNoSuchBucket), + }, + }, + { + name: "successful response", + input: testInput{ + locals: defaultLocals, + beRes: s3response.InitiateMultipartUploadResult{}, + headers: map[string]string{ + "x-amz-checksum-algorithm": string(types.ChecksumAlgorithmCrc32), + }, + }, + output: testOutput{ + response: &Response{ + Data: s3response.InitiateMultipartUploadResult{}, + Headers: map[string]*string{ + "x-amz-checksum-algorithm": utils.ConvertToStringPtr(types.ChecksumAlgorithmCrc32), + }, + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BackendMock{ + CreateMultipartUploadFunc: func(contextMoqParam context.Context, createMultipartUploadInput s3response.CreateMultipartUploadInput) (s3response.InitiateMultipartUploadResult, error) { + return tt.input.beRes.(s3response.InitiateMultipartUploadResult), tt.input.beErr + }, + GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + }, + } + + ctrl := S3ApiController{ + be: be, + } + + testController( + t, + ctrl.CreateMultipartUpload, + tt.output.response, + tt.output.err, + ctxInputs{ + locals: tt.input.locals, + body: tt.input.body, + headers: tt.input.headers, + }) + }) + } +} + +func TestS3ApiController_CompleteMultipartUpload(t *testing.T) { + emptyMpPartsBody, err := xml.Marshal(s3response.CompleteMultipartUploadRequestBody{ + Parts: []types.CompletedPart{}, + }) + assert.NoError(t, err) + pn := int32(1) + + validMpBody, err := xml.Marshal(s3response.CompleteMultipartUploadRequestBody{ + Parts: []types.CompletedPart{ + { + PartNumber: &pn, + ETag: utils.GetStringPtr("ETag"), + }, + }, + }) + assert.NoError(t, err) + + versionId, ETag := "versionId", "mock-ETag" + + tests := []struct { + name string + input testInput + output testOutput + }{ + { + name: "verify access fails", + input: testInput{ + locals: accessDeniedLocals, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrAccessDenied), + }, + }, + { + name: "invalid request body", + input: testInput{ + locals: defaultLocals, + body: []byte("invalid_body"), + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrMalformedXML), + }, + }, + { + name: "request body empty mp parts", + input: testInput{ + locals: defaultLocals, + body: emptyMpPartsBody, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrEmptyParts), + }, + }, + { + name: "invalid mp parts header string", + input: testInput{ + locals: defaultLocals, + body: validMpBody, + headers: map[string]string{ + "X-Amz-Mp-Object-Size": "invalid_mp_object_size", + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrInvalidRequest), + }, + }, + { + name: "negative mp parts header value", + input: testInput{ + locals: defaultLocals, + body: validMpBody, + headers: map[string]string{ + "X-Amz-Mp-Object-Size": "-4", + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetInvalidMpObjectSizeErr(-4), + }, + }, + { + name: "invalid checksum headers", + input: testInput{ + locals: defaultLocals, + body: validMpBody, + headers: map[string]string{ + "X-Amz-Sdk-Checksum-Algorithm": "invalid_checksum_algo", + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrInvalidChecksumAlgorithm), + }, + }, + { + name: "invalid checksum type", + input: testInput{ + locals: defaultLocals, + body: validMpBody, + headers: map[string]string{ + "X-Amz-Checksum-Type": "invalid_checksum_type", + }, + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-type"), + }, + }, + { + name: "backend returns error", + input: testInput{ + locals: defaultLocals, + body: validMpBody, + beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket), + beRes: s3response.CompleteMultipartUploadResult{}, + }, + output: testOutput{ + response: &Response{ + Data: s3response.CompleteMultipartUploadResult{}, + Headers: map[string]*string{ + "x-amz-version-id": &versionId, + }, + MetaOpts: &MetaOptions{ + BucketOwner: "root", + EventName: s3event.EventCompleteMultipartUpload, + VersionId: &versionId, + ObjectETag: nil, + }, + }, + err: s3err.GetAPIError(s3err.ErrNoSuchBucket), + }, + }, + { + name: "successful response", + input: testInput{ + locals: defaultLocals, + body: validMpBody, + beRes: s3response.CompleteMultipartUploadResult{ + ETag: &ETag, + }, + headers: map[string]string{ + "X-Amz-Mp-Object-Size": "3", + }, + }, + output: testOutput{ + response: &Response{ + Data: s3response.CompleteMultipartUploadResult{ + ETag: &ETag, + }, + Headers: map[string]*string{ + "x-amz-version-id": &versionId, + }, + MetaOpts: &MetaOptions{ + BucketOwner: "root", + EventName: s3event.EventCompleteMultipartUpload, + VersionId: &versionId, + ObjectETag: &ETag, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BackendMock{ + CompleteMultipartUploadFunc: func(contextMoqParam context.Context, completeMultipartUploadInput *s3.CompleteMultipartUploadInput) (s3response.CompleteMultipartUploadResult, string, error) { + return tt.input.beRes.(s3response.CompleteMultipartUploadResult), versionId, tt.input.beErr + }, + GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrAccessDenied) + }, + } + + ctrl := S3ApiController{ + be: be, + } + + testController( + t, + ctrl.CompleteMultipartUpload, + tt.output.response, + tt.output.err, + ctxInputs{ + locals: tt.input.locals, + body: tt.input.body, + headers: tt.input.headers, + }) + }) + } +} diff --git a/s3response/s3response.go b/s3response/s3response.go index 810f0b7..223e6f0 100644 --- a/s3response/s3response.go +++ b/s3response/s3response.go @@ -412,6 +412,10 @@ func (r CopyPartResult) MarshalXML(e *xml.Encoder, start xml.StartElement) error return e.EncodeElement(aux, start) } +type CompleteMultipartUploadRequestBody struct { + Parts []types.CompletedPart `xml:"Part"` +} + type CompleteMultipartUploadResult struct { XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUploadResult" json:"-"` Location *string