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.
		
		
		
		
		
			
		
			
				
					
					
						
							257 lines
						
					
					
						
							8.2 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							257 lines
						
					
					
						
							8.2 KiB
						
					
					
				| package s3api | |
| 
 | |
| import ( | |
| 	"bytes" | |
| 	"context" | |
| 	"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" | |
| ) | |
| 
 | |
| // TestSuspendedVersioningNullOverwrite tests the scenario where: | |
| // 1. Create object before versioning is enabled (pre-versioning object) | |
| // 2. Enable versioning, then suspend it | |
| // 3. Overwrite the object (should replace the null version, not create duplicate) | |
| // 4. List versions should show only 1 version with versionId "null" | |
| // | |
| // This test corresponds to: test_versioning_obj_plain_null_version_overwrite_suspended | |
| func TestSuspendedVersioningNullOverwrite(t *testing.T) { | |
| 	ctx := context.Background() | |
| 	client := getS3Client(t) | |
| 
 | |
| 	// Create bucket | |
| 	bucketName := getNewBucketName() | |
| 	createBucket(t, client, bucketName) | |
| 	defer deleteBucket(t, client, bucketName) | |
| 
 | |
| 	objectKey := "testobjbar" | |
| 
 | |
| 	// Step 1: Put object before versioning is configured (pre-versioning object) | |
| 	content1 := []byte("foooz") | |
| 	_, err := client.PutObject(ctx, &s3.PutObjectInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 		Key:    aws.String(objectKey), | |
| 		Body:   bytes.NewReader(content1), | |
| 	}) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to create pre-versioning object: %v", err) | |
| 	} | |
| 	t.Logf("Created pre-versioning object") | |
| 
 | |
| 	// Step 2: Enable versioning | |
| 	_, err = client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 		VersioningConfiguration: &types.VersioningConfiguration{ | |
| 			Status: types.BucketVersioningStatusEnabled, | |
| 		}, | |
| 	}) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to enable versioning: %v", err) | |
| 	} | |
| 	t.Logf("Enabled versioning") | |
| 
 | |
| 	// Step 3: Suspend versioning | |
| 	_, err = client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 		VersioningConfiguration: &types.VersioningConfiguration{ | |
| 			Status: types.BucketVersioningStatusSuspended, | |
| 		}, | |
| 	}) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to suspend versioning: %v", err) | |
| 	} | |
| 	t.Logf("Suspended versioning") | |
| 
 | |
| 	// Step 4: Overwrite the object during suspended versioning | |
| 	content2 := []byte("zzz") | |
| 	putResp, err := client.PutObject(ctx, &s3.PutObjectInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 		Key:    aws.String(objectKey), | |
| 		Body:   bytes.NewReader(content2), | |
| 	}) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to overwrite object during suspended versioning: %v", err) | |
| 	} | |
| 
 | |
| 	// Verify no VersionId is returned for suspended versioning | |
| 	if putResp.VersionId != nil { | |
| 		t.Errorf("Suspended versioning should NOT return VersionId, but got: %s", *putResp.VersionId) | |
| 	} | |
| 	t.Logf("Overwrote object during suspended versioning (no VersionId returned as expected)") | |
| 
 | |
| 	// Step 5: Verify content is updated | |
| 	getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 		Key:    aws.String(objectKey), | |
| 	}) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to get object: %v", err) | |
| 	} | |
| 	defer getResp.Body.Close() | |
| 
 | |
| 	gotContent := new(bytes.Buffer) | |
| 	gotContent.ReadFrom(getResp.Body) | |
| 	if !bytes.Equal(gotContent.Bytes(), content2) { | |
| 		t.Errorf("Expected content %q, got %q", content2, gotContent.Bytes()) | |
| 	} | |
| 	t.Logf("Object content is correctly updated to: %q", content2) | |
| 
 | |
| 	// Step 6: List object versions - should have only 1 version | |
| 	listResp, err := client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 	}) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to list object versions: %v", err) | |
| 	} | |
| 
 | |
| 	// Count versions (excluding delete markers) | |
| 	versionCount := len(listResp.Versions) | |
| 	deleteMarkerCount := len(listResp.DeleteMarkers) | |
| 
 | |
| 	t.Logf("List results: %d versions, %d delete markers", versionCount, deleteMarkerCount) | |
| 	for i, v := range listResp.Versions { | |
| 		t.Logf("  Version %d: Key=%s, VersionId=%s, IsLatest=%v, Size=%d", | |
| 			i, *v.Key, *v.VersionId, v.IsLatest, v.Size) | |
| 	} | |
| 
 | |
| 	// THIS IS THE KEY ASSERTION: Should have exactly 1 version, not 2 | |
| 	if versionCount != 1 { | |
| 		t.Errorf("Expected 1 version after suspended versioning overwrite, got %d versions", versionCount) | |
| 		t.Error("BUG: Duplicate null versions detected! The overwrite should have replaced the pre-versioning object.") | |
| 	} else { | |
| 		t.Logf("PASS: Only 1 version found (no duplicate null versions)") | |
| 	} | |
| 
 | |
| 	if deleteMarkerCount != 0 { | |
| 		t.Errorf("Expected 0 delete markers, got %d", deleteMarkerCount) | |
| 	} | |
| 
 | |
