| 1 | //go:build linux |
| 2 | |
| 3 | package home |
| 4 | |
| 5 | import ( |
| 6 | "amadan.net/oficina/home/internal/operator" |
| 7 | "amadan.net/rastrillo/rastrillo/migrate" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | func TestOperatorSchemaMatchesSelectedRoleAndRejectsDrift(t *testing.T) { |
| 12 | for _, role := range []string{"credential", "directory"} { |
| 13 | for _, mode := range []string{"valid", "other-role", "missing-migration", "changed-checksum", "extra-trigger", "extra-table"} { |
| 14 | t.Run(role+"/"+mode, func(t *testing.T) { |
| 15 | check, err := operatorSchemaCheck(role) |
| 16 | if err != nil { |
| 17 | t.Fatal(err) |
| 18 | } |
| 19 | databaseRole := role |
| 20 | if mode == "other-role" { |
| 21 | if role == "credential" { |
| 22 | databaseRole = "directory" |
| 23 | } else { |
| 24 | databaseRole = "credential" |
| 25 | } |
| 26 | } |
| 27 | db := compatibilityFixture(t, databaseRole) |
| 28 | defer db.Close() |
| 29 | var sql string |
| 30 | switch mode { |
| 31 | case "missing-migration": |
| 32 | sql = "DELETE FROM rastrillo_migrations WHERE id=(SELECT id FROM rastrillo_migrations LIMIT 1)" |
| 33 | case "changed-checksum": |
| 34 | sql = "UPDATE rastrillo_migrations SET checksum='wrong' WHERE id=(SELECT id FROM rastrillo_migrations LIMIT 1)" |
| 35 | case "extra-trigger": |
| 36 | sql = "CREATE TRIGGER unexpected AFTER DELETE ON rastrillo_migrations BEGIN SELECT 1; END" |
| 37 | case "extra-table": |
| 38 | sql = "CREATE TABLE unexpected(value TEXT)" |
| 39 | } |
| 40 | if sql != "" { |
| 41 | if _, err := db.Exec(sql); err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | } |
| 45 | conn, err := db.Conn(t.Context()) |
| 46 | if err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | defer conn.Close() |
| 50 | if _, err := conn.ExecContext(t.Context(), "BEGIN IMMEDIATE"); err != nil { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | err = check(t.Context(), conn) |
| 54 | if mode == "valid" && err != nil { |
| 55 | t.Fatal(err) |
| 56 | } |
| 57 | if mode != "valid" && err != operator.ErrRefused { |
| 58 | t.Fatal("schema drift accepted", err) |
| 59 | } |
| 60 | if _, err := conn.ExecContext(t.Context(), "ROLLBACK"); err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | }) |
| 64 | } |
| 65 | } |
| 66 | if _, err := operatorSchemaCheck(""); err != operator.ErrRefused { |
| 67 | t.Fatal("missing role accepted", err) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func TestRuntimeOperatorSchemasAreAdditiveAndRoleSpecific(t *testing.T) { |
| 72 | for _, role := range []string{"", "credential", "directory"} { |
| 73 | t.Run(role, func(t *testing.T) { |
| 74 | cfg := Runtime{role: role} |
| 75 | db, err := migrate.Replay(t.Context(), cfg.Schema().All()) |
| 76 | if err != nil { |
| 77 | t.Fatal(err) |
| 78 | } |
| 79 | defer db.Close() |
| 80 | tables := map[string]bool{} |
| 81 | rows, err := db.Query("SELECT name FROM sqlite_schema WHERE type='table'") |
| 82 | if err != nil { |
| 83 | t.Fatal(err) |
| 84 | } |
| 85 | for rows.Next() { |
| 86 | var name string |
| 87 | if rows.Scan(&name) != nil { |
| 88 | t.Fatal("schema read") |
| 89 | } |
| 90 | tables[name] = true |
| 91 | } |
| 92 | if rows.Err() != nil || rows.Close() != nil { |
| 93 | t.Fatal("schema scan") |
| 94 | } |
| 95 | if tables["credential_operator_results"] != (role == "credential") || tables["directory_operator_results"] != (role == "directory") || tables["people"] == (role == "directory") || tables["teams"] != (role == "directory") { |
| 96 | t.Fatal("role schema crossed boundary") |
| 97 | } |
| 98 | }) |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | func TestServingRetainsAdditiveLegacyLedgerButOperatorRefusesIt(t *testing.T) { |
| 103 | pair := newExchangePair(t) |
| 104 | db := pair.directory.db |
| 105 | if _, err := db.Writer().Exec("CREATE TABLE legacy_additive_state(value INTEGER); INSERT INTO rastrillo_migrations(id,applied_at,checksum) VALUES('legacy_additive/0001','2026-01-01T00:00:00Z','historical-checksum')"); err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | if _, err := NewRuntime(*pair.directory.runtime, db, quietLogger()); err != nil { |
| 109 | t.Fatal("ordinary serving rejected historical additive migration", err) |
| 110 | } |
| 111 | conn, err := db.Writer().Conn(t.Context()) |
| 112 | if err != nil { |
| 113 | t.Fatal(err) |
| 114 | } |
| 115 | defer conn.Close() |
| 116 | if err := operator.VerifyLedger(t.Context(), conn, pair.directory.runtime.Schema()); err != operator.ErrRefused { |
| 117 | t.Fatal("operator accepted foreign ledger", err) |
| 118 | } |
| 119 | } |
| 120 | |