From 54954bb3ebe333ab07425d58c201df7d76b64944 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sat, 9 May 2026 22:58:31 -0700 Subject: [PATCH] test(s3/lifecycle): integration coverage for multi-bucket sweep A single shell-driven shard sweep must process every bucket carrying lifecycle config, not just the first one alphabetically. Pinned because the scheduler iterates the buckets directory and a regression that returns early after the first match would silently disable lifecycle for every later bucket. Two buckets, each with their own prefix-expiration rule and a backdated object. Both must be expired after the same sweep. --- .../s3_lifecycle_multi_bucket_test.go | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 test/s3/lifecycle/s3_lifecycle_multi_bucket_test.go diff --git a/test/s3/lifecycle/s3_lifecycle_multi_bucket_test.go b/test/s3/lifecycle/s3_lifecycle_multi_bucket_test.go new file mode 100644 index 000000000..d00ac33bf --- /dev/null +++ b/test/s3/lifecycle/s3_lifecycle_multi_bucket_test.go @@ -0,0 +1,61 @@ +// Multiple-bucket integration scenario. +package lifecycle + +import ( + "context" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/require" +) + +// TestLifecycleMultipleBucketsInOneSweep: a single shell-driven shard +// sweep must process every bucket carrying lifecycle config, not just +// the first one alphabetically. Pinned because the scheduler iterates +// the buckets directory and a regression that returns early after the +// first match would silently disable lifecycle for every later bucket. +// +// Two buckets, each with its own 1-day prefix-expiration rule and one +// backdated object. After the worker runs, both objects must be gone. +func TestLifecycleMultipleBucketsInOneSweep(t *testing.T) { + c := s3Client(t) + fc, fcClose := filerClient(t) + defer fcClose() + + bucketA := uniqueBucket("multi-a") + bucketB := uniqueBucket("multi-b") + mustCreateBucket(t, c, bucketA) + mustCreateBucket(t, c, bucketB) + + putExpirationLifecycle(t, c, bucketA, "exp/", 1) + putExpirationLifecycle(t, c, bucketB, "exp/", 1) + + const keyA = "exp/a.txt" + const keyB = "exp/b.txt" + putObject(t, c, bucketA, keyA, "a") + putObject(t, c, bucketB, keyB, "b") + backdateMtime(t, fc, bucketA, keyA, 30) + backdateMtime(t, fc, bucketB, keyB, 30) + + out := runLifecycleShard(t) + t.Logf("shell output:\n%s", out) + + // Both buckets must have their objects expired in this single sweep. + for _, c2 := range []struct { + bucket, key string + }{ + {bucketA, keyA}, + {bucketB, keyB}, + } { + c2 := c2 + require.Eventuallyf(t, func() bool { + _, err := c.HeadObject(context.Background(), &s3.HeadObjectInput{ + Bucket: aws.String(c2.bucket), Key: aws.String(c2.key), + }) + return err != nil + }, 30*time.Second, 500*time.Millisecond, + "%s/%s must be expired by the multi-bucket sweep", c2.bucket, c2.key) + } +}