[#195] Add tests

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
This commit is contained in:
Denis Kirillov 2022-03-01 18:07:15 +03:00 committed by Angira Kekteeva
parent 7d6271be8a
commit 6cf01bed14
12 changed files with 1078 additions and 253 deletions

View file

@ -119,7 +119,8 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if err = formObjectLock(params.Lock, bktInfo, settings.LockConfiguration, r.Header); err != nil { params.Lock, err = formObjectLock(bktInfo, settings.LockConfiguration, r.Header)
if err != nil {
h.logAndSendError(w, "could not form object lock", reqInfo, err) h.logAndSendError(w, "could not form object lock", reqInfo, err)
return return
} }

View file

@ -0,0 +1,139 @@
package handler
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/nspcc-dev/neofs-s3-gw/api"
"github.com/nspcc-dev/neofs-s3-gw/api/data"
"github.com/nspcc-dev/neofs-s3-gw/api/resolver"
"github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-s3-gw/api/layer"
"github.com/nspcc-dev/neofs-s3-gw/api/mock"
"github.com/nspcc-dev/neofs-sdk-go/logger"
"github.com/stretchr/testify/require"
)
type handlerContext struct {
h *handler
tp *mock.TestPool
}
func (hc *handlerContext) Handler() *handler {
return hc.h
}
func (hc *handlerContext) MockedPool() *mock.TestPool {
return hc.tp
}
func (hc *handlerContext) Layer() layer.Client {
return hc.h.obj
}
func prepareHandlerContext(t *testing.T) *handlerContext {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
l, err := logger.New(logger.WithTraceLevel("panic"))
require.NoError(t, err)
tp := mock.NewTestPool()
testResolver := &resolver.BucketResolver{Name: "test_resolver"}
testResolver.SetResolveFunc(func(ctx context.Context, name string) (*cid.ID, error) {
for id, cnr := range tp.Containers {
for _, attr := range cnr.Attributes() {
if attr.Key() == container.AttributeName && attr.Value() == name {
cnrID := cid.New()
return cnrID, cnrID.Parse(id)
}
}
}
return nil, fmt.Errorf("couldn't resolve container name")
})
layerCfg := &layer.Config{
Caches: layer.DefaultCachesConfigs(),
AnonKey: layer.AnonymousKey{Key: key},
Resolver: testResolver,
}
h := &handler{
log: l,
obj: layer.NewLayer(l, tp, layerCfg),
cfg: &Config{},
}
return &handlerContext{
h: h,
tp: tp,
}
}
func createTestBucket(ctx context.Context, t *testing.T, h *handlerContext, bktName string) {
cnr := container.New(container.WithAttribute(container.AttributeName, bktName))
_, err := h.MockedPool().PutContainer(ctx, cnr)
require.NoError(t, err)
}
func createTestBucketWithLock(ctx context.Context, t *testing.T, h *handlerContext, bktName string, conf *data.ObjectLockConfiguration) {
cnr := container.New(container.WithAttribute(container.AttributeName, bktName),
container.WithAttribute(layer.AttributeLockEnabled, strconv.FormatBool(true)))
cnrID, err := h.MockedPool().PutContainer(ctx, cnr)
require.NoError(t, err)
sp := &layer.PutSettingsParams{
BktInfo: &data.BucketInfo{
CID: cnrID,
Name: bktName,
ObjectLockEnabled: true,
},
Settings: &data.BucketSettings{
VersioningEnabled: true,
LockConfiguration: conf,
},
}
err = h.Layer().PutBucketSettings(ctx, sp)
require.NoError(t, err)
}
func createTestObject(ctx context.Context, t *testing.T, h *handlerContext, bktName, objName string) {
content := make([]byte, 1024)
_, err := rand.Read(content)
require.NoError(t, err)
_, err = h.Layer().PutObject(ctx, &layer.PutObjectParams{
Bucket: bktName,
Object: objName,
Size: int64(len(content)),
Reader: bytes.NewReader(content),
Header: make(map[string]string),
})
require.NoError(t, err)
}
func prepareTestRequest(t *testing.T, bktName, objName string, body interface{}) (*httptest.ResponseRecorder, *http.Request) {
rawBody, err := xml.Marshal(body)
require.NoError(t, err)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPut, defaultUrl, bytes.NewReader(rawBody))
reqInfo := api.NewReqInfo(w, r, api.ObjectRequest{Bucket: bktName, Object: objName})
r = r.WithContext(api.SetReqInfo(r.Context(), reqInfo))
return w, r
}

View file

