diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go index 14b3cf4..fd2f86e 100644 --- a/api/handler/acl_test.go +++ b/api/handler/acl_test.go @@ -26,7 +26,7 @@ func TestPutObjectACLErrorAPE(t *testing.T) { hc := prepareHandlerContext(t) bktName, objName := "bucket-for-acl-ape", "object" - info := createBucket(hc, bktName) + info := createBucket(hc, bucketPrm{bktName: bktName}) putObjectWithHeadersAssertS3Error(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPublic}, apierr.ErrAccessControlListNotSupported) putObjectWithHeaders(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPrivate}) // only `private` canned acl is allowed, that is actually ignored @@ -43,7 +43,7 @@ func TestCreateObjectACLErrorAPE(t *testing.T) { hc := prepareHandlerContext(t) bktName, objName, objNameCopy := "bucket-for-acl-ape", "object", "copy" - createBucket(hc, bktName) + createBucket(hc, bucketPrm{bktName: bktName}) putObject(hc, bktName, objName) copyObject(hc, bktName, objName, objNameCopy, CopyMeta{Headers: map[string]string{api.AmzACL: basicACLPublic}}, http.StatusBadRequest) @@ -57,7 +57,7 @@ func TestBucketACLAPE(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-for-acl-ape" - info := createBucket(hc, bktName) + info := createBucket(hc, bucketPrm{bktName: bktName}) aclBody := &AccessControlPolicy{} putBucketACLAssertS3Error(hc, bktName, info.Box, nil, aclBody, apierr.ErrAccessControlListNotSupported) @@ -297,30 +297,30 @@ type createBucketInfo struct { Key *keys.PrivateKey } -func createBucket(hc *handlerContext, bktName string) *createBucketInfo { - box, key := createAccessBox(hc.t) +func createBucket(hc *handlerContext, prm bucketPrm) *createBucketInfo { + prm.box, prm.key = createAccessBox(hc.t) - w := createBucketBase(hc, bktName, box) + w := createBucketBase(hc, prm) assertStatus(hc.t, w, http.StatusOK) - bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName) + bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), prm.bktName) require.NoError(hc.t, err) return &createBucketInfo{ BktInfo: bktInfo, - Box: box, - Key: key, + Box: prm.box, + Key: prm.key, } } -func createBucketAssertS3Error(hc *handlerContext, bktName string, box *accessbox.Box, code apierr.ErrorCode) { - w := createBucketBase(hc, bktName, box) +func createBucketAssertS3Error(hc *handlerContext, prm bucketPrm, code apierr.ErrorCode) { + w := createBucketBase(hc, prm) 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 createBucketBase(hc *handlerContext, prm bucketPrm) *httptest.ResponseRecorder { + w, r := prepareTestFullRequest(hc, prm.bktName, "", nil, prm.body) + 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..8a0c5dc --- /dev/null +++ b/api/handler/bucket_list_test.go @@ -0,0 +1,147 @@ +package handler + +import ( + "encoding/xml" + "net/http" + "net/http/httptest" + "net/url" + "sort" + "testing" + + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" + "github.com/stretchr/testify/require" +) + +func TestHandler_ListBucketsHandler(t *testing.T) { + const defaultConstraint = "default" + + type bktProp struct { + name string + region string + cnrID cid.ID + } + + region := "us-west-1" + hc := prepareHandlerContext(t) + hc.config.putLocationConstraint(region) + + props := []bktProp{ + {"first", "", cid.ID{}}, + {"regional", "us-west-1", cid.ID{}}, + {"third", "", cid.ID{}}, + } + for i, bkt := range props { + var body createBucketParams + if bkt.region != "" { + body.LocationConstraint = bkt.region + } + info := createBucket(hc, bucketPrm{bktName: bkt.name, body: body}) + props[i].cnrID = info.BktInfo.CID + } + + for _, tt := range []struct { + title string + query url.Values + expected []bktProp + expectedToken string + }{ + { + title: "without params", + query: url.Values{}, + expected: []bktProp{ + {"first", defaultConstraint, cid.ID{}}, + {"regional", "us-west-1", cid.ID{}}, + {"third", defaultConstraint, cid.ID{}}, + }, + }, + { + title: "with prefix", + query: url.Values{"prefix": []string{"thi"}}, + expected: []bktProp{{"third", defaultConstraint, cid.ID{}}}, + }, + { + title: "wrong prefix", + query: url.Values{"prefix": []string{"sdh"}}, + expected: []bktProp{}, + }, + { + title: "with bucket region", + query: url.Values{"bucket-region": []string{region}}, + expected: []bktProp{{"regional", "us-west-1", cid.ID{}}}, + }, + { + title: "with default bucket region", + query: url.Values{"bucket-region": []string{"default"}}, + expected: []bktProp{ + {"first", defaultConstraint, cid.ID{}}, + {"third", defaultConstraint, cid.ID{}}, + }, + }, + { + title: "wrong bucket region", + query: url.Values{"bucket-region": []string{"sjdfjl"}}, + expected: []bktProp{}, + }, + { + title: "pagination max-buckets", + query: url.Values{"max-buckets": []string{"1"}}, + expected: []bktProp{{"first", defaultConstraint, cid.ID{}}}, + expectedToken: props[1].cnrID.String(), + }, + { + title: "pagination continuation token", + query: url.Values{ + "max-buckets": []string{"1"}, + "continuation-token": []string{props[1].cnrID.String()}, + }, + expected: []bktProp{{"regional", region, cid.ID{}}}, + expectedToken: props[2].cnrID.String(), + }, + { + title: "pagination last element", + query: url.Values{ + "max-buckets": []string{"1"}, + "continuation-token": []string{props[2].cnrID.String()}, + }, + expected: []bktProp{{"third", defaultConstraint, cid.ID{}}}, + expectedToken: "", + }, + } { + t.Run(tt.title, func(t *testing.T) { + params := bucketPrm{query: tt.query} + resp := listBuckets(hc, params) + require.Len(t, resp.Buckets.Buckets, len(tt.expected)) + require.Equal(t, params.query.Get("prefix"), resp.Prefix) + require.Equal(t, tt.expectedToken, resp.ContinuationToken) + respProps := make([]bktProp, 0, len(resp.Buckets.Buckets)) + for _, bkt := range resp.Buckets.Buckets { + respProps = append(respProps, bktProp{bkt.Name, bkt.BucketRegion, cid.ID{}}) + } + sort.Slice(respProps, func(i, j int) bool { + return respProps[i].name < respProps[j].name + }) + require.Equal(t, tt.expected, respProps) + }) + } +} + +func listBuckets(hc *handlerContext, prm bucketPrm) ListBucketsResponse { + prm.box, prm.key = createAccessBox(hc.t) + w := listBucketsBase(hc, prm) + assertStatus(hc.t, w, http.StatusOK) + var resp ListBucketsResponse + err := xml.NewDecoder(w.Body).Decode(&resp) + require.NoError(hc.t, err) + + return resp +} + +func listBucketsBase(hc *handlerContext, prm bucketPrm) *httptest.ResponseRecorder { + w, r := prepareTestFullRequest(hc, "", "", prm.query, nil) + ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: prm.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 8c886a1..b5ca3dc 100644 --- a/api/handler/handlers_test.go +++ b/api/handler/handlers_test.go @@ -23,6 +23,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" @@ -74,6 +75,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 +87,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 +149,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 +219,8 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas } cfg := &configMock{ - defaultPolicy: pp, + defaultPolicy: pp, + placementPolicies: make(map[string]netmap.PlacementPolicy), } h := &handler{ log: log, @@ -394,8 +402,16 @@ func (f *frostfsidMock) GetUserKey(account, user string) (string, error) { return hex.EncodeToString(res.Bytes()), nil } +type bucketPrm struct { + bktName string + query url.Values + box *accessbox.Box + key *keys.PrivateKey + body any +} + func createTestBucket(hc *handlerContext, bktName string) *data.BucketInfo { - info := createBucket(hc, bktName) + info := createBucket(hc, bucketPrm{bktName: bktName}) return info.BktInfo } diff --git a/api/handler/lifecycle_test.go b/api/handler/lifecycle_test.go index 69382ab..27b71ab 100644 --- a/api/handler/lifecycle_test.go +++ b/api/handler/lifecycle_test.go @@ -24,7 +24,7 @@ func TestPutBucketLifecycleConfiguration(t *testing.T) { hc := prepareHandlerContextWithMinCache(t) bktName := "bucket-lifecycle" - createBucket(hc, bktName) + createBucket(hc, bucketPrm{bktName: bktName}) for _, tc := range []struct { name string @@ -429,7 +429,7 @@ func TestPutBucketLifecycleIDGeneration(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-lifecycle-id" - createBucket(hc, bktName) + createBucket(hc, bucketPrm{bktName: bktName}) lifecycle := &data.LifecycleConfiguration{ Rules: []data.LifecycleRule{ @@ -459,7 +459,7 @@ func TestPutBucketLifecycleInvalidMD5(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-lifecycle-md5" - createBucket(hc, bktName) + createBucket(hc, bucketPrm{bktName: bktName}) lifecycle := &data.LifecycleConfiguration{ Rules: []data.LifecycleRule{ @@ -491,7 +491,7 @@ func TestPutBucketLifecycleInvalidXML(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bucket-lifecycle-invalid-xml" - createBucket(hc, bktName) + createBucket(hc, bucketPrm{bktName: bktName}) cfg := &data.CORSConfiguration{} body, err := xml.Marshal(cfg) diff --git a/api/handler/put_test.go b/api/handler/put_test.go index 53968a6..f34d1f0 100644 --- a/api/handler/put_test.go +++ b/api/handler/put_test.go @@ -546,11 +546,11 @@ func TestCreateBucket(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bkt-name" - info := createBucket(hc, bktName) - createBucketAssertS3Error(hc, bktName, info.Box, apierr.ErrBucketAlreadyOwnedByYou) + info := createBucket(hc, bucketPrm{bktName: bktName}) + createBucketAssertS3Error(hc, bucketPrm{bktName: bktName, box: info.Box}, apierr.ErrBucketAlreadyOwnedByYou) box2, _ := createAccessBox(t) - createBucketAssertS3Error(hc, bktName, box2, apierr.ErrBucketAlreadyExists) + createBucketAssertS3Error(hc, bucketPrm{bktName: bktName, box: box2}, apierr.ErrBucketAlreadyExists) } func TestCreateBucketWithoutPermissions(t *testing.T) { @@ -560,7 +560,7 @@ func TestCreateBucketWithoutPermissions(t *testing.T) { hc.h.ape.(*apeMock).err = errors.New("no permissions") box, _ := createAccessBox(t) - createBucketAssertS3Error(hc, bktName, box, apierr.ErrInternalError) + createBucketAssertS3Error(hc, bucketPrm{bktName: bktName, box: box}, apierr.ErrInternalError) _, err := hc.tp.ContainerID(bktName) require.Errorf(t, err, "container exists after failed creation, but shouldn't")