carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "os"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/svcrole"
15)
16
17// The console's pointer digest doors (#253, spec §4.6): `carlos channels
18// resign`, `carlos channels digests` and the digest state on `carlos
19// channels`.
20
21func TestResignDoorPlansThenSigns(t *testing.T) {
22 fx := newDigestsFixture(t)
23 st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]string{"channel": "beta"}, fx.member)
24 if st != http.StatusOK {
25 t.Fatalf("plan: %d %s", st, out)
26 }
27 var plan struct {
28 Version string `json:"version"`
29 Digest string `json:"digest"`
30 History []string `json:"history"`
31 HistoryDigests []string `json:"history_digests"`
32 Computed []bool `json:"computed"`
33 }
34 if err := json.Unmarshal([]byte(out), &plan); err != nil || plan.Digest == "" {
35 t.Fatalf("plan body %s: %v", out, err)
36 }
37 if plan.Version != "v2" || len(plan.History) != 1 || plan.History[0] != "v1" || len(plan.HistoryDigests) != 1 ||
38 plan.HistoryDigests[0] == "" || len(plan.Computed) != 2 || !plan.Computed[0] || !plan.Computed[1] {
39 t.Fatalf("plan = %+v, want v2 over v1, both digests read from the bucket now (beta carries no rules, so a member signs it)", plan)
40 }
41 if p, _ := release.Channel(context.Background(), fx.store, fx.pub, "bab", "km", "beta"); p.Digest != "" {
42 t.Fatal("planning signed something")
43 }
44 st, out = postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]any{
45 "channel": "beta", "digest": plan.Digest, "history_digests": plan.HistoryDigests}, fx.member)
46 if st != http.StatusOK || !strings.Contains(out, `"result":"resigned"`) {
47 t.Fatalf("sign: %d %s", st, out)
48 }
49 p, err := release.Channel(context.Background(), fx.store, fx.pub, "bab", "km", "beta")
50 if err != nil || p.Digest != plan.Digest || len(p.HistoryDigests) != 1 || p.HistoryDigests[0] != plan.HistoryDigests[0] {
51 t.Fatalf("pointer after resign = %+v, %v; want the plan's digests", p, err)
52 }
53 events, err := fx.st.Events(OpsAccountID, "km", 10)
54 if err != nil {
55 t.Fatal(err)
56 }
57 found := false
58 for _, e := range events {
59 if e.Action == "resign" && e.Address == "alice@keymail.dev" && strings.Contains(e.Detail, "(cli)") {
60 found = true
61 }
62 }
63 if !found {
64 t.Fatalf("events = %v, want a resign row by the member that names the CLI", events)
65 }
66}
67
68func TestResignDoorRefusesDigestsThatAreNotThePlan(t *testing.T) {
69 fx := newDigestsFixture(t)
70 st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]any{
71 "channel": "beta", "digest": release.ManifestDigest([]byte("not it")), "history_digests": []string{""}}, fx.member)
72 if st != http.StatusConflict || jsonErrorText(t, out) != fmt.Sprintf(release.TextResignChanged, "beta") {
73 t.Fatalf("mismatched confirmation: %d %s", st, out)
74 }
75 if p, _ := release.Channel(context.Background(), fx.store, fx.pub, "bab", "km", "beta"); p.Digest != "" {
76 t.Fatal("a refused confirmation signed something")
77 }
78}
79
80func TestResignDoorRefusesServiceCredentials(t *testing.T) {
81 fx := newDigestsFixture(t)
82 st, _ := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]string{"channel": "stable"}, fx.serviceToken)
83 if st != http.StatusNotFound && st != http.StatusForbidden {
84 t.Fatalf("a service credential reached resign: %d", st)
85 }
86}
87
88// TestResignDoorSpendsTheWriteBudget: resign signs a pointer, so it draws on
89// the same per-person write budget as rollback, before any store work.
90func TestResignDoorSpendsTheWriteBudget(t *testing.T) {
91 fx := newDigestsFixture(t)
92 fx.srv.cli.write = newRateLimiter(1, time.Minute)
93 body := map[string]string{"channel": "stable"}
94 if st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", body, fx.member); st != http.StatusOK {
95 t.Fatalf("first request: %d %s", st, out)
96 }
97 if st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", body, fx.member); st != http.StatusTooManyRequests || jsonErrorText(t, out) != textTooManyRequests {
98 t.Fatalf("second request over the budget: %d %s", st, out)
99 }
100}
101
102// Astra r1 #8: the digest doors reuse the rollback door's one definition of
103// the rate-limit refusal instead of repeating its literal, so their source
104// adds no em dash.
105func TestDigestDoorsDefineNoEmDashLiteral(t *testing.T) {
106 src, err := os.ReadFile("apidigests.go")
107 if err != nil {
108 t.Fatal(err)
109 }
110 if n := strings.Count(string(src), "\u2014"); n != 0 {
111 t.Fatalf("apidigests.go carries %d em dash(es); reuse textTooManyRequests", n)
112 }
113}
114
115func TestSystemDigestsDoorTurnsOnAndListsStragglers(t *testing.T) {
116 fx := newDigestsFixture(t)
117 ctx := context.Background()
118 st, out := postRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", map[string]any{"on": false}, fx.member)
119 if st != http.StatusNotFound {
120 t.Fatalf("a member who is not an operator turned digests off: %d %s", st, out)
121 }
122 if m, err := release.ReadDigestMarker(ctx, fx.store, fx.pub, "bab"); m == nil || err != nil {
123 t.Fatalf("the member's refused request moved the marker: %+v, %v", m, err)
124 }
125 st, out = postRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", map[string]any{"on": false}, fx.operator)
126 if st != http.StatusOK || !strings.Contains(out, `"account":"bab","on":false`) {
127 t.Fatalf("operator off: %d %s", st, out)
128 }
129 if m, err := release.ReadDigestMarker(ctx, fx.store, fx.pub, "bab"); m != nil || err != nil {
130 t.Fatalf("marker after off = %+v, %v", m, err)
131 }
132 st, out = postRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", map[string]any{"on": true, "accounts": []string{"bab"}}, fx.operator)
133 if st != http.StatusOK || !strings.Contains(out, `"account":"bab","on":true`) {
134 t.Fatalf("operator on: %d %s", st, out)
135 }
136 if m, err := release.ReadDigestMarker(ctx, fx.store, fx.pub, "bab"); m == nil || err != nil {
137 t.Fatalf("marker = %+v, %v", m, err)
138 }
139 st, out = getRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", fx.operator)
140 if st != http.StatusOK || !strings.Contains(out, `"account":"bab","app":"km","channel":"stable"`) {
141 t.Fatalf("stable has no digests yet and must be listed: %d %s", st, out)
142 }
143 if st, out := getRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", fx.member); st != http.StatusNotFound {
144 t.Fatalf("a member who is not an operator read the report: %d %s", st, out)
145 }
146}
147
148// TestSystemDigestsDoorListsOnlyHeadsWithoutDigests: once canary/x's head
149// carries a digest it leaves the list, and stable stays on it.
150func TestSystemDigestsDoorListsOnlyHeadsWithoutDigests(t *testing.T) {
151 fx := newDigestsFixture(t)
152 fx.markerThenPromote(t, "v2")
153 st, out := getRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", fx.operator)
154 if st != http.StatusOK {
155 t.Fatalf("report: %d %s", st, out)
156 }
157 var rep struct {
158 Channels []struct{ Account, App, Channel string } `json:"channels"`
159 }
160 if err := json.Unmarshal([]byte(out), &rep); err != nil {
161 t.Fatalf("report body %s: %v", out, err)
162 }
163 listed := map[string]bool{}
164 for _, c := range rep.Channels {
165 listed[c.Account+"/"+c.App+"/"+c.Channel] = true
166 }
167 if !listed["bab/km/stable"] || listed["bab/km/canary/x"] {
168 t.Fatalf("listed = %v, want km's stable and not its canary/x", listed)
169 }
170}
171
172// Astra r1 #4: a channel whose pointer cannot be verified is not skipped as
173// if it carried digests. The account's row carries the error, so the CLI
174// reports it unknown and never calls the fleet ready.
175func TestSystemDigestsDoorReportsAChannelItCannotRead(t *testing.T) {
176 fx := newDigestsFixture(t)
177 ctx := context.Background()
178 key := release.ChannelKey(release.OpsAccount, "km", "beta")
179 _, etag, err := fx.store.Get(ctx, key)
180 if err != nil {
181 t.Fatal(err)
182 }
183 if _, err := fx.store.Put(ctx, key, []byte("junk"), etag); err != nil {
184 t.Fatal(err)
185 }
186 st, out := getRaw(t, fx.ts.URL+"/api/cli/system/pointer-digests", fx.operator)
187 if st != http.StatusOK {
188 t.Fatalf("report: %d %s", st, out)
189 }
190 var rep struct {
191 Accounts []struct{ Account, Error string } `json:"accounts"`
192 Channels []struct{ Account, App, Channel string } `json:"channels"`
193 }
194 if err := json.Unmarshal([]byte(out), &rep); err != nil {
195 t.Fatalf("report body %s: %v", out, err)
196 }
197 rows, _, err := release.DigestReport(ctx, fx.store, fx.pub, release.OpsAccount, "km")
198 if err != nil {
199 t.Fatal(err)
200 }
201 want := ""
202 for _, r := range rows {
203 if r.Channel == "beta" {
204 want = r.Err
205 }
206 }
207 if want == "" {
208 t.Fatal("control: the junk pointer read as a channel state")
209 }
210 found := false
211 for _, a := range rep.Accounts {
212 if a.Account == release.OpsAccount {
213 found = true
214 if a.Error != want {
215 t.Fatalf("bab's row error = %q, want %q", a.Error, want)
216 }
217 }
218 }
219 if !found {
220 t.Fatalf("no row for bab: %s", out)
221 }
222 for _, c := range rep.Channels {
223 if c.Channel == "beta" {
224 t.Fatalf("the unreadable channel was listed as a state: %s", out)
225 }
226 }
227}
228
229func TestChannelsDoorCarriesDigestState(t *testing.T) {
230 fx := newDigestsFixture(t)
231 st, out := getRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels", fx.member)
232 if st != http.StatusOK || !strings.Contains(out, `"digests":false`) || !strings.Contains(out, `"straggler":false`) ||
233 !strings.Contains(out, `"history_missing":1`) || !strings.Contains(out, `"pointer_digests":{"on":true`) {
234 t.Fatalf("channels: %d %s", st, out)
235 }
236}
237
238func TestRollbackDoorNamesResign(t *testing.T) {
239 fx := newDigestsFixture(t) // v1 on canary/x before the marker
240 fx.markerThenPromote(t, "v2")
241 st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/rollback", map[string]string{"channel": "canary/x"}, fx.member)
242 want := (&release.ResignNeededError{App: "km", Channel: "canary/x", Version: "v1"}).Error()
243 if st != http.StatusConflict || jsonErrorText(t, out) != want {
244 t.Fatalf("rollback onto a digest-less entry: %d %s, want 409 %q", st, out, want)
245 }
246 if cp := fx.channelVersion(t, "km", "canary/x"); cp.Version != "v2" {
247 t.Fatalf("canary/x after a refused rollback = %q, want v2", cp.Version)
248 }
249}
250
251// resignPlanOf asks the resign door for channel's plan, as a member may.
252func (fx *digestsFixture) resignPlanOf(t *testing.T, channel string) (digest string, historyDigests []string) {
253 t.Helper()
254 st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]string{"channel": channel}, fx.member)
255 if st != http.StatusOK {
256 t.Fatalf("plan %s: %d %s", channel, st, out)
257 }
258 var plan struct {
259 Digest string `json:"digest"`
260 HistoryDigests []string `json:"history_digests"`
261 }
262 if err := json.Unmarshal([]byte(out), &plan); err != nil || plan.Digest == "" {
263 t.Fatalf("plan body %s: %v", out, err)
264 }
265 return plan.Digest, plan.HistoryDigests
266}
267
268// rollbackRefusal is what the rollback door answers the member for channel:
269// the refusal a resign of that channel must repeat.
270func (fx *digestsFixture) rollbackRefusal(t *testing.T, channel string) (int, string) {
271 t.Helper()
272 st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/rollback", map[string]string{"channel": channel}, fx.member)
273 if st != http.StatusForbidden {
274 t.Fatalf("control: the rollback door let the member roll %s back: %d %s", channel, st, out)
275 }
276 return st, out
277}
278
279// Astra r1 #2: signing a resign takes the rollback door's authority. A
280// member may read stable's plan, but confirming it is refused exactly as a
281// rollback of stable is, and the pointer is unchanged.
282func TestResignDoorRefusesAMemberOnStable(t *testing.T) {
283 fx := newDigestsFixture(t)
284 digest, hd := fx.resignPlanOf(t, "stable")
285 wantSt, wantBody := fx.rollbackRefusal(t, "stable")
286 st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]any{
287 "channel": "stable", "digest": digest, "history_digests": hd}, fx.member)
288 if st != wantSt || out != wantBody {
289 t.Fatalf("member resign of stable: %d %s, want the rollback door's %d %s", st, out, wantSt, wantBody)
290 }
291 if p := fx.channelVersion(t, "km", "stable"); p.Digest != "" || p.Version != "v2" {
292 t.Fatalf("a refused resign moved stable: %+v", p)
293 }
294}
295
296// Astra r1 #2's attack: a legacy stable pointer names v2, and v2's manifest
297// is replaced in the bucket (pruned and reshipped). The plan offers the new
298// bytes' digest; a member confirming it would bind stable's v2 to bytes no
299// one released. Refused as rollback is, and the pointer still signs nothing.
300func TestResignDoorRefusesALegacyHeadSubstitution(t *testing.T) {
301 fx := newDigestsFixture(t)
302 ctx := context.Background()
303 key := release.ManifestKey(release.OpsAccount, "km", "v2")
304 orig, etag, err := fx.store.Get(ctx, key)
305 if err != nil {
306 t.Fatal(err)
307 }
308 var m map[string]any
309 if err := json.Unmarshal(orig, &m); err != nil {
310 t.Fatal(err)
311 }
312 m["built_at"] = "2026-10-07T00:00:00Z" // other bytes under the same version
313 swapped, err := json.Marshal(m)
314 if err != nil {
315 t.Fatal(err)
316 }
317 if _, err := fx.store.Put(ctx, key, swapped, etag); err != nil {
318 t.Fatal(err)
319 }
320 digest, hd := fx.resignPlanOf(t, "stable")
321 if digest != release.ManifestDigest(swapped) {
322 t.Fatalf("the plan offers %s, want the substituted manifest's digest", digest)
323 }
324 wantSt, wantBody := fx.rollbackRefusal(t, "stable")
325 st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]any{
326 "channel": "stable", "digest": digest, "history_digests": hd}, fx.member)
327 if st != wantSt || out != wantBody {
328 t.Fatalf("member resign of a substituted stable head: %d %s, want %d %s", st, out, wantSt, wantBody)
329 }
330 if p := fx.channelVersion(t, "km", "stable"); p.Digest != "" {
331 t.Fatalf("stable was bound to the substituted bytes: %+v", p)
332 }
333}
334
335// A pipelined channel's rules govern resign as they govern rollback, asked
336// inside the admitted mutation: a passkey rule refuses with the rollback
337// door's refusal, and an approvals rule (which rollback turns into a
338// proposal; resign has none to make) refuses the same way. The pointer is
339// unchanged either way.
340func TestResignDoorAsksTheChannelsPipelineRules(t *testing.T) {
341 for _, rules := range []release.ChannelRules{{Passkey: true}, {PromoteApprovals: 1}} {
342 fx := newDigestsFixture(t)
343 digest, hd := fx.resignPlanOf(t, "beta")
344 setPipeline(t, fx.consoleFixture, "km", release.PipelineChannel{Name: "beta", Rules: release.ChannelRules{Passkey: true}})
345 wantSt, wantBody := fx.rollbackRefusal(t, "beta")
346 setPipeline(t, fx.consoleFixture, "km", release.PipelineChannel{Name: "beta", Rules: rules})
347 st, out := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/km/channels/resign", map[string]any{
348 "channel": "beta", "digest": digest, "history_digests": hd}, fx.member)
349 if st != wantSt || out != wantBody {
350 t.Fatalf("rules %+v: member resign of beta: %d %s, want the rollback door's %d %s", rules, st, out, wantSt, wantBody)
351 }
352 if p := fx.channelVersion(t, "km", "beta"); p.Digest != "" {
353 t.Fatalf("rules %+v: a refused resign signed beta: %+v", rules, p)
354 }
355 }
356}
357
358// jsonErrorText is the "error" of a jsonError body.
359func jsonErrorText(t *testing.T, body string) string {
360 t.Helper()
361 var e struct {
362 Error string `json:"error"`
363 }
364 if err := json.Unmarshal([]byte(body), &e); err != nil {
365 t.Fatalf("not a JSON error body: %s: %v", body, err)
366 }
367 return e.Error
368}
369
370// digestsFixture is newConsoleFixture with app km in the ops account bab:
371// v1 and v2 shipped, promoted v1 then v2 on stable and on beta, and v1 on
372// canary/x, all before the account's digest marker went on, so none of them carries a
373// digest. Then the marker, written with the root key (resign needs it, Astra
374// I3 #4).
375type digestsFixture struct {
376 *consoleFixture
377 store release.Store
378 member string // a CLI bearer for alice, a member of bab
379 operator string // a CLI bearer for paul, an owner of the ops account
380 serviceToken string // an app-scoped svcrole.Publish credential for km
381}
382
383func newDigestsFixture(t *testing.T) *digestsFixture {
384 t.Helper()
385 fx := newConsoleFixture(t)
386 now := time.Now()
387 if _, err := fx.st.CreateAppIn(OpsAccountID, "km", "paul@keymail.dev", now); err != nil {
388 t.Fatal(err)
389 }
390 if err := fx.st.AddMember(OpsAccountID, "alice@keymail.dev", RoleMember, now); err != nil {
391 t.Fatal(err)
392 }
393 shipVersion(t, fx, "km", "v1")
394 shipVersion(t, fx, "km", "v2")
395 seedChannel(t, fx, "km", "stable", "v1")
396 seedChannel(t, fx, "km", "stable", "v2")
397 seedChannel(t, fx, "km", "canary/x", "v1")
398 seedChannel(t, fx, "km", "beta", "v1")
399 seedChannel(t, fx, "km", "beta", "v2")
400 dx := &digestsFixture{
401 consoleFixture: fx,
402 store: fx.rel,
403 member: cliToken(t, fx, "alice@keymail.dev", "code-digests-member"),
404 operator: cliToken(t, fx, "paul@keymail.dev", "code-digests-operator"),
405 serviceToken: serviceToken(t, fx.st, OpsAccountID, "digests-ci", "km", svcrole.Publish),
406 }
407 dx.writeMarker(t)
408 return dx
409}
410
411func (fx *digestsFixture) writeMarker(t *testing.T) {
412 t.Helper()
413 if err := release.WriteDigestMarker(context.Background(), fx.store, release.RootSigner(fx.priv), release.OpsAccount, "paul@keymail.dev"); err != nil {
414 t.Fatal(err)
415 }
416}
417
418// markerThenPromote writes the marker (a no-op when it already verifies) and
419// promotes v to canary/x, which therefore carries v's digest.
420func (fx *digestsFixture) markerThenPromote(t *testing.T, v string) {
421 t.Helper()
422 fx.writeMarker(t)
423 seedChannel(t, fx.consoleFixture, "km", "canary/x", v)
424 if cp := fx.channelVersion(t, "km", "canary/x"); cp.Digest == "" {
425 t.Fatalf("canary/x promoted to %s with the marker on carries no digest", v)
426 }
427}
428