You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
275 lines
9.0 KiB
275 lines
9.0 KiB
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")
|
|
}
|