forked from TrueCloudLab/frostfs-s3-gw
Denis Kirillov
14ef9ff091
To be able to handle cases and return appropriate http status code when object missed in storage but gate cache contains its metadata we need write code after init object reader. So we separate init reader from actual reading. Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
226 lines
7.1 KiB
Go
226 lines
7.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
|
apiErrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
|
s3errors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestFetchRangeHeader(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
header string
|
|
expected *layer.RangeParams
|
|
fullSize uint64
|
|
err bool
|
|
}{
|
|
{header: "bytes=0-256", expected: &layer.RangeParams{Start: 0, End: 256}, fullSize: 257, err: false},
|
|
{header: "bytes=0-0", expected: &layer.RangeParams{Start: 0, End: 0}, fullSize: 1, err: false},
|
|
{header: "bytes=0-256", expected: &layer.RangeParams{Start: 0, End: 255}, fullSize: 256, err: false},
|
|
{header: "bytes=0-", expected: &layer.RangeParams{Start: 0, End: 99}, fullSize: 100, err: false},
|
|
{header: "bytes=-10", expected: &layer.RangeParams{Start: 90, End: 99}, fullSize: 100, err: false},
|
|
{header: "", err: false},
|
|
{header: "bytes=-1-256", err: true},
|
|
{header: "bytes=256-0", err: true},
|
|
{header: "bytes=string-0", err: true},
|
|
{header: "bytes=0-string", err: true},
|
|
{header: "bytes:0-256", err: true},
|
|
{header: "bytes:-", err: true},
|
|
{header: "bytes=0-0", fullSize: 0, err: true},
|
|
{header: "bytes=10-20", fullSize: 5, err: true},
|
|
} {
|
|
h := make(http.Header)
|
|
h.Add("Range", tc.header)
|
|
params, err := fetchRangeHeader(h, tc.fullSize)
|
|
if tc.err {
|
|
require.Error(t, err)
|
|
continue
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expected, params)
|
|
}
|
|
}
|
|
|
|
func newInfo(etag string, created time.Time) *data.ObjectInfo {
|
|
return &data.ObjectInfo{
|
|
HashSum: etag,
|
|
Created: created,
|
|
}
|
|
}
|
|
|
|
func TestPreconditions(t *testing.T) {
|
|
today := time.Now()
|
|
yesterday := today.Add(-24 * time.Hour)
|
|
etag := "etag"
|
|
etag2 := "etag2"
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
info *data.ObjectInfo
|
|
args *conditionalArgs
|
|
expected error
|
|
}{
|
|
{
|
|
name: "no conditions",
|
|
info: new(data.ObjectInfo),
|
|
args: new(conditionalArgs),
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "IfMatch true",
|
|
info: newInfo(etag, today),
|
|
args: &conditionalArgs{IfMatch: etag},
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "IfMatch false",
|
|
info: newInfo(etag, today),
|
|
args: &conditionalArgs{IfMatch: etag2},
|
|
expected: errors.GetAPIError(errors.ErrPreconditionFailed)},
|
|
{
|
|
name: "IfNoneMatch true",
|
|
info: newInfo(etag, today),
|
|
args: &conditionalArgs{IfNoneMatch: etag2},
|
|
expected: nil},
|
|
{
|
|
name: "IfNoneMatch false",
|
|
info: newInfo(etag, today),
|
|
args: &conditionalArgs{IfNoneMatch: etag},
|
|
expected: errors.GetAPIError(errors.ErrNotModified)},
|
|
{
|
|
name: "IfModifiedSince true",
|
|
info: newInfo(etag, today),
|
|
args: &conditionalArgs{IfModifiedSince: &yesterday},
|
|
expected: nil},
|
|
{
|
|
name: "IfModifiedSince false",
|
|
info: newInfo(etag, yesterday),
|
|
args: &conditionalArgs{IfModifiedSince: &today},
|
|
expected: errors.GetAPIError(errors.ErrNotModified)},
|
|
{
|
|
name: "IfUnmodifiedSince true",
|
|
info: newInfo(etag, yesterday),
|
|
args: &conditionalArgs{IfUnmodifiedSince: &today},
|
|
expected: nil},
|
|
{
|
|
name: "IfUnmodifiedSince false",
|
|
info: newInfo(etag, today),
|
|
args: &conditionalArgs{IfUnmodifiedSince: &yesterday},
|
|
expected: errors.GetAPIError(errors.ErrPreconditionFailed)},
|
|
|
|
{
|
|
name: "IfMatch true, IfUnmodifiedSince false",
|
|
info: newInfo(etag, today),
|
|
args: &conditionalArgs{IfMatch: etag, IfUnmodifiedSince: &yesterday},
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "IfMatch false, IfUnmodifiedSince true",
|
|
info: newInfo(etag, yesterday),
|
|
args: &conditionalArgs{IfMatch: etag2, IfUnmodifiedSince: &today},
|
|
expected: errors.GetAPIError(errors.ErrPreconditionFailed),
|
|
},
|
|
{
|
|
name: "IfNoneMatch false, IfModifiedSince true",
|
|
info: newInfo(etag, today),
|
|
args: &conditionalArgs{IfNoneMatch: etag, IfModifiedSince: &yesterday},
|
|
expected: errors.GetAPIError(errors.ErrNotModified),
|
|
},
|
|
{
|
|
name: "IfNoneMatch true, IfModifiedSince false",
|
|
info: newInfo(etag, yesterday),
|
|
args: &conditionalArgs{IfNoneMatch: etag2, IfModifiedSince: &today},
|
|
expected: errors.GetAPIError(errors.ErrNotModified),
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
actual := checkPreconditions(tc.info, tc.args)
|
|
require.Equal(t, tc.expected, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetRange(t *testing.T) {
|
|
tc := prepareHandlerContext(t)
|
|
|
|
bktName, objName := "bucket-for-range", "object-to-range"
|
|
createTestBucket(tc, bktName)
|
|
|
|
content := "123456789abcdef"
|
|
putObjectContent(tc, bktName, objName, content)
|
|
|
|
full := getObjectRange(t, tc, bktName, objName, 0, len(content)-1)
|
|
require.Equal(t, content, string(full))
|
|
|
|
beginning := getObjectRange(t, tc, bktName, objName, 0, 3)
|
|
require.Equal(t, content[:4], string(beginning))
|
|
|
|
middle := getObjectRange(t, tc, bktName, objName, 5, 10)
|
|
require.Equal(t, "6789ab", string(middle))
|
|
|
|
end := getObjectRange(t, tc, bktName, objName, 10, 15)
|
|
require.Equal(t, "bcdef", string(end))
|
|
}
|
|
|
|
func TestGetObject(t *testing.T) {
|
|
hc := prepareHandlerContext(t)
|
|
bktName, objName := "bucket", "obj"
|
|
bktInfo, objInfo := createVersionedBucketAndObject(hc.t, hc, bktName, objName)
|
|
|
|
putObject(hc.t, hc, bktName, objName)
|
|
|
|
checkFound(hc.t, hc, bktName, objName, objInfo.VersionID())
|
|
checkFound(hc.t, hc, bktName, objName, emptyVersion)
|
|
|
|
addr := getAddressOfLastVersion(hc, bktInfo, objName)
|
|
hc.tp.SetObjectError(addr, apistatus.ObjectNotFound{})
|
|
hc.tp.SetObjectError(objInfo.Address(), apistatus.ObjectNotFound{})
|
|
|
|
getObjectAssertS3Error(hc, bktName, objName, objInfo.VersionID(), s3errors.ErrNoSuchVersion)
|
|
getObjectAssertS3Error(hc, bktName, objName, emptyVersion, s3errors.ErrNoSuchKey)
|
|
}
|
|
|
|
func putObjectContent(hc *handlerContext, bktName, objName, content string) {
|
|
body := bytes.NewReader([]byte(content))
|
|
w, r := prepareTestPayloadRequest(hc, bktName, objName, body)
|
|
hc.Handler().PutObjectHandler(w, r)
|
|
assertStatus(hc.t, w, http.StatusOK)
|
|
}
|
|
|
|
func getObjectRange(t *testing.T, tc *handlerContext, bktName, objName string, start, end int) []byte {
|
|
w, r := prepareTestRequest(tc, bktName, objName, nil)
|
|
r.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
|
|
tc.Handler().GetObjectHandler(w, r)
|
|
assertStatus(t, w, http.StatusPartialContent)
|
|
content, err := io.ReadAll(w.Result().Body)
|
|
require.NoError(t, err)
|
|
return content
|
|
}
|
|
|
|
func getObjectAssertS3Error(hc *handlerContext, bktName, objName, version string, code apiErrors.ErrorCode) {
|
|
w := getObjectBase(hc, bktName, objName, version)
|
|
assertS3Error(hc.t, w, apiErrors.GetAPIError(code))
|
|
}
|
|
|
|
func getObjectBase(hc *handlerContext, bktName, objName, version string) *httptest.ResponseRecorder {
|
|
query := make(url.Values)
|
|
query.Add(api.QueryVersionID, version)
|
|
|
|
w, r := prepareTestFullRequest(hc, bktName, objName, query, nil)
|
|
hc.Handler().GetObjectHandler(w, r)
|
|
return w
|
|
}
|