feat: adds unit tests for the object HEAD and GET controllers.

This commit is contained in:
niksis02
2025-07-10 03:12:52 +04:00
parent 67d0750ee0
commit ba76aea17a
5 changed files with 1042 additions and 33 deletions

View File

@@ -220,6 +220,25 @@ func (c S3ApiController) ListParts(ctx *fiber.Ctx) (*Response, error) {
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{
Readonly: c.readonly,
Acl: parsedAcl,
AclPermission: auth.PermissionRead,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.ListMultipartUploadPartsAction,
IsBucketPublic: isPublicBucket,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
// parse the part number marker
if partNumberMarker != "" {
n, err := strconv.Atoi(partNumberMarker)
@@ -247,25 +266,6 @@ func (c S3ApiController) ListParts(ctx *fiber.Ctx) (*Response, error) {
}, s3err.GetAPIError(s3err.ErrInvalidMaxParts)
}
err = auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{
Readonly: c.readonly,
Acl: parsedAcl,
AclPermission: auth.PermissionRead,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.ListMultipartUploadPartsAction,
IsBucketPublic: isPublicBucket,
})
if err != nil {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, err
}
res, err := c.be.ListParts(ctx.Context(), &s3.ListPartsInput{
Bucket: &bucket,
Key: &key,

View File

@@ -0,0 +1,820 @@
// 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"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"testing"
"time"
"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/s3response"
)
func TestS3ApiController_GetObjectTagging(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,
beRes: map[string]string{},
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "successful response",
input: testInput{
locals: defaultLocals,
beRes: map[string]string{
"key": "val",
},
},
output: testOutput{
response: &Response{
Data: s3response.Tagging{
TagSet: s3response.TagSet{
Tags: []s3response.Tag{
{Key: "key", Value: "val"},
},
},
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
GetObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object string) (map[string]string, error) {
return tt.input.beRes.(map[string]string), 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.GetObjectTagging,
tt.output.response,
tt.output.err,
ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
})
})
}
}
func TestS3ApiController_GetObjectRetention(t *testing.T) {
retBytes, err := json.Marshal(types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeCompliance,
})
assert.NoError(t, err)
var retention *types.ObjectLockRetention
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,
beRes: []byte{},
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "invalid data from backend",
input: testInput{
locals: defaultLocals,
beRes: []byte{},
},
output: testOutput{
response: &Response{
Data: retention,
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: fmt.Errorf("parse object lock retention: "),
},
},
{
name: "successful response",
input: testInput{
locals: defaultLocals,
beRes: retBytes,
},
output: testOutput{
response: &Response{
Data: &types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeCompliance,
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string) ([]byte, error) {
return tt.input.beRes.([]byte), 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.GetObjectRetention,
tt.output.response,
tt.output.err,
ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
})
})
}
}
func TestS3ApiController_GetObjectLegalHold(t *testing.T) {
var legalHold *bool
var emptyLegalHold *s3response.GetObjectLegalHoldResult
status := true
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,
beRes: legalHold,
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
output: testOutput{
response: &Response{
Data: emptyLegalHold,
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "successful response",
input: testInput{
locals: defaultLocals,
beRes: &status,
},
output: testOutput{
response: &Response{
Data: &s3response.GetObjectLegalHoldResult{
Status: types.ObjectLockLegalHoldStatusOn,
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string) (*bool, error) {
return tt.input.beRes.(*bool), 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.GetObjectLegalHold,
tt.output.response,
tt.output.err,
ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
})
})
}
}
func TestS3ApiController_GetObjectAcl(t *testing.T) {
var emptyRes *s3.GetObjectAclOutput
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,
beRes: emptyRes,
beErr: s3err.GetAPIError(s3err.ErrNotImplemented),
},
output: testOutput{
response: &Response{
Data: emptyRes,
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrNotImplemented),
},
},
{
name: "successful response",
input: testInput{
locals: defaultLocals,
beRes: &s3.GetObjectAclOutput{
Owner: &types.Owner{
ID: utils.GetStringPtr("something"),
},
},
},
output: testOutput{
response: &Response{
Data: &s3.GetObjectAclOutput{
Owner: &types.Owner{
ID: utils.GetStringPtr("something"),
},
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
GetObjectAclFunc: func(contextMoqParam context.Context, getObjectAclInput *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) {
return tt.input.beRes.(*s3.GetObjectAclOutput), 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.GetObjectAcl,
tt.output.response,
tt.output.err,
ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
})
})
}
}
func TestS3ApiController_ListParts(t *testing.T) {
listPartsResult := s3response.ListPartsResult{
Bucket: "my-bucket",
Key: "obj",
IsTruncated: false,
Parts: []s3response.Part{
{ETag: "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 part number marker",
input: testInput{
locals: defaultLocals,
queries: map[string]string{
"part-number-marker": "-1",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrInvalidPartNumberMarker),
},
},
{
name: "invalid max parts",
input: testInput{
locals: defaultLocals,
queries: map[string]string{
"max-parts": "-1",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrInvalidMaxParts),
},
},
{
name: "backend returns error",
input: testInput{
locals: defaultLocals,
beRes: s3response.ListPartsResult{},
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
output: testOutput{
response: &Response{
Data: s3response.ListPartsResult{},
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "successful response",
input: testInput{
locals: defaultLocals,
beRes: listPartsResult,
},
output: testOutput{
response: &Response{
Data: listPartsResult,
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
ListPartsFunc: func(contextMoqParam context.Context, listPartsInput *s3.ListPartsInput) (s3response.ListPartsResult, error) {
return tt.input.beRes.(s3response.ListPartsResult), 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.ListParts,
tt.output.response,
tt.output.err,
ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
queries: tt.input.queries,
})
})
}
}
func TestS3ApiController_GetObjectAttributes(t *testing.T) {
delMarker, lastModTime, etag := true, time.Now(), "ETag"
timeFormatted := lastModTime.UTC().Format(iso8601TimeFormatExtended)
validRes := s3response.GetObjectAttributesResponse{
DeleteMarker: &delMarker,
LastModified: &lastModTime,
VersionId: utils.GetStringPtr("versionId"),
ETag: &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 max parts",
input: testInput{
locals: defaultLocals,
headers: map[string]string{
"X-Amz-Max-Parts": "-1",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrInvalidMaxParts),
},
},
{
name: "invalid object attributes",
input: testInput{
locals: defaultLocals,
headers: map[string]string{
"X-Amz-Object-Attributes": "invalid_attribute",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrInvalidObjectAttributes),
},
},
{
name: "backend returns error",
input: testInput{
locals: defaultLocals,
beRes: validRes,
beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket),
headers: map[string]string{
"X-Amz-Object-Attributes": "ETag",
},
},
output: testOutput{
response: &Response{
Headers: map[string]*string{
"x-amz-version-id": utils.GetStringPtr("versionId"),
"x-amz-delete-marker": utils.GetStringPtr("true"),
},
Data: nil,
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrNoSuchBucket),
},
},
{
name: "successful response",
input: testInput{
locals: defaultLocals,
beRes: validRes,
headers: map[string]string{
"X-Amz-Object-Attributes": "ETag",
},
},
output: testOutput{
response: &Response{
Headers: map[string]*string{
"x-amz-version-id": utils.GetStringPtr("versionId"),
"x-amz-delete-marker": utils.GetStringPtr("true"),
"Last-Modified": &timeFormatted,
},
Data: s3response.GetObjectAttributesResponse{
ETag: &etag,
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
GetObjectAttributesFunc: func(contextMoqParam context.Context, getObjectAttributesInput *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResponse, error) {
return tt.input.beRes.(s3response.GetObjectAttributesResponse), 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.GetObjectAttributes,
tt.output.response,
tt.output.err,
ctxInputs{
locals: tt.input.locals,
body: tt.input.body,
headers: tt.input.headers,
})
})
}
}
func TestS3ApiController_GetObject(t *testing.T) {
tm := time.Now()
cLength := int64(11)
rdr := io.NopCloser(strings.NewReader("hello world"))
delMarker := true
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 checksum mode",
input: testInput{
locals: defaultLocals,
headers: map[string]string{
"x-amz-checksum-mode": "invalid_checksum_mode",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode"),
},
},
{
name: "backend returns error",
input: testInput{
locals: defaultLocals,
beErr: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
beRes: &s3.GetObjectOutput{
DeleteMarker: &delMarker,
LastModified: &tm,
},
},
output: testOutput{
response: &Response{
Headers: map[string]*string{
"x-amz-delete-marker": utils.GetStringPtr("true"),
"Last-Modified": utils.GetStringPtr(tm.UTC().Format(timefmt)),
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
},
},
// TODO: add a test case for overflowing content-length
// simulate a 32 bit arch to test the case
{
name: "successful response",
input: testInput{
headers: map[string]string{
"Range": "100-200",
},
queries: map[string]string{
"versionId": "versionId",
},
locals: defaultLocals,
beRes: &s3.GetObjectOutput{
ETag: utils.GetStringPtr("ETag"),
ContentType: utils.GetStringPtr("application/xml"),
ContentLength: &cLength,
Body: rdr,
},
},
output: testOutput{
response: &Response{
Headers: map[string]*string{
"ETag": utils.GetStringPtr("ETag"),
"x-amz-restore": nil,
"accept-ranges": nil,
"Content-Range": nil,
"Content-Disposition": nil,
"Content-Encoding": nil,
"Content-Language": nil,
"Cache-Control": nil,
"Expires": nil,
"x-amz-checksum-crc32": nil,
"x-amz-checksum-crc64nvme": nil,
"x-amz-checksum-crc32c": nil,
"x-amz-checksum-sha1": nil,
"x-amz-checksum-sha256": nil,
"x-amz-version-id": nil,
"x-amz-mp-parts-count": nil,
"x-amz-object-lock-mode": nil,
"x-amz-object-lock-legal-hold": nil,
"x-amz-storage-class": nil,
"x-amz-checksum-type": nil,
"x-amz-object-lock-retain-until-date": nil,
"Last-Modified": nil,
"x-amz-tagging-count": nil,
"Content-Type": utils.GetStringPtr("application/xml"),
"Content-Length": utils.GetStringPtr("11"),
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
Status: http.StatusPartialContent,
ContentLength: cLength,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
GetObjectFunc: func(contextMoqParam context.Context, getObjectInput *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
return tt.input.beRes.(*s3.GetObjectOutput), 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.GetObject,
tt.output.response,
tt.output.err,
ctxInputs{
locals: tt.input.locals,
headers: tt.input.headers,
queries: tt.input.queries,
})
})
}
}

View File

@@ -41,20 +41,6 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) {
objRange := ctx.Get("Range")
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
var partNumber *int32
if ctx.Request().URI().QueryArgs().Has("partNumber") {
if partNumberQuery < 1 || partNumberQuery > 10000 {
debuglogger.Logf("invalid part number: %d", partNumberQuery)
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetAPIError(s3err.ErrInvalidPartNumber)
}
partNumber = &partNumberQuery
}
err := auth.VerifyAccess(ctx.Context(), c.be,
auth.AccessOptions{
Readonly: c.readonly,
@@ -75,6 +61,20 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) {
}, err
}
var partNumber *int32
if ctx.Request().URI().QueryArgs().Has("partNumber") {
if partNumberQuery < 1 || partNumberQuery > 10000 {
debuglogger.Logf("invalid part number: %d", partNumberQuery)
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetAPIError(s3err.ErrInvalidPartNumber)
}
partNumber = &partNumberQuery
}
checksumMode := types.ChecksumMode(ctx.Get("x-amz-checksum-mode"))
if checksumMode != "" && checksumMode != types.ChecksumModeEnabled {
debuglogger.Logf("invalid x-amz-checksum-mode header value: %v", checksumMode)

View File

@@ -0,0 +1,183 @@
// 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"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
)
func TestS3ApiController_HeadObject(t *testing.T) {
tm := time.Now()
cLength := int64(100)
failingBeRes := &s3.HeadObjectOutput{
LastModified: &tm,
}
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 part number",
input: testInput{
locals: defaultLocals,
queries: map[string]string{
"partNumber": "-4",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrInvalidPartNumber),
},
},
{
name: "invalid checksum mode",
input: testInput{
locals: defaultLocals,
headers: map[string]string{
"x-amz-checksum-mode": "invalid_checksum_mode",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode"),
},
},
{
name: "backend returns error",
input: testInput{
locals: defaultLocals,
beErr: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
beRes: failingBeRes,
},
output: testOutput{
response: &Response{
Headers: map[string]*string{
"x-amz-delete-marker": utils.GetStringPtr("true"),
"Last-Modified": utils.GetStringPtr(tm.UTC().Format(timefmt)),
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrInvalidAccessKeyID),
},
},
{
name: "successful response",
input: testInput{
queries: map[string]string{
"partNumber": "4",
},
locals: defaultLocals,
beRes: &s3.HeadObjectOutput{
ETag: utils.GetStringPtr("ETag"),
ContentType: utils.GetStringPtr("application/xml"),
ContentLength: &cLength,
},
},
output: testOutput{
response: &Response{
Headers: map[string]*string{
"ETag": utils.GetStringPtr("ETag"),
"x-amz-restore": nil,
"accept-ranges": nil,
"Content-Range": nil,
"Content-Disposition": nil,
"Content-Encoding": nil,
"Content-Language": nil,
"Cache-Control": nil,
"Expires": nil,
"x-amz-checksum-crc32": nil,
"x-amz-checksum-crc64nvme": nil,
"x-amz-checksum-crc32c": nil,
"x-amz-checksum-sha1": nil,
"x-amz-checksum-sha256": nil,
"x-amz-version-id": nil,
"x-amz-mp-parts-count": nil,
"x-amz-object-lock-mode": nil,
"x-amz-object-lock-legal-hold": nil,
"x-amz-storage-class": nil,
"x-amz-checksum-type": nil,
"x-amz-object-lock-retain-until-date": nil,
"Last-Modified": nil,
"Content-Type": utils.GetStringPtr("application/xml"),
"Content-Length": utils.GetStringPtr("100"),
},
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
be := &BackendMock{
HeadObjectFunc: func(contextMoqParam context.Context, headObjectInput *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
return tt.input.beRes.(*s3.HeadObjectOutput), 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.HeadObject,
tt.output.response,
tt.output.err,
ctxInputs{
locals: tt.input.locals,
queries: tt.input.queries,
headers: tt.input.headers,
})
})
}
}

View File

@@ -677,6 +677,9 @@ func GetStringPtr(str string) *string {
// Converts any type to a string pointer
func ConvertToStringPtr[T any](val T) *string {
str := fmt.Sprint(val)
if str == "" {
return nil
}
return &str
}
@@ -694,6 +697,9 @@ func FormatDatePtrToString(date *time.Time, format string) *string {
if date == nil {
return nil
}
if date.IsZero() {
return nil
}
formatted := date.UTC().Format(format)
return &formatted