From 0972a0acf36112644cc9d30b621472fe9fbfac52 Mon Sep 17 00:00:00 2001 From: chrislu Date: Mon, 15 Dec 2025 23:06:52 -0800 Subject: [PATCH] test: add pagination stress tests for S3 versioning with >1000 versions --- test/s3/versioning/Makefile | 7 + .../s3_versioning_pagination_stress_test.go | 297 ++++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 test/s3/versioning/s3_versioning_pagination_stress_test.go diff --git a/test/s3/versioning/Makefile b/test/s3/versioning/Makefile index 91fd84fc1..2adfca5e3 100644 --- a/test/s3/versioning/Makefile +++ b/test/s3/versioning/Makefile @@ -239,6 +239,13 @@ test-versioning-stress: check-deps @go test -v -timeout=20m -run "TestVersioningConcurrentOperations" . -count=5 @echo "✅ Stress tests completed" +# Pagination stress testing (tests >1000 versions) +test-versioning-pagination-stress: check-deps + @echo "Running pagination stress tests (>1000 versions)..." + @echo "This test creates 1500+ versions and may take several minutes..." + @ENABLE_STRESS_TESTS=true go test -v -timeout=30m -run "TestVersioningPagination" . + @echo "✅ Pagination stress tests completed" + # Generate test reports test-report: check-deps @echo "Generating test reports..." diff --git a/test/s3/versioning/s3_versioning_pagination_stress_test.go b/test/s3/versioning/s3_versioning_pagination_stress_test.go new file mode 100644 index 000000000..0c00558a8 --- /dev/null +++ b/test/s3/versioning/s3_versioning_pagination_stress_test.go @@ -0,0 +1,297 @@ +package s3api + +import ( + "context" + "fmt" + "os" + "strings" + "testing" + "time" + + "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/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestVersioningPaginationOver1000Versions tests that ListObjectVersions correctly +// handles objects with more than 1000 versions using pagination. +// This is a stress test that creates 1500 versions and verifies all are listed. +// +// Run with: ENABLE_STRESS_TESTS=true go test -v -run TestVersioningPaginationOver1000Versions -timeout 30m +func TestVersioningPaginationOver1000Versions(t *testing.T) { + if os.Getenv("ENABLE_STRESS_TESTS") != "true" { + t.Skip("Skipping stress test. Set ENABLE_STRESS_TESTS=true to run.") + } + + 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) + + objectKey := "test-many-versions" + numVersions := 1500 // More than 1000 to test pagination + versionIds := make([]string, 0, numVersions) + + t.Logf("Creating %d versions of object %s...", numVersions, objectKey) + startTime := time.Now() + + // Create many versions of the same object + for i := 0; i < numVersions; i++ { + content := fmt.Sprintf("content-version-%d", i+1) + putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + Body: strings.NewReader(content), + }) + require.NoError(t, err, "Failed to create version %d", i+1) + require.NotNil(t, putResp.VersionId) + versionIds = append(versionIds, *putResp.VersionId) + + if (i+1)%100 == 0 { + t.Logf("Created %d/%d versions...", i+1, numVersions) + } + } + + createDuration := time.Since(startTime) + t.Logf("Created %d versions in %v", numVersions, createDuration) + + // Test 1: List all versions without pagination limit + t.Run("ListAllVersionsNoPagination", func(t *testing.T) { + allVersions := listAllVersions(t, client, bucketName, objectKey) + assert.Len(t, allVersions, numVersions, "Should have all %d versions", numVersions) + + // Verify all version IDs are unique + versionIdSet := make(map[string]bool) + for _, v := range allVersions { + assert.False(t, versionIdSet[*v.VersionId], "Duplicate version ID: %s", *v.VersionId) + versionIdSet[*v.VersionId] = true + } + + // Verify only one version is marked as latest + latestCount := 0 + for _, v := range allVersions { + if *v.IsLatest { + latestCount++ + assert.Equal(t, versionIds[numVersions-1], *v.VersionId, "Latest should be the last created") + } + } + assert.Equal(t, 1, latestCount, "Only one version should be marked as latest") + }) + + // Test 2: List with explicit small max-keys to force multiple pages + t.Run("ListWithSmallMaxKeys", func(t *testing.T) { + maxKeys := int32(100) + allVersions := make([]types.ObjectVersion, 0, numVersions) + var keyMarker, versionIdMarker *string + pageCount := 0 + + for { + resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{ + Bucket: aws.String(bucketName), + Prefix: aws.String(objectKey), + MaxKeys: aws.Int32(maxKeys), + KeyMarker: keyMarker, + VersionIdMarker: versionIdMarker, + }) + require.NoError(t, err) + pageCount++ + + allVersions = append(allVersions, resp.Versions...) + + if !*resp.IsTruncated { + break + } + keyMarker = resp.NextKeyMarker + versionIdMarker = resp.NextVersionIdMarker + } + + t.Logf("Listed %d versions in %d pages (max-keys=%d)", len(allVersions), pageCount, maxKeys) + assert.Len(t, allVersions, numVersions, "Should have all %d versions", numVersions) + assert.Greater(t, pageCount, 1, "Should require multiple pages") + }) + + // Test 3: List with max-keys=1000 (S3 default) to test the boundary + t.Run("ListWithDefaultMaxKeys", func(t *testing.T) { + maxKeys := int32(1000) + allVersions := make([]types.ObjectVersion, 0, numVersions) + var keyMarker, versionIdMarker *string + pageCount := 0 + + for { + resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{ + Bucket: aws.String(bucketName), + Prefix: aws.String(objectKey), + MaxKeys: aws.Int32(maxKeys), + KeyMarker: keyMarker, + VersionIdMarker: versionIdMarker, + }) + require.NoError(t, err) + pageCount++ + + allVersions = append(allVersions, resp.Versions...) + t.Logf("Page %d: got %d versions, truncated=%v", pageCount, len(resp.Versions), *resp.IsTruncated) + + if !*resp.IsTruncated { + break + } + keyMarker = resp.NextKeyMarker + versionIdMarker = resp.NextVersionIdMarker + } + + t.Logf("Listed %d versions in %d pages (max-keys=%d)", len(allVersions), pageCount, maxKeys) + assert.Len(t, allVersions, numVersions, "Should have all %d versions", numVersions) + assert.Equal(t, 2, pageCount, "Should require exactly 2 pages for 1500 versions with max-keys=1000") + }) + + // Test 4: Verify we can retrieve specific versions + t.Run("RetrieveSpecificVersions", func(t *testing.T) { + // Test first, middle, and last versions + testIndices := []int{0, numVersions / 2, numVersions - 1} + for _, idx := range testIndices { + expectedContent := fmt.Sprintf("content-version-%d", idx+1) + getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + VersionId: aws.String(versionIds[idx]), + }) + require.NoError(t, err, "Failed to get version at index %d", idx) + + buf := make([]byte, len(expectedContent)) + _, err = getResp.Body.Read(buf) + getResp.Body.Close() + + assert.Equal(t, expectedContent, string(buf), "Content mismatch for version %d", idx+1) + } + }) +} + +// TestVersioningPaginationMultipleObjectsManyVersions tests pagination with multiple objects +// each having many versions, ensuring correct handling of key-marker and version-id-marker. +// +// Run with: ENABLE_STRESS_TESTS=true go test -v -run TestVersioningPaginationMultipleObjectsManyVersions -timeout 30m +func TestVersioningPaginationMultipleObjectsManyVersions(t *testing.T) { + if os.Getenv("ENABLE_STRESS_TESTS") != "true" { + t.Skip("Skipping stress test. Set ENABLE_STRESS_TESTS=true to run.") + } + + client := getS3Client(t) + bucketName := getNewBucketName() + + createBucket(t, client, bucketName) + defer deleteBucket(t, client, bucketName) + + enableVersioning(t, client, bucketName) + checkVersioningStatus(t, client, bucketName, types.BucketVersioningStatusEnabled) + + numObjects := 5 + versionsPerObject := 300 + totalExpectedVersions := numObjects * versionsPerObject + + t.Logf("Creating %d objects with %d versions each (%d total)...", numObjects, versionsPerObject, totalExpectedVersions) + startTime := time.Now() + + // Create multiple objects with many versions each + for i := 0; i < numObjects; i++ { + objectKey := fmt.Sprintf("object-%03d", i) + for j := 0; j < versionsPerObject; j++ { + content := fmt.Sprintf("obj%d-ver%d", i, j+1) + _, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + Body: strings.NewReader(content), + }) + require.NoError(t, err) + } + t.Logf("Created object %s with %d versions", objectKey, versionsPerObject) + } + + createDuration := time.Since(startTime) + t.Logf("Created %d total versions in %v", totalExpectedVersions, createDuration) + + // List all versions using pagination + t.Run("ListAllWithPagination", func(t *testing.T) { + maxKeys := int32(500) + allVersions := make([]types.ObjectVersion, 0, totalExpectedVersions) + var keyMarker, versionIdMarker *string + pageCount := 0 + + for { + resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{ + Bucket: aws.String(bucketName), + MaxKeys: aws.Int32(maxKeys), + KeyMarker: keyMarker, + VersionIdMarker: versionIdMarker, + }) + require.NoError(t, err) + pageCount++ + + allVersions = append(allVersions, resp.Versions...) + t.Logf("Page %d: got %d versions", pageCount, len(resp.Versions)) + + if !*resp.IsTruncated { + break + } + keyMarker = resp.NextKeyMarker + versionIdMarker = resp.NextVersionIdMarker + } + + t.Logf("Listed %d versions in %d pages", len(allVersions), pageCount) + assert.Len(t, allVersions, totalExpectedVersions, "Should have all versions") + + // Verify each object has correct number of versions + versionsByKey := make(map[string]int) + for _, v := range allVersions { + versionsByKey[*v.Key]++ + } + assert.Len(t, versionsByKey, numObjects, "Should have %d unique objects", numObjects) + for key, count := range versionsByKey { + assert.Equal(t, versionsPerObject, count, "Object %s should have %d versions", key, versionsPerObject) + } + + // Verify each object has exactly one latest version + latestByKey := make(map[string]int) + for _, v := range allVersions { + if *v.IsLatest { + latestByKey[*v.Key]++ + } + } + for key, count := range latestByKey { + assert.Equal(t, 1, count, "Object %s should have exactly one latest version", key) + } + }) +} + +// listAllVersions is a helper to list all versions of a specific object using pagination +func listAllVersions(t *testing.T, client *s3.Client, bucketName, objectKey string) []types.ObjectVersion { + var allVersions []types.ObjectVersion + var keyMarker, versionIdMarker *string + + for { + resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{ + Bucket: aws.String(bucketName), + Prefix: aws.String(objectKey), + KeyMarker: keyMarker, + VersionIdMarker: versionIdMarker, + }) + require.NoError(t, err) + + allVersions = append(allVersions, resp.Versions...) + + if !*resp.IsTruncated { + break + } + keyMarker = resp.NextKeyMarker + versionIdMarker = resp.NextVersionIdMarker + } + + return allVersions +} +