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.
 
 
 
 
 
 

101 lines
3.9 KiB

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")
}