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.
		
		
		
		
		
			
		
			
				
					
					
						
							1014 lines
						
					
					
						
							30 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							1014 lines
						
					
					
						
							30 KiB
						
					
					
				
								package copying_test
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"bytes"
							 | 
						|
									"context"
							 | 
						|
									"crypto/rand"
							 | 
						|
									"fmt"
							 | 
						|
									"io"
							 | 
						|
									mathrand "math/rand"
							 | 
						|
									"net/url"
							 | 
						|
									"strings"
							 | 
						|
									"testing"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/aws/aws-sdk-go-v2/aws"
							 | 
						|
									"github.com/aws/aws-sdk-go-v2/config"
							 | 
						|
									"github.com/aws/aws-sdk-go-v2/credentials"
							 | 
						|
									"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"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// S3TestConfig holds configuration for S3 tests
							 | 
						|
								type S3TestConfig struct {
							 | 
						|
									Endpoint      string
							 | 
						|
									AccessKey     string
							 | 
						|
									SecretKey     string
							 | 
						|
									Region        string
							 | 
						|
									BucketPrefix  string
							 | 
						|
									UseSSL        bool
							 | 
						|
									SkipVerifySSL bool
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Default test configuration - should match test_config.json
							 | 
						|
								var defaultConfig = &S3TestConfig{
							 | 
						|
									Endpoint:      "http://127.0.0.1:8000", // Use explicit IPv4 address
							 | 
						|
									AccessKey:     "some_access_key1",
							 | 
						|
									SecretKey:     "some_secret_key1",
							 | 
						|
									Region:        "us-east-1",
							 | 
						|
									BucketPrefix:  "test-copying-",
							 | 
						|
									UseSSL:        false,
							 | 
						|
									SkipVerifySSL: true,
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Initialize math/rand with current time to ensure randomness
							 | 
						|
								func init() {
							 | 
						|
									mathrand.Seed(time.Now().UnixNano())
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// getS3Client creates an AWS S3 client for testing
							 | 
						|
								func getS3Client(t *testing.T) *s3.Client {
							 | 
						|
									cfg, err := config.LoadDefaultConfig(context.TODO(),
							 | 
						|
										config.WithRegion(defaultConfig.Region),
							 | 
						|
										config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
							 | 
						|
											defaultConfig.AccessKey,
							 | 
						|
											defaultConfig.SecretKey,
							 | 
						|
											"",
							 | 
						|
										)),
							 | 
						|
										config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(
							 | 
						|
											func(service, region string, options ...interface{}) (aws.Endpoint, error) {
							 | 
						|
												return aws.Endpoint{
							 | 
						|
													URL:               defaultConfig.Endpoint,
							 | 
						|
													SigningRegion:     defaultConfig.Region,
							 | 
						|
													HostnameImmutable: true,
							 | 
						|
												}, nil
							 | 
						|
											})),
							 | 
						|
									)
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									return s3.NewFromConfig(cfg, func(o *s3.Options) {
							 | 
						|
										o.UsePathStyle = true // Important for SeaweedFS
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// waitForS3Service waits for the S3 service to be ready
							 | 
						|
								func waitForS3Service(t *testing.T, client *s3.Client, timeout time.Duration) {
							 | 
						|
									start := time.Now()
							 | 
						|
									for time.Since(start) < timeout {
							 | 
						|
										_, err := client.ListBuckets(context.TODO(), &s3.ListBucketsInput{})
							 | 
						|
										if err == nil {
							 | 
						|
											return
							 | 
						|
										}
							 | 
						|
										t.Logf("Waiting for S3 service to be ready... (error: %v)", err)
							 | 
						|
										time.Sleep(time.Second)
							 | 
						|
									}
							 | 
						|
									t.Fatalf("S3 service not ready after %v", timeout)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// getNewBucketName generates a unique bucket name
							 | 
						|
								func getNewBucketName() string {
							 | 
						|
									timestamp := time.Now().UnixNano()
							 | 
						|
									// Add random suffix to prevent collisions when tests run quickly
							 | 
						|
									randomSuffix := mathrand.Intn(100000)
							 | 
						|
									return fmt.Sprintf("%s%d-%d", defaultConfig.BucketPrefix, timestamp, randomSuffix)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// cleanupTestBuckets removes any leftover test buckets from previous runs
							 | 
						|
								func cleanupTestBuckets(t *testing.T, client *s3.Client) {
							 | 
						|
									resp, err := client.ListBuckets(context.TODO(), &s3.ListBucketsInput{})
							 | 
						|
									if err != nil {
							 | 
						|
										t.Logf("Warning: failed to list buckets for cleanup: %v", err)
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, bucket := range resp.Buckets {
							 | 
						|
										bucketName := *bucket.Name
							 | 
						|
										// Only delete buckets that match our test prefix
							 | 
						|
										if strings.HasPrefix(bucketName, defaultConfig.BucketPrefix) {
							 | 
						|
											t.Logf("Cleaning up leftover test bucket: %s", bucketName)
							 | 
						|
											deleteBucket(t, client, bucketName)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// createBucket creates a new bucket for testing
							 | 
						|
								func createBucket(t *testing.T, client *s3.Client, bucketName string) {
							 | 
						|
									// First, try to delete the bucket if it exists (cleanup from previous failed tests)
							 | 
						|
									deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Create the bucket
							 | 
						|
									_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
							 | 
						|
										Bucket: aws.String(bucketName),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// deleteBucket deletes a bucket and all its contents
							 | 
						|
								func deleteBucket(t *testing.T, client *s3.Client, bucketName string) {
							 | 
						|
									// First, delete all objects
							 | 
						|
									deleteAllObjects(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Then delete the bucket
							 | 
						|
									_, err := client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{
							 | 
						|
										Bucket: aws.String(bucketName),
							 | 
						|
									})
							 | 
						|
									if err != nil {
							 | 
						|
										// Only log warnings for actual errors, not "bucket doesn't exist"
							 | 
						|
										if !strings.Contains(err.Error(), "NoSuchBucket") {
							 | 
						|
											t.Logf("Warning: failed to delete bucket %s: %v", bucketName, err)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// deleteAllObjects deletes all objects in a bucket
							 | 
						|
								func deleteAllObjects(t *testing.T, client *s3.Client, bucketName string) {
							 | 
						|
									// List all objects
							 | 
						|
									paginator := s3.NewListObjectsV2Paginator(client, &s3.ListObjectsV2Input{
							 | 
						|
										Bucket: aws.String(bucketName),
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									for paginator.HasMorePages() {
							 | 
						|
										page, err := paginator.NextPage(context.TODO())
							 | 
						|
										if err != nil {
							 | 
						|
											// Only log warnings for actual errors, not "bucket doesn't exist"
							 | 
						|
											if !strings.Contains(err.Error(), "NoSuchBucket") {
							 | 
						|
												t.Logf("Warning: failed to list objects in bucket %s: %v", bucketName, err)
							 | 
						|
											}
							 | 
						|
											return
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if len(page.Contents) == 0 {
							 | 
						|
											break
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										var objectsToDelete []types.ObjectIdentifier
							 | 
						|
										for _, obj := range page.Contents {
							 | 
						|
											objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{
							 | 
						|
												Key: obj.Key,
							 | 
						|
											})
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Delete objects in batches
							 | 
						|
										if len(objectsToDelete) > 0 {
							 | 
						|
											_, err := client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
							 | 
						|
												Bucket: aws.String(bucketName),
							 | 
						|
												Delete: &types.Delete{
							 | 
						|
													Objects: objectsToDelete,
							 | 
						|
													Quiet:   aws.Bool(true),
							 | 
						|
												},
							 | 
						|
											})
							 | 
						|
											if err != nil {
							 | 
						|
												t.Logf("Warning: failed to delete objects in bucket %s: %v", bucketName, err)
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// putObject puts an object into a bucket
							 | 
						|
								func putObject(t *testing.T, client *s3.Client, bucketName, key, content string) *s3.PutObjectOutput {
							 | 
						|
									resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
							 | 
						|
										Bucket: aws.String(bucketName),
							 | 
						|
										Key:    aws.String(key),
							 | 
						|
										Body:   strings.NewReader(content),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
									return resp
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// putObjectWithMetadata puts an object with metadata into a bucket
							 | 
						|
								func putObjectWithMetadata(t *testing.T, client *s3.Client, bucketName, key, content string, metadata map[string]string, contentType string) *s3.PutObjectOutput {
							 | 
						|
									input := &s3.PutObjectInput{
							 | 
						|
										Bucket: aws.String(bucketName),
							 | 
						|
										Key:    aws.String(key),
							 | 
						|
										Body:   strings.NewReader(content),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if metadata != nil {
							 | 
						|
										input.Metadata = metadata
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if contentType != "" {
							 | 
						|
										input.ContentType = aws.String(contentType)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									resp, err := client.PutObject(context.TODO(), input)
							 | 
						|
									require.NoError(t, err)
							 | 
						|
									return resp
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// getObject gets an object from a bucket
							 | 
						|
								func getObject(t *testing.T, client *s3.Client, bucketName, key string) *s3.GetObjectOutput {
							 | 
						|
									resp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
							 | 
						|
										Bucket: aws.String(bucketName),
							 | 
						|
										Key:    aws.String(key),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
									return resp
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// getObjectBody gets the body content of an object
							 | 
						|
								func getObjectBody(t *testing.T, resp *s3.GetObjectOutput) string {
							 | 
						|
									body, err := io.ReadAll(resp.Body)
							 | 
						|
									require.NoError(t, err)
							 | 
						|
									resp.Body.Close()
							 | 
						|
									return string(body)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// generateRandomData generates random data of specified size
							 | 
						|
								func generateRandomData(size int) []byte {
							 | 
						|
									data := make([]byte, size)
							 | 
						|
									_, err := rand.Read(data)
							 | 
						|
									if err != nil {
							 | 
						|
										panic(err)
							 | 
						|
									}
							 | 
						|
									return data
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// createCopySource creates a properly URL-encoded copy source string
							 | 
						|
								func createCopySource(bucketName, key string) string {
							 | 
						|
									// URL encode the key to handle special characters like spaces
							 | 
						|
									encodedKey := url.PathEscape(key)
							 | 
						|
									return fmt.Sprintf("%s/%s", bucketName, encodedKey)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestBasicPutGet tests basic S3 put and get operations
							 | 
						|
								func TestBasicPutGet(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
									bucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create bucket
							 | 
						|
									createBucket(t, client, bucketName)
							 | 
						|
									defer deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Test 1: Put and get a simple text object
							 | 
						|
									t.Run("Simple text object", func(t *testing.T) {
							 | 
						|
										key := "test-simple.txt"
							 | 
						|
										content := "Hello, SeaweedFS S3!"
							 | 
						|
								
							 | 
						|
										// Put object
							 | 
						|
										putResp := putObject(t, client, bucketName, key, content)
							 | 
						|
										assert.NotNil(t, putResp.ETag)
							 | 
						|
								
							 | 
						|
										// Get object
							 | 
						|
										getResp := getObject(t, client, bucketName, key)
							 | 
						|
										body := getObjectBody(t, getResp)
							 | 
						|
										assert.Equal(t, content, body)
							 | 
						|
										assert.Equal(t, putResp.ETag, getResp.ETag)
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Test 2: Put and get an empty object
							 | 
						|
									t.Run("Empty object", func(t *testing.T) {
							 | 
						|
										key := "test-empty.txt"
							 | 
						|
										content := ""
							 | 
						|
								
							 | 
						|
										putResp := putObject(t, client, bucketName, key, content)
							 | 
						|
										assert.NotNil(t, putResp.ETag)
							 | 
						|
								
							 | 
						|
										getResp := getObject(t, client, bucketName, key)
							 | 
						|
										body := getObjectBody(t, getResp)
							 | 
						|
										assert.Equal(t, content, body)
							 | 
						|
										assert.Equal(t, putResp.ETag, getResp.ETag)
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Test 3: Put and get a binary object
							 | 
						|
									t.Run("Binary object", func(t *testing.T) {
							 | 
						|
										key := "test-binary.bin"
							 | 
						|
										content := string(generateRandomData(1024)) // 1KB of random data
							 | 
						|
								
							 | 
						|
										putResp := putObject(t, client, bucketName, key, content)
							 | 
						|
										assert.NotNil(t, putResp.ETag)
							 | 
						|
								
							 | 
						|
										getResp := getObject(t, client, bucketName, key)
							 | 
						|
										body := getObjectBody(t, getResp)
							 | 
						|
										assert.Equal(t, content, body)
							 | 
						|
										assert.Equal(t, putResp.ETag, getResp.ETag)
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Test 4: Put and get object with metadata
							 | 
						|
									t.Run("Object with metadata", func(t *testing.T) {
							 | 
						|
										key := "test-metadata.txt"
							 | 
						|
										content := "Content with metadata"
							 | 
						|
										metadata := map[string]string{
							 | 
						|
											"author":      "test",
							 | 
						|
											"description": "test object with metadata",
							 | 
						|
										}
							 | 
						|
										contentType := "text/plain"
							 | 
						|
								
							 | 
						|
										putResp := putObjectWithMetadata(t, client, bucketName, key, content, metadata, contentType)
							 | 
						|
										assert.NotNil(t, putResp.ETag)
							 | 
						|
								
							 | 
						|
										getResp := getObject(t, client, bucketName, key)
							 | 
						|
										body := getObjectBody(t, getResp)
							 | 
						|
										assert.Equal(t, content, body)
							 | 
						|
										assert.Equal(t, putResp.ETag, getResp.ETag)
							 | 
						|
										assert.Equal(t, contentType, *getResp.ContentType)
							 | 
						|
										assert.Equal(t, metadata["author"], getResp.Metadata["author"])
							 | 
						|
										assert.Equal(t, metadata["description"], getResp.Metadata["description"])
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestBasicBucketOperations tests basic bucket operations
							 | 
						|
								func TestBasicBucketOperations(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
									bucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Test 1: Create bucket
							 | 
						|
									t.Run("Create bucket", func(t *testing.T) {
							 | 
						|
										createBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
										// Verify bucket exists by listing buckets
							 | 
						|
										resp, err := client.ListBuckets(context.TODO(), &s3.ListBucketsInput{})
							 | 
						|
										require.NoError(t, err)
							 | 
						|
								
							 | 
						|
										found := false
							 | 
						|
										for _, bucket := range resp.Buckets {
							 | 
						|
											if *bucket.Name == bucketName {
							 | 
						|
												found = true
							 | 
						|
												break
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
										assert.True(t, found, "Bucket should exist after creation")
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Test 2: Put objects and list them
							 | 
						|
									t.Run("List objects", func(t *testing.T) {
							 | 
						|
										// Put multiple objects
							 | 
						|
										objects := []string{"test1.txt", "test2.txt", "dir/test3.txt"}
							 | 
						|
										for _, key := range objects {
							 | 
						|
											putObject(t, client, bucketName, key, fmt.Sprintf("content of %s", key))
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// List objects
							 | 
						|
										resp, err := client.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
							 | 
						|
											Bucket: aws.String(bucketName),
							 | 
						|
										})
							 | 
						|
										require.NoError(t, err)
							 | 
						|
								
							 | 
						|
										assert.Equal(t, len(objects), len(resp.Contents))
							 | 
						|
								
							 | 
						|
										// Verify each object exists
							 | 
						|
										for _, obj := range resp.Contents {
							 | 
						|
											found := false
							 | 
						|
											for _, expected := range objects {
							 | 
						|
												if *obj.Key == expected {
							 | 
						|
													found = true
							 | 
						|
													break
							 | 
						|
												}
							 | 
						|
											}
							 | 
						|
											assert.True(t, found, "Object %s should be in list", *obj.Key)
							 | 
						|
										}
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Test 3: Delete bucket (cleanup)
							 | 
						|
									t.Run("Delete bucket", func(t *testing.T) {
							 | 
						|
										deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
										// Verify bucket is deleted by trying to list its contents
							 | 
						|
										_, err := client.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
							 | 
						|
											Bucket: aws.String(bucketName),
							 | 
						|
										})
							 | 
						|
										assert.Error(t, err, "Bucket should not exist after deletion")
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestBasicLargeObject tests handling of larger objects (up to volume limit)
							 | 
						|
								func TestBasicLargeObject(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
									bucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									createBucket(t, client, bucketName)
							 | 
						|
									defer deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Test with progressively larger objects
							 | 
						|
									sizes := []int{
							 | 
						|
										1024,             // 1KB
							 | 
						|
										1024 * 10,        // 10KB
							 | 
						|
										1024 * 100,       // 100KB
							 | 
						|
										1024 * 1024,      // 1MB
							 | 
						|
										1024 * 1024 * 5,  // 5MB
							 | 
						|
										1024 * 1024 * 10, // 10MB
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, size := range sizes {
							 | 
						|
										t.Run(fmt.Sprintf("Size_%dMB", size/(1024*1024)), func(t *testing.T) {
							 | 
						|
											key := fmt.Sprintf("large-object-%d.bin", size)
							 | 
						|
											content := string(generateRandomData(size))
							 | 
						|
								
							 | 
						|
											putResp := putObject(t, client, bucketName, key, content)
							 | 
						|
											assert.NotNil(t, putResp.ETag)
							 | 
						|
								
							 | 
						|
											getResp := getObject(t, client, bucketName, key)
							 | 
						|
											body := getObjectBody(t, getResp)
							 | 
						|
											assert.Equal(t, len(content), len(body))
							 | 
						|
											assert.Equal(t, content, body)
							 | 
						|
											assert.Equal(t, putResp.ETag, getResp.ETag)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestObjectCopySameBucket tests copying an object within the same bucket
							 | 
						|
								func TestObjectCopySameBucket(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
								
							 | 
						|
									// Wait for S3 service to be ready
							 | 
						|
									waitForS3Service(t, client, 30*time.Second)
							 | 
						|
								
							 | 
						|
									bucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create bucket
							 | 
						|
									createBucket(t, client, bucketName)
							 | 
						|
									defer deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Put source object
							 | 
						|
									sourceKey := "foo123bar"
							 | 
						|
									sourceContent := "foo"
							 | 
						|
									putObject(t, client, bucketName, sourceKey, sourceContent)
							 | 
						|
								
							 | 
						|
									// Copy object within the same bucket
							 | 
						|
									destKey := "bar321foo"
							 | 
						|
									copySource := createCopySource(bucketName, sourceKey)
							 | 
						|
									_, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{
							 | 
						|
										Bucket:     aws.String(bucketName),
							 | 
						|
										Key:        aws.String(destKey),
							 | 
						|
										CopySource: aws.String(copySource),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err, "Failed to copy object within same bucket")
							 | 
						|
								
							 | 
						|
									// Verify the copied object
							 | 
						|
									resp := getObject(t, client, bucketName, destKey)
							 | 
						|
									body := getObjectBody(t, resp)
							 | 
						|
									assert.Equal(t, sourceContent, body)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestObjectCopyDiffBucket tests copying an object to a different bucket
							 | 
						|
								func TestObjectCopyDiffBucket(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
									sourceBucketName := getNewBucketName()
							 | 
						|
									destBucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create buckets
							 | 
						|
									createBucket(t, client, sourceBucketName)
							 | 
						|
									defer deleteBucket(t, client, sourceBucketName)
							 | 
						|
									createBucket(t, client, destBucketName)
							 | 
						|
									defer deleteBucket(t, client, destBucketName)
							 | 
						|
								
							 | 
						|
									// Put source object
							 | 
						|
									sourceKey := "foo123bar"
							 | 
						|
									sourceContent := "foo"
							 | 
						|
									putObject(t, client, sourceBucketName, sourceKey, sourceContent)
							 | 
						|
								
							 | 
						|
									// Copy object to different bucket
							 | 
						|
									destKey := "bar321foo"
							 | 
						|
									copySource := createCopySource(sourceBucketName, sourceKey)
							 | 
						|
									_, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{
							 | 
						|
										Bucket:     aws.String(destBucketName),
							 | 
						|
										Key:        aws.String(destKey),
							 | 
						|
										CopySource: aws.String(copySource),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Verify the copied object
							 | 
						|
									resp := getObject(t, client, destBucketName, destKey)
							 | 
						|
									body := getObjectBody(t, resp)
							 | 
						|
									assert.Equal(t, sourceContent, body)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestObjectCopyCannedAcl tests copying with ACL settings
							 | 
						|
								func TestObjectCopyCannedAcl(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
									bucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create bucket
							 | 
						|
									createBucket(t, client, bucketName)
							 | 
						|
									defer deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Put source object
							 | 
						|
									sourceKey := "foo123bar"
							 | 
						|
									sourceContent := "foo"
							 | 
						|
									putObject(t, client, bucketName, sourceKey, sourceContent)
							 | 
						|
								
							 | 
						|
									// Copy object with public-read ACL
							 | 
						|
									destKey := "bar321foo"
							 | 
						|
									copySource := createCopySource(bucketName, sourceKey)
							 | 
						|
									_, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{
							 | 
						|
										Bucket:     aws.String(bucketName),
							 | 
						|
										Key:        aws.String(destKey),
							 | 
						|
										CopySource: aws.String(copySource),
							 | 
						|
										ACL:        types.ObjectCannedACLPublicRead,
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Verify the copied object
							 | 
						|
									resp := getObject(t, client, bucketName, destKey)
							 | 
						|
									body := getObjectBody(t, resp)
							 | 
						|
									assert.Equal(t, sourceContent, body)
							 | 
						|
								
							 | 
						|
									// Test metadata replacement with ACL
							 | 
						|
									metadata := map[string]string{"abc": "def"}
							 | 
						|
									destKey2 := "foo123bar2"
							 | 
						|
									copySource2 := createCopySource(bucketName, destKey)
							 | 
						|
									_, err = client.CopyObject(context.TODO(), &s3.CopyObjectInput{
							 | 
						|
										Bucket:            aws.String(bucketName),
							 | 
						|
										Key:               aws.String(destKey2),
							 | 
						|
										CopySource:        aws.String(copySource2),
							 | 
						|
										ACL:               types.ObjectCannedACLPublicRead,
							 | 
						|
										Metadata:          metadata,
							 | 
						|
										MetadataDirective: types.MetadataDirectiveReplace,
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Verify the copied object with metadata
							 | 
						|
									resp2 := getObject(t, client, bucketName, destKey2)
							 | 
						|
									body2 := getObjectBody(t, resp2)
							 | 
						|
									assert.Equal(t, sourceContent, body2)
							 | 
						|
									assert.Equal(t, metadata, resp2.Metadata)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestObjectCopyRetainingMetadata tests copying while retaining metadata
							 | 
						|
								func TestObjectCopyRetainingMetadata(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
									bucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create bucket
							 | 
						|
									createBucket(t, client, bucketName)
							 | 
						|
									defer deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Test with different sizes
							 | 
						|
									sizes := []int{3, 1024 * 1024} // 3 bytes and 1MB
							 | 
						|
									for _, size := range sizes {
							 | 
						|
										t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
							 | 
						|
											sourceKey := fmt.Sprintf("foo123bar_%d", size)
							 | 
						|
											sourceContent := string(generateRandomData(size))
							 | 
						|
											contentType := "audio/ogg"
							 | 
						|
											metadata := map[string]string{"key1": "value1", "key2": "value2"}
							 | 
						|
								
							 | 
						|
											// Put source object with metadata
							 | 
						|
											putObjectWithMetadata(t, client, bucketName, sourceKey, sourceContent, metadata, contentType)
							 | 
						|
								
							 | 
						|
											// Copy object (should retain metadata)
							 | 
						|
											destKey := fmt.Sprintf("bar321foo_%d", size)
							 | 
						|
											copySource := createCopySource(bucketName, sourceKey)
							 | 
						|
											_, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{
							 | 
						|
												Bucket:     aws.String(bucketName),
							 | 
						|
												Key:        aws.String(destKey),
							 | 
						|
												CopySource: aws.String(copySource),
							 | 
						|
											})
							 | 
						|
											require.NoError(t, err)
							 | 
						|
								
							 | 
						|
											// Verify the copied object
							 | 
						|
											resp := getObject(t, client, bucketName, destKey)
							 | 
						|
											body := getObjectBody(t, resp)
							 | 
						|
											assert.Equal(t, sourceContent, body)
							 | 
						|
											assert.Equal(t, contentType, *resp.ContentType)
							 | 
						|
											assert.Equal(t, metadata, resp.Metadata)
							 | 
						|
											require.NotNil(t, resp.ContentLength)
							 | 
						|
											assert.Equal(t, int64(size), *resp.ContentLength)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestMultipartCopySmall tests multipart copying of small files
							 | 
						|
								func TestMultipartCopySmall(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
								
							 | 
						|
									// Clean up any leftover buckets from previous test runs
							 | 
						|
									cleanupTestBuckets(t, client)
							 | 
						|
								
							 | 
						|
									sourceBucketName := getNewBucketName()
							 | 
						|
									destBucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create buckets
							 | 
						|
									createBucket(t, client, sourceBucketName)
							 | 
						|
									defer deleteBucket(t, client, sourceBucketName)
							 | 
						|
									createBucket(t, client, destBucketName)
							 | 
						|
									defer deleteBucket(t, client, destBucketName)
							 | 
						|
								
							 | 
						|
									// Put source object
							 | 
						|
									sourceKey := "foo"
							 | 
						|
									sourceContent := "x" // 1 byte
							 | 
						|
									putObject(t, client, sourceBucketName, sourceKey, sourceContent)
							 | 
						|
								
							 | 
						|
									// Create multipart upload
							 | 
						|
									destKey := "mymultipart"
							 | 
						|
									createResp, err := client.CreateMultipartUpload(context.TODO(), &s3.CreateMultipartUploadInput{
							 | 
						|
										Bucket: aws.String(destBucketName),
							 | 
						|
										Key:    aws.String(destKey),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
									uploadID := *createResp.UploadId
							 | 
						|
								
							 | 
						|
									// Upload part copy
							 | 
						|
									copySource := createCopySource(sourceBucketName, sourceKey)
							 | 
						|
									copyResp, err := client.UploadPartCopy(context.TODO(), &s3.UploadPartCopyInput{
							 | 
						|
										Bucket:          aws.String(destBucketName),
							 | 
						|
										Key:             aws.String(destKey),
							 | 
						|
										UploadId:        aws.String(uploadID),
							 | 
						|
										PartNumber:      aws.Int32(1),
							 | 
						|
										CopySource:      aws.String(copySource),
							 | 
						|
										CopySourceRange: aws.String("bytes=0-0"),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Complete multipart upload
							 | 
						|
									_, err = client.CompleteMultipartUpload(context.TODO(), &s3.CompleteMultipartUploadInput{
							 | 
						|
										Bucket:   aws.String(destBucketName),
							 | 
						|
										Key:      aws.String(destKey),
							 | 
						|
										UploadId: aws.String(uploadID),
							 | 
						|
										MultipartUpload: &types.CompletedMultipartUpload{
							 | 
						|
											Parts: []types.CompletedPart{
							 | 
						|
												{
							 | 
						|
													ETag:       copyResp.CopyPartResult.ETag,
							 | 
						|
													PartNumber: aws.Int32(1),
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										},
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Verify the copied object
							 | 
						|
									resp := getObject(t, client, destBucketName, destKey)
							 | 
						|
									body := getObjectBody(t, resp)
							 | 
						|
									assert.Equal(t, sourceContent, body)
							 | 
						|
									require.NotNil(t, resp.ContentLength)
							 | 
						|
									assert.Equal(t, int64(1), *resp.ContentLength)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestMultipartCopyWithoutRange tests multipart copying without range specification
							 | 
						|
								func TestMultipartCopyWithoutRange(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
								
							 | 
						|
									// Clean up any leftover buckets from previous test runs
							 | 
						|
									cleanupTestBuckets(t, client)
							 | 
						|
								
							 | 
						|
									sourceBucketName := getNewBucketName()
							 | 
						|
									destBucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create buckets
							 | 
						|
									createBucket(t, client, sourceBucketName)
							 | 
						|
									defer deleteBucket(t, client, sourceBucketName)
							 | 
						|
									createBucket(t, client, destBucketName)
							 | 
						|
									defer deleteBucket(t, client, destBucketName)
							 | 
						|
								
							 | 
						|
									// Put source object
							 | 
						|
									sourceKey := "source"
							 | 
						|
									sourceContent := string(generateRandomData(10))
							 | 
						|
									putObject(t, client, sourceBucketName, sourceKey, sourceContent)
							 | 
						|
								
							 | 
						|
									// Create multipart upload
							 | 
						|
									destKey := "mymultipartcopy"
							 | 
						|
									createResp, err := client.CreateMultipartUpload(context.TODO(), &s3.CreateMultipartUploadInput{
							 | 
						|
										Bucket: aws.String(destBucketName),
							 | 
						|
										Key:    aws.String(destKey),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
									uploadID := *createResp.UploadId
							 | 
						|
								
							 | 
						|
									// Upload part copy without range (should copy entire object)
							 | 
						|
									copySource := createCopySource(sourceBucketName, sourceKey)
							 | 
						|
									copyResp, err := client.UploadPartCopy(context.TODO(), &s3.UploadPartCopyInput{
							 | 
						|
										Bucket:     aws.String(destBucketName),
							 | 
						|
										Key:        aws.String(destKey),
							 | 
						|
										UploadId:   aws.String(uploadID),
							 | 
						|
										PartNumber: aws.Int32(1),
							 | 
						|
										CopySource: aws.String(copySource),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Complete multipart upload
							 | 
						|
									_, err = client.CompleteMultipartUpload(context.TODO(), &s3.CompleteMultipartUploadInput{
							 | 
						|
										Bucket:   aws.String(destBucketName),
							 | 
						|
										Key:      aws.String(destKey),
							 | 
						|
										UploadId: aws.String(uploadID),
							 | 
						|
										MultipartUpload: &types.CompletedMultipartUpload{
							 | 
						|
											Parts: []types.CompletedPart{
							 | 
						|
												{
							 | 
						|
													ETag:       copyResp.CopyPartResult.ETag,
							 | 
						|
													PartNumber: aws.Int32(1),
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										},
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Verify the copied object
							 | 
						|
									resp := getObject(t, client, destBucketName, destKey)
							 | 
						|
									body := getObjectBody(t, resp)
							 | 
						|
									assert.Equal(t, sourceContent, body)
							 | 
						|
									require.NotNil(t, resp.ContentLength)
							 | 
						|
									assert.Equal(t, int64(10), *resp.ContentLength)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestMultipartCopySpecialNames tests multipart copying with special character names
							 | 
						|
								func TestMultipartCopySpecialNames(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
								
							 | 
						|
									// Clean up any leftover buckets from previous test runs
							 | 
						|
									cleanupTestBuckets(t, client)
							 | 
						|
								
							 | 
						|
									sourceBucketName := getNewBucketName()
							 | 
						|
									destBucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create buckets
							 | 
						|
									createBucket(t, client, sourceBucketName)
							 | 
						|
									defer deleteBucket(t, client, sourceBucketName)
							 | 
						|
									createBucket(t, client, destBucketName)
							 | 
						|
									defer deleteBucket(t, client, destBucketName)
							 | 
						|
								
							 | 
						|
									// Test with special key names
							 | 
						|
									specialKeys := []string{" ", "_", "__", "?versionId"}
							 | 
						|
									sourceContent := "x" // 1 byte
							 | 
						|
									destKey := "mymultipart"
							 | 
						|
								
							 | 
						|
									for i, sourceKey := range specialKeys {
							 | 
						|
										t.Run(fmt.Sprintf("special_key_%d", i), func(t *testing.T) {
							 | 
						|
											// Put source object
							 | 
						|
											putObject(t, client, sourceBucketName, sourceKey, sourceContent)
							 | 
						|
								
							 | 
						|
											// Create multipart upload
							 | 
						|
											createResp, err := client.CreateMultipartUpload(context.TODO(), &s3.CreateMultipartUploadInput{
							 | 
						|
												Bucket: aws.String(destBucketName),
							 | 
						|
												Key:    aws.String(destKey),
							 | 
						|
											})
							 | 
						|
											require.NoError(t, err)
							 | 
						|
											uploadID := *createResp.UploadId
							 | 
						|
								
							 | 
						|
											// Upload part copy
							 | 
						|
											copySource := createCopySource(sourceBucketName, sourceKey)
							 | 
						|
											copyResp, err := client.UploadPartCopy(context.TODO(), &s3.UploadPartCopyInput{
							 | 
						|
												Bucket:          aws.String(destBucketName),
							 | 
						|
												Key:             aws.String(destKey),
							 | 
						|
												UploadId:        aws.String(uploadID),
							 | 
						|
												PartNumber:      aws.Int32(1),
							 | 
						|
												CopySource:      aws.String(copySource),
							 | 
						|
												CopySourceRange: aws.String("bytes=0-0"),
							 | 
						|
											})
							 | 
						|
											require.NoError(t, err)
							 | 
						|
								
							 | 
						|
											// Complete multipart upload
							 | 
						|
											_, err = client.CompleteMultipartUpload(context.TODO(), &s3.CompleteMultipartUploadInput{
							 | 
						|
												Bucket:   aws.String(destBucketName),
							 | 
						|
												Key:      aws.String(destKey),
							 | 
						|
												UploadId: aws.String(uploadID),
							 | 
						|
												MultipartUpload: &types.CompletedMultipartUpload{
							 | 
						|
													Parts: []types.CompletedPart{
							 | 
						|
														{
							 | 
						|
															ETag:       copyResp.CopyPartResult.ETag,
							 | 
						|
															PartNumber: aws.Int32(1),
							 | 
						|
														},
							 | 
						|
													},
							 | 
						|
												},
							 | 
						|
											})
							 | 
						|
											require.NoError(t, err)
							 | 
						|
								
							 | 
						|
											// Verify the copied object
							 | 
						|
											resp := getObject(t, client, destBucketName, destKey)
							 | 
						|
											body := getObjectBody(t, resp)
							 | 
						|
											assert.Equal(t, sourceContent, body)
							 | 
						|
											require.NotNil(t, resp.ContentLength)
							 | 
						|
											assert.Equal(t, int64(1), *resp.ContentLength)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestMultipartCopyMultipleSizes tests multipart copying with various file sizes
							 | 
						|
								func TestMultipartCopyMultipleSizes(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
								
							 | 
						|
									// Clean up any leftover buckets from previous test runs
							 | 
						|
									cleanupTestBuckets(t, client)
							 | 
						|
								
							 | 
						|
									sourceBucketName := getNewBucketName()
							 | 
						|
									destBucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create buckets
							 | 
						|
									createBucket(t, client, sourceBucketName)
							 | 
						|
									defer deleteBucket(t, client, sourceBucketName)
							 | 
						|
									createBucket(t, client, destBucketName)
							 | 
						|
									defer deleteBucket(t, client, destBucketName)
							 | 
						|
								
							 | 
						|
									// Put source object (12MB)
							 | 
						|
									sourceKey := "foo"
							 | 
						|
									sourceSize := 12 * 1024 * 1024
							 | 
						|
									sourceContent := generateRandomData(sourceSize)
							 | 
						|
									_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
							 | 
						|
										Bucket: aws.String(sourceBucketName),
							 | 
						|
										Key:    aws.String(sourceKey),
							 | 
						|
										Body:   bytes.NewReader(sourceContent),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									destKey := "mymultipart"
							 | 
						|
									partSize := 5 * 1024 * 1024 // 5MB parts
							 | 
						|
								
							 | 
						|
									// Test different copy sizes
							 | 
						|
									testSizes := []int{
							 | 
						|
										5 * 1024 * 1024,         // 5MB
							 | 
						|
										5*1024*1024 + 100*1024,  // 5MB + 100KB
							 | 
						|
										5*1024*1024 + 600*1024,  // 5MB + 600KB
							 | 
						|
										10*1024*1024 + 100*1024, // 10MB + 100KB
							 | 
						|
										10*1024*1024 + 600*1024, // 10MB + 600KB
							 | 
						|
										10 * 1024 * 1024,        // 10MB
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, size := range testSizes {
							 | 
						|
										t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
							 | 
						|
											// Create multipart upload
							 | 
						|
											createResp, err := client.CreateMultipartUpload(context.TODO(), &s3.CreateMultipartUploadInput{
							 | 
						|
												Bucket: aws.String(destBucketName),
							 | 
						|
												Key:    aws.String(destKey),
							 | 
						|
											})
							 | 
						|
											require.NoError(t, err)
							 | 
						|
											uploadID := *createResp.UploadId
							 | 
						|
								
							 | 
						|
											// Upload parts
							 | 
						|
											var parts []types.CompletedPart
							 | 
						|
											copySource := createCopySource(sourceBucketName, sourceKey)
							 | 
						|
								
							 | 
						|
											for i := 0; i < size; i += partSize {
							 | 
						|
												partNum := int32(len(parts) + 1)
							 | 
						|
												endOffset := i + partSize - 1
							 | 
						|
												if endOffset >= size {
							 | 
						|
													endOffset = size - 1
							 | 
						|
												}
							 | 
						|
								
							 | 
						|
												copyRange := fmt.Sprintf("bytes=%d-%d", i, endOffset)
							 | 
						|
												copyResp, err := client.UploadPartCopy(context.TODO(), &s3.UploadPartCopyInput{
							 | 
						|
													Bucket:          aws.String(destBucketName),
							 | 
						|
													Key:             aws.String(destKey),
							 | 
						|
													UploadId:        aws.String(uploadID),
							 | 
						|
													PartNumber:      aws.Int32(partNum),
							 | 
						|
													CopySource:      aws.String(copySource),
							 | 
						|
													CopySourceRange: aws.String(copyRange),
							 | 
						|
												})
							 | 
						|
												require.NoError(t, err)
							 | 
						|
								
							 | 
						|
												parts = append(parts, types.CompletedPart{
							 | 
						|
													ETag:       copyResp.CopyPartResult.ETag,
							 | 
						|
													PartNumber: aws.Int32(partNum),
							 | 
						|
												})
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											// Complete multipart upload
							 | 
						|
											_, err = client.CompleteMultipartUpload(context.TODO(), &s3.CompleteMultipartUploadInput{
							 | 
						|
												Bucket:   aws.String(destBucketName),
							 | 
						|
												Key:      aws.String(destKey),
							 | 
						|
												UploadId: aws.String(uploadID),
							 | 
						|
												MultipartUpload: &types.CompletedMultipartUpload{
							 | 
						|
													Parts: parts,
							 | 
						|
												},
							 | 
						|
											})
							 | 
						|
											require.NoError(t, err)
							 | 
						|
								
							 | 
						|
											// Verify the copied object
							 | 
						|
											resp := getObject(t, client, destBucketName, destKey)
							 | 
						|
											body, err := io.ReadAll(resp.Body)
							 | 
						|
											require.NoError(t, err)
							 | 
						|
											resp.Body.Close()
							 | 
						|
								
							 | 
						|
											require.NotNil(t, resp.ContentLength)
							 | 
						|
											assert.Equal(t, int64(size), *resp.ContentLength)
							 | 
						|
											assert.Equal(t, sourceContent[:size], body)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestCopyObjectIfMatchGood tests copying with matching ETag condition
							 | 
						|
								func TestCopyObjectIfMatchGood(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
									bucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create bucket
							 | 
						|
									createBucket(t, client, bucketName)
							 | 
						|
									defer deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Put source object
							 | 
						|
									sourceKey := "foo"
							 | 
						|
									sourceContent := "bar"
							 | 
						|
									putResp := putObject(t, client, bucketName, sourceKey, sourceContent)
							 | 
						|
								
							 | 
						|
									// Copy object with matching ETag
							 | 
						|
									destKey := "bar"
							 | 
						|
									copySource := createCopySource(bucketName, sourceKey)
							 | 
						|
									_, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{
							 | 
						|
										Bucket:            aws.String(bucketName),
							 | 
						|
										Key:               aws.String(destKey),
							 | 
						|
										CopySource:        aws.String(copySource),
							 | 
						|
										CopySourceIfMatch: putResp.ETag,
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Verify the copied object
							 | 
						|
									resp := getObject(t, client, bucketName, destKey)
							 | 
						|
									body := getObjectBody(t, resp)
							 | 
						|
									assert.Equal(t, sourceContent, body)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestCopyObjectIfNoneMatchFailed tests copying with non-matching ETag condition
							 | 
						|
								func TestCopyObjectIfNoneMatchFailed(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
									bucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create bucket
							 | 
						|
									createBucket(t, client, bucketName)
							 | 
						|
									defer deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Put source object
							 | 
						|
									sourceKey := "foo"
							 | 
						|
									sourceContent := "bar"
							 | 
						|
									putObject(t, client, bucketName, sourceKey, sourceContent)
							 | 
						|
								
							 | 
						|
									// Copy object with non-matching ETag (should succeed)
							 | 
						|
									destKey := "bar"
							 | 
						|
									copySource := createCopySource(bucketName, sourceKey)
							 | 
						|
									_, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{
							 | 
						|
										Bucket:                aws.String(bucketName),
							 | 
						|
										Key:                   aws.String(destKey),
							 | 
						|
										CopySource:            aws.String(copySource),
							 | 
						|
										CopySourceIfNoneMatch: aws.String("ABCORZ"),
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Verify the copied object
							 | 
						|
									resp := getObject(t, client, bucketName, destKey)
							 | 
						|
									body := getObjectBody(t, resp)
							 | 
						|
									assert.Equal(t, sourceContent, body)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestCopyObjectIfMatchFailed tests copying with non-matching ETag condition (should fail)
							 | 
						|
								func TestCopyObjectIfMatchFailed(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
									bucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create bucket
							 | 
						|
									createBucket(t, client, bucketName)
							 | 
						|
									defer deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Put source object
							 | 
						|
									sourceKey := "foo"
							 | 
						|
									sourceContent := "bar"
							 | 
						|
									putObject(t, client, bucketName, sourceKey, sourceContent)
							 | 
						|
								
							 | 
						|
									// Copy object with non-matching ETag (should fail)
							 | 
						|
									destKey := "bar"
							 | 
						|
									copySource := createCopySource(bucketName, sourceKey)
							 | 
						|
									_, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{
							 | 
						|
										Bucket:            aws.String(bucketName),
							 | 
						|
										Key:               aws.String(destKey),
							 | 
						|
										CopySource:        aws.String(copySource),
							 | 
						|
										CopySourceIfMatch: aws.String("ABCORZ"),
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Should fail with precondition failed
							 | 
						|
									require.Error(t, err)
							 | 
						|
									// Note: We could check for specific error types, but SeaweedFS might return different error codes
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestCopyObjectIfNoneMatchGood tests copying with matching ETag condition (should fail)
							 | 
						|
								func TestCopyObjectIfNoneMatchGood(t *testing.T) {
							 | 
						|
									client := getS3Client(t)
							 | 
						|
									bucketName := getNewBucketName()
							 | 
						|
								
							 | 
						|
									// Create bucket
							 | 
						|
									createBucket(t, client, bucketName)
							 | 
						|
									defer deleteBucket(t, client, bucketName)
							 | 
						|
								
							 | 
						|
									// Put source object
							 | 
						|
									sourceKey := "foo"
							 | 
						|
									sourceContent := "bar"
							 | 
						|
									putResp := putObject(t, client, bucketName, sourceKey, sourceContent)
							 | 
						|
								
							 | 
						|
									// Copy object with matching ETag for IfNoneMatch (should fail)
							 | 
						|
									destKey := "bar"
							 | 
						|
									copySource := createCopySource(bucketName, sourceKey)
							 | 
						|
									_, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{
							 | 
						|
										Bucket:                aws.String(bucketName),
							 | 
						|
										Key:                   aws.String(destKey),
							 | 
						|
										CopySource:            aws.String(copySource),
							 | 
						|
										CopySourceIfNoneMatch: putResp.ETag,
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Should fail with precondition failed
							 | 
						|
									require.Error(t, err)
							 | 
						|
								}
							 |