Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
574 lines
20 KiB
Go
574 lines
20 KiB
Go
package example
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
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/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"
|
|
"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)
|
|
})
|
|
t.Run("VersionedObject", func(t *testing.T) {
|
|
testGetObjectAttributesVersioned(t, cluster)
|
|
})
|
|
t.Run("ConditionalHeaders", func(t *testing.T) {
|
|
testGetObjectAttributesConditionalHeaders(t, cluster)
|
|
})
|
|
t.Run("VersionedConditionalHeaders", func(t *testing.T) {
|
|
testGetObjectAttributesVersionedConditionalHeaders(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)
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.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")
|
|
}
|
|
|
|
func testGetObjectAttributesVersioned(t *testing.T, cluster *TestCluster) {
|
|
client := newS3V2Client(cluster)
|
|
bucketName := createTestBucket(t, cluster, "test-goa-ver-")
|
|
|
|
// Enable versioning
|
|
_, err := client.PutBucketVersioning(context.Background(), &v2s3.PutBucketVersioningInput{
|
|
Bucket: v2aws.String(bucketName),
|
|
VersioningConfiguration: &types.VersioningConfiguration{
|
|
Status: types.BucketVersioningStatusEnabled,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Put two versions of the same object
|
|
v1Data := "version 1 content"
|
|
putResp1, err := client.PutObject(context.Background(), &v2s3.PutObjectInput{
|
|
Bucket: v2aws.String(bucketName),
|
|
Key: v2aws.String("versioned-key"),
|
|
Body: strings.NewReader(v1Data),
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, putResp1.VersionId)
|
|
versionId1 := *putResp1.VersionId
|
|
|
|
v2Data := "version 2 content - longer"
|
|
putResp2, err := client.PutObject(context.Background(), &v2s3.PutObjectInput{
|
|
Bucket: v2aws.String(bucketName),
|
|
Key: v2aws.String("versioned-key"),
|
|
Body: strings.NewReader(v2Data),
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, putResp2.VersionId)
|
|
versionId2 := *putResp2.VersionId
|
|
|
|
assert.NotEqual(t, versionId1, versionId2, "versions should differ")
|
|
|
|
// GetObjectAttributes for latest version (v2)
|
|
resp, err := client.GetObjectAttributes(context.Background(), &v2s3.GetObjectAttributesInput{
|
|
Bucket: v2aws.String(bucketName),
|
|
Key: v2aws.String("versioned-key"),
|
|
ObjectAttributes: []types.ObjectAttributes{
|
|
types.ObjectAttributesObjectSize,
|
|
types.ObjectAttributesEtag,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp.ObjectSize)
|
|
assert.Equal(t, int64(len(v2Data)), *resp.ObjectSize)
|
|
require.NotNil(t, resp.VersionId)
|
|
assert.Equal(t, versionId2, *resp.VersionId)
|
|
|
|
// GetObjectAttributes for specific older version (v1)
|
|
resp1, err := client.GetObjectAttributes(context.Background(), &v2s3.GetObjectAttributesInput{
|
|
Bucket: v2aws.String(bucketName),
|
|
Key: v2aws.String("versioned-key"),
|
|
VersionId: v2aws.String(versionId1),
|
|
ObjectAttributes: []types.ObjectAttributes{
|
|
types.ObjectAttributesObjectSize,
|
|
types.ObjectAttributesEtag,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp1.ObjectSize)
|
|
assert.Equal(t, int64(len(v1Data)), *resp1.ObjectSize)
|
|
require.NotNil(t, resp1.VersionId)
|
|
assert.Equal(t, versionId1, *resp1.VersionId)
|
|
|
|
t.Logf("Versioned GetObjectAttributes passed: v1 size=%d (id=%s), v2 size=%d (id=%s)",
|
|
*resp1.ObjectSize, versionId1, *resp.ObjectSize, versionId2)
|
|
}
|
|
|
|
// signedGetObjectAttributes creates a signed GET request for ?attributes with custom headers.
|
|
func signedGetObjectAttributes(t *testing.T, cluster *TestCluster, bucketName, objectKey string, extraHeaders map[string]string) *http.Response {
|
|
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", "ETag,ObjectSize")
|
|
for k, v := range extraHeaders {
|
|
req.Header.Set(k, v)
|
|
}
|
|
signer := v1signer.NewSigner(v1credentials.NewStaticCredentials(testAccessKey, testSecretKey, ""))
|
|
_, err = signer.Sign(req, nil, "s3", testRegion, time.Now())
|
|
require.NoError(t, err)
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
return resp
|
|
}
|
|
|
|
func testGetObjectAttributesConditionalHeaders(t *testing.T, cluster *TestCluster) {
|
|
bucketName := createTestBucket(t, cluster, "test-goa-cond-")
|
|
objectKey := "cond-test.txt"
|
|
|
|
_, err := cluster.s3Client.PutObject(&v1s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
Body: bytes.NewReader([]byte("conditional headers test")),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Get the ETag and Last-Modified for the object
|
|
headResp, err := cluster.s3Client.HeadObject(&v1s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
})
|
|
require.NoError(t, err)
|
|
etag := aws.StringValue(headResp.ETag)
|
|
lastModified := headResp.LastModified
|
|
require.NotNil(t, lastModified)
|
|
|
|
pastDate := lastModified.Add(-1 * time.Hour).UTC().Format(http.TimeFormat)
|
|
futureDate := lastModified.Add(1 * time.Hour).UTC().Format(http.TimeFormat)
|
|
|
|
// RFC 7232: If-Match true + If-Unmodified-Since false => 200 OK
|
|
// If-Unmodified-Since is ignored when If-Match is present
|
|
t.Run("IfMatch_true_IfUnmodifiedSince_false", func(t *testing.T) {
|
|
resp := signedGetObjectAttributes(t, cluster, bucketName, objectKey, map[string]string{
|
|
"If-Match": etag,
|
|
"If-Unmodified-Since": pastDate, // object was modified after this => false
|
|
})
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
assert.Equal(t, 200, resp.StatusCode,
|
|
"If-Match=true should return 200 even when If-Unmodified-Since=false (RFC 7232 Section 3.4)")
|
|
})
|
|
|
|
// RFC 7232: If-None-Match false + If-Modified-Since true => 304 Not Modified
|
|
// If-Modified-Since is ignored when If-None-Match is present
|
|
t.Run("IfNoneMatch_false_IfModifiedSince_true", func(t *testing.T) {
|
|
resp := signedGetObjectAttributes(t, cluster, bucketName, objectKey, map[string]string{
|
|
"If-None-Match": etag,
|
|
"If-Modified-Since": pastDate, // object was modified after this => true
|
|
})
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
assert.Equal(t, 304, resp.StatusCode,
|
|
"If-None-Match=false (ETag match) should return 304 even when If-Modified-Since=true (RFC 7232 Section 3.3)")
|
|
})
|
|
|
|
// If-Match succeeds, If-Unmodified-Since also succeeds => 200
|
|
t.Run("IfMatch_true_IfUnmodifiedSince_true", func(t *testing.T) {
|
|
resp := signedGetObjectAttributes(t, cluster, bucketName, objectKey, map[string]string{
|
|
"If-Match": etag,
|
|
"If-Unmodified-Since": futureDate,
|
|
})
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
})
|
|
|
|
// If-None-Match passes (ETag differs), If-Modified-Since ignored => 200
|
|
// Per RFC 7232, If-Modified-Since is ignored when If-None-Match is present
|
|
t.Run("IfNoneMatch_true_IfModifiedSince_ignored", func(t *testing.T) {
|
|
resp := signedGetObjectAttributes(t, cluster, bucketName, objectKey, map[string]string{
|
|
"If-None-Match": `"nonexistent-etag"`,
|
|
"If-Modified-Since": futureDate, // would fail alone, but is ignored
|
|
})
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
assert.Equal(t, 200, resp.StatusCode,
|
|
"If-None-Match=true means If-Modified-Since is ignored, should return 200 (RFC 7232 Section 3.3)")
|
|
})
|
|
|
|
// If-Match fails => 412 regardless of If-Unmodified-Since
|
|
t.Run("IfMatch_false", func(t *testing.T) {
|
|
resp := signedGetObjectAttributes(t, cluster, bucketName, objectKey, map[string]string{
|
|
"If-Match": `"wrong-etag"`,
|
|
"If-Unmodified-Since": futureDate,
|
|
})
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
assert.Equal(t, 412, resp.StatusCode)
|
|
})
|
|
|
|
t.Logf("Conditional headers tests passed")
|
|
}
|
|
|
|
// signedGetObjectAttributesVersioned creates a signed GET request for ?attributes&versionId=... with custom headers.
|
|
func signedGetObjectAttributesVersioned(t *testing.T, cluster *TestCluster, bucketName, objectKey, versionId string, extraHeaders map[string]string) *http.Response {
|
|
reqURL := fmt.Sprintf("%s/%s/%s?attributes&versionId=%s", cluster.s3Endpoint, bucketName, objectKey, versionId)
|
|
req, err := http.NewRequest("GET", reqURL, nil)
|
|
require.NoError(t, err)
|
|
req.Header.Set("X-Amz-Object-Attributes", "ETag,ObjectSize")
|
|
for k, v := range extraHeaders {
|
|
req.Header.Set(k, v)
|
|
}
|
|
signer := v1signer.NewSigner(v1credentials.NewStaticCredentials(testAccessKey, testSecretKey, ""))
|
|
_, err = signer.Sign(req, nil, "s3", testRegion, time.Now())
|
|
require.NoError(t, err)
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
return resp
|
|
}
|
|
|
|
func testGetObjectAttributesVersionedConditionalHeaders(t *testing.T, cluster *TestCluster) {
|
|
client := newS3V2Client(cluster)
|
|
bucketName := createTestBucket(t, cluster, "test-goa-vcond-")
|
|
|
|
// Enable versioning
|
|
_, err := client.PutBucketVersioning(context.Background(), &v2s3.PutBucketVersioningInput{
|
|
Bucket: v2aws.String(bucketName),
|
|
VersioningConfiguration: &types.VersioningConfiguration{
|
|
Status: types.BucketVersioningStatusEnabled,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Put two versions with different content (different ETags)
|
|
v1Data := "version 1 - original"
|
|
putResp1, err := client.PutObject(context.Background(), &v2s3.PutObjectInput{
|
|
Bucket: v2aws.String(bucketName),
|
|
Key: v2aws.String("vcond-key"),
|
|
Body: strings.NewReader(v1Data),
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, putResp1.VersionId)
|
|
vid1 := *putResp1.VersionId
|
|
|
|
v2Data := "version 2 - updated content"
|
|
putResp2, err := client.PutObject(context.Background(), &v2s3.PutObjectInput{
|
|
Bucket: v2aws.String(bucketName),
|
|
Key: v2aws.String("vcond-key"),
|
|
Body: strings.NewReader(v2Data),
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, putResp2.VersionId)
|
|
vid2 := *putResp2.VersionId
|
|
|
|
// Get ETags for each version
|
|
headV1, err := client.HeadObject(context.Background(), &v2s3.HeadObjectInput{
|
|
Bucket: v2aws.String(bucketName),
|
|
Key: v2aws.String("vcond-key"),
|
|
VersionId: v2aws.String(vid1),
|
|
})
|
|
require.NoError(t, err)
|
|
etagV1 := *headV1.ETag
|
|
|
|
headV2, err := client.HeadObject(context.Background(), &v2s3.HeadObjectInput{
|
|
Bucket: v2aws.String(bucketName),
|
|
Key: v2aws.String("vcond-key"),
|
|
VersionId: v2aws.String(vid2),
|
|
})
|
|
require.NoError(t, err)
|
|
etagV2 := *headV2.ETag
|
|
require.NotEqual(t, etagV1, etagV2, "versions should have different ETags")
|
|
|
|
// If-Match with v1's ETag + versionId=v1 => 200
|
|
// Before the fix, this would fail with 412 because conditional headers
|
|
// were evaluated against the latest version (v2) whose ETag differs
|
|
t.Run("IfMatch_v1_etag_versionId_v1", func(t *testing.T) {
|
|
resp := signedGetObjectAttributesVersioned(t, cluster, bucketName, "vcond-key", vid1, map[string]string{
|
|
"If-Match": etagV1,
|
|
})
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
assert.Equal(t, 200, resp.StatusCode,
|
|
"If-Match with v1 ETag targeting versionId=v1 should return 200")
|
|
})
|
|
|
|
// If-Match with v2's ETag + versionId=v1 => 412
|
|
// The ETag doesn't match v1, so this should fail
|
|
t.Run("IfMatch_v2_etag_versionId_v1", func(t *testing.T) {
|
|
resp := signedGetObjectAttributesVersioned(t, cluster, bucketName, "vcond-key", vid1, map[string]string{
|
|
"If-Match": etagV2,
|
|
})
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
assert.Equal(t, 412, resp.StatusCode,
|
|
"If-Match with v2 ETag targeting versionId=v1 should return 412")
|
|
})
|
|
|
|
// If-None-Match with v1's ETag + versionId=v1 => 304
|
|
t.Run("IfNoneMatch_v1_etag_versionId_v1", func(t *testing.T) {
|
|
resp := signedGetObjectAttributesVersioned(t, cluster, bucketName, "vcond-key", vid1, map[string]string{
|
|
"If-None-Match": etagV1,
|
|
})
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
assert.Equal(t, 304, resp.StatusCode,
|
|
"If-None-Match with v1 ETag targeting versionId=v1 should return 304")
|
|
})
|
|
|
|
t.Logf("Versioned conditional headers tests passed: vid1=%s, vid2=%s", vid1, vid2)
|
|
}
|