carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package maintenanceguard_test
2
3import (
4 "encoding/json"
5 "errors"
6 "math"
7 "strings"
8 "testing"
9 "time"
10
11 guard "github.com/carlosframework/platform/internal/maintenanceguard"
12)
13
14func replace(t *testing.T, f fixture, key string, raw []byte) {
15 t.Helper()
16 _, etag, e := f.store.Get(ctx, key)
17 if e != nil {
18 t.Fatal(e)
19 }
20 if _, e = f.store.Put(ctx, key, raw, etag); e != nil {
21 t.Fatal(e)
22 }
23}
24func TestWireMalformedGuardAlwaysDenies(t *testing.T) {
25 cases := map[string]func([]byte) []byte{
26 "unknown": func(b []byte) []byte { return append(b[:len(b)-1], []byte(`,"unknown":1}`)...) },
27 "duplicate": func(b []byte) []byte { return append(b[:len(b)-1], []byte(`,"version":1}`)...) },
28 "case alias": func(b []byte) []byte { return []byte(strings.Replace(string(b), `"version"`, `"Version"`, 1)) },
29 "unknown phase": func(b []byte) []byte {
30 return []byte(strings.Replace(string(b), `"requested"`, `"awaiting-approval"`, 1))
31 },
32 "zero generation": func(b []byte) []byte {
33 return []byte(strings.Replace(string(b), `"generation":1`, `"generation":0`, 1))
34 },
35 "negative generation": func(b []byte) []byte {
36 return []byte(strings.Replace(string(b), `"generation":1`, `"generation":-1`, 1))
37 },
38 "overflow generation": func(b []byte) []byte {
39 return []byte(strings.Replace(string(b), `"generation":1`, `"generation":9223372036854775808`, 1))
40 },
41 "fraction": func(b []byte) []byte {
42 return []byte(strings.Replace(string(b), `"generation":1`, `"generation":1.5`, 1))
43 },
44 "version": func(b []byte) []byte { return []byte(strings.Replace(string(b), `"version":1`, `"version":2`, 1)) },
45 "trailing": func(b []byte) []byte { return append(b, []byte(` {}`)...) },
46 "null": func([]byte) []byte { return []byte(`null`) },
47 "deep": func([]byte) []byte { return []byte(strings.Repeat("[", 1000) + strings.Repeat("]", 1000)) },
48 "oversize": func([]byte) []byte { return []byte(strings.Repeat(" ", guard.MaxWireBytes+1)) },
49 "null reference": func(b []byte) []byte {
50 return []byte(strings.Replace(string(b), `"required_contracts":[]`, `"required_contracts":null`, 1))
51 },
52 }
53 for name, mutate := range cases {
54 t.Run(name, func(t *testing.T) {
55 f := setup(t)
56 c := f.claim(t, 1)
57 s := f.create(t, c)
58 p, _ := s.Projection()
59 key, _ := guard.GuardKey(f.identity.Host)
60 raw, _, e := f.store.Get(ctx, key)
61 if e != nil {
62 t.Fatal(e)
63 }
64 replace(t, f, key, mutate(raw))
65 if _, e = f.repo.ReadGuard(ctx, f.identity.Host); e == nil {
66 t.Fatal("invalid guard parsed")
67 }
68 denied(t, f, &p, nil)
69 })
70 }
71}
72func TestProjectionClosedAndBounded(t *testing.T) {
73 p := guard.Projection{Version: 1, Generation: 3, GuardDigest: hash("guard")}
74 b, e := guard.EncodeProjection(p)
75 if e != nil {
76 t.Fatal(e)
77 }
78 if got, e := guard.DecodeProjection(b); e != nil || got != p {
79 t.Fatal(got, e)
80 }
81 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)} {
82 if _, e := guard.DecodeProjection([]byte(raw)); e == nil {
83 t.Fatal("invalid projection parsed")
84 }
85 }
86}
87func TestLifecycleRecoveryNoTimeoutAndRetainedHistory(t *testing.T) {
88 f := setup(t)
89 c := f.claim(t, 1)
90 restarted := guard.New(f.store)
91 recovered, e := restarted.RecoverClaim(ctx, "aaa", "home", id(1), guard.MaintenanceOperation)
92 if e != nil || recovered.Value() != c.Value() {
93 t.Fatal(e)
94 }
95 for _, kind := range []guard.OperationKind{guard.TrashOperation, guard.RestoreOperation, guard.PurgeOperation, guard.MaintenanceOperation} {
96 if _, e = restarted.AcquireClaim(ctx, "aaa", "home", id(2), kind); !errors.Is(e, guard.ErrBusy) {
97 t.Fatal("occupied claim bypassed", e)
98 }
99 }
100 if _, e = restarted.RecoverClaim(ctx, "aaa", "home", id(2), guard.MaintenanceOperation); !errors.Is(e, guard.ErrConflict) {
101 t.Fatal(e)
102 }
103 if _, e = restarted.RecoverClaim(ctx, "aaa", "home", id(1), guard.PurgeOperation); !errors.Is(e, guard.ErrConflict) {
104 t.Fatal(e)
105 }
106 released, e := restarted.ReleaseClaim(ctx, recovered)
107 if e != nil || released.Value().Generation != 2 || released.Value().State != "idle" {
108 t.Fatal(e)
109 }
110 next, e := restarted.AcquireClaim(ctx, "aaa", "home", id(2), guard.PurgeOperation)
111 if e != nil || next.Value().Generation != 3 {
112 t.Fatal(e)
113 }
114 if _, e = restarted.ReleaseClaim(ctx, recovered); !errors.Is(e, guard.ErrConflict) {
115 t.Fatal("stale release cleared new claim", e)
116 }
117 if _, e = restarted.ReleaseClaim(ctx, next); e != nil {
118 t.Fatal(e)
119 }
120 if _, e = restarted.AcquireClaim(ctx, "aaa", "home", id(1), guard.MaintenanceOperation); !errors.Is(e, guard.ErrConflict) {
121 t.Fatal("old operation ID reused", e)
122 }
123 keys, e := f.store.List(ctx, "control/maintenance/apps/aaa/home/operations/")
124 if e != nil || len(keys) != 4 {
125 t.Fatal("immutable history missing", len(keys), e)
126 }
127}
128func TestMalformedLifecycleAndOverflowRefuse(t *testing.T) {
129 f := setup(t)
130 c := f.claim(t, 1)
131 key, _ := guard.LifecycleKey("aaa", "home")
132 raw, _, e := f.store.Get(ctx, key)
133 if e != nil {
134 t.Fatal(e)
135 }
136 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}`} {
137 replace(t, f, key, []byte(bad))
138 if _, e = f.repo.AcquireClaim(ctx, "aaa", "home", id(2), guard.PurgeOperation); e == nil {
139 t.Fatal("malformed claim acquired")
140 }
141 }
142 replace(t, f, key, raw)
143 l := c.Value()
144 l.Generation = math.MaxInt64
145 l.ClaimGeneration = math.MaxInt64
146 b, _ := json.Marshal(l)
147 replace(t, f, key, b)
148 intentKey := "control/maintenance/apps/aaa/home/operations/" + id(1) + "/claim.json"
149 var intent map[string]json.RawMessage
150 old, _, e := f.store.Get(ctx, intentKey)
151 if e != nil {
152 t.Fatal(e)
153 }
154 if e = json.Unmarshal(old, &intent); e != nil {
155 t.Fatal(e)
156 }
157 intent["generation"] = []byte("9223372036854775807")
158 b, _ = json.Marshal(intent)
159 replace(t, f, intentKey, b)
160 loaded, e := f.repo.ReadLifecycle(ctx, "aaa", "home")
161 if e != nil {
162 t.Fatal(e)
163 }
164 if _, e = f.repo.ReleaseClaim(ctx, loaded); !errors.Is(e, guard.ErrConflict) {
165 t.Fatal("generation overflow", e)
166 }
167}
168func TestGuardGenerationOverflowRefuses(t *testing.T) {
169 f := setup(t)
170 c := f.claim(t, 1)
171 s := f.create(t, c)
172 g := s.Value()
173 g.Generation = math.MaxInt64
174 b, _ := json.Marshal(g)
175 key, _ := guard.GuardKey(g.Identity.Host)
176 replace(t, f, key, b)
177 s, e := f.repo.ReadGuard(ctx, g.Identity.Host)
178 if e != nil {
179 t.Fatal(e)
180 }
181 g.Phase = guard.Quiescing
182 if _, e = f.repo.AdvanceGuard(ctx, c, s, g); !errors.Is(e, guard.ErrConflict) {
183 t.Fatal("guard generation overflow", e)
184 }
185}
186func TestIdentityFramingKeysAndContracts(t *testing.T) {
187 at := time.Date(2026, 9, 8, 0, 1, 2, 3, time.FixedZone("offset", 3600))
188 a, e := guard.RecordIdentity("aaa", "bc", "one.home.example", at)
189 if e != nil {
190 t.Fatal(e)
191 }
192 same, e := guard.RecordIdentity("aaa", "bc", "one.home.example", at.UTC())
193 if e != nil || same != a {
194 t.Fatal("UTC identity differs")
195 }
196 other, _ := guard.RecordIdentity("aaab", "c", "one.home.example", at)
197 if other == a {
198 t.Fatal("unframed identity collision")
199 }
200 if _, e = guard.RecordIdentity("aaa", "home", "one.home.example", time.Time{}); e == nil {
201 t.Fatal("zero creation")
202 }
203 if _, e = guard.RecordIdentity("aaa", "home", "one.home.example", time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC)); e == nil {
204 t.Fatal("non-RFC3339 creation")
205 }
206 for _, host := range []string{"", "../other", "A.example", "a.example:443", "a.example.", strings.Repeat("a", 254)} {
207 if _, e := guard.GuardKey(host); e == nil {
208 t.Fatal("host key accepted", host)
209 }
210 }
211 for _, bad := range []string{"*", "Doc.v1", "docs..v1", "docs/v1", "docs-", "docs_v1", strings.Repeat("a", 65)} {
212 if guard.ValidContract(bad) {
213 t.Fatal("contract accepted", bad)
214 }
215 }
216 set := []string{}
217 for i := 0; i < 16; i++ {
218 set = append(set, "docs.v"+string(rune('a'+i)))
219 }
220 if !guard.ValidContracts(set) || guard.ValidContracts(append(set, "docs.vz")) || guard.ValidContracts([]string{"docs.v1", "docs.v1"}) || guard.ValidContracts([]string{"z.v1", "a.v1"}) {
221 t.Fatal("contract set bound/order")
222 }
223 if !guard.ValidID(id(1)) || guard.ValidID(id(1)+"=") || guard.ValidDigest(strings.ToUpper(hash("digest"))) {
224 t.Fatal("canonical digest/id")
225 }
226}
227
228func TestPermanentReferenceDigestMismatchDenies(t *testing.T) {
229 f := setup(t)
230 c := f.claim(t, 1)
231 s := f.resumed(t, c)
232 p, _ := s.Projection()
233 key, _ := guard.RequestKey(f.identity.Host, s.Value().Request.ID)
234 replace(t, f, key, []byte(`{"metadata":"changed outside immutable writer"}`))
235 denied(t, f, &p, []string{"home.operator.v1"})
236}
237
238func TestDirectHelpersBoundBeforeAllocation(t *testing.T) {
239 raw := strings.Repeat("a", 1<<20)
240 for name, fn := range map[string]func(string) bool{"id": guard.ValidID, "digest": guard.ValidDigest} {
241 t.Run(name, func(t *testing.T) {
242 if allocations := testing.AllocsPerRun(10, func() {
243 if fn(raw) {
244 panic("oversized helper input accepted")
245 }
246 }); allocations != 0 {
247 t.Fatalf("oversized direct input allocated before refusal: %v", allocations)
248 }
249 })
250 }
251}
252func TestLegacyUnmanagedIdentityNeedsNoCreationDigest(t *testing.T) {
253 f := setup(t)
254 legacy := f.identity
255 legacy.Record = ""
256 if d, e := f.repo.CheckServing(ctx, legacy, nil, nil); e != nil || d != guard.Unmanaged {
257 t.Fatalf("legacy route: %v %v", d, e)
258 }
259 bogus := legacy
260 bogus.Host = "../foreign"
261 if d, e := f.repo.CheckServing(ctx, bogus, nil, nil); e == nil || d != guard.Denied {
262 t.Fatal("invalid location accepted")
263 }
264 p := guard.Projection{Version: 1, Generation: 1, GuardDigest: hash("absent")}
265 if d, e := f.repo.CheckServing(ctx, legacy, &p, nil); e == nil || d != guard.Denied {
266 t.Fatal("projection alone accepted")
267 }
268 c := f.claim(t, 1)
269 s := f.create(t, c)
270 for _, phase := range []guard.Phase{guard.Quiescing, guard.Fenced, guard.ResumeRequested, guard.Resumed} {
271 s = f.step(t, c, s, phase)
272 }
273 p, e := s.Projection()
274 if e != nil {
275 t.Fatal(e)
276 }
277 for _, projection := range []*guard.Projection{nil, &p} {
278 if d, e := f.repo.CheckServing(ctx, legacy, projection, []string{"home.operator.v1"}); e == nil || d != guard.Denied {
279 t.Fatal("managed record lacking identity accepted")
280 }
281 }
282}
283