From a2a84b398d40732e4954ec89110c1d41e34fb9f8 Mon Sep 17 00:00:00 2001 From: Nikita Zinkevich Date: Thu, 19 Dec 2024 17:14:22 +0300 Subject: [PATCH] [#585] Add ListBuckets handler test Modify containers field in TestFrostFS in order to get determined order of containers between test runs Signed-off-by: Nikita Zinkevich --- api/handler/acl_test.go | 37 ++++++- api/handler/bucket_list_test.go | 174 ++++++++++++++++++++++++++++++++ api/handler/handlers_test.go | 13 ++- 3 files changed, 216 insertions(+), 8 deletions(-) create mode 100644 api/handler/bucket_list_test.go diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go index 14b3cf4..2533ca5 100644 --- a/api/handler/acl_test.go +++ b/api/handler/acl_test.go @@ -7,6 +7,7 @@ import ( "encoding/xml" "net/http" "net/http/httptest" + "net/url" "testing" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" @@ -297,10 +298,17 @@ type createBucketInfo struct { Key *keys.PrivateKey } +type bucketPrm struct { + bktName string + query url.Values + box *accessbox.Box + createParams createBucketParams +} + func createBucket(hc *handlerContext, bktName string) *createBucketInfo { box, key := createAccessBox(hc.t) - w := createBucketBase(hc, bktName, box) + w := createBucketBase(hc, bucketPrm{bktName: bktName, box: box}) assertStatus(hc.t, w, http.StatusOK) bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName) @@ -314,13 +322,32 @@ func createBucket(hc *handlerContext, bktName string) *createBucketInfo { } func createBucketAssertS3Error(hc *handlerContext, bktName string, box *accessbox.Box, code apierr.ErrorCode) { - w := createBucketBase(hc, bktName, box) + w := createBucketBase(hc, bucketPrm{bktName: bktName, box: box}) assertS3Error(hc.t, w, apierr.GetAPIError(code)) } -func createBucketBase(hc *handlerContext, bktName string, box *accessbox.Box) *httptest.ResponseRecorder { - w, r := prepareTestRequest(hc, bktName, "", nil) - ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box}) +func createBucketWithConstraint(hc *handlerContext, bktName, constraint string) *createBucketInfo { + box, key := createAccessBox(hc.t) + var prm createBucketParams + if constraint != "" { + prm.LocationConstraint = constraint + } + w := createBucketBase(hc, bucketPrm{bktName: bktName, box: box, createParams: prm}) + assertStatus(hc.t, w, http.StatusOK) + + bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName) + require.NoError(hc.t, err) + + return &createBucketInfo{ + BktInfo: bktInfo, + Box: box, + Key: key, + } +} + +func createBucketBase(hc *handlerContext, prm bucketPrm) *httptest.ResponseRecorder { + w, r := prepareTestFullRequest(hc, prm.bktName, "", nil, prm.createParams) + ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: prm.box}) r = r.WithContext(ctx) hc.Handler().CreateBucketHandler(w, r) return w diff --git a/api/handler/bucket_list_test.go b/api/handler/bucket_list_test.go new file mode 100644 index 0000000..8ac2a12 --- /dev/null +++ b/api/handler/bucket_list_test.go @@ -0,0 +1,174 @@ +package handler + +import ( + "encoding/xml" + "net/http" + "net/http/httptest" + "net/url" + "sort" + "testing" + + apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" + "github.com/stretchr/testify/require" +) + +func TestHandler_ListBucketsHandler(t *testing.T) { + const defaultConstraint = "default" + + region := "us-west-1" + hc := prepareHandlerContext(t) + hc.config.putLocationConstraint(region) + + props := []Bucket{ + {Name: "first"}, + {Name: "regional", BucketRegion: "us-west-1"}, + {Name: "third"}, + } + sort.Slice(props, func(i, j int) bool { + return props[i].Name < props[j].Name + }) + for _, bkt := range props { + createBucketWithConstraint(hc, bkt.Name, bkt.BucketRegion) + } + + for _, tt := range []struct { + title string + token string + prefix string + bucketRegion string + maxBuckets string + expectErr bool + expected []Bucket + expectedToken string + }{ + { + title: "no params", + expected: []Bucket{ + {Name: "first", BucketRegion: defaultConstraint}, + {Name: "regional", BucketRegion: "us-west-1"}, + {Name: "third", BucketRegion: defaultConstraint}, + }, + }, + { + title: "negative max-buckets", + maxBuckets: "-1", + expected: []Bucket{}, + expectErr: true, + }, + { + title: "zero max-buckets", + maxBuckets: "0", + expected: []Bucket{}, + }, + { + title: "prefix", + prefix: "thi", + expected: []Bucket{{Name: "third", BucketRegion: defaultConstraint}}, + }, + { + title: "wrong prefix", + prefix: "sdh", + expected: []Bucket{}, + }, + { + title: "bucket region", + bucketRegion: region, + expected: []Bucket{{Name: "regional", BucketRegion: "us-west-1"}}, + }, + { + title: "default bucket region", + bucketRegion: defaultConstraint, + expected: []Bucket{ + {Name: "first", BucketRegion: defaultConstraint}, + {Name: "third", BucketRegion: defaultConstraint}, + }, + }, + { + title: "wrong bucket region", + bucketRegion: "sj dfdlsj", + expected: []Bucket{}, + }, + } { + t.Run(tt.title, func(t *testing.T) { + if tt.expectErr { + listBucketsErr(hc, tt.prefix, tt.token, tt.bucketRegion, tt.maxBuckets, apierr.GetAPIError(apierr.ErrInvalidMaxKeys)) + return + } + + resp := listBuckets(hc, tt.prefix, tt.token, tt.bucketRegion, tt.maxBuckets) + require.Len(t, resp.Buckets.Buckets, len(tt.expected)) + require.Equal(t, tt.prefix, resp.Prefix) + require.Equal(t, hc.owner.String(), resp.Owner.ID) + if len(resp.Buckets.Buckets) > 0 { + t.Log(resp.Buckets.Buckets[0].Name) + } + for i, bkt := range resp.Buckets.Buckets { + require.Equal(t, tt.expected[i].Name, bkt.Name) + require.Equal(t, tt.expected[i].BucketRegion, bkt.BucketRegion) + } + }) + } + + t.Run("pagination", func(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + resp := listBuckets(hc, "", "", "", "1") + require.Len(t, resp.Buckets.Buckets, 1) + require.Equal(t, props[0].Name, resp.Buckets.Buckets[0].Name) + require.NotEmpty(t, resp.ContinuationToken) + + resp = listBuckets(hc, "", resp.ContinuationToken, "", "1") + require.Len(t, resp.Buckets.Buckets, 1) + require.Equal(t, props[1].Name, resp.Buckets.Buckets[0].Name) + require.NotEmpty(t, resp.ContinuationToken) + + resp = listBuckets(hc, "", resp.ContinuationToken, "", "1") + require.Len(t, resp.Buckets.Buckets, 1) + require.Equal(t, props[2].Name, resp.Buckets.Buckets[0].Name) + require.Empty(t, resp.ContinuationToken) + }) + + t.Run("wrong continuation-token", func(t *testing.T) { + resp := listBuckets(hc, "", "CebuVwfRpdMqi9dvgV2SUNbrkfteGtudchKKhNabXUu9", "", "1") + require.Len(t, resp.Buckets.Buckets, 0) + require.Empty(t, resp.ContinuationToken) + }) + }) +} + +func listBuckets(hc *handlerContext, prefix, token, bucketRegion, maxBuckets string) ListBucketsResponse { + query := url.Values{ + middleware.QueryPrefix: []string{prefix}, + middleware.QueryContinuationToken: []string{token}, + middleware.QueryBucketRegion: []string{bucketRegion}, + middleware.QueryMaxBuckets: []string{maxBuckets}, + } + w := listBucketsBase(hc, bucketPrm{query: query}) + assertStatus(hc.t, w, http.StatusOK) + var resp ListBucketsResponse + err := xml.NewDecoder(w.Body).Decode(&resp) + require.NoError(hc.t, err) + + return resp +} + +func listBucketsErr(hc *handlerContext, prefix, token, bucketRegion, maxBuckets string, err apierr.Error) { + query := url.Values{ + middleware.QueryPrefix: []string{prefix}, + middleware.QueryContinuationToken: []string{token}, + middleware.QueryBucketRegion: []string{bucketRegion}, + middleware.QueryMaxBuckets: []string{maxBuckets}, + } + w := listBucketsBase(hc, bucketPrm{query: query}) + assertS3Error(hc.t, w, err) +} + +func listBucketsBase(hc *handlerContext, prm bucketPrm) *httptest.ResponseRecorder { + box, _ := createAccessBox(hc.t) + w, r := prepareTestFullRequest(hc, "", "", prm.query, nil) + ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box}) + r = r.WithContext(ctx) + hc.Handler().ListBucketsHandler(w, r) + + return w +} diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go index dae87fc..78e8f74 100644 --- a/api/handler/handlers_test.go +++ b/api/handler/handlers_test.go @@ -74,6 +74,7 @@ func (hc *handlerContextBase) Context() context.Context { type configMock struct { defaultPolicy netmap.PlacementPolicy + placementPolicies map[string]netmap.PlacementPolicy copiesNumbers map[string][]uint32 defaultCopiesNumbers []uint32 bypassContentEncodingInChunks bool @@ -85,8 +86,9 @@ func (c *configMock) DefaultPlacementPolicy(_ string) netmap.PlacementPolicy { return c.defaultPolicy } -func (c *configMock) PlacementPolicy(_, _ string) (netmap.PlacementPolicy, bool) { - return netmap.PlacementPolicy{}, false +func (c *configMock) PlacementPolicy(_, constraint string) (netmap.PlacementPolicy, bool) { + policy, ok := c.placementPolicies[constraint] + return policy, ok } func (c *configMock) CopiesNumbers(_, locationConstraint string) ([]uint32, bool) { @@ -146,6 +148,10 @@ func (c *configMock) TLSTerminationHeader() string { return c.tlsTerminationHeader } +func (c *configMock) putLocationConstraint(constraint string) { + c.placementPolicies[constraint] = c.defaultPolicy +} + func prepareHandlerContext(t *testing.T) *handlerContext { hc, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(zap.NewExample())) require.NoError(t, err) @@ -212,7 +218,8 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas } cfg := &configMock{ - defaultPolicy: pp, + defaultPolicy: pp, + placementPolicies: make(map[string]netmap.PlacementPolicy), } h := &handler{ log: log,