test(s3/lifecycle): integration coverage for DeleteBucketLifecycle stops dispatching

Operator UX: after DeleteBucketLifecycle, the worker must observe the
removal on the next sweep and stop expiring objects under the now-gone
rule. A regression that caches old configs across
PutBucketLifecycleConfiguration → DeleteBucketLifecycle would keep
silently dropping objects.

Setup: positive control (rule active, backdated obj expires) →
DeleteBucketLifecycle → PUT + backdate a fresh object → second
sweep. The fresh object must remain.
This commit is contained in:
Chris Lu
2026-05-09 23:22:29 -07:00
parent ab1f448472
commit 191a088c09
@@ -0,0 +1,67 @@
// Operator-removes-lifecycle-config 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"
)
// TestLifecycleDeleteBucketLifecycleStopsDispatching: when an operator
// runs DeleteBucketLifecycle, subsequent sweeps must NOT continue
// expiring objects under the now-removed rule. The worker reads each
// bucket's XML config from filer state on every run; if it cached the
// previous config across PutBucketLifecycleConfiguration → DeleteBucket
// Lifecycle the bucket would keep losing objects.
//
// Setup: positive control (rule active, backdated obj expires), then
// DeleteBucketLifecycle, then PUT + backdate a fresh object, run the
// worker again. The fresh object must remain — no rule, no dispatch.
func TestLifecycleDeleteBucketLifecycleStopsDispatching(t *testing.T) {
c := s3Client(t)
fc, fcClose := filerClient(t)
defer fcClose()
bucket := uniqueBucket("delete-config")
mustCreateBucket(t, c, bucket)
putExpirationLifecycle(t, c, bucket, "x/", 1)
const firstKey = "x/initial.txt"
putObject(t, c, bucket, firstKey, "first")
backdateMtime(t, fc, bucket, firstKey, 30)
out := runLifecycleShard(t)
t.Logf("sweep 1 output (rule active):\n%s", out)
require.Eventuallyf(t, func() bool {
_, err := c.HeadObject(context.Background(), &s3.HeadObjectInput{
Bucket: aws.String(bucket), Key: aws.String(firstKey),
})
return isS3NotFound(err)
}, 30*time.Second, 500*time.Millisecond, "positive control: %s should expire while rule is active", firstKey)
// Now remove the rule. The worker must observe the removal on the
// next sweep.
_, err := c.DeleteBucketLifecycle(context.Background(), &s3.DeleteBucketLifecycleInput{
Bucket: aws.String(bucket),
})
require.NoError(t, err)
const survivorKey = "x/post-delete.txt"
putObject(t, c, bucket, survivorKey, "should-stay")
backdateMtime(t, fc, bucket, survivorKey, 30)
out = runLifecycleShard(t)
t.Logf("sweep 2 output (rule deleted):\n%s", out)
// Wait the same window; survivor MUST still exist after the worker
// has had every opportunity to (incorrectly) act on it.
time.Sleep(2 * time.Second)
_, err = c.HeadObject(context.Background(), &s3.HeadObjectInput{
Bucket: aws.String(bucket), Key: aws.String(survivorKey),
})
require.NoError(t, err, "after DeleteBucketLifecycle, sweeps must not expire %s", survivorKey)
}