forked from TrueCloudLab/frostfs-s3-gw
563 lines
14 KiB
Go
563 lines
14 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/xml"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
|
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
|
"github.com/mr-tron/base58"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestPutBucketLifecycleConfiguration(t *testing.T) {
|
|
hc := prepareHandlerContextWithMinCache(t)
|
|
|
|
bktName := "bucket-lifecycle"
|
|
createBucket(hc, bktName)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
body *data.LifecycleConfiguration
|
|
errorCode apierr.ErrorCode
|
|
}{
|
|
{
|
|
name: "correct configuration",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
ID: "rule-1",
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
Date: time.Now().Format("2006-01-02T15:04:05.000Z"),
|
|
},
|
|
Filter: &data.LifecycleRuleFilter{
|
|
And: &data.LifecycleRuleAndOperator{
|
|
Prefix: "prefix/",
|
|
Tags: []data.Tag{{Key: "key", Value: "value"}, {Key: "tag", Value: ""}},
|
|
ObjectSizeGreaterThan: ptr(uint64(100)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "rule-2",
|
|
Status: data.LifecycleStatusEnabled,
|
|
AbortIncompleteMultipartUpload: &data.AbortIncompleteMultipartUpload{
|
|
DaysAfterInitiation: ptr(14),
|
|
},
|
|
Expiration: &data.LifecycleExpiration{
|
|
ExpiredObjectDeleteMarker: ptr(true),
|
|
},
|
|
Filter: &data.LifecycleRuleFilter{
|
|
ObjectSizeLessThan: ptr(uint64(100)),
|
|
},
|
|
NonCurrentVersionExpiration: &data.NonCurrentVersionExpiration{
|
|
NewerNonCurrentVersions: ptr(1),
|
|
NonCurrentDays: ptr(21),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "too many rules",
|
|
body: func() *data.LifecycleConfiguration {
|
|
lifecycle := new(data.LifecycleConfiguration)
|
|
for i := 0; i <= maxRules; i++ {
|
|
lifecycle.Rules = append(lifecycle.Rules, data.LifecycleRule{
|
|
ID: "Rule" + strconv.Itoa(i),
|
|
})
|
|
}
|
|
return lifecycle
|
|
}(),
|
|
errorCode: apierr.ErrInvalidRequest,
|
|
},
|
|
{
|
|
name: "duplicate rule ID",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
ID: "Rule",
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
},
|
|
},
|
|
{
|
|
ID: "Rule",
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "too long rule ID",
|
|
body: func() *data.LifecycleConfiguration {
|
|
id := make([]byte, maxRuleIDLen+1)
|
|
_, err := io.ReadFull(rand.Reader, id)
|
|
require.NoError(t, err)
|
|
return &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
ID: base58.Encode(id)[:maxRuleIDLen+1],
|
|
},
|
|
},
|
|
}
|
|
}(),
|
|
errorCode: apierr.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "invalid status",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: "invalid",
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrMalformedXML,
|
|
},
|
|
{
|
|
name: "no actions",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Filter: &data.LifecycleRuleFilter{
|
|
Prefix: "prefix/",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidRequest,
|
|
},
|
|
{
|
|
name: "invalid days after initiation",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
AbortIncompleteMultipartUpload: &data.AbortIncompleteMultipartUpload{
|
|
DaysAfterInitiation: ptr(0),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "invalid expired object delete marker declaration",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
ExpiredObjectDeleteMarker: ptr(false),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrMalformedXML,
|
|
},
|
|
{
|
|
name: "invalid expiration days",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(0),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "invalid expiration date",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Date: "invalid",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "newer noncurrent versions is too small",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
NonCurrentVersionExpiration: &data.NonCurrentVersionExpiration{
|
|
NonCurrentDays: ptr(1),
|
|
NewerNonCurrentVersions: ptr(0),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "newer noncurrent versions is too large",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
NonCurrentVersionExpiration: &data.NonCurrentVersionExpiration{
|
|
NonCurrentDays: ptr(1),
|
|
NewerNonCurrentVersions: ptr(101),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "invalid noncurrent days",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
NonCurrentVersionExpiration: &data.NonCurrentVersionExpiration{
|
|
NonCurrentDays: ptr(0),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "more than one filter field",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
},
|
|
Filter: &data.LifecycleRuleFilter{
|
|
Prefix: "prefix/",
|
|
ObjectSizeGreaterThan: ptr(uint64(100)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrMalformedXML,
|
|
},
|
|
{
|
|
name: "invalid tag in filter",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
},
|
|
Filter: &data.LifecycleRuleFilter{
|
|
Tag: &data.Tag{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidTagKey,
|
|
},
|
|
{
|
|
name: "abort incomplete multipart upload with tag",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
AbortIncompleteMultipartUpload: &data.AbortIncompleteMultipartUpload{
|
|
DaysAfterInitiation: ptr(14),
|
|
},
|
|
Filter: &data.LifecycleRuleFilter{
|
|
Tag: &data.Tag{Key: "key", Value: "value"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidRequest,
|
|
},
|
|
{
|
|
name: "expired object delete marker with tag",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
ExpiredObjectDeleteMarker: ptr(true),
|
|
},
|
|
Filter: &data.LifecycleRuleFilter{
|
|
And: &data.LifecycleRuleAndOperator{
|
|
Tags: []data.Tag{{Key: "key", Value: "value"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidRequest,
|
|
},
|
|
{
|
|
name: "invalid size range",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
},
|
|
Filter: &data.LifecycleRuleFilter{
|
|
And: &data.LifecycleRuleAndOperator{
|
|
ObjectSizeGreaterThan: ptr(uint64(100)),
|
|
ObjectSizeLessThan: ptr(uint64(100)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidRequest,
|
|
},
|
|
{
|
|
name: "two prefixes",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
},
|
|
Filter: &data.LifecycleRuleFilter{
|
|
Prefix: "prefix-1/",
|
|
},
|
|
Prefix: "prefix-2/",
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrMalformedXML,
|
|
},
|
|
{
|
|
name: "newer noncurrent versions without noncurrent days",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
NonCurrentVersionExpiration: &data.NonCurrentVersionExpiration{
|
|
NewerNonCurrentVersions: ptr(10),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrMalformedXML,
|
|
},
|
|
{
|
|
name: "invalid maximum object size in filter",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
},
|
|
Filter: &data.LifecycleRuleFilter{
|
|
ObjectSizeLessThan: ptr(uint64(0)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidRequest,
|
|
},
|
|
{
|
|
name: "invalid maximum object size in filter and",
|
|
body: &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
},
|
|
Filter: &data.LifecycleRuleFilter{
|
|
And: &data.LifecycleRuleAndOperator{
|
|
Prefix: "prefix/",
|
|
ObjectSizeLessThan: ptr(uint64(0)),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
errorCode: apierr.ErrInvalidRequest,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if tc.errorCode > 0 {
|
|
putBucketLifecycleConfigurationErr(hc, bktName, tc.body, apierr.GetAPIError(tc.errorCode))
|
|
return
|
|
}
|
|
|
|
putBucketLifecycleConfiguration(hc, bktName, tc.body)
|
|
|
|
cfg := getBucketLifecycleConfiguration(hc, bktName)
|
|
require.Equal(t, tc.body.Rules, cfg.Rules)
|
|
|
|
deleteBucketLifecycleConfiguration(hc, bktName)
|
|
getBucketLifecycleConfigurationErr(hc, bktName, apierr.GetAPIError(apierr.ErrNoSuchLifecycleConfiguration))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPutBucketLifecycleIDGeneration(t *testing.T) {
|
|
hc := prepareHandlerContext(t)
|
|
|
|
bktName := "bucket-lifecycle-id"
|
|
createBucket(hc, bktName)
|
|
|
|
lifecycle := &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
},
|
|
},
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
AbortIncompleteMultipartUpload: &data.AbortIncompleteMultipartUpload{
|
|
DaysAfterInitiation: ptr(14),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
putBucketLifecycleConfiguration(hc, bktName, lifecycle)
|
|
cfg := getBucketLifecycleConfiguration(hc, bktName)
|
|
require.Len(t, cfg.Rules, 2)
|
|
require.NotEmpty(t, cfg.Rules[0].ID)
|
|
require.NotEmpty(t, cfg.Rules[1].ID)
|
|
}
|
|
|
|
func TestPutBucketLifecycleInvalidMD5(t *testing.T) {
|
|
hc := prepareHandlerContext(t)
|
|
|
|
bktName := "bucket-lifecycle-md5"
|
|
createBucket(hc, bktName)
|
|
|
|
lifecycle := &data.LifecycleConfiguration{
|
|
Rules: []data.LifecycleRule{
|
|
{
|
|
Status: data.LifecycleStatusEnabled,
|
|
Expiration: &data.LifecycleExpiration{
|
|
Days: ptr(21),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
w, r := prepareTestRequest(hc, bktName, "", lifecycle)
|
|
hc.Handler().PutBucketLifecycleHandler(w, r)
|
|
assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrMissingContentMD5))
|
|
|
|
w, r = prepareTestRequest(hc, bktName, "", lifecycle)
|
|
r.Header.Set(api.ContentMD5, "")
|
|
hc.Handler().PutBucketLifecycleHandler(w, r)
|
|
assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrInvalidDigest))
|
|
|
|
w, r = prepareTestRequest(hc, bktName, "", lifecycle)
|
|
r.Header.Set(api.ContentMD5, "some-hash")
|
|
hc.Handler().PutBucketLifecycleHandler(w, r)
|
|
assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrInvalidDigest))
|
|
}
|
|
|
|
func TestPutBucketLifecycleInvalidXML(t *testing.T) {
|
|
hc := prepareHandlerContext(t)
|
|
|
|
bktName := "bucket-lifecycle-invalid-xml"
|
|
createBucket(hc, bktName)
|
|
|
|
cfg := &data.CORSConfiguration{}
|
|
body, err := xml.Marshal(cfg)
|
|
require.NoError(t, err)
|
|
contentMD5, err := getContentMD5(bytes.NewReader(body))
|
|
require.NoError(t, err)
|
|
|
|
w, r := prepareTestRequest(hc, bktName, "", cfg)
|
|
r.Header.Set(api.ContentMD5, base64.StdEncoding.EncodeToString(contentMD5))
|
|
hc.Handler().PutBucketLifecycleHandler(w, r)
|
|
assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrMalformedXML))
|
|
}
|
|
|
|
func putBucketLifecycleConfiguration(hc *handlerContext, bktName string, cfg *data.LifecycleConfiguration) {
|
|
w := putBucketLifecycleConfigurationBase(hc, bktName, cfg)
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
}
|
|
|
|
func putBucketLifecycleConfigurationErr(hc *handlerContext, bktName string, cfg *data.LifecycleConfiguration, err apierr.Error) {
|
|
w := putBucketLifecycleConfigurationBase(hc, bktName, cfg)
|
|
assertS3Error(hc.t, w, err)
|
|
}
|
|
|
|
func putBucketLifecycleConfigurationBase(hc *handlerContext, bktName string, cfg *data.LifecycleConfiguration) *httptest.ResponseRecorder {
|
|
w, r := prepareTestRequest(hc, bktName, "", cfg)
|
|
|
|
rawBody, err := xml.Marshal(cfg)
|
|
require.NoError(hc.t, err)
|
|
|
|
hash := md5.New()
|
|
hash.Write(rawBody)
|
|
r.Header.Set(api.ContentMD5, base64.StdEncoding.EncodeToString(hash.Sum(nil)))
|
|
hc.Handler().PutBucketLifecycleHandler(w, r)
|
|
return w
|
|
}
|
|
|
|
func getBucketLifecycleConfiguration(hc *handlerContext, bktName string) *data.LifecycleConfiguration {
|
|
w := getBucketLifecycleConfigurationBase(hc, bktName)
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
res := &data.LifecycleConfiguration{}
|
|
parseTestResponse(hc.t, w, res)
|
|
return res
|
|
}
|
|
|
|
func getBucketLifecycleConfigurationErr(hc *handlerContext, bktName string, err apierr.Error) {
|
|
w := getBucketLifecycleConfigurationBase(hc, bktName)
|
|
assertS3Error(hc.t, w, err)
|
|
}
|
|
|
|
func getBucketLifecycleConfigurationBase(hc *handlerContext, bktName string) *httptest.ResponseRecorder {
|
|
w, r := prepareTestRequest(hc, bktName, "", nil)
|
|
hc.Handler().GetBucketLifecycleHandler(w, r)
|
|
return w
|
|
}
|
|
|
|
func deleteBucketLifecycleConfiguration(hc *handlerContext, bktName string) {
|
|
w := deleteBucketLifecycleConfigurationBase(hc, bktName)
|
|
assertStatus(hc.t, w, http.StatusNoContent)
|
|
}
|
|
|
|
func deleteBucketLifecycleConfigurationBase(hc *handlerContext, bktName string) *httptest.ResponseRecorder {
|
|
w, r := prepareTestRequest(hc, bktName, "", nil)
|
|
hc.Handler().DeleteBucketLifecycleHandler(w, r)
|
|
return w
|
|
}
|
|
|
|
func ptr[T any](t T) *T {
|
|
return &t
|
|
}
|