oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package home
4
5import (
6 "bytes"
7 "context"
8 "crypto/sha256"
9 "database/sql"
10 "database/sql/driver"
11 "encoding/hex"
12 "encoding/json"
13 "errors"
14 "io/fs"
15 "log/slog"
16 "os"
17 "path/filepath"
18 "strings"
19 "testing"
20
21 "amadan.net/oficina/home/internal/home/sealtest"
22 "amadan.net/oficina/home/internal/operator"
23 "amadan.net/rastrillo/rastrillo/db"
24 "amadan.net/rastrillo/rastrillo/migrate"
25)
26
27func legacyDiagnosticFixture(t *testing.T, historical, replication bool) *db.DB {
28 t.Helper()
29 RegisterSecureDelete()
30 d, err := db.Open(filepath.Join(t.TempDir(), "legacy.db"), quietLogger())
31 if err != nil {
32 t.Fatal(err)
33 }
34 t.Cleanup(func() { d.Close() })
35 if historical {
36 if _, err = migrate.Apply(t.Context(), d, BootSchema); err != nil {
37 t.Fatal(err)
38 }
39 }
40 if _, err = migrate.Apply(t.Context(), d, (Runtime{role: "directory"}).Schema()); err != nil {
41 t.Fatal(err)
42 }
43 if replication {
44 var profile []legacyDiagnosticObject
45 raw, err := compatibilityProfiles.ReadFile("compatibility_profiles/directory.json")
46 if err != nil || json.Unmarshal(raw, &profile) != nil {
47 t.Fatal("frozen replication profile")
48 }
49 count := 0
50 for _, obj := range profile {
51 if obj.Type == "table" && (obj.Name == "_litestream_lock" || obj.Name == "_litestream_seq") {
52 if _, err = d.Writer().Exec(obj.SQL); err != nil {
53 t.Fatal(err)
54 }
55 count++
56 }
57 }
58 if count != 2 {
59 t.Fatal("replication profile changed")
60 }
61 }
62 return d
63}
64
65func legacyObjects(t *testing.T, d *sql.DB) []legacyDiagnosticObject {
66 t.Helper()
67 rows, err := d.Query("SELECT type,name,tbl_name,coalesce(sql,'') FROM sqlite_schema ORDER BY type,name")
68 if err != nil {
69 t.Fatal(err)
70 }
71 defer rows.Close()
72 var objects []legacyDiagnosticObject
73 for rows.Next() {
74 var object legacyDiagnosticObject
75 if rows.Scan(&object.Type, &object.Name, &object.Table, &object.SQL) != nil {
76 t.Fatal("schema scan")
77 }
78 objects = append(objects, object)
79 }
80 if rows.Err() != nil {
81 t.Fatal(rows.Err())
82 }
83 return objects
84}
85
86func TestLegacyDiagnosticFrozenProfile(t *testing.T) {
87 d := legacyDiagnosticFixture(t, true, true)
88 objects := legacyObjects(t, d.Writer())
89 raw, err := json.MarshalIndent(objects, "", " ")
90 if err != nil {
91 t.Fatal(err)
92 }
93 raw = append(raw, '\n')
94 if os.Getenv("OFICINA_GENERATE_LEGACY_DIAGNOSTIC_PROFILE") == "1" {
95 if err = os.WriteFile("diagnostic_profiles/legacy-directory-v1.json", raw, 0600); err != nil {
96 t.Fatal(err)
97 }
98 return
99 }
100 if !bytes.Equal(raw, legacyDiagnosticJSON) {
101 t.Fatal("frozen historical diagnostic schema changed")
102 }
103 if len(objects) == 0 || len(objects) > legacyDiagnosticMaxRows {
104 t.Fatal("invalid profile size")
105 }
106 for _, o := range objects {
107 if len(o.Type) > 16 || len(o.Name) > legacyDiagnosticNameBytes || len(o.Table) > legacyDiagnosticNameBytes || len(o.SQL) > legacyDiagnosticSQLBytes {
108 t.Fatal("profile exceeds bounded fields")
109 }
110 }
111 migrations, ok := legacyDiagnosticMigrations()
112 if !ok {
113 t.Fatal("migration union invalid")
114 }
115 replay, err := migrate.Replay(t.Context(), migrations)
116 if err != nil {
117 t.Fatal(err)
118 }
119 defer replay.Close()
120 for _, o := range objects {
121 if o.Type == "table" && (o.Name == "rastrillo_migrations" || o.Name == "_litestream_lock" || o.Name == "_litestream_seq") {
122 if _, err = replay.Exec(o.SQL); err != nil {
123 t.Fatal(err)
124 }
125 }
126 }
127 reraw, _ := json.MarshalIndent(legacyObjects(t, replay), "", " ")
128 if !bytes.Equal(raw, append(reraw, '\n')) {
129 t.Fatal("historical sequential apply differs from unique replay")
130 }
131}
132
133func TestLegacyDiagnosticExactProfileAndOccupancy(t *testing.T) {
134 d := legacyDiagnosticFixture(t, true, true)
135 before := legacyObjects(t, d.Writer())
136 var ledgerBefore, ledgerAfter string
137 if err := d.Writer().QueryRow("SELECT group_concat(id || checksum || applied_at) FROM (SELECT * FROM rastrillo_migrations ORDER BY id)").Scan(&ledgerBefore); err != nil {
138 t.Fatal(err)
139 }
140 result := inspectLegacySchema(t.Context(), d.Writer())
141 if result.status != "match" || result.occupied {
142 t.Fatal("empty historical schema not recognized", result)
143 }
144 inserts := map[string]string{
145 "people": "INSERT INTO people VALUES('person','index','time')",
146 "signin_links": "INSERT INTO signin_links VALUES('token',x'00','purpose','time')",
147 "passkey_credentials": "INSERT INTO passkey_credentials VALUES('id','person',x'00',0,'time')",
148 "passkey_challenges": "INSERT INTO passkey_challenges VALUES('challenge','person','purpose','time')",
149 "passkey_pending": "INSERT INTO passkey_pending VALUES('token','person','method','/','time')",
150 "passkey_recovery_codes": "INSERT INTO passkey_recovery_codes VALUES('code','person','time')",
151 }
152 for table, statement := range inserts {
153 t.Run(table, func(t *testing.T) {
154 if _, err := d.Writer().Exec(statement); err != nil {
155 t.Fatal(err)
156 }
157 result := inspectLegacySchema(t.Context(), d.Writer())
158 if result.status != "match" || !result.occupied {
159 t.Fatal("historical credentials were missed", result)
160 }
161 var n int
162 if d.Writer().QueryRow("SELECT count(*) FROM "+table).Scan(&n) != nil || n != 1 {
163 t.Fatal("diagnostic changed rows")
164 }
165 if _, err := d.Writer().Exec("DELETE FROM " + table); err != nil {
166 t.Fatal(err)
167 }
168 })
169 }
170 after := legacyObjects(t, d.Writer())
171 if err := d.Writer().QueryRow("SELECT group_concat(id || checksum || applied_at) FROM (SELECT * FROM rastrillo_migrations ORDER BY id)").Scan(&ledgerAfter); err != nil || ledgerAfter != ledgerBefore {
172 t.Fatal("diagnostic changed migration ledger")
173 }
174 a, _ := json.Marshal(before)
175 b, _ := json.Marshal(after)
176 if !bytes.Equal(a, b) {
177 t.Fatal("diagnostic changed schema")
178 }
179 conn, err := d.Writer().Conn(t.Context())
180 if err != nil {
181 t.Fatal(err)
182 }
183 defer conn.Close()
184 if operator.VerifyLedger(t.Context(), conn, (Runtime{role: "directory"}).Schema()) != operator.ErrRefused {
185 t.Fatal("operator accepted historical extras")
186 }
187}
188
189func TestLegacyDiagnosticRefusesDrift(t *testing.T) {
190 changes := map[string]string{
191 "wrong_checksum": "UPDATE rastrillo_migrations SET checksum='" + strings.Repeat("0", 64) + "' WHERE id='home/0001_people'",
192 "missing_migration": "DELETE FROM rastrillo_migrations WHERE id='home/0001_people'",
193 "unknown_migration": "INSERT INTO rastrillo_migrations VALUES('foreign/0001','time','" + strings.Repeat("0", 64) + "')",
194 "wrong_type": "UPDATE rastrillo_migrations SET checksum=x'00' WHERE id='home/0001_people'",
195 "oversized_id": "INSERT INTO rastrillo_migrations VALUES('" + strings.Repeat("x", 257) + "','time','" + strings.Repeat("0", 64) + "')",
196 "unknown_table": "CREATE TABLE foreign_table(value INTEGER)",
197 "unknown_index": "CREATE INDEX foreign_index ON people(created_at)",
198 "unknown_trigger": "CREATE TRIGGER foreign_trigger AFTER INSERT ON people BEGIN SELECT 1; END",
199 "changed_table": "ALTER TABLE people ADD COLUMN foreign_value INTEGER",
200 "missing_replication": "DROP TABLE _litestream_lock",
201 "oversized_object_name": "CREATE TABLE \"" + strings.Repeat("x", 257) + "\"(value INTEGER)",
202 "oversized_sql": "CREATE TABLE foreign_big(value TEXT DEFAULT '" + strings.Repeat("x", legacyDiagnosticSQLBytes) + "')",
203 }
204 for name, statement := range changes {
205 t.Run(name, func(t *testing.T) {
206 d := legacyDiagnosticFixture(t, true, true)
207 if _, err := d.Writer().Exec(statement); err != nil {
208 t.Fatal(err)
209 }
210 result := inspectLegacySchema(t.Context(), d.Writer())
211 if result.status != "mismatch" || result.occupied {
212 t.Fatal("drift accepted", result)
213 }
214 })
215 }
216 for _, test := range []struct{ historical, replication bool }{{false, true}, {true, false}} {
217 d := legacyDiagnosticFixture(t, test.historical, test.replication)
218 if result := inspectLegacySchema(t.Context(), d.Writer()); result.status != "mismatch" || result.occupied {
219 t.Fatal("partial profile accepted", result)
220 }
221 }
222}
223
224func TestLegacyDiagnosticLogAndCancellation(t *testing.T) {
225 d := legacyDiagnosticFixture(t, true, true)
226 if _, err := d.Writer().Exec("INSERT INTO people VALUES(?,?,'time')", sealtest.CanaryAddress, "secret-marker"); err != nil {
227 t.Fatal(err)
228 }
229 var out bytes.Buffer
230 logger := slog.New(slog.NewJSONHandler(&out, nil))
231 logLegacySchemaDiagnostic(t.Context(), d.Writer(), "credential", logger)
232 if out.Len() != 0 {
233 t.Fatal("credential role emitted diagnostic")
234 }
235 logLegacySchemaDiagnostic(t.Context(), d.Writer(), "directory", logger)
236 var event map[string]any
237 if json.Unmarshal(out.Bytes(), &event) != nil {
238 t.Fatal("invalid event")
239 }
240 if len(event) != 6 || event["msg"] != "legacy schema diagnostic" || event["profile"] != legacyDiagnosticProfile || event["result"] != "match" || event["legacy_credentials_present"] != true {
241 t.Fatal("wrong event keys or values")
242 }
243 if strings.Contains(out.String(), sealtest.CanaryAddress) || strings.Contains(out.String(), "secret-marker") {
244 t.Fatal("diagnostic leaked row data")
245 }
246 if _, err := d.Writer().Exec("CREATE TABLE \"" + sealtest.CanaryAddress + "\"(value TEXT)"); err != nil {
247 t.Fatal(err)
248 }
249 out.Reset()
250 logLegacySchemaDiagnostic(t.Context(), d.Writer(), "directory", logger)
251 event = map[string]any{}
252 if json.Unmarshal(out.Bytes(), &event) != nil || len(event) != 5 || event["result"] != "mismatch" {
253 t.Fatal("mismatch event included occupancy")
254 }
255 if strings.Contains(out.String(), sealtest.CanaryAddress) {
256 t.Fatal("diagnostic leaked schema name")
257 }
258 ctx, cancel := context.WithCancel(t.Context())
259 cancel()
260 out.Reset()
261 logLegacySchemaDiagnostic(ctx, d.Writer(), "directory", logger)
262 event = map[string]any{}
263 if json.Unmarshal(out.Bytes(), &event) != nil || len(event) != 5 || event["result"] != "unavailable" {
264 t.Fatal("cancellation event incorrect")
265 }
266 if _, err := d.Writer().Exec("INSERT INTO people VALUES('after','after','time')"); err != nil {
267 t.Fatal("diagnostic stranded writer", err)
268 }
269}
270
271func TestLegacyDiagnosticCannotExpandAcceptedProfiles(t *testing.T) {
272 paths, err := fs.Glob(compatibilityProfiles, "compatibility_profiles/*.json")
273 if err != nil || strings.Join(paths, ",") != "compatibility_profiles/credential.json,compatibility_profiles/directory.json" {
274 t.Fatal("diagnostic expanded accepted profile embed")
275 }
276 raw, err := os.ReadFile("legacy_schema_diagnostic.go")
277 if err != nil {
278 t.Fatal(err)
279 }
280 if bytes.Contains(raw, []byte(`"amadan.net/oficina/home/internal/operator"`)) || bytes.Contains(raw, []byte("compatibilityProfiles")) {
281 t.Fatal("diagnostic depends on authority profiles")
282 }
283 migrations, ok := legacyDiagnosticMigrations()
284 if !ok {
285 t.Fatal("migration union")
286 }
287 if _, ok = uniqueLegacyMigrations(migrations, []migrate.Migration{{ID: migrations[0].ID, SQL: "SELECT 1"}}); ok {
288 t.Fatal("conflicting duplicate migration accepted")
289 }
290}
291
292func TestLegacyDiagnosticOriginalMigrationProvenance(t *testing.T) {
293 raw, err := os.ReadFile("diagnostic_profiles/provenance.json")
294 if err != nil {
295 t.Fatal(err)
296 }
297 var provenance map[string]struct{ SHA256, Checksum string }
298 if json.Unmarshal(raw, &provenance) != nil || len(provenance) != 3 {
299 t.Fatal("invalid original provenance")
300 }
301 found := 0
302 for _, m := range BootSchema.All() {
303 if p, ok := provenance[m.ID]; ok {
304 sum := sha256.Sum256([]byte(m.SQL))
305 if hex.EncodeToString(sum[:]) != p.SHA256 || migrate.Checksum(m.SQL) != p.Checksum {
306 t.Fatal("original migration changed", m.ID)
307 }
308 found++
309 }
310 }
311 if found != 3 {
312 t.Fatal("original migration missing")
313 }
314}
315
316type legacyRollbackProbe struct {
317 t *testing.T
318 fail, evicted bool
319}
320
321func (p *legacyRollbackProbe) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
322 if ctx.Err() != nil || query != "ROLLBACK" || len(args) != 0 {
323 p.t.Fatal("cleanup reused cancellation or changed SQL")
324 }
325 if p.fail {
326 return nil, errors.New("fixture rollback failure")
327 }
328 return nil, nil
329}
330func (p *legacyRollbackProbe) Raw(f func(any) error) error {
331 if f(nil) != driver.ErrBadConn {
332 p.t.Fatal("failed cleanup did not evict driver connection")
333 }
334 p.evicted = true
335 return nil
336}
337func TestLegacyDiagnosticRollbackAndEviction(t *testing.T) {
338 ctx, cancel := context.WithCancel(t.Context())
339 cancel()
340 for _, fail := range []bool{false, true} {
341 probe := &legacyRollbackProbe{t: t, fail: fail}
342 if rollbackLegacyDiagnostic(ctx, probe) == fail || probe.evicted != fail {
343 t.Fatal("cleanup result or eviction wrong")
344 }
345 }
346 d := legacyDiagnosticFixture(t, true, true)
347 conn, err := d.Writer().Conn(t.Context())
348 if err != nil {
349 t.Fatal(err)
350 }
351 if _, err = conn.ExecContext(t.Context(), "BEGIN"); err != nil {
352 t.Fatal(err)
353 }
354 if !rollbackLegacyDiagnostic(ctx, conn) {
355 t.Fatal("canceled context prevented real rollback")
356 }
357 conn.Close()
358 // A query error after BEGIN must also leave the sole writer usable.
359 if _, err = d.Writer().Exec("DROP TABLE rastrillo_migrations"); err != nil {
360 t.Fatal(err)
361 }
362 if result := inspectLegacySchema(t.Context(), d.Writer()); result.status != "unavailable" {
363 t.Fatal("malformed database accepted")
364 }
365 if _, err = d.Writer().Exec("BEGIN IMMEDIATE; INSERT INTO people VALUES('after','after','time'); COMMIT"); err != nil {
366 t.Fatal("failed diagnostic stranded writer", err)
367 }
368}
369
370type legacyBeginProbe struct {
371 t *testing.T
372 err error
373 opened, evicted bool
374}
375
376func (p *legacyBeginProbe) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
377 if query != "BEGIN" || len(args) != 0 {
378 p.t.Fatal("unexpected begin statement")
379 }
380 p.opened = true
381 return nil, p.err
382}
383func (p *legacyBeginProbe) Raw(f func(any) error) error {
384 if f(nil) != driver.ErrBadConn {
385 p.t.Fatal("uncertain BEGIN did not discard connection")
386 }
387 p.evicted = true
388 p.opened = false
389 return nil
390}
391func TestLegacyDiagnosticBeginFailureEvictsOpenTransaction(t *testing.T) {
392 for _, err := range []error{nil, context.DeadlineExceeded, errors.New("driver error after begin")} {
393 p := &legacyBeginProbe{t: t, err: err}
394 ok := beginLegacyDiagnostic(t.Context(), p)
395 if err != nil && (ok || !p.evicted || p.opened) {
396 t.Fatal("failed BEGIN left an open transaction in the pool")
397 }
398 if err == nil && (!ok || p.evicted || !p.opened) {
399 t.Fatal("successful BEGIN discarded its connection")
400 }
401 }
402}
403