forked from TrueCloudLab/frostfs-s3-gw
458 lines
12 KiB
Go
458 lines
12 KiB
Go
|
package handler
|
||
|
|
||
|
import (
|
||
|
"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"
|
||
|
apiErrors "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
|
||
|
error bool
|
||
|
}{
|
||
|
{
|
||
|
name: "correct configuration",
|
||
|
body: &data.LifecycleConfiguration{
|
||
|
XMLName: xml.Name{
|
||
|
Space: `http://s3.amazonaws.com/doc/2006-03-01/`,
|
||
|
Local: "LifecycleConfiguration",
|
||
|
},
|
||
|
Rules: []data.LifecycleRule{
|
||
|
{
|
||
|
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)),
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
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
|
||
|
}(),
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
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),
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
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],
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
}(),
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
name: "invalid status",
|
||
|
body: &data.LifecycleConfiguration{
|
||
|
Rules: []data.LifecycleRule{
|
||
|
{
|
||
|
Status: "invalid",
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
name: "no actions",
|
||
|
body: &data.LifecycleConfiguration{
|
||
|
Rules: []data.LifecycleRule{
|
||
|
{
|
||
|
Status: data.LifecycleStatusEnabled,
|
||
|
Filter: &data.LifecycleRuleFilter{
|
||
|
Prefix: "prefix/",
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
name: "invalid days after initiation",
|
||
|
body: &data.LifecycleConfiguration{
|
||
|
Rules: []data.LifecycleRule{
|
||
|
{
|
||
|
Status: data.LifecycleStatusEnabled,
|
||
|
AbortIncompleteMultipartUpload: &data.AbortIncompleteMultipartUpload{
|
||
|
DaysAfterInitiation: ptr(0),
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
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),
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
name: "invalid expiration days",
|
||
|
body: &data.LifecycleConfiguration{
|
||
|
Rules: []data.LifecycleRule{
|
||
|
{
|
||
|
Status: data.LifecycleStatusEnabled,
|
||
|
Expiration: &data.LifecycleExpiration{
|
||
|
Days: ptr(0),
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
name: "invalid expiration date",
|
||
|
body: &data.LifecycleConfiguration{
|
||
|
Rules: []data.LifecycleRule{
|
||
|
{
|
||
|
Status: data.LifecycleStatusEnabled,
|
||
|
Expiration: &data.LifecycleExpiration{
|
||
|
Date: "invalid",
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
name: "newer noncurrent versions is too small",
|
||
|
body: &data.LifecycleConfiguration{
|
||
|
Rules: []data.LifecycleRule{
|
||
|
{
|
||
|
Status: data.LifecycleStatusEnabled,
|
||
|
NonCurrentVersionExpiration: &data.NonCurrentVersionExpiration{
|
||
|
NewerNonCurrentVersions: ptr(0),
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
name: "newer noncurrent versions is too large",
|
||
|
body: &data.LifecycleConfiguration{
|
||
|
Rules: []data.LifecycleRule{
|
||
|
{
|
||
|
Status: data.LifecycleStatusEnabled,
|
||
|
NonCurrentVersionExpiration: &data.NonCurrentVersionExpiration{
|
||
|
NewerNonCurrentVersions: ptr(101),
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
name: "invalid noncurrent days",
|
||
|
body: &data.LifecycleConfiguration{
|
||
|
Rules: []data.LifecycleRule{
|
||
|
{
|
||
|
Status: data.LifecycleStatusEnabled,
|
||
|
NonCurrentVersionExpiration: &data.NonCurrentVersionExpiration{
|
||
|
NonCurrentDays: ptr(0),
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
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)),
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
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{},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
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"},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
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"}},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
{
|
||
|
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)),
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
error: true,
|
||
|
},
|
||
|
} {
|
||
|
t.Run(tc.name, func(t *testing.T) {
|
||
|
if tc.error {
|
||
|
putBucketLifecycleConfigurationErr(hc, bktName, tc.body, apiErrors.GetAPIError(apiErrors.ErrMalformedXML))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
putBucketLifecycleConfiguration(hc, bktName, tc.body)
|
||
|
|
||
|
cfg := getBucketLifecycleConfiguration(hc, bktName)
|
||
|
require.Equal(t, *tc.body, *cfg)
|
||
|
|
||
|
deleteBucketLifecycleConfiguration(hc, bktName)
|
||
|
getBucketLifecycleConfigurationErr(hc, bktName, apiErrors.GetAPIError(apiErrors.ErrNoSuchLifecycleConfiguration))
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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, apiErrors.GetAPIError(apiErrors.ErrMissingContentMD5))
|
||
|
|
||
|
w, r = prepareTestRequest(hc, bktName, "", lifecycle)
|
||
|
r.Header.Set(api.ContentMD5, "")
|
||
|
hc.Handler().PutBucketLifecycleHandler(w, r)
|
||
|
assertS3Error(hc.t, w, apiErrors.GetAPIError(apiErrors.ErrInvalidDigest))
|
||
|
|
||
|
w, r = prepareTestRequest(hc, bktName, "", lifecycle)
|
||
|
r.Header.Set(api.ContentMD5, "some-hash")
|
||
|
hc.Handler().PutBucketLifecycleHandler(w, r)
|
||
|
assertS3Error(hc.t, w, apiErrors.GetAPIError(apiErrors.ErrInvalidDigest))
|
||
|
}
|
||
|
|
||
|
func TestPutBucketLifecycleInvalidXML(t *testing.T) {
|
||
|
hc := prepareHandlerContext(t)
|
||
|
|
||
|
bktName := "bucket-lifecycle-invalid-xml"
|
||
|
createBucket(hc, bktName)
|
||
|
|
||
|
w, r := prepareTestRequest(hc, bktName, "", &data.CORSConfiguration{})
|
||
|
r.Header.Set(api.ContentMD5, "")
|
||
|
hc.Handler().PutBucketLifecycleHandler(w, r)
|
||
|
assertS3Error(hc.t, w, apiErrors.GetAPIError(apiErrors.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 apiErrors.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 apiErrors.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
|
||
|
}
|