@ -86,18 +86,18 @@ func (h *handler) GetBucketObjectLockConfigHandler(w http.ResponseWriter, r *htt
return return
} }
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
if err != nil {
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
return
}
if !bktInfo.ObjectLockEnabled { if !bktInfo.ObjectLockEnabled {
h.logAndSendError(w, "object lock disabled", reqInfo, h.logAndSendError(w, "object lock disabled", reqInfo,
apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound)) apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound))
return return
} }
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
if err != nil {
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
return
}
if settings.LockConfiguration == nil { if settings.LockConfiguration == nil {
settings.LockConfiguration = &data.ObjectLockConfiguration{} settings.LockConfiguration = &data.ObjectLockConfiguration{}
} }
@ -174,6 +174,7 @@ func (h *handler) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Reque
BktInfo: bktInfo, BktInfo: bktInfo,
ObjName: objInfo.LegalHoldObject(), ObjName: objInfo.LegalHoldObject(),
Lock: &data.ObjectLock{LegalHold: true}, Lock: &data.ObjectLock{LegalHold: true},
Metadata: make(map[string]string),
} }
if _, err = h.obj.PutSystemObject(r.Context(), ps); err != nil { if _, err = h.obj.PutSystemObject(r.Context(), ps); err != nil {
h.logAndSendError(w, "couldn't put legal hold", reqInfo, err) h.logAndSendError(w, "couldn't put legal hold", reqInfo, err)
@ -278,8 +279,8 @@ func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Reque
return return
} }
if lockInfo != nil && lockInfo.Headers[layer.AttributeComplianceMode] != "" { if err = checkLockInfo(lockInfo, r.Header); err != nil {
h.logAndSendError(w, "couldn't change compliance lock mode", reqInfo, err) h.logAndSendError(w, "couldn't change lock mode", reqInfo, err)
return return
} }
@ -287,6 +288,7 @@ func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Reque
BktInfo: bktInfo, BktInfo: bktInfo,
ObjName: objInfo.RetentionObject(), ObjName: objInfo.RetentionObject(),
Lock: lock, Lock: lock,
Metadata: make(map[string]string),
} }
if _, err = h.obj.PutSystemObject(r.Context(), ps); err != nil { if _, err = h.obj.PutSystemObject(r.Context(), ps); err != nil {
h.logAndSendError(w, "couldn't put legal hold", reqInfo, err) h.logAndSendError(w, "couldn't put legal hold", reqInfo, err)
@ -294,6 +296,22 @@ func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Reque
} }
} }
func checkLockInfo(lock *data.ObjectInfo, header http.Header) error {
if lock == nil {
return nil
}
if lock.Headers[layer.AttributeComplianceMode] != "" {
return fmt.Errorf("it's forbidden to change compliance lock mode")
}
if bypass, err := strconv.ParseBool(header.Get(api.AmzBypassGovernanceRetention)); err != nil || !bypass {
return fmt.Errorf("cannot bypass governance mode")
}
return nil
}
func (h *handler) GetObjectRetentionHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetObjectRetentionHandler(w http.ResponseWriter, r *http.Request) {
reqInfo := api.GetReqInfo(r.Context()) reqInfo := api.GetReqInfo(r.Context())
@ -369,14 +387,16 @@ func checkLockConfiguration(conf *data.ObjectLockConfiguration) error {
return nil return nil
} }
func formObjectLock(objectLock *data.ObjectLock, bktInfo *data.BucketInfo, defaultConfig *data.ObjectLockConfiguration, header http.Header) error { func formObjectLock(bktInfo *data.BucketInfo, defaultConfig *data.ObjectLockConfiguration, header http.Header) (*data.ObjectLock, error) {
if !bktInfo.ObjectLockEnabled { if !bktInfo.ObjectLockEnabled {
if existLockHeaders(header) { if existLockHeaders(header) {
return apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound) return nil, apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound)
} }
return nil return nil, nil
} }
objectLock := &data.ObjectLock{}
if defaultConfig == nil { if defaultConfig == nil {
defaultConfig = &data.ObjectLockConfiguration{} defaultConfig = &data.ObjectLockConfiguration{}
} }
@ -403,12 +423,12 @@ func formObjectLock(objectLock *data.ObjectLock, bktInfo *data.BucketInfo, defau
if until != "" { if until != "" {
retentionDate, err := time.Parse(time.RFC3339, until) retentionDate, err := time.Parse(time.RFC3339, until)
if err != nil { if err != nil {
return fmt.Errorf("invalid header %s: '%s'", api.AmzObjectLockRetainUntilDate, until) return nil, fmt.Errorf("invalid header %s: '%s'", api.AmzObjectLockRetainUntilDate, until)
} }
objectLock.Until = retentionDate objectLock.Until = retentionDate
} }
return nil return objectLock, nil
} }
func existLockHeaders(header http.Header) bool { func existLockHeaders(header http.Header) bool {
@ -418,15 +438,6 @@ func existLockHeaders(header http.Header) bool {
} }
func formObjectLockFromRetention(retention *data.Retention, header http.Header) (*data.ObjectLock, error) { func formObjectLockFromRetention(retention *data.Retention, header http.Header) (*data.ObjectLock, error) {
var err error
var bypassGovernance bool
bypass := header.Get(api.AmzBypassGovernanceRetention)
if bypass != "" {
if bypassGovernance, err = strconv.ParseBool(bypass); err != nil {
return nil, fmt.Errorf("couldn't parse '%s' header", api.AmzBypassGovernanceRetention)
}
}
if retention.Mode != governanceMode && retention.Mode != complianceMode { if retention.Mode != governanceMode && retention.Mode != complianceMode {
return nil, fmt.Errorf("invalid retention mode: %s", retention.Mode) return nil, fmt.Errorf("invalid retention mode: %s", retention.Mode)
} }
@ -441,9 +452,5 @@ func formObjectLockFromRetention(retention *data.Retention, header http.Header)
IsCompliance: retention.Mode == complianceMode, IsCompliance: retention.Mode == complianceMode,
} }
if !lock.IsCompliance && !bypassGovernance {
return nil, fmt.Errorf("you cannot bypase governance mode")
}
return lock, nil return lock, nil
} }

632
api/handler/locking_test.go Normal file
View file

