carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "crypto/ed25519"
6 "encoding/json"
7 "errors"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14func testWriter(t *testing.T) (*ControlWriter, *release.DirStore, func() time.Time) {
15 t.Helper()
16 raw := release.NewDirStore(t.TempDir())
17 pub, _, err := ed25519.GenerateKey(nil)
18 if err != nil {
19 t.Fatal(err)
20 }
21 now := time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC)
22 clock := func() time.Time { return now }
23 w, err := NewControlWriter(raw, pub, clock)
24 if err != nil {
25 t.Fatal(err)
26 }
27 return w, raw, clock
28}
29
30func seedAdmission(t *testing.T, s release.Store, account, app string, a AppAdmission) {
31 t.Helper()
32 body, _ := json.Marshal(a)
33 key, _ := AdmissionKey(account, app)
34 _, tag, _ := s.Get(context.Background(), key)
35 if _, err := s.Put(context.Background(), key, body, tag); err != nil {
36 t.Fatal(err)
37 }
38}
39
40func openAdmission(appID string) AppAdmission {
41 return AppAdmission{V: 1, AppID: appID, Revision: 1, Epoch: 1, State: "open", Registrations: map[string]Registration{}, Effects: map[string]Effect{}}
42}
43
44var testOwner = RegistrationOwner{Console: "console", Enrollment: "enrollment", Controller: "controller"}
45
46// Review P1-1: the data boundary refuses every canonical control object even
47// when its checker would allow everything, and even with a live permit.
48func TestDataBoundaryAlwaysRefusesControlObjects(t *testing.T) {
49 ctx := t.Context()
50 w, raw, clock := testWriter(t)
51 seedAdmission(t, raw, "bab", "demo", openAdmission("app"))
52 data, err := NewMutationBoundary(true, raw, func(context.Context, MutationTarget) error { return nil })
53 if err != nil {
54 t.Fatal(err)
55 }
56 // A real permit in context changes nothing.
57 h, err := w.BeginExclusive(ctx, "bab", "demo", "app", AppOperation{ID: "op", Kind: OpAppMutation, AppID: "app"}, testOwner)
58 if err != nil {
59 t.Fatal(err)
60 }
61 permitted, settle, err := h.Runner(clock).Issue(ctx, Effect{ID: "op", Resource: "app-mutation", Action: "test"})
62 if err != nil {
63 t.Fatal(err)
64 }
65 defer settle("joined", nil)
66 for _, key := range []string{"apps/bab/demo/admission.json", "apps/bab/demo/purge.json", "apps/bab/demo/purge-request.json", MailGateKey} {
67 if _, err := data.Put(permitted, key, []byte(`{}`), ""); !errors.Is(err, ErrControlKey) {
68 t.Fatalf("Put %s: %v", key, err)
69 }
70 if err := data.Delete(permitted, key); !errors.Is(err, ErrControlKey) {
71 t.Fatalf("Delete %s: %v", key, err)
72 }
73 if err := release.DeleteIfMatch(permitted, data, key, `"x"`); !errors.Is(err, ErrControlKey) {
74 t.Fatalf("DeleteIfMatch %s: %v", key, err)
75 }
76 }
77 if _, err := data.Put(permitted, "apps/BAD!/demo/x.json", []byte(`{}`), ""); !errors.Is(err, ErrIdentity) {
78 t.Fatalf("malformed app key: %v", err)
79 }
80}
81
82// Review P1-1: no transition can reopen a completed purge or forge
83// last_purge / last_restore; the only way to last_purge is CompletePurge.
84func TestTypedTransitionsCannotReopenOrForge(t *testing.T) {
85 ctx := t.Context()
86 w, raw, _ := testWriter(t)
87 done := openAdmission("old")
88 done.State, done.Revision, done.Epoch = "closed", 8, 3
89 done.LastPurge = &PurgeMark{ID: "purge", AppID: "old"}
90 seedAdmission(t, raw, "bab", "demo", done)
91 for _, kind := range []string{OpDeleting, OpPlacing, OpAppMutation} {
92 if _, err := w.BeginExclusive(ctx, "bab", "demo", "old", AppOperation{ID: "new-" + kind, Kind: kind, AppID: "old"}, testOwner); !errors.Is(err, ErrPending) {
93 t.Fatalf("%s reopened completed admission: %v", kind, err)
94 }
95 }
96 if _, err := w.BeginExclusive(ctx, "bab", "demo", "old", AppOperation{ID: "p", Kind: OpPurging, AppID: "old"}, testOwner); !errors.Is(err, ErrIdentity) {
97 t.Fatalf("purging through BeginExclusive: %v", err)
98 }
99 if _, err := w.ResumeExclusive(ctx, "bab", "demo", "old", "purge", OpPurging, testOwner); !errors.Is(err, ErrPending) {
100 t.Fatalf("resumed a finished purge: %v", err)
101 }
102 // nextAdmission refuses any last_purge change outside purge completion.
103 before, err := ReadAdmission(ctx, raw, "bab", "demo", "old")
104 if err != nil {
105 t.Fatal(err)
106 }
107 if _, err = nextAdmission(before, func(a *AppAdmission) error { a.LastPurge = &PurgeMark{ID: "forged", AppID: "old"}; return nil }); !errors.Is(err, ErrIdentity) {
108 t.Fatalf("forged last_purge: %v", err)
109 }
110}
111
112// Review P2-3: a crash after the receipt and before (or after) the tombstone
113// delete is finished by RecoverMutation; nothing stays wedged.
114func TestRestoreReceiptRecoversEveryCrashPoint(t *testing.T) {
115 for _, crash := range []string{"before-delete", "after-delete"} {
116 t.Run(crash, func(t *testing.T) {
117 ctx := t.Context()
118 w, raw, clock := testWriter(t)
119 seedAdmission(t, raw, "bab", "demo", openAdmission("app"))
120 if err := release.TrashApp(ctx, raw, "bab", "demo", "owner", clock()); err != nil {
121 t.Fatal(err)
122 }
123 _, tag, err := raw.Get(ctx, release.AppTombstoneKey("bab", "demo"))
124 if err != nil {
125 t.Fatal(err)
126 }
127 h, err := w.BeginExclusive(ctx, "bab", "demo", "app", AppOperation{ID: "op", Kind: OpAppMutation, AppID: "app", Intent: map[string]string{"action": "restore"}}, testOwner)
128 if err != nil {
129 t.Fatal(err)
130 }
131 if _, _, err = h.Runner(clock).Issue(ctx, Effect{ID: "op", Resource: "app-mutation", Action: "restore"}); err != nil {
132 t.Fatal(err)
133 }
134 if err = h.PrepareRestore(ctx, tag, clock().Unix()); err != nil {
135 t.Fatal(err)
136 }
137 if crash == "after-delete" {
138 if err = release.DeleteIfMatch(ctx, raw, release.AppTombstoneKey("bab", "demo"), tag); err != nil {
139 t.Fatal(err)
140 }
141 }
142 // The issuer is gone; the effect is stuck at issued.
143 if err = w.RecoverMutation(ctx, "bab", "demo", "app", "op"); err != nil {
144 t.Fatal(err)
145 }
146 a, err := ReadAdmission(ctx, raw, "bab", "demo", "app")
147 if err != nil {
148 t.Fatal(err)
149 }
150 if a.Value.State != "open" || a.Value.Op != nil || a.Value.LastRestore == nil || a.Value.LastRestore.State != "complete" || a.Value.Effects["op"].State != "complete" {
151 t.Fatalf("restore not reconciled: %+v", a.Value)
152 }
153 if _, _, err = raw.Get(ctx, release.AppTombstoneKey("bab", "demo")); err == nil {
154 t.Fatal("tombstone survived a completed restore")
155 }
156 if _, err = w.RestoreProjection(ctx, "bab", "demo", "app"); !errors.Is(err, ErrPending) {
157 t.Fatalf("projection without its own operation: %v", err)
158 }
159 p, err := w.BeginExclusive(ctx, "bab", "demo", "app", AppOperation{ID: "projection", Kind: OpAppMutation, AppID: "app", Intent: map[string]string{"action": "projection"}}, testOwner)
160 if err != nil {
161 t.Fatal(err)
162 }
163 pctx, settle, err := p.Runner(clock).Issue(ctx, Effect{ID: "projection", Resource: "app-mutation", Action: "projection"})
164 if err != nil {
165 t.Fatal(err)
166 }
167 if g, err := w.RestoreProjection(pctx, "bab", "demo", "app"); err != nil || g.Kind() != ProjectionRestore {
168 t.Fatalf("no projection grant inside its operation: %v", err)
169 }
170 if err = settle("joined", nil); err != nil {
171 t.Fatal(err)
172 }
173 if err = p.FinishMutation(ctx); err != nil {
174 t.Fatal(err)
175 }
176 })
177 }
178}
179
180// The mail gate cannot be released or checked by a reader of the object.
181func TestMailGatePossessionIsTheToken(t *testing.T) {
182 ctx := t.Context()
183 w, _, _ := testWriter(t)
184 owner := MailGateOwner{Console: "c", Enrollment: "e", Controller: "one"}
185 l, err := w.AcquireMailGate(ctx, owner, MailGateScope{})
186 if err != nil {
187 t.Fatal(err)
188 }
189 if _, err = w.AcquireMailGate(ctx, owner, MailGateScope{}); !errors.Is(err, ErrPending) {
190 t.Fatalf("second holder: %v", err)
191 }
192 forged := &MailGateLease{ID: l.ID, Owner: l.Owner, revision: l.revision, token: "guess"}
193 if err = w.ReleaseMailGate(ctx, forged); !errors.Is(err, ErrPending) {
194 t.Fatalf("forged release: %v", err)
195 }
196 if err = w.ReleaseMailGate(ctx, l); err != nil {
197 t.Fatal(err)
198 }
199 if err = w.CheckMailGate(ctx, l); !errors.Is(err, ErrPending) {
200 t.Fatalf("released lease still checks: %v", err)
201 }
202}
203
204// Review P3: a non-matching earlier permit does not hide a valid later one.
205func TestRequireAppMutationTriesEveryPermit(t *testing.T) {
206 ctx := t.Context()
207 w, raw, clock := testWriter(t)
208 seedAdmission(t, raw, "bab", "demo", openAdmission("app"))
209 h, err := w.BeginExclusive(ctx, "bab", "demo", "app", AppOperation{ID: "op", Kind: OpAppMutation, AppID: "app"}, testOwner)
210 if err != nil {
211 t.Fatal(err)
212 }
213 stale := &AppMutationPermit{account: "bab", app: "demo", appID: "app", epoch: 1, operation: "op", effect: "missing", owner: testOwner, exclusive: true, now: clock}
214 stale.live.Store(true)
215 ctx = context.WithValue(ctx, appMutationPermitKey{}, []*AppMutationPermit{stale})
216 issued, settle, err := h.Runner(clock).Issue(ctx, Effect{ID: "op", Resource: "app-mutation", Action: "test"})
217 if err != nil {
218 t.Fatal(err)
219 }
220 if err = RequireAppMutation(issued, raw, "bab", "demo", "app"); err != nil {
221 t.Fatalf("valid second permit ignored: %v", err)
222 }
223 if err = settle("joined", nil); err != nil {
224 t.Fatal(err)
225 }
226 if err = RequireAppMutation(issued, raw, "bab", "demo", "app"); !errors.Is(err, ErrPending) {
227 t.Fatalf("settled permit still live: %v", err)
228 }
229}
230