Browse Source
s3api: add integration tests for GetObjectAttributes
s3api: add integration tests for GetObjectAttributes
Test coverage: - Basic: simple object with all attribute types - MultipartObject: multipart upload with parts pagination - SelectiveAttributes: requesting only specific attributes - InvalidAttribute: server rejects invalid attribute names - NonExistentObject: returns NoSuchKey for missing objects Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>pull/8504/head
1 changed files with 275 additions and 0 deletions
@ -0,0 +1,275 @@ |
|||
package example |
|||
|
|||
import ( |
|||
"bytes" |
|||
"context" |
|||
"fmt" |
|||
"io" |
|||
"net/http" |
|||
"strings" |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/aws/aws-sdk-go/aws" |
|||
v1credentials "github.com/aws/aws-sdk-go/aws/credentials" |
|||
v1signer "github.com/aws/aws-sdk-go/aws/signer/v4" |
|||
v1s3 "github.com/aws/aws-sdk-go/service/s3" |
|||
v2aws "github.com/aws/aws-sdk-go-v2/aws" |
|||
"github.com/aws/aws-sdk-go-v2/credentials" |
|||
v2s3 "github.com/aws/aws-sdk-go-v2/service/s3" |
|||
"github.com/aws/aws-sdk-go-v2/service/s3/types" |
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/stretchr/testify/require" |
|||
) |
|||
|
|||
// newS3V2Client creates an AWS SDK v2 S3 client from the test cluster.
|
|||
func newS3V2Client(cluster *TestCluster) *v2s3.Client { |
|||
return v2s3.New(v2s3.Options{ |
|||
Region: testRegion, |
|||
BaseEndpoint: v2aws.String(cluster.s3Endpoint), |
|||
Credentials: v2aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(testAccessKey, testSecretKey, "")), |
|||
UsePathStyle: true, |
|||
}) |
|||
} |
|||
|
|||
func TestGetObjectAttributes(t *testing.T) { |
|||
if testing.Short() { |
|||
t.Skip("Skipping integration test in short mode") |
|||
} |
|||
|
|||
cluster, err := startMiniCluster(t) |
|||
require.NoError(t, err) |
|||
defer cluster.Stop() |
|||
|
|||
t.Run("Basic", func(t *testing.T) { |
|||
testGetObjectAttributesBasic(t, cluster) |
|||
}) |
|||
t.Run("MultipartObject", func(t *testing.T) { |
|||
testGetObjectAttributesMultipart(t, cluster) |
|||
}) |
|||
t.Run("SelectiveAttributes", func(t *testing.T) { |
|||
testGetObjectAttributesSelective(t, cluster) |
|||
}) |
|||
t.Run("InvalidAttribute", func(t *testing.T) { |
|||
testGetObjectAttributesInvalid(t, cluster) |
|||
}) |
|||
t.Run("NonExistentObject", func(t *testing.T) { |
|||
testGetObjectAttributesNotFound(t, cluster) |
|||
}) |
|||
} |
|||
|
|||
func testGetObjectAttributesBasic(t *testing.T, cluster *TestCluster) { |
|||
bucketName := createTestBucket(t, cluster, "test-goa-basic-") |
|||
objectKey := "test-object.txt" |
|||
objectData := "Hello, GetObjectAttributes!" |
|||
|
|||
_, err := cluster.s3Client.PutObject(&v1s3.PutObjectInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
Body: bytes.NewReader([]byte(objectData)), |
|||
}) |
|||
require.NoError(t, err) |
|||
|
|||
client := newS3V2Client(cluster) |
|||
resp, err := client.GetObjectAttributes(context.Background(), &v2s3.GetObjectAttributesInput{ |
|||
Bucket: v2aws.String(bucketName), |
|||
Key: v2aws.String(objectKey), |
|||
ObjectAttributes: []types.ObjectAttributes{ |
|||
types.ObjectAttributesEtag, |
|||
types.ObjectAttributesStorageClass, |
|||
types.ObjectAttributesObjectSize, |
|||
types.ObjectAttributesObjectParts, |
|||
}, |
|||
}) |
|||
require.NoError(t, err) |
|||
|
|||
// ETag should be present and non-empty
|
|||
require.NotNil(t, resp.ETag) |
|||
assert.NotEmpty(t, *resp.ETag) |
|||
assert.False(t, strings.Contains(*resp.ETag, `"`), "ETag in XML body should not have quotes") |
|||
|
|||
// ObjectSize should match
|
|||
require.NotNil(t, resp.ObjectSize) |
|||
assert.Equal(t, int64(len(objectData)), *resp.ObjectSize) |
|||
|
|||
// StorageClass should be STANDARD (default)
|
|||
assert.Equal(t, "STANDARD", string(resp.StorageClass)) |
|||
|
|||
// ObjectParts should be nil for non-multipart objects
|
|||
assert.Nil(t, resp.ObjectParts) |
|||
|
|||
// LastModified header should be present
|
|||
assert.NotNil(t, resp.LastModified) |
|||
|
|||
t.Logf("Basic GetObjectAttributes passed: ETag=%s, Size=%d, StorageClass=%s", |
|||
*resp.ETag, *resp.ObjectSize, resp.StorageClass) |
|||
} |
|||
|
|||
func testGetObjectAttributesMultipart(t *testing.T, cluster *TestCluster) { |
|||
bucketName := createTestBucket(t, cluster, "test-goa-mp-") |
|||
objectKey := "test-multipart.bin" |
|||
|
|||
// Create a 2-part multipart upload
|
|||
part1Data := bytes.Repeat([]byte("A"), 5*1024*1024) // 5MB (minimum part size)
|
|||
part2Data := bytes.Repeat([]byte("B"), 3*1024*1024) // 3MB
|
|||
|
|||
initResp, err := cluster.s3Client.CreateMultipartUpload(&v1s3.CreateMultipartUploadInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
}) |
|||
require.NoError(t, err) |
|||
uploadID := initResp.UploadId |
|||
|
|||
part1Resp, err := cluster.s3Client.UploadPart(&v1s3.UploadPartInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
PartNumber: aws.Int64(1), |
|||
UploadId: uploadID, |
|||
Body: bytes.NewReader(part1Data), |
|||
}) |
|||
require.NoError(t, err) |
|||
|
|||
part2Resp, err := cluster.s3Client.UploadPart(&v1s3.UploadPartInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
PartNumber: aws.Int64(2), |
|||
UploadId: uploadID, |
|||
Body: bytes.NewReader(part2Data), |
|||
}) |
|||
require.NoError(t, err) |
|||
|
|||
_, err = cluster.s3Client.CompleteMultipartUpload(&v1s3.CompleteMultipartUploadInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
UploadId: uploadID, |
|||
MultipartUpload: &v1s3.CompletedMultipartUpload{ |
|||
Parts: []*v1s3.CompletedPart{ |
|||
{ETag: part1Resp.ETag, PartNumber: aws.Int64(1)}, |
|||
{ETag: part2Resp.ETag, PartNumber: aws.Int64(2)}, |
|||
}, |
|||
}, |
|||
}) |
|||
require.NoError(t, err) |
|||
|
|||
// Wait briefly for metadata to settle
|
|||
time.Sleep(200 * time.Millisecond) |
|||
|
|||
client := newS3V2Client(cluster) |
|||
resp, err := client.GetObjectAttributes(context.Background(), &v2s3.GetObjectAttributesInput{ |
|||
Bucket: v2aws.String(bucketName), |
|||
Key: v2aws.String(objectKey), |
|||
ObjectAttributes: []types.ObjectAttributes{ |
|||
types.ObjectAttributesObjectParts, |
|||
types.ObjectAttributesObjectSize, |
|||
}, |
|||
}) |
|||
require.NoError(t, err) |
|||
|
|||
require.NotNil(t, resp.ObjectSize) |
|||
assert.Equal(t, int64(len(part1Data)+len(part2Data)), *resp.ObjectSize) |
|||
|
|||
require.NotNil(t, resp.ObjectParts, "ObjectParts should be present for multipart objects") |
|||
assert.Equal(t, int32(2), *resp.ObjectParts.TotalPartsCount) |
|||
require.Len(t, resp.ObjectParts.Parts, 2) |
|||
assert.Equal(t, int32(1), *resp.ObjectParts.Parts[0].PartNumber) |
|||
assert.Equal(t, int64(len(part1Data)), *resp.ObjectParts.Parts[0].Size) |
|||
assert.Equal(t, int32(2), *resp.ObjectParts.Parts[1].PartNumber) |
|||
assert.Equal(t, int64(len(part2Data)), *resp.ObjectParts.Parts[1].Size) |
|||
|
|||
// Test pagination: MaxParts=1
|
|||
resp2, err := client.GetObjectAttributes(context.Background(), &v2s3.GetObjectAttributesInput{ |
|||
Bucket: v2aws.String(bucketName), |
|||
Key: v2aws.String(objectKey), |
|||
MaxParts: v2aws.Int32(1), |
|||
ObjectAttributes: []types.ObjectAttributes{ |
|||
types.ObjectAttributesObjectParts, |
|||
}, |
|||
}) |
|||
require.NoError(t, err) |
|||
require.NotNil(t, resp2.ObjectParts) |
|||
assert.Len(t, resp2.ObjectParts.Parts, 1) |
|||
assert.True(t, *resp2.ObjectParts.IsTruncated) |
|||
assert.Equal(t, int32(2), *resp2.ObjectParts.TotalPartsCount) |
|||
|
|||
t.Logf("Multipart GetObjectAttributes passed: %d parts, total size %d", |
|||
*resp.ObjectParts.TotalPartsCount, *resp.ObjectSize) |
|||
} |
|||
|
|||
func testGetObjectAttributesSelective(t *testing.T, cluster *TestCluster) { |
|||
bucketName := createTestBucket(t, cluster, "test-goa-sel-") |
|||
objectKey := "test-selective.txt" |
|||
objectData := "Selective attributes test" |
|||
|
|||
_, err := cluster.s3Client.PutObject(&v1s3.PutObjectInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
Body: bytes.NewReader([]byte(objectData)), |
|||
}) |
|||
require.NoError(t, err) |
|||
|
|||
client := newS3V2Client(cluster) |
|||
|
|||
// Request only ETag
|
|||
resp, err := client.GetObjectAttributes(context.Background(), &v2s3.GetObjectAttributesInput{ |
|||
Bucket: v2aws.String(bucketName), |
|||
Key: v2aws.String(objectKey), |
|||
ObjectAttributes: []types.ObjectAttributes{ |
|||
types.ObjectAttributesEtag, |
|||
}, |
|||
}) |
|||
require.NoError(t, err) |
|||
require.NotNil(t, resp.ETag) |
|||
assert.NotEmpty(t, *resp.ETag) |
|||
assert.Nil(t, resp.ObjectSize, "ObjectSize should not be present when not requested") |
|||
assert.Empty(t, string(resp.StorageClass), "StorageClass should not be present when not requested") |
|||
assert.Nil(t, resp.ObjectParts, "ObjectParts should not be present when not requested") |
|||
|
|||
t.Logf("Selective GetObjectAttributes passed: ETag=%s", *resp.ETag) |
|||
} |
|||
|
|||
func testGetObjectAttributesInvalid(t *testing.T, cluster *TestCluster) { |
|||
bucketName := createTestBucket(t, cluster, "test-goa-inv-") |
|||
objectKey := "test-object.txt" |
|||
|
|||
_, err := cluster.s3Client.PutObject(&v1s3.PutObjectInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
Body: bytes.NewReader([]byte("test")), |
|||
}) |
|||
require.NoError(t, err) |
|||
|
|||
// Use raw HTTP to send an invalid attribute name since the SDK validates
|
|||
reqURL := fmt.Sprintf("%s/%s/%s?attributes", cluster.s3Endpoint, bucketName, objectKey) |
|||
req, err := http.NewRequest("GET", reqURL, nil) |
|||
require.NoError(t, err) |
|||
req.Header.Set("X-Amz-Object-Attributes", "InvalidAttr") |
|||
|
|||
signer := v1signer.NewSigner(v1credentials.NewStaticCredentials(testAccessKey, testSecretKey, "")) |
|||
_, err = signer.Sign(req, nil, "s3", testRegion, time.Now()) |
|||
require.NoError(t, err) |
|||
|
|||
resp, err := http.DefaultClient.Do(req) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
io.Copy(io.Discard, resp.Body) |
|||
|
|||
assert.Equal(t, 400, resp.StatusCode) |
|||
t.Logf("Invalid attribute test passed: got %d", resp.StatusCode) |
|||
} |
|||
|
|||
func testGetObjectAttributesNotFound(t *testing.T, cluster *TestCluster) { |
|||
bucketName := createTestBucket(t, cluster, "test-goa-nf-") |
|||
|
|||
client := newS3V2Client(cluster) |
|||
_, err := client.GetObjectAttributes(context.Background(), &v2s3.GetObjectAttributesInput{ |
|||
Bucket: v2aws.String(bucketName), |
|||
Key: v2aws.String("nonexistent-key"), |
|||
ObjectAttributes: []types.ObjectAttributes{ |
|||
types.ObjectAttributesEtag, |
|||
}, |
|||
}) |
|||
require.Error(t, err) |
|||
assert.Contains(t, err.Error(), "NoSuchKey") |
|||
|
|||
t.Logf("NotFound GetObjectAttributes passed") |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue