From 5bfb8fd291ef08d270333a4e553f6d635964a37e Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Sat, 5 Mar 2022 15:58:04 +0300 Subject: [PATCH] [#192] Add base lifecycle tests Signed-off-by: Denis Kirillov --- api/data/lifecycle.go | 28 +++---- api/handler/lifecycle.go | 24 ++++++ api/handler/lifecycle_test.go | 148 ++++++++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 api/handler/lifecycle_test.go diff --git a/api/data/lifecycle.go b/api/data/lifecycle.go index 4f9b710c..df62908c 100644 --- a/api/data/lifecycle.go +++ b/api/data/lifecycle.go @@ -9,15 +9,15 @@ type ( } Rule struct { - AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload" json:"AbortIncompleteMultipartUpload"` - Expiration Expiration `xml:"Expiration" json:"Expiration"` - Filter LifecycleRuleFilter `xml:"Filter" json:"Filter"` - ID string `xml:"ID" json:"ID"` - NoncurrentVersionExpiration NoncurrentVersionExpiration `xml:"NoncurrentVersionExpiration" json:"NoncurrentVersionExpiration"` - NoncurrentVersionTransitions []NoncurrentVersionTransition `xml:"NoncurrentVersionTransition" json:"NoncurrentVersionTransition"` - Prefix string `xml:"Prefix" json:"Prefix"` - Status string `xml:"Status" json:"Status"` - Transitions []Transition `xml:"Transition" json:"Transition"` + AbortIncompleteMultipartUpload *AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload" json:"AbortIncompleteMultipartUpload"` + Expiration *Expiration `xml:"Expiration" json:"Expiration"` + Filter *LifecycleRuleFilter `xml:"Filter" json:"Filter"` + ID string `xml:"ID" json:"ID"` + NoncurrentVersionExpiration *NoncurrentVersionExpiration `xml:"NoncurrentVersionExpiration" json:"NoncurrentVersionExpiration"` + NoncurrentVersionTransitions []NoncurrentVersionTransition `xml:"NoncurrentVersionTransition" json:"NoncurrentVersionTransition"` + Prefix string `xml:"Prefix" json:"Prefix"` + Status string `xml:"Status" json:"Status"` + Transitions []Transition `xml:"Transition" json:"Transition"` } AbortIncompleteMultipartUpload struct { @@ -31,11 +31,11 @@ type ( } LifecycleRuleFilter struct { - And LifecycleRuleAndOperator `xml:"And" json:"And"` - ObjectSizeGreaterThan int64 `xml:"ObjectSizeGreaterThan" json:"ObjectSizeGreaterThan"` - ObjectSizeLessThan int64 `xml:"ObjectSizeLessThan" json:"ObjectSizeLessThan"` - Prefix string `xml:"Prefix" json:"Prefix"` - Tag Tag `xml:"Tag" json:"Tag"` + And *LifecycleRuleAndOperator `xml:"And" json:"And"` + ObjectSizeGreaterThan int64 `xml:"ObjectSizeGreaterThan" json:"ObjectSizeGreaterThan"` + ObjectSizeLessThan int64 `xml:"ObjectSizeLessThan" json:"ObjectSizeLessThan"` + Prefix string `xml:"Prefix" json:"Prefix"` + Tag *Tag `xml:"Tag" json:"Tag"` } LifecycleRuleAndOperator struct { diff --git a/api/handler/lifecycle.go b/api/handler/lifecycle.go index 781d2563..660ab7a3 100644 --- a/api/handler/lifecycle.go +++ b/api/handler/lifecycle.go @@ -123,7 +123,31 @@ func checkLifecycleConfiguration(conf *data.LifecycleConfiguration) error { if rule.Status != enabledValue && rule.Status != disabledValue { return apiErrors.GetAPIError(apiErrors.ErrMalformedXML) } + if rule.Filter != nil { + if rule.Filter.ObjectSizeGreaterThan < 0 || rule.Filter.ObjectSizeLessThan < 0 { + return apiErrors.GetAPIError(apiErrors.ErrMalformedXML) + } + + if !filterContainsOneOption(rule.Filter) { + return apiErrors.GetAPIError(apiErrors.ErrMalformedXML) + } + } } return nil } + +func filterContainsOneOption(filter *data.LifecycleRuleFilter) bool { + exactlyOneOption := 0 + if filter.Prefix != "" { + exactlyOneOption++ + } + if filter.And != nil { + exactlyOneOption++ + } + if filter.Tag != nil { + exactlyOneOption++ + } + + return exactlyOneOption == 1 +} diff --git a/api/handler/lifecycle_test.go b/api/handler/lifecycle_test.go new file mode 100644 index 00000000..65f44590 --- /dev/null +++ b/api/handler/lifecycle_test.go @@ -0,0 +1,148 @@ +package handler + +import ( + "context" + "encoding/xml" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/nspcc-dev/neofs-s3-gw/api/data" + apiErrors "github.com/nspcc-dev/neofs-s3-gw/api/errors" + "github.com/stretchr/testify/require" +) + +func TestCheckLifecycleConfiguration(t *testing.T) { + numRules := 1001 + rules := make([]data.Rule, numRules) + for i := 0; i < numRules; i++ { + rules[i] = data.Rule{ID: strconv.Itoa(i), Status: disabledValue} + } + + for _, tc := range []struct { + name string + configuration *data.LifecycleConfiguration + noError bool + }{ + { + name: "basic", + configuration: &data.LifecycleConfiguration{Rules: []data.Rule{{ + ID: "Some ID", + Status: "Disabled", + }}}, + noError: true, + }, + { + name: "invalid status", + configuration: &data.LifecycleConfiguration{Rules: []data.Rule{{ + ID: "Some ID", + Status: "", + }}}, + }, + { + name: "zero rules", + configuration: &data.LifecycleConfiguration{}, + }, + { + name: "more than max rules", + configuration: &data.LifecycleConfiguration{Rules: rules}, + }, + { + name: "invalid empty filter", + configuration: &data.LifecycleConfiguration{Rules: []data.Rule{{ + Status: enabledValue, + Filter: &data.LifecycleRuleFilter{}, + }}}, + }, + { + name: "invalid filter not exactly one option", + configuration: &data.LifecycleConfiguration{Rules: []data.Rule{{ + Status: enabledValue, + Filter: &data.LifecycleRuleFilter{ + Prefix: "prefix", + Tag: &data.Tag{}, + }, + }}}, + }, + { + name: "invalid filter greater obj size", + configuration: &data.LifecycleConfiguration{Rules: []data.Rule{{ + Status: enabledValue, + Filter: &data.LifecycleRuleFilter{ + ObjectSizeGreaterThan: -1, + }, + }}}, + }, + { + name: "invalid filter less obj size", + configuration: &data.LifecycleConfiguration{Rules: []data.Rule{{ + Status: enabledValue, + Filter: &data.LifecycleRuleFilter{ + ObjectSizeLessThan: -1, + }, + }}}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + err := checkLifecycleConfiguration(tc.configuration) + if tc.noError { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} + +func TestBucketLifecycleConfiguration(t *testing.T) { + ctx := context.Background() + hc := prepareHandlerContext(t) + + bktName := "bucket-for-lifecycle" + createTestBucket(ctx, t, hc, bktName) + + w, r := prepareTestRequest(t, bktName, "", nil) + hc.Handler().GetBucketLifecycleHandler(w, r) + assertS3Error(t, w, apiErrors.GetAPIError(apiErrors.ErrNoSuchLifecycleConfiguration)) + + lifecycleConf := &data.LifecycleConfiguration{ + XMLName: xmlName("LifecycleConfiguration"), + Rules: []data.Rule{ + { + AbortIncompleteMultipartUpload: &data.AbortIncompleteMultipartUpload{}, + ID: "Test", + Status: "Disabled", + }, + }} + w, r = prepareTestRequest(t, bktName, "", lifecycleConf) + hc.Handler().PutBucketLifecycleHandler(w, r) + require.Equal(t, http.StatusOK, w.Code) + + w, r = prepareTestRequest(t, bktName, "", nil) + hc.Handler().GetBucketLifecycleHandler(w, r) + assertXMLEqual(t, w, lifecycleConf, &data.LifecycleConfiguration{}) + + w, r = prepareTestRequest(t, bktName, "", lifecycleConf) + hc.Handler().DeleteBucketLifecycleHandler(w, r) + require.Equal(t, http.StatusNoContent, w.Code) + + // make sure deleting is idempotent operation + w, r = prepareTestRequest(t, bktName, "", lifecycleConf) + hc.Handler().DeleteBucketLifecycleHandler(w, r) + require.Equal(t, http.StatusNoContent, w.Code) +} + +func assertXMLEqual(t *testing.T, w *httptest.ResponseRecorder, expected, actual interface{}) { + err := xml.NewDecoder(w.Result().Body).Decode(actual) + require.NoError(t, err) + require.Equal(t, expected, actual) + require.Equal(t, http.StatusOK, w.Code) +} + +func xmlName(local string) xml.Name { + return xml.Name{ + Space: "http://s3.amazonaws.com/doc/2006-03-01/", + Local: local, + } +}