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)) } }