package maintenanceguard_test import ( "encoding/json" "errors" "math" "strings" "testing" "time" guard "github.com/carlosframework/platform/internal/maintenanceguard" ) func replace(t *testing.T, f fixture, key string, raw []byte) { t.Helper() _, etag, e := f.store.Get(ctx, key) if e != nil { t.Fatal(e) } if _, e = f.store.Put(ctx, key, raw, etag); e != nil { t.Fatal(e) } } func TestWireMalformedGuardAlwaysDenies(t *testing.T) { cases := map[string]func([]byte) []byte{ "unknown": func(b []byte) []byte { return append(b[:len(b)-1], []byte(`,"unknown":1}`)...) }, "duplicate": func(b []byte) []byte { return append(b[:len(b)-1], []byte(`,"version":1}`)...) }, "case alias": func(b []byte) []byte { return []byte(strings.Replace(string(b), `"version"`, `"Version"`, 1)) }, "unknown phase": func(b []byte) []byte { return []byte(strings.Replace(string(b), `"requested"`, `"awaiting-approval"`, 1)) }, "zero generation": func(b []byte) []byte { return []byte(strings.Replace(string(b), `"generation":1`, `"generation":0`, 1)) }, "negative generation": func(b []byte) []byte { return []byte(strings.Replace(string(b), `"generation":1`, `"generation":-1`, 1)) }, "overflow generation": func(b []byte) []byte { return []byte(strings.Replace(string(b), `"generation":1`, `"generation":9223372036854775808`, 1)) }, "fraction": func(b []byte) []byte { return []byte(strings.Replace(string(b), `"generation":1`, `"generation":1.5`, 1)) }, "version": func(b []byte) []byte { return []byte(strings.Replace(string(b), `"version":1`, `"version":2`, 1)) }, "trailing": func(b []byte) []byte { return append(b, []byte(` {}`)...) }, "null": func([]byte) []byte { return []byte(`null`) }, "deep": func([]byte) []byte { return []byte(strings.Repeat("[", 1000) + strings.Repeat("]", 1000)) }, "oversize": func([]byte) []byte { return []byte(strings.Repeat(" ", guard.MaxWireBytes+1)) }, "null reference": func(b []byte) []byte { return []byte(strings.Replace(string(b), `"required_contracts":[]`, `"required_contracts":null`, 1)) }, } for name, mutate := range cases { t.Run(name, func(t *testing.T) { f := setup(t) c := f.claim(t, 1) s := f.create(t, c) p, _ := s.Projection() key, _ := guard.GuardKey(f.identity.Host) raw, _, e := f.store.Get(ctx, key) if e != nil { t.Fatal(e) } replace(t, f, key, mutate(raw)) if _, e = f.repo.ReadGuard(ctx, f.identity.Host); e == nil { t.Fatal("invalid guard parsed") } denied(t, f, &p, nil) }) } } func TestProjectionClosedAndBounded(t *testing.T) { p := guard.Projection{Version: 1, Generation: 3, GuardDigest: hash("guard")} b, e := guard.EncodeProjection(p) if e != nil { t.Fatal(e) } if got, e := guard.DecodeProjection(b); e != nil || got != p { t.Fatal(got, e) } for _, raw := range []string{`null`, `{}`, `{"version":2,"generation":3,"guard_digest":"` + p.GuardDigest + `"}`, string(b[:len(b)-1]) + `,"version":1}`, string(b[:len(b)-1]) + `,"other":true}`, strings.Replace(string(b), `"version"`, `"Version"`, 1), strings.Repeat("x", guard.MaxWireBytes+1)} { if _, e := guard.DecodeProjection([]byte(raw)); e == nil { t.Fatal("invalid projection parsed") } } } func TestLifecycleRecoveryNoTimeoutAndRetainedHistory(t *testing.T) { f := setup(t) c := f.claim(t, 1) restarted := guard.New(f.store) recovered, e := restarted.RecoverClaim(ctx, "aaa", "home", id(1), guard.MaintenanceOperation) if e != nil || recovered.Value() != c.Value() { t.Fatal(e) } for _, kind := range []guard.OperationKind{guard.TrashOperation, guard.RestoreOperation, guard.PurgeOperation, guard.MaintenanceOperation} { if _, e = restarted.AcquireClaim(ctx, "aaa", "home", id(2), kind); !errors.Is(e, guard.ErrBusy) { t.Fatal("occupied claim bypassed", e) } } if _, e = restarted.RecoverClaim(ctx, "aaa", "home", id(2), guard.MaintenanceOperation); !errors.Is(e, guard.ErrConflict) { t.Fatal(e) } if _, e = restarted.RecoverClaim(ctx, "aaa", "home", id(1), guard.PurgeOperation); !errors.Is(e, guard.ErrConflict) { t.Fatal(e) } released, e := restarted.ReleaseClaim(ctx, recovered) if e != nil || released.Value().Generation != 2 || released.Value().State != "idle" { t.Fatal(e) } next, e := restarted.AcquireClaim(ctx, "aaa", "home", id(2), guard.PurgeOperation) if e != nil || next.Value().Generation != 3 { t.Fatal(e) } if _, e = restarted.ReleaseClaim(ctx, recovered); !errors.Is(e, guard.ErrConflict) { t.Fatal("stale release cleared new claim", e) } if _, e = restarted.ReleaseClaim(ctx, next); e != nil { t.Fatal(e) } if _, e = restarted.AcquireClaim(ctx, "aaa", "home", id(1), guard.MaintenanceOperation); !errors.Is(e, guard.ErrConflict) { t.Fatal("old operation ID reused", e) } keys, e := f.store.List(ctx, "control/maintenance/apps/aaa/home/operations/") if e != nil || len(keys) != 4 { t.Fatal("immutable history missing", len(keys), e) } } func TestMalformedLifecycleAndOverflowRefuse(t *testing.T) { f := setup(t) c := f.claim(t, 1) key, _ := guard.LifecycleKey("aaa", "home") raw, _, e := f.store.Get(ctx, key) if e != nil { t.Fatal(e) } for _, bad := range []string{strings.Replace(string(raw), `"claimed"`, `"expired"`, 1), strings.Replace(string(raw), `"version":1`, `"version":2`, 1), string(raw[:len(raw)-1]) + `,"expires_at":1}`} { replace(t, f, key, []byte(bad)) if _, e = f.repo.AcquireClaim(ctx, "aaa", "home", id(2), guard.PurgeOperation); e == nil { t.Fatal("malformed claim acquired") } } replace(t, f, key, raw) l := c.Value() l.Generation = math.MaxInt64 l.ClaimGeneration = math.MaxInt64 b, _ := json.Marshal(l) replace(t, f, key, b) intentKey := "control/maintenance/apps/aaa/home/operations/" + id(1) + "/claim.json" var intent map[string]json.RawMessage old, _, e := f.store.Get(ctx, intentKey) if e != nil { t.Fatal(e) } if e = json.Unmarshal(old, &intent); e != nil { t.Fatal(e) } intent["generation"] = []byte("9223372036854775807") b, _ = json.Marshal(intent) replace(t, f, intentKey, b) loaded, e := f.repo.ReadLifecycle(ctx, "aaa", "home") if e != nil { t.Fatal(e) } if _, e = f.repo.ReleaseClaim(ctx, loaded); !errors.Is(e, guard.ErrConflict) { t.Fatal("generation overflow", e) } } func TestGuardGenerationOverflowRefuses(t *testing.T) { f := setup(t) c := f.claim(t, 1) s := f.create(t, c) g := s.Value() g.Generation = math.MaxInt64 b, _ := json.Marshal(g) key, _ := guard.GuardKey(g.Identity.Host) replace(t, f, key, b) s, e := f.repo.ReadGuard(ctx, g.Identity.Host) if e != nil { t.Fatal(e) } g.Phase = guard.Quiescing if _, e = f.repo.AdvanceGuard(ctx, c, s, g); !errors.Is(e, guard.ErrConflict) { t.Fatal("guard generation overflow", e) } } func TestIdentityFramingKeysAndContracts(t *testing.T) { at := time.Date(2026, 9, 8, 0, 1, 2, 3, time.FixedZone("offset", 3600)) a, e := guard.RecordIdentity("aaa", "bc", "one.home.example", at) if e != nil { t.Fatal(e) } same, e := guard.RecordIdentity("aaa", "bc", "one.home.example", at.UTC()) if e != nil || same != a { t.Fatal("UTC identity differs") } other, _ := guard.RecordIdentity("aaab", "c", "one.home.example", at) if other == a { t.Fatal("unframed identity collision") } if _, e = guard.RecordIdentity("aaa", "home", "one.home.example", time.Time{}); e == nil { t.Fatal("zero creation") } if _, e = guard.RecordIdentity("aaa", "home", "one.home.example", time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC)); e == nil { t.Fatal("non-RFC3339 creation") } for _, host := range []string{"", "../other", "A.example", "a.example:443", "a.example.", strings.Repeat("a", 254)} { if _, e := guard.GuardKey(host); e == nil { t.Fatal("host key accepted", host) } } for _, bad := range []string{"*", "Doc.v1", "docs..v1", "docs/v1", "docs-", "docs_v1", strings.Repeat("a", 65)} { if guard.ValidContract(bad) { t.Fatal("contract accepted", bad) } } set := []string{} for i := 0; i < 16; i++ { set = append(set, "docs.v"+string(rune('a'+i))) } if !guard.ValidContracts(set) || guard.ValidContracts(append(set, "docs.vz")) || guard.ValidContracts([]string{"docs.v1", "docs.v1"}) || guard.ValidContracts([]string{"z.v1", "a.v1"}) { t.Fatal("contract set bound/order") } if !guard.ValidID(id(1)) || guard.ValidID(id(1)+"=") || guard.ValidDigest(strings.ToUpper(hash("digest"))) { t.Fatal("canonical digest/id") } } func TestPermanentReferenceDigestMismatchDenies(t *testing.T) { f := setup(t) c := f.claim(t, 1) s := f.resumed(t, c) p, _ := s.Projection() key, _ := guard.RequestKey(f.identity.Host, s.Value().Request.ID) replace(t, f, key, []byte(`{"metadata":"changed outside immutable writer"}`)) denied(t, f, &p, []string{"home.operator.v1"}) } func TestDirectHelpersBoundBeforeAllocation(t *testing.T) { raw := strings.Repeat("a", 1<<20) for name, fn := range map[string]func(string) bool{"id": guard.ValidID, "digest": guard.ValidDigest} { t.Run(name, func(t *testing.T) { if allocations := testing.AllocsPerRun(10, func() { if fn(raw) { panic("oversized helper input accepted") } }); allocations != 0 { t.Fatalf("oversized direct input allocated before refusal: %v", allocations) } }) } } func TestLegacyUnmanagedIdentityNeedsNoCreationDigest(t *testing.T) { f := setup(t) legacy := f.identity legacy.Record = "" if d, e := f.repo.CheckServing(ctx, legacy, nil, nil); e != nil || d != guard.Unmanaged { t.Fatalf("legacy route: %v %v", d, e) } bogus := legacy bogus.Host = "../foreign" if d, e := f.repo.CheckServing(ctx, bogus, nil, nil); e == nil || d != guard.Denied { t.Fatal("invalid location accepted") } p := guard.Projection{Version: 1, Generation: 1, GuardDigest: hash("absent")} if d, e := f.repo.CheckServing(ctx, legacy, &p, nil); e == nil || d != guard.Denied { t.Fatal("projection alone accepted") } c := f.claim(t, 1) s := f.create(t, c) for _, phase := range []guard.Phase{guard.Quiescing, guard.Fenced, guard.ResumeRequested, guard.Resumed} { s = f.step(t, c, s, phase) } p, e := s.Projection() if e != nil { t.Fatal(e) } for _, projection := range []*guard.Projection{nil, &p} { if d, e := f.repo.CheckServing(ctx, legacy, projection, []string{"home.operator.v1"}); e == nil || d != guard.Denied { t.Fatal("managed record lacking identity accepted") } } }