test(s3/lifecycle): integration coverage for ExpirationDate (past)

Rules with Expiration{Date: <past>} 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.
This commit is contained in:
Chris Lu
2026-05-09 23:20:58 -07:00
parent 794e59ea61
commit 957ac1335c
@@ -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: <past>} 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")
}