mirror of
https://github.com/versity/versitygw.git
synced 2026-04-22 21:50:29 +00:00
Fixes #2052 Fixes #2056 Fixes #2057 Previously, GetObject and HeadObject used the request's `Range` header to determine the response status code, which caused incorrect 206 responses for invalid Range header values. The status is now driven by whether res.ContentRange is set in the response, rather than by the presence of a range in the request. Backends (posix and azure) now set Content-Range for PartNumber=1 on non-multipart objects, skipping zero-size objects where no range applies. HeadObject was also fixed to return 206 when Content-Range is present, and to only return checksums when the full object is requested.
1805 lines
54 KiB
Go
1805 lines
54 KiB
Go
// Copyright 2023 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/versity/versitygw/s3err"
|
|
)
|
|
|
|
func GetObject_non_existing_key(s *S3Conf) error {
|
|
testName := "GetObject_non_existing_key"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("non-existing-key"),
|
|
})
|
|
cancel()
|
|
var bae *types.NoSuchKey
|
|
if !errors.As(err, &bae) {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_directory_object_noslash(s *S3Conf) error {
|
|
testName := "GetObject_directory_object_noslash"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
obj := "my-obj/"
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
obj = "my-obj"
|
|
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
})
|
|
cancel()
|
|
var bae *types.NoSuchKey
|
|
if !errors.As(err, &bae) {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_with_range(s *S3Conf) error {
|
|
testName := "GetObject_with_range"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
// Match HeadObject_with_range: 100-byte object
|
|
obj, objLength := "my-obj", int64(100)
|
|
res, err := putObjectWithData(objLength, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
testGetObjectRange := func(rng, contentRange string, cLength int64, expData []byte, expErr error) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
defer cancel()
|
|
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
Range: &rng,
|
|
})
|
|
if err == nil && expErr != nil {
|
|
return fmt.Errorf("expected err %v, instead got nil", expErr)
|
|
}
|
|
if err != nil {
|
|
if expErr == nil {
|
|
return err
|
|
}
|
|
parsedErr, ok := expErr.(s3err.APIError)
|
|
if !ok {
|
|
return fmt.Errorf("invalid error type provided, expected s3err.APIError")
|
|
}
|
|
return checkApiErr(err, parsedErr)
|
|
}
|
|
|
|
if out.ContentLength == nil {
|
|
return fmt.Errorf("expected non nil content-length")
|
|
}
|
|
if *out.ContentLength != cLength {
|
|
return fmt.Errorf("expected content-length to be %v, instead got %v", cLength, *out.ContentLength)
|
|
}
|
|
if getString(out.AcceptRanges) != "bytes" {
|
|
return fmt.Errorf("expected accept-ranges to be 'bytes', instead got %v", getString(out.AcceptRanges))
|
|
}
|
|
if getString(out.ContentRange) != contentRange {
|
|
return fmt.Errorf("expected content-range to be %v, instead got %v", contentRange, getString(out.ContentRange))
|
|
}
|
|
|
|
outData, err := io.ReadAll(out.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("read object data: %w", err)
|
|
}
|
|
out.Body.Close()
|
|
|
|
if !isSameData(outData, expData) {
|
|
return fmt.Errorf("incorrect data retrieved")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for _, el := range []struct {
|
|
rng string
|
|
contentRange string
|
|
cLength int64
|
|
expData []byte
|
|
expErr error
|
|
}{
|
|
// Invalid / ignored ranges (return full object, empty Content-Range)
|
|
{"bytes=,", "", objLength, res.data, nil},
|
|
{"bytes= -1", "", objLength, res.data, nil},
|
|
{"bytes=--1", "", objLength, res.data, nil},
|
|
{"bytes=0 -1", "", objLength, res.data, nil},
|
|
{"bytes=0--1", "", objLength, res.data, nil},
|
|
{"bytes=10-5", "", objLength, res.data, nil},
|
|
{"bytes=abc", "", objLength, res.data, nil},
|
|
{"bytes=a-z", "", objLength, res.data, nil},
|
|
{"foo=0-1", "", objLength, res.data, nil},
|
|
{"bytes=abc-xyz", "", objLength, res.data, nil},
|
|
{"bytes=100-x", "", objLength, res.data, nil},
|
|
{"bytes=0-0,1-2", "", objLength, res.data, nil},
|
|
{fmt.Sprintf("bytes=%v-%v", objLength+2, objLength-100), "", objLength, res.data, nil},
|
|
|
|
// Valid numeric with leading zeros
|
|
{"bytes=00-01", "bytes 0-1/100", 2, res.data[0:2], nil},
|
|
|
|
// Suffix ranges
|
|
{"bytes=-1", "bytes 99-99/100", 1, res.data[99:], nil},
|
|
{"bytes=-2", "bytes 98-99/100", 2, res.data[98:], nil},
|
|
{"bytes=-10", "bytes 90-99/100", 10, res.data[90:], nil},
|
|
{"bytes=-100", "bytes 0-99/100", objLength, res.data, nil},
|
|
{"bytes=-101", "bytes 0-99/100", objLength, res.data, nil},
|
|
|
|
// Standard byte ranges
|
|
{"bytes=0-0", "bytes 0-0/100", 1, res.data[0:1], nil},
|
|
{"bytes=0-99", "bytes 0-99/100", objLength, res.data, nil},
|
|
{"bytes=0-100", "bytes 0-99/100", objLength, res.data, nil},
|
|
{"bytes=0-999999", "bytes 0-99/100", objLength, res.data, nil},
|
|
{"bytes=1-99", "bytes 1-99/100", 99, res.data[1:], nil},
|
|
{"bytes=50-99", "bytes 50-99/100", 50, res.data[50:], nil},
|
|
{"bytes=50-", "bytes 50-99/100", 50, res.data[50:], nil},
|
|
{"bytes=0-", "bytes 0-99/100", objLength, res.data, nil},
|
|
{"bytes=99-99", "bytes 99-99/100", 1, res.data[99:], nil},
|
|
|
|
// Unsatisfiable -> error
|
|
{"bytes=-0", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
|
{"bytes=100-100", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
|
{"bytes=100-110", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
|
} {
|
|
if err := testGetObjectRange(el.rng, el.contentRange, el.cLength, el.expData, el.expErr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_zero_len_with_range(s *S3Conf) error {
|
|
testName := "GetObject_zero_len_with_range"
|
|
return getObject_zero_len_with_range_helper(testName, "my-obj", s)
|
|
}
|
|
|
|
func GetObject_dir_with_range(s *S3Conf) error {
|
|
testName := "GetObject_dir_with_range"
|
|
return getObject_zero_len_with_range_helper(testName, "my-dir/", s)
|
|
}
|
|
|
|
func GetObject_invalid_parent(s *S3Conf) error {
|
|
testName := "GetObject_invalid_parent"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
dataLength, obj := int64(1234567), "not-a-dir"
|
|
|
|
_, err := putObjectWithData(dataLength, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("not-a-dir/bad-obj"),
|
|
})
|
|
cancel()
|
|
var bae *types.NoSuchKey
|
|
if !errors.As(err, &bae) {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_checksums(s *S3Conf) error {
|
|
testName := "GetObject_checksums"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
objs := []struct {
|
|
key string
|
|
checksumAlgo types.ChecksumAlgorithm
|
|
}{
|
|
{
|
|
key: "obj-1",
|
|
checksumAlgo: types.ChecksumAlgorithmCrc32,
|
|
},
|
|
{
|
|
key: "obj-2",
|
|
checksumAlgo: types.ChecksumAlgorithmCrc32c,
|
|
},
|
|
{
|
|
key: "obj-3",
|
|
checksumAlgo: types.ChecksumAlgorithmSha1,
|
|
},
|
|
{
|
|
key: "obj-4",
|
|
checksumAlgo: types.ChecksumAlgorithmSha256,
|
|
},
|
|
{
|
|
key: "obj-5",
|
|
checksumAlgo: types.ChecksumAlgorithmCrc64nvme,
|
|
},
|
|
}
|
|
|
|
for i, el := range objs {
|
|
out, err := putObjectWithData(int64(i*120), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &el.key,
|
|
ChecksumAlgorithm: el.checksumAlgo,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
res, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &el.key,
|
|
ChecksumMode: types.ChecksumModeEnabled,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if res.ChecksumType != types.ChecksumTypeFullObject {
|
|
return fmt.Errorf("expected the %v object checksum type to be %v, instaed got %v",
|
|
el.key, types.ChecksumTypeFullObject, res.ChecksumType)
|
|
}
|
|
if getString(res.ChecksumCRC32) != getString(out.res.ChecksumCRC32) {
|
|
return fmt.Errorf("expected crc32 checksum to be %v, instead got %v",
|
|
getString(out.res.ChecksumCRC32), getString(res.ChecksumCRC32))
|
|
}
|
|
if getString(res.ChecksumCRC32C) != getString(out.res.ChecksumCRC32C) {
|
|
return fmt.Errorf("expected crc32c checksum to be %v, instead got %v",
|
|
getString(out.res.ChecksumCRC32C), getString(res.ChecksumCRC32C))
|
|
}
|
|
if getString(res.ChecksumSHA1) != getString(out.res.ChecksumSHA1) {
|
|
return fmt.Errorf("expected sha1 checksum to be %v, instead got %v",
|
|
getString(out.res.ChecksumSHA1), getString(res.ChecksumSHA1))
|
|
}
|
|
if getString(res.ChecksumSHA256) != getString(out.res.ChecksumSHA256) {
|
|
return fmt.Errorf("expected sha256 checksum to be %v, instead got %v",
|
|
getString(out.res.ChecksumSHA256), getString(res.ChecksumSHA256))
|
|
}
|
|
if getString(res.ChecksumCRC64NVME) != getString(out.res.ChecksumCRC64NVME) {
|
|
return fmt.Errorf("expected crc64nvme checksum to be %v, instead got %v",
|
|
getString(out.res.ChecksumCRC64NVME), getString(res.ChecksumCRC64NVME))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_dir_object_checksum(s *S3Conf) error {
|
|
testName := "GetObject_dir_object_checksum"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
for i, obj := range []struct {
|
|
key string
|
|
expectedSum string
|
|
checksumAlgo types.ChecksumAlgorithm
|
|
}{
|
|
{
|
|
key: "obj-1/",
|
|
expectedSum: "AAAAAA==",
|
|
checksumAlgo: types.ChecksumAlgorithmCrc32,
|
|
},
|
|
{
|
|
key: "obj-2/",
|
|
expectedSum: "AAAAAA==",
|
|
checksumAlgo: types.ChecksumAlgorithmCrc32c,
|
|
},
|
|
{
|
|
key: "obj-3/",
|
|
expectedSum: "AAAAAAAAAAA=",
|
|
checksumAlgo: types.ChecksumAlgorithmCrc64nvme,
|
|
},
|
|
{
|
|
key: "obj-4/",
|
|
expectedSum: "2jmj7l5rSw0yVb/vlWAYkK/YBwk=",
|
|
checksumAlgo: types.ChecksumAlgorithmSha1,
|
|
},
|
|
{
|
|
key: "obj-5/",
|
|
expectedSum: "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
|
|
checksumAlgo: types.ChecksumAlgorithmSha256,
|
|
},
|
|
} {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj.key,
|
|
ChecksumAlgorithm: obj.checksumAlgo,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
|
res, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj.key,
|
|
ChecksumMode: types.ChecksumModeEnabled,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if res.ChecksumType != types.ChecksumTypeFullObject {
|
|
return fmt.Errorf("test %v failed: expected the %v object checksum type to be %v, instaed got %v",
|
|
i+1, obj.key, types.ChecksumTypeFullObject, res.ChecksumType)
|
|
}
|
|
|
|
var gotSum *string
|
|
switch obj.checksumAlgo {
|
|
case types.ChecksumAlgorithmCrc32:
|
|
gotSum = res.ChecksumCRC32
|
|
case types.ChecksumAlgorithmCrc32c:
|
|
gotSum = res.ChecksumCRC32C
|
|
case types.ChecksumAlgorithmCrc64nvme:
|
|
gotSum = res.ChecksumCRC64NVME
|
|
case types.ChecksumAlgorithmSha1:
|
|
gotSum = res.ChecksumSHA1
|
|
case types.ChecksumAlgorithmSha256:
|
|
gotSum = res.ChecksumSHA256
|
|
}
|
|
|
|
if getString(gotSum) != obj.expectedSum {
|
|
return fmt.Errorf("test %v failed: expected the object %s to be %s, instead got %s", i+1, obj.checksumAlgo, obj.expectedSum, getString(gotSum))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_large_object(s *S3Conf) error {
|
|
testName := "GetObject_large_object"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
//FIXME: make the object size larger after
|
|
// resolving the context deadline exceeding issue
|
|
// in the github actions
|
|
dataLength, obj := int64(100*1024*1024), "my-obj"
|
|
ctype := defaultContentType
|
|
|
|
r, err := putObjectWithData(dataLength, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
ContentType: &ctype,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
|
|
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
})
|
|
defer cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out.ContentLength == nil {
|
|
return fmt.Errorf("expected non nil content length")
|
|
}
|
|
if *out.ContentLength != dataLength {
|
|
return fmt.Errorf("expected content-length %v, instead got %v",
|
|
dataLength, out.ContentLength)
|
|
}
|
|
|
|
bdy, err := io.ReadAll(out.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Body.Close()
|
|
outCsum := sha256.Sum256(bdy)
|
|
if outCsum != r.csum {
|
|
return fmt.Errorf("expected the output data checksum to be %v, instead got %v",
|
|
r.csum, outCsum)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_conditional_reads(s *S3Conf) error {
|
|
testName := "GetObject_conditional_reads"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
key := "my-obj"
|
|
obj, err := putObjectWithData(10, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &key,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
errMod := s3err.GetAPIError(s3err.ErrNotModified)
|
|
errCond := s3err.GetAPIError(s3err.ErrPreconditionFailed)
|
|
|
|
// sleep one second to get dates before and after
|
|
// the object creation
|
|
time.Sleep(time.Second * 1)
|
|
|
|
before := time.Now().AddDate(0, 0, -3)
|
|
after := time.Now()
|
|
etag := obj.res.ETag
|
|
etagTrimmed := strings.Trim(*etag, `"`)
|
|
|
|
for i, test := range []struct {
|
|
ifmatch *string
|
|
ifnonematch *string
|
|
ifmodifiedsince *time.Time
|
|
ifunmodifiedsince *time.Time
|
|
err error
|
|
}{
|
|
// all the cases when preconditions are either empty, true or false
|
|
{getPtr("invalid_etag"), getPtr("invalid_etag"), &before, &before, errCond},
|
|
{getPtr("invalid_etag"), getPtr("invalid_etag"), &before, &after, errCond},
|
|
{getPtr("invalid_etag"), getPtr("invalid_etag"), &before, nil, errCond},
|
|
{getPtr("invalid_etag"), getPtr("invalid_etag"), &after, &before, errCond},
|
|
{getPtr("invalid_etag"), getPtr("invalid_etag"), &after, &after, errCond},
|
|
{getPtr("invalid_etag"), getPtr("invalid_etag"), &after, nil, errCond},
|
|
{getPtr("invalid_etag"), getPtr("invalid_etag"), nil, &before, errCond},
|
|
{getPtr("invalid_etag"), getPtr("invalid_etag"), nil, &after, errCond},
|
|
{getPtr("invalid_etag"), getPtr("invalid_etag"), nil, nil, errCond},
|
|
|
|
{getPtr("invalid_etag"), etag, &before, &before, errCond},
|
|
{getPtr("invalid_etag"), etag, &before, &after, errCond},
|
|
{getPtr("invalid_etag"), etag, &before, nil, errCond},
|
|
{getPtr("invalid_etag"), etag, &after, &before, errCond},
|
|
{getPtr("invalid_etag"), etag, &after, &after, errCond},
|
|
{getPtr("invalid_etag"), etag, &after, nil, errCond},
|
|
{getPtr("invalid_etag"), etag, nil, &before, errCond},
|
|
{getPtr("invalid_etag"), etag, nil, &after, errCond},
|
|
{getPtr("invalid_etag"), etag, nil, nil, errCond},
|
|
|
|
{getPtr("invalid_etag"), nil, &before, &before, errCond},
|
|
{getPtr("invalid_etag"), nil, &before, &after, errCond},
|
|
{getPtr("invalid_etag"), nil, &before, nil, errCond},
|
|
{getPtr("invalid_etag"), nil, &after, &before, errCond},
|
|
{getPtr("invalid_etag"), nil, &after, &after, errCond},
|
|
{getPtr("invalid_etag"), nil, &after, nil, errCond},
|
|
{getPtr("invalid_etag"), nil, nil, &before, errCond},
|
|
{getPtr("invalid_etag"), nil, nil, &after, errCond},
|
|
{getPtr("invalid_etag"), nil, nil, nil, errCond},
|
|
|
|
{etag, getPtr("invalid_etag"), &before, &before, nil},
|
|
{etag, getPtr("invalid_etag"), &before, &after, nil},
|
|
{etag, getPtr("invalid_etag"), &before, nil, nil},
|
|
{etag, getPtr("invalid_etag"), &after, &before, nil},
|
|
{etag, getPtr("invalid_etag"), &after, &after, nil},
|
|
{etag, getPtr("invalid_etag"), &after, nil, nil},
|
|
{etag, getPtr("invalid_etag"), nil, &before, nil},
|
|
{etag, getPtr("invalid_etag"), nil, &after, nil},
|
|
{etag, getPtr("invalid_etag"), nil, nil, nil},
|
|
|
|
{etag, etag, &before, &before, errMod},
|
|
{etag, etag, &before, &after, errMod},
|
|
{etag, etag, &before, nil, errMod},
|
|
{etag, etag, &after, &before, errMod},
|
|
{etag, etag, &after, &after, errMod},
|
|
{etag, etag, &after, nil, errMod},
|
|
{etag, etag, nil, &before, errMod},
|
|
{etag, etag, nil, &after, errMod},
|
|
{etag, etag, nil, nil, errMod},
|
|
|
|
{etag, nil, &before, &before, nil},
|
|
{etag, nil, &before, &after, nil},
|
|
{etag, nil, &before, nil, nil},
|
|
{etag, nil, &after, &before, errMod},
|
|
{etag, nil, &after, &after, errMod},
|
|
{etag, nil, &after, nil, errMod},
|
|
{etag, nil, nil, &before, nil},
|
|
{etag, nil, nil, &after, nil},
|
|
{etag, nil, nil, nil, nil},
|
|
|
|
{nil, getPtr("invalid_etag"), &before, &before, errCond},
|
|
{nil, getPtr("invalid_etag"), &before, &after, nil},
|
|
{nil, getPtr("invalid_etag"), &before, nil, nil},
|
|
{nil, getPtr("invalid_etag"), &after, &before, errCond},
|
|
{nil, getPtr("invalid_etag"), &after, &after, nil},
|
|
{nil, getPtr("invalid_etag"), &after, nil, nil},
|
|
{nil, getPtr("invalid_etag"), nil, &before, errCond},
|
|
{nil, getPtr("invalid_etag"), nil, &after, nil},
|
|
{nil, getPtr("invalid_etag"), nil, nil, nil},
|
|
|
|
{nil, etag, &before, &before, errCond},
|
|
{nil, etag, &before, &after, errMod},
|
|
{nil, etag, &before, nil, errMod},
|
|
{nil, etag, &after, &before, errCond},
|
|
{nil, etag, &after, &after, errMod},
|
|
{nil, etag, &after, nil, errMod},
|
|
{nil, etag, nil, &before, errCond},
|
|
{nil, etag, nil, &after, errMod},
|
|
{nil, etag, nil, nil, errMod},
|
|
|
|
{nil, nil, &before, &before, errCond},
|
|
{nil, nil, &before, &after, nil},
|
|
{nil, nil, &before, nil, nil},
|
|
{nil, nil, &after, &before, errCond},
|
|
{nil, nil, &after, &after, errMod},
|
|
{nil, nil, &after, nil, errMod},
|
|
{nil, nil, nil, &before, errCond},
|
|
{nil, nil, nil, &after, nil},
|
|
{nil, nil, nil, nil, nil},
|
|
|
|
// if-match, if-non-match without quotes
|
|
{&etagTrimmed, getPtr("invalid_etag"), &before, &before, nil},
|
|
{&etagTrimmed, getPtr("invalid_etag"), &before, &after, nil},
|
|
{&etagTrimmed, getPtr("invalid_etag"), &before, nil, nil},
|
|
{&etagTrimmed, getPtr("invalid_etag"), &after, &before, nil},
|
|
{&etagTrimmed, getPtr("invalid_etag"), &after, &after, nil},
|
|
{&etagTrimmed, getPtr("invalid_etag"), &after, nil, nil},
|
|
{&etagTrimmed, getPtr("invalid_etag"), nil, &before, nil},
|
|
{&etagTrimmed, getPtr("invalid_etag"), nil, &after, nil},
|
|
{&etagTrimmed, getPtr("invalid_etag"), nil, nil, nil},
|
|
|
|
{&etagTrimmed, &etagTrimmed, &before, &before, errMod},
|
|
{&etagTrimmed, &etagTrimmed, &before, &after, errMod},
|
|
{&etagTrimmed, &etagTrimmed, &before, nil, errMod},
|
|
{&etagTrimmed, &etagTrimmed, &after, &before, errMod},
|
|
{&etagTrimmed, &etagTrimmed, &after, &after, errMod},
|
|
{&etagTrimmed, &etagTrimmed, &after, nil, errMod},
|
|
{&etagTrimmed, &etagTrimmed, nil, &before, errMod},
|
|
{&etagTrimmed, &etagTrimmed, nil, &after, errMod},
|
|
{&etagTrimmed, &etagTrimmed, nil, nil, errMod},
|
|
|
|
{&etagTrimmed, nil, &before, &before, nil},
|
|
{&etagTrimmed, nil, &before, &after, nil},
|
|
{&etagTrimmed, nil, &before, nil, nil},
|
|
{&etagTrimmed, nil, &after, &before, errMod},
|
|
{&etagTrimmed, nil, &after, &after, errMod},
|
|
{&etagTrimmed, nil, &after, nil, errMod},
|
|
{&etagTrimmed, nil, nil, &before, nil},
|
|
{&etagTrimmed, nil, nil, &after, nil},
|
|
{&etagTrimmed, nil, nil, nil, nil},
|
|
|
|
{nil, &etagTrimmed, &before, &before, errCond},
|
|
{nil, &etagTrimmed, &before, &after, errMod},
|
|
{nil, &etagTrimmed, &before, nil, errMod},
|
|
{nil, &etagTrimmed, &after, &before, errCond},
|
|
{nil, &etagTrimmed, &after, &after, errMod},
|
|
{nil, &etagTrimmed, &after, nil, errMod},
|
|
{nil, &etagTrimmed, nil, &before, errCond},
|
|
{nil, &etagTrimmed, nil, &after, errMod},
|
|
{nil, &etagTrimmed, nil, nil, errMod},
|
|
} {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &key,
|
|
IfMatch: test.ifmatch,
|
|
IfNoneMatch: test.ifnonematch,
|
|
IfModifiedSince: test.ifmodifiedsince,
|
|
IfUnmodifiedSince: test.ifunmodifiedsince,
|
|
})
|
|
cancel()
|
|
if test.err == nil && err != nil {
|
|
return fmt.Errorf("test case %d failed: expected no error, but got %v", i, err)
|
|
}
|
|
if test.err != nil {
|
|
apiErr, ok := test.err.(s3err.APIError)
|
|
if !ok {
|
|
return fmt.Errorf("invalid error type: expected s3err.APIError")
|
|
}
|
|
if err := checkApiErr(err, apiErr); err != nil {
|
|
return fmt.Errorf("test case %d failed: %w", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_success(s *S3Conf) error {
|
|
testName := "GetObject_success"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
dataLength, obj := int64(1234567), "my-obj"
|
|
ctype, cDisp, cEnc, cLang := defaultContentType, "cont-desp", "json", "eng"
|
|
cacheControl, expires := "cache-ctrl", time.Now().Add(time.Hour*2)
|
|
meta := map[string]string{
|
|
"foo": "bar",
|
|
"baz": "quxx",
|
|
}
|
|
|
|
r, err := putObjectWithData(dataLength, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
ContentType: &ctype,
|
|
ContentDisposition: &cDisp,
|
|
ContentEncoding: &cEnc,
|
|
ContentLanguage: &cLang,
|
|
Expires: &expires,
|
|
CacheControl: &cacheControl,
|
|
Metadata: meta,
|
|
Tagging: getPtr("key=value&key1=val1"),
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
})
|
|
defer cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if *out.ContentLength != dataLength {
|
|
return fmt.Errorf("expected content-length %v, instead got %v",
|
|
dataLength, out.ContentLength)
|
|
}
|
|
if getString(out.ContentType) != defaultContentType {
|
|
return fmt.Errorf("expected Content-Type %v, instead got %v",
|
|
defaultContentType, getString(out.ContentType))
|
|
}
|
|
if getString(out.ContentDisposition) != cDisp {
|
|
return fmt.Errorf("expected Content-Disposition %v, instead got %v",
|
|
cDisp, getString(out.ContentDisposition))
|
|
}
|
|
if getString(out.ContentEncoding) != cEnc {
|
|
return fmt.Errorf("expected Content-Encoding %v, instead got %v",
|
|
cEnc, getString(out.ContentEncoding))
|
|
}
|
|
if getString(out.ContentLanguage) != cLang {
|
|
return fmt.Errorf("expected Content-Language %v, instead got %v",
|
|
cLang, getString(out.ContentLanguage))
|
|
}
|
|
if getString(out.ExpiresString) != expires.UTC().Format(timefmt) {
|
|
return fmt.Errorf("expected Expiress %v, instead got %v",
|
|
expires.UTC().Format(timefmt), getString(out.ExpiresString))
|
|
}
|
|
if getString(out.CacheControl) != cacheControl {
|
|
return fmt.Errorf("expected Cache-Control %v, instead got %v",
|
|
cacheControl, getString(out.CacheControl))
|
|
}
|
|
if out.StorageClass != types.StorageClassStandard {
|
|
return fmt.Errorf("expected the storage class to be %v, instead got %v",
|
|
types.StorageClassStandard, out.StorageClass)
|
|
}
|
|
if !areMapsSame(out.Metadata, meta) {
|
|
return fmt.Errorf("expected the object metadata to be %v, instead got %v",
|
|
meta, out.Metadata)
|
|
}
|
|
var tagCount int32
|
|
if out.TagCount != nil {
|
|
tagCount = *out.TagCount
|
|
}
|
|
if tagCount != 2 {
|
|
return fmt.Errorf("expected tag count to be 2, instead got %v", tagCount)
|
|
}
|
|
|
|
bdy, err := io.ReadAll(out.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Body.Close()
|
|
outCsum := sha256.Sum256(bdy)
|
|
if outCsum != r.csum {
|
|
return fmt.Errorf("invalid object data")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
const directoryContentType = "application/x-directory"
|
|
|
|
func GetObject_directory_success(s *S3Conf) error {
|
|
testName := "GetObject_directory_success"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
dataLength, obj := int64(0), "my-dir/"
|
|
|
|
_, err := putObjectWithData(dataLength, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
})
|
|
defer cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if out.ContentLength == nil {
|
|
return fmt.Errorf("expected non nil content length")
|
|
}
|
|
if *out.ContentLength != dataLength {
|
|
return fmt.Errorf("expected content-length %v, instead got %v",
|
|
dataLength, out.ContentLength)
|
|
}
|
|
if getString(out.ContentType) != directoryContentType {
|
|
return fmt.Errorf("expected content type %v, instead got %v",
|
|
directoryContentType, getString(out.ContentType))
|
|
}
|
|
if out.StorageClass != types.StorageClassStandard {
|
|
return fmt.Errorf("expected the storage class to be %v, instead got %v",
|
|
types.StorageClassStandard, out.StorageClass)
|
|
}
|
|
|
|
out.Body.Close()
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_by_range_resp_status(s *S3Conf) error {
|
|
testName := "GetObject_by_range_resp_status"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
obj := "my-obj"
|
|
objLength := int64(100)
|
|
_, err := putObjectWithData(objLength, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
checkRangeStatus := func(rng string, expectedStatus int) error {
|
|
req, err := createSignedReq(
|
|
http.MethodGet,
|
|
s.endpoint,
|
|
fmt.Sprintf("%v/%v", bucket, obj),
|
|
s.awsID,
|
|
s.awsSecret,
|
|
"s3",
|
|
s.awsRegion,
|
|
nil,
|
|
time.Now(),
|
|
map[string]string{"Range": rng},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode != expectedStatus {
|
|
return fmt.Errorf("range %q: expected status %d, instead got %d", rng, expectedStatus, resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
rng string
|
|
expectedStatus int
|
|
}{
|
|
// Invalid/ignored ranges → full object, no Content-Range → 200
|
|
{"bytes=,", http.StatusOK},
|
|
{"bytes= -1", http.StatusOK},
|
|
{"bytes=--1", http.StatusOK},
|
|
{"bytes=0 -1", http.StatusOK},
|
|
{"bytes=0--1", http.StatusOK},
|
|
{"bytes=10-5", http.StatusOK},
|
|
{"bytes=abc", http.StatusOK},
|
|
{"bytes=a-z", http.StatusOK},
|
|
{"foo=0-1", http.StatusOK},
|
|
{"bytes=abc-xyz", http.StatusOK},
|
|
{"bytes=100-x", http.StatusOK},
|
|
{"bytes=0-0,1-2", http.StatusOK},
|
|
{fmt.Sprintf("bytes=%v-%v", objLength+2, objLength-100), http.StatusOK},
|
|
|
|
// Valid ranges → partial content, non-empty Content-Range → 206
|
|
{"bytes=00-01", http.StatusPartialContent},
|
|
{"bytes=-1", http.StatusPartialContent},
|
|
{"bytes=-2", http.StatusPartialContent},
|
|
{"bytes=-10", http.StatusPartialContent},
|
|
{"bytes=-100", http.StatusPartialContent},
|
|
{"bytes=-101", http.StatusPartialContent},
|
|
{"bytes=0-0", http.StatusPartialContent},
|
|
{"bytes=0-99", http.StatusPartialContent},
|
|
{"bytes=0-100", http.StatusPartialContent},
|
|
{"bytes=0-999999", http.StatusPartialContent},
|
|
{"bytes=1-99", http.StatusPartialContent},
|
|
{"bytes=50-99", http.StatusPartialContent},
|
|
{"bytes=50-", http.StatusPartialContent},
|
|
{"bytes=0-", http.StatusPartialContent},
|
|
{"bytes=99-99", http.StatusPartialContent},
|
|
} {
|
|
if err := checkRangeStatus(tc.rng, tc.expectedStatus); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_not_enabled_checksum_mode(s *S3Conf) error {
|
|
testName := "GetObject_not_enabled_checksum_mode"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
obj := "my-obj"
|
|
|
|
_, err := putObjectWithData(500, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
ChecksumAlgorithm: types.ChecksumAlgorithmSha1,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
res, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
}, func(o *s3.Options) {
|
|
// config sdk to not automatically set the `x-amz-checksum-mode: ENABLED` header
|
|
o.RequestChecksumCalculation = aws.RequestChecksumCalculationUnset
|
|
o.ResponseChecksumValidation = aws.ResponseChecksumValidationUnset
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if res.ChecksumCRC32 != nil {
|
|
return fmt.Errorf("expected nil crc32 checksum, instead got %v", *res.ChecksumCRC32)
|
|
}
|
|
if res.ChecksumCRC32C != nil {
|
|
return fmt.Errorf("expected nil crc32c checksum, instead got %v", *res.ChecksumCRC32C)
|
|
}
|
|
if res.ChecksumSHA1 != nil {
|
|
return fmt.Errorf("expected nil sha1 checksum, instead got %v", *res.ChecksumSHA1)
|
|
}
|
|
if res.ChecksumSHA256 != nil {
|
|
return fmt.Errorf("expected nil sha256 checksum, instead got %v", *res.ChecksumSHA256)
|
|
}
|
|
if res.ChecksumCRC64NVME != nil {
|
|
return fmt.Errorf("expected nil crc64nvme checksum, instead got %v", *res.ChecksumCRC64NVME)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_non_existing_dir_object(s *S3Conf) error {
|
|
testName := "GetObject_non_existing_dir_object"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
dataLength, obj := int64(1234567), "my-obj"
|
|
|
|
_, err := putObjectWithData(dataLength, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
obj = "my-obj/"
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
})
|
|
defer cancel()
|
|
if err := checkSdkApiErr(err, "NoSuchKey"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_overrides_success(s *S3Conf) error {
|
|
testName := "GetObject_overrides_success"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
// Test data
|
|
objKey := "test-object"
|
|
objContent := "test content for response overrides"
|
|
exp := time.Now()
|
|
|
|
// Put an object first
|
|
_, err := s3client.PutObject(context.Background(), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
Body: strings.NewReader(objContent),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to put object: %v", err)
|
|
}
|
|
|
|
for _, test := range []PublicBucketTestCase{
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseCacheControl: getPtr("max-age=90"),
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: nil,
|
|
},
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseContentDisposition: getPtr("inline"),
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: nil,
|
|
},
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseContentEncoding: getPtr("txt"),
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: nil,
|
|
},
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseContentLanguage: getPtr("en"),
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: nil,
|
|
},
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseContentType: getPtr("application/json"),
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: nil,
|
|
},
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseExpires: &exp,
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: nil,
|
|
},
|
|
} {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
err := test.Call(ctx)
|
|
cancel()
|
|
if err == nil && test.ExpectedErr != nil {
|
|
return fmt.Errorf("%v: expected err %v, instead got successful response", test.Action, test.ExpectedErr)
|
|
}
|
|
if err != nil {
|
|
if test.ExpectedErr == nil {
|
|
return fmt.Errorf("%v: expected no error, instead got %v", test.Action, err)
|
|
}
|
|
|
|
apiErr, ok := test.ExpectedErr.(s3err.APIError)
|
|
if !ok {
|
|
return fmt.Errorf("invalid error type provided in the test, expected s3err.APIError")
|
|
}
|
|
|
|
if err := checkApiErr(err, apiErr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_overrides_presign_success(s *S3Conf) error {
|
|
testName := "GetObject_overrides_presign_success"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
// Test data
|
|
objKey := "test-object"
|
|
objContent := "test content for response overrides"
|
|
|
|
// Put an object first
|
|
_, err := s3client.PutObject(context.Background(), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
Body: strings.NewReader(objContent),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to put object: %v", err)
|
|
}
|
|
|
|
// Test cases for each response override parameter
|
|
testCases := []struct {
|
|
name string
|
|
queryParam string
|
|
expectedHeader string
|
|
expectedValue string
|
|
}{
|
|
{
|
|
name: "response-cache-control",
|
|
queryParam: "response-cache-control=no-cache",
|
|
expectedHeader: "Cache-Control",
|
|
expectedValue: "no-cache",
|
|
},
|
|
{
|
|
name: "response-content-disposition",
|
|
queryParam: "response-content-disposition=attachment%3B%20filename%3D%22test.txt%22",
|
|
expectedHeader: "Content-Disposition",
|
|
expectedValue: "attachment; filename=\"test.txt\"",
|
|
},
|
|
{
|
|
name: "response-content-encoding",
|
|
queryParam: "response-content-encoding=txt",
|
|
expectedHeader: "Content-Encoding",
|
|
expectedValue: "txt",
|
|
},
|
|
{
|
|
name: "response-content-language",
|
|
queryParam: "response-content-language=en-US",
|
|
expectedHeader: "Content-Language",
|
|
expectedValue: "en-US",
|
|
},
|
|
{
|
|
name: "response-content-type",
|
|
queryParam: "response-content-type=text%2Fplain",
|
|
expectedHeader: "Content-Type",
|
|
expectedValue: "text/plain",
|
|
},
|
|
{
|
|
name: "response-expires",
|
|
queryParam: "response-expires=Thu%2C%2001%20Dec%202024%2016%3A00%3A00%20GMT",
|
|
expectedHeader: "Expires",
|
|
expectedValue: "Thu, 01 Dec 2024 16:00:00 GMT",
|
|
},
|
|
}
|
|
|
|
// Test each override parameter individually
|
|
for _, tc := range testCases {
|
|
// Create a signed request with the response override parameter
|
|
req, err := createSignedReq(
|
|
http.MethodGet,
|
|
s.endpoint,
|
|
fmt.Sprintf("%s/%s?%s", bucket, objKey, tc.queryParam),
|
|
s.awsID,
|
|
s.awsSecret,
|
|
"s3",
|
|
s.awsRegion,
|
|
nil,
|
|
time.Now(),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create signed request for %s: %v", tc.name, err)
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute request for %s: %v", tc.name, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("expected status 200 for %s, got %d", tc.name, resp.StatusCode)
|
|
}
|
|
|
|
// Verify the response override header is set correctly
|
|
actualValue := resp.Header.Get(tc.expectedHeader)
|
|
if actualValue != tc.expectedValue {
|
|
return fmt.Errorf("expected %s header to be %q for %s, got %q",
|
|
tc.expectedHeader, tc.expectedValue, tc.name, actualValue)
|
|
}
|
|
|
|
// Verify content is still correct
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read response body for %s: %v", tc.name, err)
|
|
}
|
|
|
|
if string(body) != objContent {
|
|
return fmt.Errorf("expected content %q for %s, got %q", objContent, tc.name, string(body))
|
|
}
|
|
}
|
|
|
|
// Test multiple override parameters together
|
|
multiParam := "response-cache-control=max-age%3D3600&response-content-type=application%2Fjson&response-content-disposition=inline"
|
|
req, err := createSignedReq(
|
|
http.MethodGet,
|
|
s.endpoint,
|
|
fmt.Sprintf("%s/%s?%s", bucket, objKey, multiParam),
|
|
s.awsID,
|
|
s.awsSecret,
|
|
"s3",
|
|
s.awsRegion,
|
|
nil,
|
|
time.Now(),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create signed request for multiple overrides: %v", err)
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute request for multiple overrides: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("expected status 200 for multiple overrides, got %d", resp.StatusCode)
|
|
}
|
|
|
|
// Verify all override headers are set correctly
|
|
expectedHeaders := map[string]string{
|
|
"Cache-Control": "max-age=3600",
|
|
"Content-Type": "application/json",
|
|
"Content-Disposition": "inline",
|
|
}
|
|
|
|
for headerName, expectedValue := range expectedHeaders {
|
|
actualValue := resp.Header.Get(headerName)
|
|
if actualValue != expectedValue {
|
|
return fmt.Errorf("expected %s header to be %q for multiple overrides, got %q",
|
|
headerName, expectedValue, actualValue)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_overrides_fail_public(s *S3Conf) error {
|
|
testName := "GetObject_overrides_fail_public"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
rootClient := s.GetClient()
|
|
// Grant public access to the bucket for bucket operations
|
|
err := grantPublicBucketPolicy(rootClient, bucket, policyTypeObject)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Test data
|
|
objKey := "test-object"
|
|
objContent := "test content for response overrides"
|
|
exp := time.Now()
|
|
|
|
// Put an object first
|
|
_, err = rootClient.PutObject(context.Background(), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
Body: strings.NewReader(objContent),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to put object: %v", err)
|
|
}
|
|
|
|
for _, test := range []PublicBucketTestCase{
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseCacheControl: getPtr("max-age=90"),
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders),
|
|
},
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseContentDisposition: getPtr("inline"),
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders),
|
|
},
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseContentEncoding: getPtr("txt"),
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders),
|
|
},
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseContentLanguage: getPtr("en"),
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders),
|
|
},
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseContentType: getPtr("application/json"),
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders),
|
|
},
|
|
{
|
|
Action: "GetObject",
|
|
Call: func(ctx context.Context) error {
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &objKey,
|
|
ResponseExpires: &exp,
|
|
})
|
|
return err
|
|
},
|
|
ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders),
|
|
},
|
|
} {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
err := test.Call(ctx)
|
|
cancel()
|
|
if err == nil && test.ExpectedErr != nil {
|
|
return fmt.Errorf("%v: expected err %v, instead got successful response", test.Action, test.ExpectedErr)
|
|
}
|
|
if err != nil {
|
|
if test.ExpectedErr == nil {
|
|
return fmt.Errorf("%v: expected no error, instead got %v", test.Action, err)
|
|
}
|
|
|
|
apiErr, ok := test.ExpectedErr.(s3err.APIError)
|
|
if !ok {
|
|
return fmt.Errorf("invalid error type provided in the test, expected s3err.APIError")
|
|
}
|
|
|
|
if err := checkApiErr(err, apiErr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}, withAnonymousClient())
|
|
}
|
|
|
|
func GetObject_invalid_part_number(s *S3Conf) error {
|
|
testName := "GetObject_invalid_part_number"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
defer cancel()
|
|
_, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("obj"),
|
|
PartNumber: getPtr(int32(-3)),
|
|
})
|
|
|
|
return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidPartNumber))
|
|
})
|
|
}
|
|
|
|
func GetObject_range_and_part_number(s *S3Conf) error {
|
|
testName := "GetObject_range_and_part_number"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
obj := "my-obj"
|
|
_, err := putObjectWithData(100, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pn := int32(1)
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
Range: getPtr("bytes=0-9"),
|
|
PartNumber: &pn,
|
|
})
|
|
cancel()
|
|
return checkApiErr(err, s3err.GetAPIError(s3err.ErrRangeAndPartNumber))
|
|
})
|
|
}
|
|
|
|
func GetObject_mp_part_number_exceeds_parts_count(s *S3Conf) error {
|
|
testName := "GetObject_mp_part_number_exceeds_parts_count"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
obj := "my-obj"
|
|
out, err := createMp(s3client, bucket, obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
const partCount = int64(5)
|
|
parts, _, err := uploadParts(s3client, partCount*5*1024*1024, partCount, bucket, obj, *out.UploadId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
compParts := make([]types.CompletedPart, len(parts))
|
|
for i, p := range parts {
|
|
compParts[i] = types.CompletedPart{
|
|
ETag: p.ETag,
|
|
PartNumber: p.PartNumber,
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
UploadId: out.UploadId,
|
|
MultipartUpload: &types.CompletedMultipartUpload{
|
|
Parts: compParts,
|
|
},
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// partNumber exceeds the number of parts in the completed upload
|
|
pn := int32(partCount + 1)
|
|
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
PartNumber: &pn,
|
|
})
|
|
cancel()
|
|
return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidPartNumberRange))
|
|
})
|
|
}
|
|
|
|
func GetObject_mp_part_number_success(s *S3Conf) error {
|
|
testName := "GetObject_mp_part_number_success"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
obj := "my-obj"
|
|
out, err := createMp(s3client, bucket, obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
const partCount = 3
|
|
const partSize = int64(5 * 1024 * 1024)
|
|
const totalSize = int64(partCount) * partSize
|
|
|
|
// Upload parts manually to capture per-part data for integrity checks
|
|
partNumbers := make([]int32, partCount)
|
|
partChecksums := make([][32]byte, partCount)
|
|
compParts := make([]types.CompletedPart, partCount)
|
|
|
|
for i := range partCount {
|
|
partNumbers[i] = int32(i + 1)
|
|
buf := make([]byte, partSize)
|
|
for j := range buf {
|
|
buf[j] = byte(i*17 + j%251)
|
|
}
|
|
partChecksums[i] = sha256.Sum256(buf)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
res, err := s3client.UploadPart(ctx, &s3.UploadPartInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
UploadId: out.UploadId,
|
|
Body: bytes.NewReader(buf),
|
|
PartNumber: &partNumbers[i],
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return fmt.Errorf("upload part %d: %w", partNumbers[i], err)
|
|
}
|
|
compParts[i] = types.CompletedPart{
|
|
ETag: res.ETag,
|
|
PartNumber: &partNumbers[i],
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
UploadId: out.UploadId,
|
|
MultipartUpload: &types.CompletedMultipartUpload{
|
|
Parts: compParts,
|
|
},
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range partCount {
|
|
pn := int32(i + 1)
|
|
startByte := int64(i) * partSize
|
|
endByte := startByte + partSize - 1
|
|
expectedContentRange := fmt.Sprintf("bytes %d-%d/%d", startByte, endByte, totalSize)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
|
|
res, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
PartNumber: &pn,
|
|
})
|
|
if err != nil {
|
|
cancel()
|
|
return fmt.Errorf("part %d: %w", pn, err)
|
|
}
|
|
|
|
if res.PartsCount == nil {
|
|
cancel()
|
|
return fmt.Errorf("part %d: expected non-nil x-amz-mp-parts-count", pn)
|
|
}
|
|
if *res.PartsCount != int32(partCount) {
|
|
cancel()
|
|
return fmt.Errorf("part %d: expected PartsCount %d, got %d", pn, partCount, *res.PartsCount)
|
|
}
|
|
if getString(res.ContentRange) != expectedContentRange {
|
|
cancel()
|
|
return fmt.Errorf("part %d: expected Content-Range %q, got %q", pn, expectedContentRange, getString(res.ContentRange))
|
|
}
|
|
if res.ContentLength == nil || *res.ContentLength != partSize {
|
|
cancel()
|
|
return fmt.Errorf("part %d: expected Content-Length %d, got %v", pn, partSize, res.ContentLength)
|
|
}
|
|
if getString(res.AcceptRanges) != "bytes" {
|
|
cancel()
|
|
return fmt.Errorf("part %d: expected Accept-Ranges 'bytes', got %q", pn, getString(res.AcceptRanges))
|
|
}
|
|
|
|
body, readErr := io.ReadAll(res.Body)
|
|
cancel()
|
|
res.Body.Close()
|
|
if readErr != nil {
|
|
return fmt.Errorf("part %d: read body: %w", pn, readErr)
|
|
}
|
|
gotSum := sha256.Sum256(body)
|
|
if gotSum != partChecksums[i] {
|
|
return fmt.Errorf("part %d: data integrity check failed: body checksum mismatch", pn)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_non_mp_part_number_1_success(s *S3Conf) error {
|
|
testName := "GetObject_non_mp_part_number_1_success"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
obj := "put-object-part1"
|
|
const objSize = int64(1234)
|
|
|
|
out, err := putObjectWithData(objSize, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pn := int32(1)
|
|
ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
|
|
res, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
PartNumber: &pn,
|
|
})
|
|
if err != nil {
|
|
cancel()
|
|
return err
|
|
}
|
|
|
|
if res.ContentLength == nil || *res.ContentLength != objSize {
|
|
cancel()
|
|
return fmt.Errorf("expected ContentLength %d, got %v", objSize, res.ContentLength)
|
|
}
|
|
expectedCRange := fmt.Sprintf("bytes 0-%d/%d", objSize-1, objSize)
|
|
if getString(res.ContentRange) != expectedCRange {
|
|
cancel()
|
|
return fmt.Errorf("expected Content-Range to be %s, instead got %s", expectedCRange, getString(res.ContentRange))
|
|
}
|
|
if res.PartsCount != nil {
|
|
cancel()
|
|
return fmt.Errorf("expected nil PartsCount for non-multipart object, got %d", *res.PartsCount)
|
|
}
|
|
if getString(res.AcceptRanges) != "bytes" {
|
|
cancel()
|
|
return fmt.Errorf("expected Accept-Ranges 'bytes', got %q", getString(res.AcceptRanges))
|
|
}
|
|
|
|
body, readErr := io.ReadAll(res.Body)
|
|
cancel()
|
|
res.Body.Close()
|
|
if readErr != nil {
|
|
return fmt.Errorf("read body: %w", readErr)
|
|
}
|
|
if gotSum := sha256.Sum256(body); gotSum != out.csum {
|
|
return fmt.Errorf("data integrity check failed: body checksum mismatch")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_empty_object_part_number_1(s *S3Conf) error {
|
|
testName := "GetObject_empty_object_part_number_1"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
obj := "empty-obj"
|
|
|
|
out, err := putObjectWithData(0, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pn := int32(1)
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
res, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
PartNumber: &pn,
|
|
ChecksumMode: types.ChecksumModeEnabled,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if res.ContentRange != nil {
|
|
return fmt.Errorf("expected nil Content-Range for empty object with partNumber=1, got %q", *res.ContentRange)
|
|
}
|
|
if res.ContentLength == nil || *res.ContentLength != 0 {
|
|
return fmt.Errorf("expected ContentLength 0, got %v", res.ContentLength)
|
|
}
|
|
if getString(res.ChecksumSHA256) != getString(out.res.ChecksumSHA256) {
|
|
return fmt.Errorf("expected sha256 checksum %v, got %v",
|
|
getString(out.res.ChecksumSHA256), getString(res.ChecksumSHA256))
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_mp_part_number_resp_status(s *S3Conf) error {
|
|
testName := "GetObject_mp_part_number_resp_status"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
obj := "my-obj"
|
|
out, err := createMp(s3client, bucket, obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
const partCount = int64(2)
|
|
parts, _, err := uploadParts(s3client, partCount*5*1024*1024, partCount, bucket, obj, *out.UploadId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
compParts := make([]types.CompletedPart, len(parts))
|
|
for i, p := range parts {
|
|
compParts[i] = types.CompletedPart{
|
|
ETag: p.ETag,
|
|
PartNumber: p.PartNumber,
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
UploadId: out.UploadId,
|
|
MultipartUpload: &types.CompletedMultipartUpload{
|
|
Parts: compParts,
|
|
},
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := createSignedReq(
|
|
http.MethodGet,
|
|
s.endpoint,
|
|
fmt.Sprintf("%v/%v?partNumber=1", bucket, obj),
|
|
s.awsID,
|
|
s.awsSecret,
|
|
"s3",
|
|
s.awsRegion,
|
|
nil,
|
|
time.Now(),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusPartialContent {
|
|
return fmt.Errorf("expected response status to be %v, instead got %v",
|
|
http.StatusPartialContent, resp.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetObject_ranged_with_checksum_mode(s *S3Conf) error {
|
|
testName := "GetObject_ranged_with_checksum_mode"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
checkNoChecksums := func(res *s3.GetObjectOutput) error {
|
|
if res.ChecksumCRC32 != nil {
|
|
return fmt.Errorf("expected nil crc32 checksum, instead got %v", *res.ChecksumCRC32)
|
|
}
|
|
if res.ChecksumCRC32C != nil {
|
|
return fmt.Errorf("expected nil crc32c checksum, instead got %v", *res.ChecksumCRC32C)
|
|
}
|
|
if res.ChecksumSHA1 != nil {
|
|
return fmt.Errorf("expected nil sha1 checksum, instead got %v", *res.ChecksumSHA1)
|
|
}
|
|
if res.ChecksumSHA256 != nil {
|
|
return fmt.Errorf("expected nil sha256 checksum, instead got %v", *res.ChecksumSHA256)
|
|
}
|
|
if res.ChecksumCRC64NVME != nil {
|
|
return fmt.Errorf("expected nil crc64nvme checksum, instead got %v", *res.ChecksumCRC64NVME)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Sub-test 1: regular object with Range header and ChecksumMode enabled
|
|
regularObj := "regular-obj"
|
|
_, err := putObjectWithData(500, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: ®ularObj,
|
|
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
res, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: ®ularObj,
|
|
Range: getPtr("bytes=0-99"),
|
|
ChecksumMode: types.ChecksumModeEnabled,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return fmt.Errorf("ranged GET on regular object: %w", err)
|
|
}
|
|
if err := checkNoChecksums(res); err != nil {
|
|
return fmt.Errorf("ranged GET on regular object: %w", err)
|
|
}
|
|
|
|
// Sub-test 2: multipart object with partNumber and ChecksumMode enabled
|
|
mpObj := "mp-obj"
|
|
mpOut, err := createMp(s3client, bucket, mpObj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
const partCount = int64(2)
|
|
parts, _, err := uploadParts(s3client, partCount*5*1024*1024, partCount, bucket, mpObj, *mpOut.UploadId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
compParts := make([]types.CompletedPart, len(parts))
|
|
for i, p := range parts {
|
|
compParts[i] = types.CompletedPart{
|
|
ETag: p.ETag,
|
|
PartNumber: p.PartNumber,
|
|
}
|
|
}
|
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
|
|
Bucket: &bucket,
|
|
Key: &mpObj,
|
|
UploadId: mpOut.UploadId,
|
|
MultipartUpload: &types.CompletedMultipartUpload{
|
|
Parts: compParts,
|
|
},
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pn := int32(1)
|
|
ctx, cancel = context.WithTimeout(context.Background(), longTimeout)
|
|
mpRes, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &mpObj,
|
|
PartNumber: &pn,
|
|
ChecksumMode: types.ChecksumModeEnabled,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return fmt.Errorf("partNumber GET on MP object: %w", err)
|
|
}
|
|
if err := checkNoChecksums(mpRes); err != nil {
|
|
return fmt.Errorf("partNumber GET on MP object: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|