From 957ac1335ce64962cce1f8d94253e951d0cc85b8 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sat, 9 May 2026 23:15:59 -0700 Subject: [PATCH] test(s3/lifecycle): integration coverage for ExpirationDate (past) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rules with Expiration{Date: } route through ScanAtDate in the engine (decideMode's ActionKindExpirationDate case) — a separate compile + dispatch branch from the EventDriven delay-group path the Days-based tests exercise. Past date + in-prefix object → must expire. Out-of-prefix object → must remain. Object also backdated as defense-in-depth so the assertion doesn't depend on whether the dispatcher consults MinTriggerAge for date kinds. --- .../s3_lifecycle_expiration_date_test.go | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 test/s3/lifecycle/s3_lifecycle_expiration_date_test.go diff --git a/test/s3/lifecycle/s3_lifecycle_expiration_date_test.go b/test/s3/lifecycle/s3_lifecycle_expiration_date_test.go new file mode 100644 index 000000000..a60efeb82 --- /dev/null +++ b/test/s3/lifecycle/s3_lifecycle_expiration_date_test.go @@ -0,0 +1,75 @@ +// ExpirationDate (date-based, not Days) 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/aws/aws-sdk-go-v2/service/s3/types" + "github.com/stretchr/testify/require" +) + +// TestLifecycleExpirationDateInThePast: an Expiration{Date: } rule +// routes through the engine's ScanAtDate mode rather than the +// EventDriven delay-group path. The worker's dispatcher must still fire +// when the date has already passed at the time the worker runs. Pinning +// this path because most tests use Days-based rules; ScanAtDate is a +// separate compile + dispatch branch (engine.decideMode case +// ActionKindExpirationDate) that wouldn't be exercised otherwise. +func TestLifecycleExpirationDateInThePast(t *testing.T) { + c := s3Client(t) + fc, fcClose := filerClient(t) + defer fcClose() + + bucket := uniqueBucket("expdate") + mustCreateBucket(t, c, bucket) + + // Date in the past: AWS rejects ExpirationDate in the future from + // being processed early but a past date is the natural integration + // test — every object hit by the rule is immediately eligible. + pastDate := time.Now().Add(-7 * 24 * time.Hour).UTC().Truncate(24 * time.Hour) + _, err := c.PutBucketLifecycleConfiguration(context.Background(), &s3.PutBucketLifecycleConfigurationInput{ + Bucket: aws.String(bucket), + LifecycleConfiguration: &types.BucketLifecycleConfiguration{ + Rules: []types.LifecycleRule{{ + ID: aws.String("expire-by-date"), + Status: types.ExpirationStatusEnabled, + Filter: &types.LifecycleRuleFilter{Prefix: aws.String("d/")}, + Expiration: &types.LifecycleExpiration{ + Date: aws.Time(pastDate), + }, + }}, + }, + }) + require.NoError(t, err) + + const oldKey = "d/in-prefix.txt" + const otherKey = "other/skip.txt" + putObject(t, c, bucket, oldKey, "old") + putObject(t, c, bucket, otherKey, "other") + + // Backdate oldKey too — defense-in-depth so the test doesn't + // depend on whether the worker also considers MinTriggerAge for + // date kinds; a fresh object with a past date should still expire, + // but aging it locks the assertion either way. + backdateMtime(t, fc, bucket, oldKey, 30) + + out := runLifecycleShard(t) + t.Logf("shell output:\n%s", out) + + require.Eventuallyf(t, func() bool { + _, err := c.HeadObject(context.Background(), &s3.HeadObjectInput{ + Bucket: aws.String(bucket), Key: aws.String(oldKey), + }) + return isS3NotFound(err) + }, 30*time.Second, 500*time.Millisecond, "%s/%s must be expired by past-date rule", bucket, oldKey) + + // Out-of-prefix object stays — the rule's Filter.Prefix gates this. + _, err = c.HeadObject(context.Background(), &s3.HeadObjectInput{ + Bucket: aws.String(bucket), Key: aws.String(otherKey), + }) + require.NoError(t, err, "object outside the rule's prefix must remain") +}