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 }