@ -0,0 +1,632 @@
package handler
import (
"bytes"
"context"
"encoding/xml"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
"github.com/nspcc-dev/neofs-s3-gw/api"
"github.com/nspcc-dev/neofs-s3-gw/api/data"
apiErrors "github.com/nspcc-dev/neofs-s3-gw/api/errors"
"github.com/nspcc-dev/neofs-s3-gw/api/layer"
"github.com/stretchr/testify/require"
)
const defaultUrl = "http://localhost/"
func TestFormObjectLock(t *testing.T) {
for _, tc := range []struct {
name string
bktInfo *data.BucketInfo
config *data.ObjectLockConfiguration
header http.Header
expectedError bool
expectedLock *data.ObjectLock
}{
{
name: "default days",
bktInfo: &data.BucketInfo{ObjectLockEnabled: true},
config: &data.ObjectLockConfiguration{Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{Mode: complianceMode, Days: 1}}},
expectedLock: &data.ObjectLock{IsCompliance: true, Until: time.Now().Add(24 * time.Hour)},
},
{
name: "default years",
bktInfo: &data.BucketInfo{ObjectLockEnabled: true},
config: &data.ObjectLockConfiguration{Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{Mode: governanceMode, Years: 1}}},
expectedLock: &data.ObjectLock{Until: time.Now().Add(365 * 24 * time.Hour)},
},
{
name: "basic override",
bktInfo: &data.BucketInfo{ObjectLockEnabled: true},
config: &data.ObjectLockConfiguration{Rule: &data.ObjectLockRule{DefaultRetention: &data.DefaultRetention{Mode: complianceMode, Days: 1}}},
header: map[string][]string{
api.AmzObjectLockRetainUntilDate: {time.Now().Format(time.RFC3339)},
api.AmzObjectLockMode: {governanceMode},
api.AmzObjectLockLegalHold: {legalHoldOn},
},
expectedLock: &data.ObjectLock{Until: time.Now(), LegalHold: true},
},
{
name: "lock disabled error",
bktInfo: &data.BucketInfo{},
header: map[string][]string{api.AmzObjectLockLegalHold: {legalHoldOn}},
expectedError: true,
},
{
name: "invalid time format error",
bktInfo: &data.BucketInfo{ObjectLockEnabled: true},
header: map[string][]string{
api.AmzObjectLockRetainUntilDate: {time.Now().Format(time.Layout)},
},
expectedError: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
actualObjLock, err := formObjectLock(tc.bktInfo, tc.config, tc.header)
if tc.expectedError {
require.Error(t, err)
return
}
require.NoError(t, err)
assertObjectLocks(t, tc.expectedLock, actualObjLock)
})
}
}
func TestFormObjectLockFromRetention(t *testing.T) {
for _, tc := range []struct {
name string
retention *data.Retention
header http.Header
expectedError bool
expectedLock *data.ObjectLock
}{
{
name: "basic compliance",
retention: &data.Retention{
Mode: complianceMode,
RetainUntilDate: time.Now().Format(time.RFC3339),
},
expectedLock: &data.ObjectLock{Until: time.Now(), IsCompliance: true},
},
{
name: "basic governance",
retention: &data.Retention{
Mode: governanceMode,
RetainUntilDate: time.Now().Format(time.RFC3339),
},
header: map[string][]string{
api.AmzBypassGovernanceRetention: {strconv.FormatBool(true)},
},
expectedLock: &data.ObjectLock{Until: time.Now()},
},
{
name: "error invalid mode",
retention: &data.Retention{
Mode: "",
RetainUntilDate: time.Now().Format(time.RFC3339),
},
expectedError: true,
},
{
name: "error invalid date",
retention: &data.Retention{
Mode: governanceMode,
RetainUntilDate: "",
},
expectedError: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
actualObjLock, err := formObjectLockFromRetention(tc.retention, tc.header)
if tc.expectedError {
require.Error(t, err)
return
}
require.NoError(t, err)
assertObjectLocks(t, tc.expectedLock, actualObjLock)
})
}
}
func assertObjectLocks(t *testing.T, expected, actual *data.ObjectLock) {
require.Equal(t, expected.LegalHold, actual.LegalHold)
require.Equal(t, expected.IsCompliance, actual.IsCompliance)
require.InDelta(t, expected.Until.Unix(), actual.Until.Unix(), 1)
}
func TestCheckLockObject(t *testing.T) {
for _, tc := range []struct {
name string
isCompliance bool
header http.Header
expectedError bool
}{
{
name: "error governance bypass",
header: map[string][]string{
api.AmzBypassGovernanceRetention: {strconv.FormatBool(false)},
},
expectedError: true,
},
{
name: "error invalid governance bypass",
header: map[string][]string{
api.AmzBypassGovernanceRetention: {"t r u e"},
},
expectedError: true,
},
{
name: "error failed change compliance mode",
isCompliance: true,
expectedError: true,
},
{
name: "valid",
header: map[string][]string{
api.AmzBypassGovernanceRetention: {strconv.FormatBool(true)},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
header := make(map[string]string)
if tc.isCompliance {
header[layer.AttributeComplianceMode] = strconv.FormatBool(true)
}
lockInfo := &data.ObjectInfo{Headers: header}
err := checkLockInfo(lockInfo, tc.header)
if tc.expectedError {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestLockConfiguration(t *testing.T) {
for _, tc := range []struct {
name string
configuration *data.ObjectLockConfiguration
expectedError bool
}{
{
name: "basic empty",
configuration: &data.ObjectLockConfiguration{},
},
{
name: "basic compliance",
configuration: &data.ObjectLockConfiguration{
ObjectLockEnabled: enabledValue,
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Days: 1,
Mode: complianceMode,
},
},
},
},
{
name: "basic governance",
configuration: &data.ObjectLockConfiguration{
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Mode: governanceMode,
Years: 1,
},
},
},
},
{
name: "error invalid enabled",
configuration: &data.ObjectLockConfiguration{
ObjectLockEnabled: "false",
},
expectedError: true,
},
{
name: "error invalid mode",
configuration: &data.ObjectLockConfiguration{
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Mode: "",
},
},
},
expectedError: true,
},
{
name: "error no duration",
configuration: &data.ObjectLockConfiguration{
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Mode: governanceMode,
},
},
},
expectedError: true,
},
{
name: "error both durations",
configuration: &data.ObjectLockConfiguration{
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Days: 1,
Mode: governanceMode,
Years: 1,
},
},
},
expectedError: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
err := checkLockConfiguration(tc.configuration)
if tc.expectedError {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestPutBucketLockConfigurationHandler(t *testing.T) {
ctx := context.Background()
hc := prepareHandlerContext(t)
bktLockDisabled := "bucket-lock-disabled"
createTestBucket(ctx, t, hc, bktLockDisabled)
bktLockEnabled := "bucket-lock-enabled"
createTestBucketWithLock(ctx, t, hc, bktLockEnabled, nil)
bktLockEnabledWithOldConfig := "bucket-lock-enabled-old-conf"
createTestBucketWithLock(ctx, t, hc, bktLockEnabledWithOldConfig,
&data.ObjectLockConfiguration{
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Days: 1,
Mode: complianceMode,
},
},
})
for _, tc := range []struct {
name string
bucket string
expectedError apiErrors.Error
noError bool
configuration *data.ObjectLockConfiguration
}{
{
name: "bkt not found",
expectedError: apiErrors.GetAPIError(apiErrors.ErrNoSuchBucket),
},
{
name: "bkt lock disabled",
bucket: bktLockDisabled,
expectedError: apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotAllowed),
},
{
name: "invalid configuration",
bucket: bktLockEnabled,
expectedError: apiErrors.GetAPIError(apiErrors.ErrInternalError),
configuration: &data.ObjectLockConfiguration{ObjectLockEnabled: "dummy"},
},
{
name: "basic",
bucket: bktLockEnabled,
noError: true,
configuration: &data.ObjectLockConfiguration{},
},
{
name: "basic override",
bucket: bktLockEnabledWithOldConfig,
noError: true,
configuration: &data.ObjectLockConfiguration{
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Mode: governanceMode,
Years: 1,
},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
body, err := xml.Marshal(tc.configuration)
require.NoError(t, err)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPut, defaultUrl, bytes.NewReader(body))
r = r.WithContext(api.SetReqInfo(r.Context(), api.NewReqInfo(w, r, api.ObjectRequest{Bucket: tc.bucket})))
hc.Handler().PutBucketObjectLockConfigHandler(w, r)
if !tc.noError {
assertS3Error(t, w, tc.expectedError)
return
}
bktInfo, err := hc.Layer().GetBucketInfo(ctx, tc.bucket)
require.NoError(t, err)
bktSettings, err := hc.Layer().GetBucketSettings(ctx, bktInfo)
require.NoError(t, err)
actualConf := bktSettings.LockConfiguration
require.True(t, bktSettings.VersioningEnabled)
require.Equal(t, tc.configuration.ObjectLockEnabled, actualConf.ObjectLockEnabled)
require.Equal(t, tc.configuration.Rule, actualConf.Rule)
})
}
}
func TestGetBucketLockConfigurationHandler(t *testing.T) {
ctx := context.Background()
hc := prepareHandlerContext(t)
bktLockDisabled := "bucket-lock-disabled"
createTestBucket(ctx, t, hc, bktLockDisabled)
bktLockEnabled := "bucket-lock-enabled"
createTestBucketWithLock(ctx, t, hc, bktLockEnabled, nil)
oldConfig := &data.ObjectLockConfiguration{
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Days: 1,
Mode: complianceMode,
},
},
}
bktLockEnabledWithOldConfig := "bucket-lock-enabled-old-conf"
createTestBucketWithLock(ctx, t, hc, bktLockEnabledWithOldConfig, oldConfig)
for _, tc := range []struct {
name string
bucket string
expectedError apiErrors.Error
noError bool
expectedConf *data.ObjectLockConfiguration
}{
{
name: "bkt not found",
expectedError: apiErrors.GetAPIError(apiErrors.ErrNoSuchBucket),
},
{
name: "bkt lock disabled",
bucket: bktLockDisabled,
expectedError: apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound),
},
{
name: "bkt lock enabled empty default",
bucket: bktLockEnabled,
noError: true,
expectedConf: &data.ObjectLockConfiguration{ObjectLockEnabled: enabledValue},
},
{
name: "bkt lock enabled",
bucket: bktLockEnabledWithOldConfig,
noError: true,
expectedConf: oldConfig,
},
} {
t.Run(tc.name, func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPut, defaultUrl, bytes.NewReader(nil))
r = r.WithContext(api.SetReqInfo(r.Context(), api.NewReqInfo(w, r, api.ObjectRequest{Bucket: tc.bucket})))
hc.Handler().GetBucketObjectLockConfigHandler(w, r)
if !tc.noError {
assertS3Error(t, w, tc.expectedError)
return
}
actualConf := &data.ObjectLockConfiguration{}
err := xml.NewDecoder(w.Result().Body).Decode(actualConf)
require.NoError(t, err)
require.Equal(t, tc.expectedConf.ObjectLockEnabled, actualConf.ObjectLockEnabled)
require.Equal(t, tc.expectedConf.Rule, actualConf.Rule)
})
}
}
func assertS3Error(t *testing.T, w *httptest.ResponseRecorder, expectedError apiErrors.Error) {
actualErrorResponse := &api.ErrorResponse{}
err := xml.NewDecoder(w.Result().Body).Decode(actualErrorResponse)
require.NoError(t, err)
require.Equal(t, expectedError.HTTPStatusCode, w.Code)
require.Equal(t, expectedError.Code, actualErrorResponse.Code)
if expectedError.ErrCode != apiErrors.ErrInternalError {
require.Equal(t, expectedError.Description, actualErrorResponse.Message)
}
}
func TestObjectLegalHold(t *testing.T) {
ctx := context.Background()
hc := prepareHandlerContext(t)
bktName := "bucket-lock-enabled"
createTestBucketWithLock(ctx, t, hc, bktName, nil)
objName := "obj-for-legal-hold"
createTestObject(ctx, t, hc, bktName, objName)
w, r := prepareTestRequest(t, bktName, objName, nil)
hc.Handler().GetObjectLegalHoldHandler(w, r)
assertLegalHold(t, w, legalHoldOff)
w, r = prepareTestRequest(t, bktName, objName, &data.LegalHold{Status: legalHoldOn})
hc.Handler().PutObjectLegalHoldHandler(w, r)
require.Equal(t, http.StatusOK, w.Code)
w, r = prepareTestRequest(t, bktName, objName, nil)
hc.Handler().GetObjectLegalHoldHandler(w, r)
assertLegalHold(t, w, legalHoldOn)
// to make sure put hold is idempotent operation
w, r = prepareTestRequest(t, bktName, objName, &data.LegalHold{Status: legalHoldOn})
hc.Handler().PutObjectLegalHoldHandler(w, r)
require.Equal(t, http.StatusOK, w.Code)
w, r = prepareTestRequest(t, bktName, objName, &data.LegalHold{Status: legalHoldOff})
hc.Handler().PutObjectLegalHoldHandler(w, r)
require.Equal(t, http.StatusOK, w.Code)
w, r = prepareTestRequest(t, bktName, objName, nil)
hc.Handler().GetObjectLegalHoldHandler(w, r)
assertLegalHold(t, w, legalHoldOff)
// to make sure put hold is idempotent operation
w, r = prepareTestRequest(t, bktName, objName, &data.LegalHold{Status: legalHoldOff})
hc.Handler().PutObjectLegalHoldHandler(w, r)
require.Equal(t, http.StatusOK, w.Code)
}
func assertLegalHold(t *testing.T, w *httptest.ResponseRecorder, status string) {
actualHold := &data.LegalHold{}
err := xml.NewDecoder(w.Result().Body).Decode(actualHold)
require.NoError(t, err)
require.Equal(t, status, actualHold.Status)
require.Equal(t, http.StatusOK, w.Code)
}
func TestObjectRetention(t *testing.T) {
ctx := context.Background()
hc := prepareHandlerContext(t)
bktName := "bucket-lock-enabled"
createTestBucketWithLock(ctx, t, hc, bktName, nil)
objName := "obj-for-retention"
createTestObject(ctx, t, hc, bktName, objName)
w, r := prepareTestRequest(t, bktName, objName, nil)
hc.Handler().GetObjectRetentionHandler(w, r)
assertS3Error(t, w, apiErrors.GetAPIError(apiErrors.ErrNoSuchKey))
retention := &data.Retention{Mode: governanceMode, RetainUntilDate: time.Now().Format(time.RFC3339)}
w, r = prepareTestRequest(t, bktName, objName, retention)
hc.Handler().PutObjectRetentionHandler(w, r)
require.Equal(t, http.StatusOK, w.Code)
w, r = prepareTestRequest(t, bktName, objName, nil)
hc.Handler().GetObjectRetentionHandler(w, r)
assertRetention(t, w, retention)
retention = &data.Retention{Mode: governanceMode, RetainUntilDate: time.Now().Format(time.RFC3339)}
w, r = prepareTestRequest(t, bktName, objName, retention)
hc.Handler().PutObjectRetentionHandler(w, r)
assertS3Error(t, w, apiErrors.GetAPIError(apiErrors.ErrInternalError))
retention = &data.Retention{Mode: complianceMode, RetainUntilDate: time.Now().Format(time.RFC3339)}
w, r = prepareTestRequest(t, bktName, objName, retention)
r.Header.Set(api.AmzBypassGovernanceRetention, strconv.FormatBool(true))
hc.Handler().PutObjectRetentionHandler(w, r)
require.Equal(t, http.StatusOK, w.Code)
w, r = prepareTestRequest(t, bktName, objName, nil)
hc.Handler().GetObjectRetentionHandler(w, r)
assertRetention(t, w, retention)
w, r = prepareTestRequest(t, bktName, objName, retention)
r.Header.Set(api.AmzBypassGovernanceRetention, strconv.FormatBool(true))
hc.Handler().PutObjectRetentionHandler(w, r)
assertS3Error(t, w, apiErrors.GetAPIError(apiErrors.ErrInternalError))
}
func assertRetention(t *testing.T, w *httptest.ResponseRecorder, retention *data.Retention) {
actualRetention := &data.Retention{}
err := xml.NewDecoder(w.Result().Body).Decode(actualRetention)
require.NoError(t, err)
require.Equal(t, retention.Mode, actualRetention.Mode)
require.Equal(t, retention.RetainUntilDate, actualRetention.RetainUntilDate)
require.Equal(t, http.StatusOK, w.Code)
}
func TestPutObjectWithLock(t *testing.T) {
ctx := context.Background()
hc := prepareHandlerContext(t)
bktName := "bucket-lock-enabled"
lockConfig := &data.ObjectLockConfiguration{
ObjectLockEnabled: enabledValue,
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Days: 1,
Mode: governanceMode,
},
},
}
createTestBucketWithLock(ctx, t, hc, bktName, lockConfig)
objDefault := "obj-default-retention"
w, r := prepareTestRequest(t, bktName, objDefault, nil)
hc.Handler().PutObjectHandler(w, r)
require.Equal(t, http.StatusOK, w.Code)
w, r = prepareTestRequest(t, bktName, objDefault, nil)
hc.Handler().GetObjectRetentionHandler(w, r)
expectedRetention := &data.Retention{
Mode: governanceMode,
RetainUntilDate: time.Now().Add(24 * time.Hour).Format(time.RFC3339),
}
assertRetentionApproximate(t, w, expectedRetention, 1)
w, r = prepareTestRequest(t, bktName, objDefault, nil)
hc.Handler().GetObjectLegalHoldHandler(w, r)
assertLegalHold(t, w, legalHoldOff)
objOverride := "obj-override-retention"
w, r = prepareTestRequest(t, bktName, objOverride, nil)
r.Header.Set(api.AmzObjectLockMode, complianceMode)
r.Header.Set(api.AmzObjectLockLegalHold, legalHoldOn)
r.Header.Set(api.AmzObjectLockRetainUntilDate, time.Now().Add(2*24*time.Hour).Format(time.RFC3339))
hc.Handler().PutObjectHandler(w, r)
require.Equal(t, http.StatusOK, w.Code)
w, r = prepareTestRequest(t, bktName, objOverride, nil)
hc.Handler().GetObjectRetentionHandler(w, r)
expectedRetention = &data.Retention{
Mode: complianceMode,
RetainUntilDate: time.Now().Add(2 * 24 * time.Hour).Format(time.RFC3339),
}
assertRetentionApproximate(t, w, expectedRetention, 1)
w, r = prepareTestRequest(t, bktName, objOverride, nil)
hc.Handler().GetObjectLegalHoldHandler(w, r)
assertLegalHold(t, w, legalHoldOn)
}
func assertRetentionApproximate(t *testing.T, w *httptest.ResponseRecorder, retention *data.Retention, delta float64) {
actualRetention := &data.Retention{}
err := xml.NewDecoder(w.Result().Body).Decode(actualRetention)
require.NoError(t, err)
require.Equal(t, retention.Mode, actualRetention.Mode)
require.Equal(t, http.StatusOK, w.Code)
actualUntil, err := time.Parse(time.RFC3339, actualRetention.RetainUntilDate)
require.NoError(t, err)
expectedUntil, err := time.Parse(time.RFC3339, retention.RetainUntilDate)
require.NoError(t, err)
require.InDelta(t, expectedUntil.Unix(), actualUntil.Unix(), delta)
}

