package lifecycle_xml
import (
"reflect"
"testing"
"time"
)
// parseLifecycle is a thin helper for tests; production code reads XML via
// the regular bucket-config decoder, this shortcut goes through the
// package's public Parse so the tests exercise the exported entrypoint.
func parseLifecycle(t *testing.T, xmlSrc string) *Lifecycle {
t.Helper()
lc, err := Parse([]byte(xmlSrc))
if err != nil {
t.Fatalf("parse: %v", err)
}
return lc
}
func TestLifecycleToCanonical_TopLevelPrefix(t *testing.T) {
lc := parseLifecycle(t, `
r1
Enabled
logs/
30
`)
got := LifecycleToCanonical(lc)
if len(got) != 1 {
t.Fatalf("want 1 rule, got %d", len(got))
}
r := got[0]
if r.ID != "r1" || r.Status != "Enabled" || r.Prefix != "logs/" || r.ExpirationDays != 30 {
t.Fatalf("unexpected: %+v", r)
}
}
func TestLifecycleToCanonical_FilterPrefix(t *testing.T) {
lc := parseLifecycle(t, `
Enabled
logs/
30
`)
got := LifecycleToCanonical(lc)
if got[0].Prefix != "logs/" {
t.Fatalf("want logs/, got %q", got[0].Prefix)
}
}
func TestLifecycleToCanonical_FilterTagAndSize(t *testing.T) {
lc := parseLifecycle(t, `
Enabled
data/
envprod
tiercold
1024
10485760
90
`)
got := LifecycleToCanonical(lc)[0]
if got.Prefix != "data/" {
t.Fatalf("prefix want data/, got %q", got.Prefix)
}
wantTags := map[string]string{"env": "prod", "tier": "cold"}
if !reflect.DeepEqual(got.FilterTags, wantTags) {
t.Fatalf("tags want %v, got %v", wantTags, got.FilterTags)
}
if got.FilterSizeGreaterThan != 1024 || got.FilterSizeLessThan != 10485760 {
t.Fatalf("size: gt=%d lt=%d", got.FilterSizeGreaterThan, got.FilterSizeLessThan)
}
}
func TestLifecycleToCanonical_SingleTagFilter(t *testing.T) {
lc := parseLifecycle(t, `
Enabled
envprod
30
`)
got := LifecycleToCanonical(lc)[0]
if !reflect.DeepEqual(got.FilterTags, map[string]string{"env": "prod"}) {
t.Fatalf("tags want {env:prod}, got %v", got.FilterTags)
}
}
func TestLifecycleToCanonical_MultipleActions(t *testing.T) {
// One XML with three concurrent actions: Expiration.Days,
// NoncurrentVersionExpiration, AbortMultipartUpload. All must populate
// the corresponding canonical fields so RuleActionKinds expands them
// into three compiled actions downstream.
lc := parseLifecycle(t, `
multi
Enabled
data/
90
30
3
7
`)
got := LifecycleToCanonical(lc)[0]
if got.ExpirationDays != 90 {
t.Fatalf("ExpirationDays want 90, got %d", got.ExpirationDays)
}
if got.NoncurrentVersionExpirationDays != 30 || got.NewerNoncurrentVersions != 3 {
t.Fatalf("NoncurrentVersion: %+v", got)
}
if got.AbortMPUDaysAfterInitiation != 7 {
t.Fatalf("AbortMPU want 7, got %d", got.AbortMPUDaysAfterInitiation)
}
}
func TestLifecycleToCanonical_ExpirationDate(t *testing.T) {
lc := parseLifecycle(t, `
Enabled
2025-06-15T00:00:00Z
`)
got := LifecycleToCanonical(lc)[0]
want, _ := time.Parse(time.RFC3339, "2025-06-15T00:00:00Z")
if !got.ExpirationDate.Equal(want) {
t.Fatalf("date want %v, got %v", want, got.ExpirationDate)
}
}
func TestLifecycleToCanonical_ExpiredObjectDeleteMarker(t *testing.T) {
lc := parseLifecycle(t, `
Enabled
true
`)
got := LifecycleToCanonical(lc)[0]
if !got.ExpiredObjectDeleteMarker {
t.Fatalf("want ExpiredObjectDeleteMarker=true")
}
}
func TestLifecycleToCanonical_DisabledRulePreserved(t *testing.T) {
// The engine's mode gate decides scheduling; conversion must preserve
// the Status verbatim regardless.
lc := parseLifecycle(t, `
Disabled
x/
30
`)
got := LifecycleToCanonical(lc)[0]
if got.Status != "Disabled" {
t.Fatalf("status want Disabled, got %q", got.Status)
}
}
func TestLifecycleToCanonical_NilSafe(t *testing.T) {
if got := LifecycleToCanonical(nil); got != nil {
t.Fatalf("nil lc should return nil, got %v", got)
}
}
func TestLifecycleToCanonical_EmptyRules(t *testing.T) {
got := LifecycleToCanonical(&Lifecycle{})
if len(got) != 0 {
t.Fatalf("empty rules should be empty slice, got %d", len(got))
}
}