package s3api import ( "context" "errors" "io" "strings" "testing" "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/aws/smithy-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestConditionalWritesWithVersioning verifies that conditional writes (If-Match) // work correctly with versioned buckets, specifically ensuring they validate against // the LATEST version of the object, not the base object. // reproduces issue #8073 func TestConditionalWritesWithVersioning(t *testing.T) { client := getS3Client(t) bucketName := getNewBucketName() // Create bucket createBucket(t, client, bucketName) defer deleteBucket(t, client, bucketName) // Enable versioning enableVersioning(t, client, bucketName) checkVersioningStatus(t, client, bucketName, types.BucketVersioningStatusEnabled) key := "cond-write-test" // 1. Create Version 1 v1Resp := putObject(t, client, bucketName, key, "content-v1") require.NotNil(t, v1Resp.ETag) require.NotNil(t, v1Resp.VersionId) v1ETag := *v1Resp.ETag t.Logf("Created Version 1: ETag=%s, VersionId=%s", v1ETag, *v1Resp.VersionId) // 2. Create Version 2 (This is now the LATEST version) v2Resp := putObject(t, client, bucketName, key, "content-v2") require.NotNil(t, v2Resp.ETag) require.NotNil(t, v2Resp.VersionId) v2ETag := *v2Resp.ETag t.Logf("Created Version 2: ETag=%s, VersionId=%s", v2ETag, *v2Resp.VersionId) require.NotEqual(t, v1ETag, v2ETag, "ETags should be different for different content") // 3. Attempt conditional PUT using Version 1's ETag (If-Match: v1ETag) // EXPECTATION: Should FAIL with 412 Precondition Failed because the latest version is V2. // BUG (Issue #8073): Previously, this might have succeeded if it checked against an old/stale entry or base entry. _, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(key), Body: strings.NewReader("content-v3-should-fail"), IfMatch: aws.String(v1ETag), }) require.Error(t, err, "Conditional PUT with stale ETag should have failed") // Verify strict error checking for 412 Precondition Failed using AWS SDK v2 structured errors var apiErr smithy.APIError if assert.True(t, errors.As(err, &apiErr), "Expected a smithy.APIError, but got %T", err) { assert.Equal(t, "PreconditionFailed", apiErr.ErrorCode(), "Expected PreconditionFailed error code") t.Logf("Received expected 412 Precondition Failed error: %v", err) } // 4. Attempt conditional PUT using Version 2's ETag (If-Match: v2ETag) // EXPECTATION: Should SUCCEED because V2 is the latest version. v4Resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(key), Body: strings.NewReader("content-v4-should-succeed"), IfMatch: aws.String(v2ETag), }) require.NoError(t, err, "Conditional PUT with correct latest ETag should succeed") require.NotNil(t, v4Resp, "PutObject response should not be nil on success") require.NotNil(t, v4Resp.ETag, "ETag should not be nil on successful PutObject") require.NotNil(t, v4Resp.VersionId, "VersionId should not be nil on successful PutObject") t.Logf("Created Version 4: ETag=%s, VersionId=%s", *v4Resp.ETag, *v4Resp.VersionId) // 5. Verify the updates // The content should be "content-v4-should-succeed" headResp := headObject(t, client, bucketName, key) require.NotNil(t, headResp.VersionId, "VersionId should not be nil on HeadObject response") assert.Equal(t, *v4Resp.VersionId, *headResp.VersionId) // Verify actual content getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(key), }) require.NoError(t, err) defer getResp.Body.Close() body, err := io.ReadAll(getResp.Body) require.NoError(t, err) assert.Equal(t, "content-v4-should-succeed", string(body), "Content should match the successful conditional write") }