View file

@ -215,7 +215,8 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if err = formObjectLock(params.Lock, bktInfo, settings.LockConfiguration, r.Header); err != nil { params.Lock, err = formObjectLock(bktInfo, settings.LockConfiguration, r.Header)
if err != nil {
h.logAndSendError(w, "could not form object lock", reqInfo, err) h.logAndSendError(w, "could not form object lock", reqInfo, err)
return return
} }

View file

@ -29,7 +29,7 @@ type (
const ( const (
attributeLocationConstraint = ".s3-location-constraint" attributeLocationConstraint = ".s3-location-constraint"
attributeLockEnabled = "LockEnabled" AttributeLockEnabled = "LockEnabled"
) )
func (n *layer) containerInfo(ctx context.Context, idCnr *cid.ID) (*data.BucketInfo, error) { func (n *layer) containerInfo(ctx context.Context, idCnr *cid.ID) (*data.BucketInfo, error) {
@ -73,7 +73,7 @@ func (n *layer) containerInfo(ctx context.Context, idCnr *cid.ID) (*data.BucketI
info.Created = time.Unix(unix, 0) info.Created = time.Unix(unix, 0)
case attributeLocationConstraint: case attributeLocationConstraint:
info.LocationConstraint = val info.LocationConstraint = val
case attributeLockEnabled: case AttributeLockEnabled:
info.ObjectLockEnabled, err = strconv.ParseBool(val) info.ObjectLockEnabled, err = strconv.ParseBool(val)
if err != nil { if err != nil {
log.Error("could not parse container object lock enabled attribute", log.Error("could not parse container object lock enabled attribute",

View file

@ -3,6 +3,7 @@ package layer
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"sort" "sort"
"strings" "strings"
@ -201,8 +202,17 @@ func (n *layer) objectPut(ctx context.Context, bkt *data.BucketInfo, p *PutObjec
} }
if p.Lock != nil { if p.Lock != nil {
// todo form lock system object objInfo := &data.ObjectInfo{ID: oid, Name: p.Object}
// attributes = append(attributes, attributesFromLock(p.Lock)...) if p.Lock.LegalHold {
if err = n.putLockObject(ctx, bkt, objInfo.LegalHoldObject(), p.Lock); err != nil {
return nil, err
}
}
if !p.Lock.Until.IsZero() {
if err = n.putLockObject(ctx, bkt, objInfo.RetentionObject(), p.Lock); err != nil {
return nil, err
}
}
} }
meta, err := n.objectHead(ctx, bkt.CID, id) meta, err := n.objectHead(ctx, bkt.CID, id)
@ -249,6 +259,21 @@ func (n *layer) objectPut(ctx context.Context, bkt *data.BucketInfo, p *PutObjec
}, nil }, nil
} }
func (n *layer) putLockObject(ctx context.Context, bktInfo *data.BucketInfo, objName string, lock *data.ObjectLock) error {
ps := &PutSystemObjectParams{
BktInfo: bktInfo,
ObjName: objName,
Lock: lock,
Metadata: make(map[string]string),
}
if _, err := n.PutSystemObject(ctx, ps); err != nil {
return fmt.Errorf("coudln't add lock for '%s': %w", objName, err)
}
return nil
}
func updateCRDT2PSetHeaders(header map[string]string, versions *objectVersions, versioningEnabled bool) []*oid.ID { func updateCRDT2PSetHeaders(header map[string]string, versions *objectVersions, versioningEnabled bool) []*oid.ID {
if !versioningEnabled { if !versioningEnabled {
header[versionsUnversionedAttr] = "true" header[versionsUnversionedAttr] = "true"
@ -638,7 +663,7 @@ func (n *layer) listAllObjects(ctx context.Context, p ListObjectsParamsCommon) (
} }
func (n *layer) isVersioningEnabled(ctx context.Context, bktInfo *data.BucketInfo) bool { func (n *layer) isVersioningEnabled(ctx context.Context, bktInfo *data.BucketInfo) bool {
settings, err := n.getBucketSettings(ctx, bktInfo) settings, err := n.GetBucketSettings(ctx, bktInfo)
if err != nil { if err != nil {
n.log.Warn("couldn't get versioning settings object", zap.Error(err)) n.log.Warn("couldn't get versioning settings object", zap.Error(err))
return false return false

View file

@ -221,7 +221,8 @@ func systemObjectKey(bktInfo *data.BucketInfo, obj string) string {
} }
func (n *layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) { func (n *layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) {
if settings := n.systemCache.GetSettings(bktInfo.SettingsObjectName()); settings != nil { systemKey := systemObjectKey(bktInfo, bktInfo.SettingsObjectName())
if settings := n.systemCache.GetSettings(systemKey); settings != nil {
return settings, nil return settings, nil
} }
@ -235,7 +236,7 @@ func (n *layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo)
return nil, err return nil, err
} }
if err = n.systemCache.PutSettings(bktInfo.SettingsObjectName(), settings); err != nil { if err = n.systemCache.PutSettings(systemKey, settings); err != nil {
n.log.Warn("couldn't put system meta to objects cache", n.log.Warn("couldn't put system meta to objects cache",
zap.Stringer("object id", obj.ID()), zap.Stringer("object id", obj.ID()),
zap.Stringer("bucket id", bktInfo.CID), zap.Stringer("bucket id", bktInfo.CID),
@ -267,7 +268,8 @@ func (n *layer) PutBucketSettings(ctx context.Context, p *PutSettingsParams) err
return errors.GetAPIError(errors.ErrInternalError) return errors.GetAPIError(errors.ErrInternalError)
} }
if err = n.systemCache.PutSettings(p.BktInfo.SettingsObjectName(), p.Settings); err != nil { systemKey := systemObjectKey(p.BktInfo, p.BktInfo.SettingsObjectName())
if err = n.systemCache.PutSettings(systemKey, p.Settings); err != nil {
n.log.Error("couldn't cache system object", zap.Error(err)) n.log.Error("couldn't cache system object", zap.Error(err))
} }
@ -276,7 +278,7 @@ func (n *layer) PutBucketSettings(ctx context.Context, p *PutSettingsParams) err
func attributesFromLock(lock *data.ObjectLock) []*object.Attribute { func attributesFromLock(lock *data.ObjectLock) []*object.Attribute {
var result []*object.Attribute var result []*object.Attribute
if !lock.LegalHold { if !lock.Until.IsZero() {
attrRetainUntil := object.NewAttribute() attrRetainUntil := object.NewAttribute()
attrRetainUntil.SetKey(AttributeRetainUntil) attrRetainUntil.SetKey(AttributeRetainUntil)
attrRetainUntil.SetValue(lock.Until.Format(time.RFC3339)) attrRetainUntil.SetValue(lock.Until.Format(time.RFC3339))

View file

@ -300,7 +300,7 @@ func (n *layer) PutBucketVersioning(ctx context.Context, p *PutSettingsParams) (
Reader: nil, Reader: nil,
} }
return n.putSystemObject(ctx, s) return n.PutSystemObject(ctx, s)
} }
func (n *layer) GetBucketVersioning(ctx context.Context, bucketName string) (*data.BucketSettings, error) { func (n *layer) GetBucketVersioning(ctx context.Context, bucketName string) (*data.BucketSettings, error) {
@ -309,7 +309,7 @@ func (n *layer) GetBucketVersioning(ctx context.Context, bucketName string) (*da
return nil, err return nil, err
} }
return n.getBucketSettings(ctx, bktInfo) return n.GetBucketSettings(ctx, bktInfo)
} }
func (n *layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) { func (n *layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) {
@ -398,15 +398,6 @@ func contains(list []string, elem string) bool {
return false return false
} }
func (n *layer) getBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) {
objInfo, err := n.HeadSystemObject(ctx, bktInfo, bktInfo.SettingsObjectName())
if err != nil {
return nil, err
}
return objectInfoToBucketSettings(objInfo), nil
}
func objectInfoToBucketSettings(info *data.ObjectInfo) *data.BucketSettings { func objectInfoToBucketSettings(info *data.ObjectInfo) *data.BucketSettings {
res := &data.BucketSettings{} res := &data.BucketSettings{}

View file

@ -3,10 +3,6 @@ package layer
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -14,207 +10,18 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-s3-gw/api" "github.com/nspcc-dev/neofs-s3-gw/api"
"github.com/nspcc-dev/neofs-s3-gw/api/data" "github.com/nspcc-dev/neofs-s3-gw/api/data"
"github.com/nspcc-dev/neofs-s3-gw/api/mock"
"github.com/nspcc-dev/neofs-s3-gw/creds/accessbox" "github.com/nspcc-dev/neofs-s3-gw/creds/accessbox"
"github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/logger" "github.com/nspcc-dev/neofs-sdk-go/logger"
"github.com/nspcc-dev/neofs-sdk-go/object" "github.com/nspcc-dev/neofs-sdk-go/object"
"github.com/nspcc-dev/neofs-sdk-go/object/address" "github.com/nspcc-dev/neofs-sdk-go/object/address"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id" oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/object/id/test" "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
"github.com/nspcc-dev/neofs-sdk-go/owner"
tokentest "github.com/nspcc-dev/neofs-sdk-go/token/test" tokentest "github.com/nspcc-dev/neofs-sdk-go/token/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type testNeoFS struct {
NeoFS
objects map[string]*object.Object
containers map[string]*container.Container
currentEpoch uint64
}
func (t *testNeoFS) CreateContainer(_ context.Context, prm PrmContainerCreate) (*cid.ID, error) {
var opts []container.Option
opts = append(opts,
container.WithOwnerID(&prm.Creator),
container.WithPolicy(&prm.Policy),
container.WithCustomBasicACL(prm.BasicACL),
container.WithAttribute(container.AttributeTimestamp, strconv.FormatInt(prm.Time.Unix(), 10)),
)
if prm.Name != "" {
opts = append(opts, container.WithAttribute(container.AttributeName, prm.Name))
}
cnr := container.New(opts...)
cnr.SetSessionToken(prm.SessionToken)
if prm.Name != "" {
container.SetNativeName(cnr, prm.Name)
}
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return nil, err
}
id := cid.New()
id.SetSHA256(sha256.Sum256(b))
t.containers[id.String()] = cnr
return id, nil
}
func (t *testNeoFS) Container(_ context.Context, id cid.ID) (*container.Container, error) {
for k, v := range t.containers {
if k == id.String() {
return v, nil
}
}
return nil, fmt.Errorf("container not found " + id.String())
}
func (t *testNeoFS) UserContainers(_ context.Context, _ owner.ID) ([]cid.ID, error) {
var res []cid.ID
for k := range t.containers {
var idCnr cid.ID
if err := idCnr.Parse(k); err != nil {
return nil, err
}
res = append(res, idCnr)
}
return res, nil
}
func (t *testNeoFS) SelectObjects(_ context.Context, prm PrmObjectSelect) ([]oid.ID, error) {
var filters object.SearchFilters
filters.AddRootFilter()
if prm.FilePrefix != "" {
filters.AddFilter(object.AttributeFileName, prm.FilePrefix, object.MatchCommonPrefix)
}
if prm.ExactAttribute[0] != "" {
filters.AddFilter(prm.ExactAttribute[0], prm.ExactAttribute[1], object.MatchStringEqual)
}
cidStr := prm.Container.String()
var res []oid.ID
if len(filters) == 1 {
for k, v := range t.objects {
if strings.Contains(k, cidStr) {
res = append(res, *v.ID())
}
}
return res, nil
}
filter := filters[1]
if len(filters) != 2 || filter.Operation() != object.MatchStringEqual ||
(filter.Header() != object.AttributeFileName && filter.Header() != objectSystemAttributeName) {
return nil, fmt.Errorf("usupported filters")
}
for k, v := range t.objects {
if strings.Contains(k, cidStr) && isMatched(v.Attributes(), filter) {
res = append(res, *v.ID())
}
}
return res, nil
}
func (t *testNeoFS) ReadObject(_ context.Context, prm PrmObjectRead) (*ObjectPart, error) {
var addr address.Address
addr.SetContainerID(&prm.Container)
addr.SetObjectID(&prm.Object)
sAddr := addr.String()
if obj, ok := t.objects[sAddr]; ok {
return &ObjectPart{
Head: obj,
Payload: io.NopCloser(bytes.NewReader(obj.Payload())),
}, nil
}
return nil, fmt.Errorf("object not found " + addr.String())
}
func (t *testNeoFS) CreateObject(_ context.Context, prm PrmObjectCreate) (*oid.ID, error) {
id := test.ID()
attrs := make([]object.Attribute, 0)
if prm.Filename != "" {
a := object.NewAttribute()
a.SetKey(object.AttributeFileName)
a.SetValue(prm.Filename)
attrs = append(attrs, *a)
}
for i := range prm.Attributes {
a := object.NewAttribute()
a.SetKey(prm.Attributes[i][0])
a.SetValue(prm.Attributes[i][1])
attrs = append(attrs, *a)
}
obj := object.New()
obj.SetContainerID(&prm.Container)
obj.SetID(id)
obj.SetPayloadSize(prm.PayloadSize)
obj.SetAttributes(attrs...)
obj.SetCreationEpoch(t.currentEpoch)
t.currentEpoch++
if prm.Payload != nil {
all, err := io.ReadAll(prm.Payload)
if err != nil {
return nil, err
}
obj.SetPayload(all)
}
addr := newAddress(obj.ContainerID(), obj.ID())
t.objects[addr.String()] = obj
return obj.ID(), nil
}
func (t *testNeoFS) DeleteObject(_ context.Context, prm PrmObjectDelete) error {
var addr address.Address
addr.SetContainerID(&prm.Container)
addr.SetObjectID(&prm.Object)
delete(t.objects, addr.String())
return nil
}
func newTestPool() *testNeoFS {
return &testNeoFS{
objects: make(map[string]*object.Object),
containers: make(map[string]*container.Container),
}
}
func isMatched(attributes []object.Attribute, filter object.SearchFilter) bool {
for _, attr := range attributes {
if attr.Key() == filter.Header() && attr.Value() == filter.Value() {
return true
}
}
return false
}
func (tc *testContext) putObject(content []byte) *data.ObjectInfo { func (tc *testContext) putObject(content []byte) *data.ObjectInfo {
objInfo, err := tc.layer.PutObject(tc.ctx, &PutObjectParams{ objInfo, err := tc.layer.PutObject(tc.ctx, &PutObjectParams{
Bucket: tc.bktID.String(), Bucket: tc.bktID.String(),
@ -307,7 +114,7 @@ func (tc *testContext) checkListObjects(ids ...*oid.ID) {
} }
func (tc *testContext) getSystemObject(objectName string) *object.Object { func (tc *testContext) getSystemObject(objectName string) *object.Object {
for _, obj := range tc.testNeoFS.objects { for _, obj := range tc.testNeoFS.Objects {
for _, attr := range obj.Attributes() { for _, attr := range obj.Attributes() {
if attr.Key() == objectSystemAttributeName && attr.Value() == objectName { if attr.Key() == objectSystemAttributeName && attr.Value() == objectName {
return obj return obj
@ -325,7 +132,7 @@ type testContext struct {
bktID *cid.ID bktID *cid.ID
bktInfo *data.BucketInfo bktInfo *data.BucketInfo
obj string obj string
testNeoFS *testNeoFS testNeoFS *mock.TestNeoFS
} }
func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext { func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext {
@ -343,7 +150,7 @@ func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext {
}) })
l, err := logger.New(logger.WithTraceLevel("panic")) l, err := logger.New(logger.WithTraceLevel("panic"))
require.NoError(t, err) require.NoError(t, err)
tp := newTestPool() tp := mock.NewTestPool()
bktName := "testbucket1" bktName := "testbucket1"
bktID, err := tp.CreateContainer(ctx, PrmContainerCreate{ bktID, err := tp.CreateContainer(ctx, PrmContainerCreate{
@ -821,7 +628,7 @@ func TestSystemObjectsVersioning(t *testing.T) {
addr.SetObjectID(objMeta.ID()) addr.SetObjectID(objMeta.ID())
// simulate failed deletion // simulate failed deletion
tc.testNeoFS.objects[addr.String()] = objMeta tc.testNeoFS.Objects[addr.String()] = objMeta
bktInfo := &data.BucketInfo{ bktInfo := &data.BucketInfo{
Name: tc.bkt, Name: tc.bkt,
@ -853,7 +660,7 @@ func TestDeleteSystemObjectsVersioning(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// simulate failed deletion // simulate failed deletion
tc.testNeoFS.objects[newAddress(objMeta.ContainerID(), objMeta.ID()).String()] = objMeta tc.testNeoFS.Objects[newAddress(objMeta.ContainerID(), objMeta.ID()).String()] = objMeta
tagging, err := tc.layer.GetBucketTagging(tc.ctx, tc.bkt) tagging, err := tc.layer.GetBucketTagging(tc.ctx, tc.bkt)
require.NoError(t, err) require.NoError(t, err)

216
api/mock/sdk_pool_mock.go Normal file
View file

@ -0,0 +1,216 @@
package mock
import (
"context"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
"strings"
"github.com/nspcc-dev/neofs-sdk-go/accounting"
"github.com/nspcc-dev/neofs-sdk-go/client"
"github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
"github.com/nspcc-dev/neofs-sdk-go/object"
"github.com/nspcc-dev/neofs-sdk-go/owner"
"github.com/nspcc-dev/neofs-sdk-go/pool"
"github.com/nspcc-dev/neofs-sdk-go/session"
)
type TestPool struct {
Objects map[string]*object.Object
Containers map[string]*container.Container
CurrentEpoch uint64
}
func NewTestPool() *TestPool {
return &TestPool{
Objects: make(map[string]*object.Object),
Containers: make(map[string]*container.Container),
}
}
func (t *TestPool) PutObject(ctx context.Context, params *client.PutObjectParams, option ...pool.CallOption) (*object.ID, error) {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return nil, err
}
oid := object.NewID()
oid.SetSHA256(sha256.Sum256(b))
raw := object.NewRawFrom(params.Object())
raw.SetID(oid)
raw.SetCreationEpoch(t.CurrentEpoch)
t.CurrentEpoch++
if params.PayloadReader() != nil {
all, err := io.ReadAll(params.PayloadReader())
if err != nil {
return nil, err
}
raw.SetPayload(all)
}
raw.SetPayloadSize(uint64(len(raw.Payload())))
addr := newAddress(raw.ContainerID(), raw.ID())
t.Objects[addr.String()] = raw.Object()
return raw.ID(), nil
}
func (t *TestPool) DeleteObject(ctx context.Context, params *client.DeleteObjectParams, option ...pool.CallOption) error {
delete(t.Objects, params.Address().String())
return nil
}
func (t *TestPool) GetObject(ctx context.Context, params *client.GetObjectParams, option ...pool.CallOption) (*object.Object, error) {
if obj, ok := t.Objects[params.Address().String()]; ok {
if params.PayloadWriter() != nil {
_, err := params.PayloadWriter().Write(obj.Payload())
if err != nil {
return nil, err
}
}
return obj, nil
}
return nil, fmt.Errorf("object not found " + params.Address().String())
}
func (t *TestPool) GetObjectHeader(ctx context.Context, params *client.ObjectHeaderParams, option ...pool.CallOption) (*object.Object, error) {
p := new(client.GetObjectParams).WithAddress(params.Address())
return t.GetObject(ctx, p)
}
func (t *TestPool) ObjectPayloadRangeData(ctx context.Context, params *client.RangeDataParams, option ...pool.CallOption) ([]byte, error) {
panic("implement me")
}
func (t *TestPool) ObjectPayloadRangeSHA256(ctx context.Context, params *client.RangeChecksumParams, option ...pool.CallOption) ([][32]byte, error) {
panic("implement me")
}
func (t *TestPool) ObjectPayloadRangeTZ(ctx context.Context, params *client.RangeChecksumParams, option ...pool.CallOption) ([][64]byte, error) {
panic("implement me")
}
func (t *TestPool) SearchObject(ctx context.Context, params *client.SearchObjectParams, option ...pool.CallOption) ([]*object.ID, error) {
cidStr := params.ContainerID().String()
var res []*object.ID
if len(params.SearchFilters()) == 1 {
for k, v := range t.Objects {
if strings.Contains(k, cidStr) {
res = append(res, v.ID())
}
}
return res, nil
}
filter := params.SearchFilters()[1]
if len(params.SearchFilters()) != 2 || filter.Operation() != object.MatchStringEqual ||
(filter.Header() != object.AttributeFileName && filter.Header() != "S3-System-name") {
return nil, fmt.Errorf("usupported filters")
}
for k, v := range t.Objects {
if strings.Contains(k, cidStr) && isMatched(v.Attributes(), filter) {
res = append(res, v.ID())
}
}
return res, nil
}
func isMatched(attributes []*object.Attribute, filter object.SearchFilter) bool {
for _, attr := range attributes {
if attr.Key() == filter.Header() && attr.Value() == filter.Value() {
return true
}
}
return false
}
func (t *TestPool) PutContainer(ctx context.Context, container *container.Container, option ...pool.CallOption) (*cid.ID, error) {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return nil, err
}
id := cid.New()
id.SetSHA256(sha256.Sum256(b))
t.Containers[id.String()] = container
return id, nil
}
func (t *TestPool) GetContainer(ctx context.Context, id *cid.ID, option ...pool.CallOption) (*container.Container, error) {
for k, v := range t.Containers {
if k == id.String() {
return v, nil
}
}
return nil, fmt.Errorf("container not found " + id.String())
}
func (t *TestPool) ListContainers(ctx context.Context, id *owner.ID, option ...pool.CallOption) ([]*cid.ID, error) {
var res []*cid.ID
for k := range t.Containers {
cID := cid.New()
if err := cID.Parse(k); err != nil {
return nil, err
}
res = append(res, cID)
}
return res, nil
}
func (t *TestPool) DeleteContainer(ctx context.Context, id *cid.ID, option ...pool.CallOption) error {
delete(t.Containers, id.String())
return nil
}
func (t *TestPool) GetEACL(ctx context.Context, id *cid.ID, option ...pool.CallOption) (*eacl.Table, error) {
panic("implement me")
}
func (t *TestPool) Balance(ctx context.Context, owner *owner.ID, opts ...pool.CallOption) (*accounting.Decimal, error) {
panic("implement me")
}
func (t *TestPool) SetEACL(ctx context.Context, table *eacl.Table, option ...pool.CallOption) error {
panic("implement me")
}
func (t *TestPool) AnnounceContainerUsedSpace(ctx context.Context, announcements []container.UsedSpaceAnnouncement, option ...pool.CallOption) error {
panic("implement me")
}
func (t *TestPool) Connection() (pool.Client, *session.Token, error) {
panic("implement me")
}
func (t *TestPool) Close() {
panic("implement me")
}
func (t *TestPool) OwnerID() *owner.ID {
return nil
}
func (t *TestPool) WaitForContainerPresence(ctx context.Context, id *cid.ID, params *pool.ContainerPollingParams) error {
return nil
}
func newAddress(cid *cid.ID, oid *object.ID) *object.Address {
address := object.NewAddress()
address.SetContainerID(cid)
address.SetObjectID(oid)
return address
}

View file

@ -35,6 +35,10 @@ type BucketResolver struct {
next *BucketResolver next *BucketResolver
} }
func (r *BucketResolver) SetResolveFunc(fn func(context.Context, string) (*cid.ID, error)) {
r.resolve = fn
}
func (r *BucketResolver) Resolve(ctx context.Context, name string) (*cid.ID, error) { func (r *BucketResolver) Resolve(ctx context.Context, name string) (*cid.ID, error) {
cnrID, err := r.resolve(ctx, name) cnrID, err := r.resolve(ctx, name)
if err != nil { if err != nil {