package handler import ( "bytes" "errors" "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" apierr "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: apierr.GetAPIError(apierr.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: apierr.GetAPIError(apierr.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: apierr.GetAPIError(apierr.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: apierr.GetAPIError(apierr.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: apierr.GetAPIError(apierr.ErrPreconditionFailed), }, { name: "IfNoneMatch false, IfModifiedSince true", info: newInfo(etag, today), args: &conditionalArgs{IfNoneMatch: etag, IfModifiedSince: &yesterday}, expected: apierr.GetAPIError(apierr.ErrNotModified), }, { name: "IfNoneMatch true, IfModifiedSince false", info: newInfo(etag, yesterday), args: &conditionalArgs{IfNoneMatch: etag2, IfModifiedSince: &today}, expected: apierr.GetAPIError(apierr.ErrNotModified), }, } { t.Run(tc.name, func(t *testing.T) { actual := checkPreconditions(tc.info, tc.args, false) if tc.expected == nil { require.NoError(t, actual) } else { require.True(t, errors.Is(actual, tc.expected), 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, 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(), apierr.ErrNoSuchVersion) getObjectAssertS3Error(hc, bktName, objName, emptyVersion, apierr.ErrNoSuchKey) } func TestGetObjectEnabledMD5(t *testing.T) { hc := prepareHandlerContext(t) bktName, objName := "bucket", "obj" _, objInfo := createBucketAndObject(hc, bktName, objName) _, headers := getObject(hc, bktName, objName) require.Equal(t, data.Quote(objInfo.HashSum), headers.Get(api.ETag)) hc.config.md5Enabled = true _, headers = getObject(hc, bktName, objName) require.Equal(t, data.Quote(objInfo.MD5Sum), headers.Get(api.ETag)) } func TestGetObjectNotModifiedHeaders(t *testing.T) { hc := prepareHandlerContextWithMinCache(t) bktName, objName, metadataHeader := "bucket", "obj", api.MetadataPrefix+"header" createVersionedBucket(hc, bktName) header := putObjectWithHeaders(hc, bktName, objName, map[string]string{api.CacheControl: "value", metadataHeader: "value"}) etag, versionID := header.Get(api.ETag), header.Get(api.AmzVersionID) require.NotEmpty(t, etag) require.NotEmpty(t, versionID) putObjectTagging(t, hc, bktName, objName, map[string]string{"key": "value"}) w := getObjectWithHeaders(hc, bktName, objName, map[string]string{api.IfNoneMatch: etag}) require.Equal(t, http.StatusNotModified, w.Code) require.Equal(t, "1", w.Header().Get(api.AmzTaggingCount)) require.Equal(t, etag, w.Header().Get(api.ETag)) require.NotEmpty(t, w.Header().Get(api.LastModified)) require.Equal(t, versionID, w.Header().Get(api.AmzVersionID)) require.Equal(t, "value", w.Header().Get(api.CacheControl)) require.Equal(t, []string{"value"}, w.Header()[metadataHeader]) } func putObjectContent(hc *handlerContext, bktName, objName, content string) http.Header { body := bytes.NewReader([]byte(content)) w, r := prepareTestPayloadRequest(hc, bktName, objName, body) hc.Handler().PutObjectHandler(w, r) assertStatus(hc.t, w, http.StatusOK) return w.Result().Header } 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 getObjectVersion(tc *handlerContext, bktName, objName, version string) []byte { w := getObjectBaseResponse(tc, bktName, objName, version) assertStatus(tc.t, w, http.StatusOK) content, err := io.ReadAll(w.Result().Body) require.NoError(tc.t, err) return content } func getObjectAssertS3Error(hc *handlerContext, bktName, objName, version string, code apierr.ErrorCode) { w := getObjectBaseResponse(hc, bktName, objName, version) assertS3Error(hc.t, w, apierr.GetAPIError(code)) } func getObjectBaseResponse(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 }