| 	// Verify the version has versionId "null" | |
| 	if versionCount > 0 { | |
| 		if listResp.Versions[0].VersionId == nil || *listResp.Versions[0].VersionId != "null" { | |
| 			t.Errorf("Expected VersionId to be 'null', got %v", listResp.Versions[0].VersionId) | |
| 		} else { | |
| 			t.Logf("Version ID is 'null' as expected") | |
| 		} | |
| 	} | |
| 
 | |
| 	// Step 7: Delete the null version | |
| 	_, err = client.DeleteObject(ctx, &s3.DeleteObjectInput{ | |
| 		Bucket:    aws.String(bucketName), | |
| 		Key:       aws.String(objectKey), | |
| 		VersionId: aws.String("null"), | |
| 	}) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to delete null version: %v", err) | |
| 	} | |
| 	t.Logf("Deleted null version") | |
| 
 | |
| 	// Step 8: Verify object no longer exists | |
| 	_, err = client.GetObject(ctx, &s3.GetObjectInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 		Key:    aws.String(objectKey), | |
| 	}) | |
| 	if err == nil { | |
| 		t.Error("Expected object to not exist after deleting null version") | |
| 	} | |
| 	t.Logf("Object no longer exists after deleting null version") | |
| 
 | |
| 	// Step 9: Verify no versions remain | |
| 	listResp, err = client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 	}) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to list object versions: %v", err) | |
| 	} | |
| 
 | |
| 	if len(listResp.Versions) != 0 || len(listResp.DeleteMarkers) != 0 { | |
| 		t.Errorf("Expected no versions or delete markers, got %d versions and %d delete markers", | |
| 			len(listResp.Versions), len(listResp.DeleteMarkers)) | |
| 	} else { | |
| 		t.Logf("No versions remain after deletion") | |
| 	} | |
| } | |
| 
 | |
| // TestEnabledVersioningReturnsVersionId tests that when versioning is ENABLED, | |
| // every PutObject operation returns a version ID | |
| // | |
| // This test corresponds to the create_multiple_versions helper function | |
| func TestEnabledVersioningReturnsVersionId(t *testing.T) { | |
| 	ctx := context.Background() | |
| 	client := getS3Client(t) | |
| 
 | |
| 	// Create bucket | |
| 	bucketName := getNewBucketName() | |
| 	createBucket(t, client, bucketName) | |
| 	defer deleteBucket(t, client, bucketName) | |
| 
 | |
| 	objectKey := "testobj" | |
| 
 | |
| 	// Enable versioning | |
| 	_, err := client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 		VersioningConfiguration: &types.VersioningConfiguration{ | |
| 			Status: types.BucketVersioningStatusEnabled, | |
| 		}, | |
| 	}) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to enable versioning: %v", err) | |
| 	} | |
| 	t.Logf("Enabled versioning") | |
| 
 | |
| 	// Create multiple versions | |
| 	numVersions := 3 | |
| 	versionIds := make([]string, 0, numVersions) | |
| 
 | |
| 	for i := 0; i < numVersions; i++ { | |
| 		content := []byte("content-" + string(rune('0'+i))) | |
| 		putResp, err := client.PutObject(ctx, &s3.PutObjectInput{ | |
| 			Bucket: aws.String(bucketName), | |
| 			Key:    aws.String(objectKey), | |
| 			Body:   bytes.NewReader(content), | |
| 		}) | |
| 		if err != nil { | |
| 			t.Fatalf("Failed to create version %d: %v", i, err) | |
| 		} | |
| 
 | |
| 		// THIS IS THE KEY ASSERTION: VersionId MUST be returned for enabled versioning | |
| 		if putResp.VersionId == nil { | |
| 			t.Errorf("FAILED: PutObject with enabled versioning MUST return VersionId, but got nil for version %d", i) | |
| 		} else { | |
| 			versionId := *putResp.VersionId | |
| 			if versionId == "" { | |
| 				t.Errorf("FAILED: PutObject returned empty VersionId for version %d", i) | |
| 			} else if versionId == "null" { | |
| 				t.Errorf("FAILED: PutObject with enabled versioning should NOT return 'null' version ID, got: %s", versionId) | |
| 			} else { | |
| 				versionIds = append(versionIds, versionId) | |
| 				t.Logf("Version %d created with VersionId: %s", i, versionId) | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	if len(versionIds) != numVersions { | |
| 		t.Errorf("Expected %d version IDs, got %d", numVersions, len(versionIds)) | |
| 	} | |
| 
 | |
| 	// List versions to verify all were created | |
| 	listResp, err := client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 	}) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to list object versions: %v", err) | |
| 	} | |
| 
 | |
| 	if len(listResp.Versions) != numVersions { | |
| 		t.Errorf("Expected %d versions in list, got %d", numVersions, len(listResp.Versions)) | |
| 	} else { | |
| 		t.Logf("All %d versions are listed", numVersions) | |
| 	} | |
| 
 | |
| 	// Verify all version IDs match | |
| 	for i, v := range listResp.Versions { | |
| 		t.Logf("  Version %d: VersionId=%s, Size=%d, IsLatest=%v", i, *v.VersionId, v.Size, v.IsLatest) | |
| 	} | |
| }
 |