oficina / home Public

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

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "testing"
5
6 "gorm.io/plugin/dbresolver"
7)
8
9// TestSecureDeleteIsOnEveryConnection is the point of the connection
10// hook, and the reason it is a hook at all.
11//
12// It asserts on the writer AND on a reader. The pragma is per
13// connection, and rastrillo/db.Open builds two pools — a writer capped
14// at one, and readers. A test that checked only whichever connection it
15// happened to get would pass while the other pool had no pragma set,
16// which is the worst kind of green. The writer is the connection that
17// matters most, because it is the only one that performs DELETEs, and it
18// is also the one opened during Open's Ping, before any Exec a caller
19// could run.
20func TestSecureDeleteIsOnEveryConnection(t *testing.T) {
21 app := newTestApp(t)
22
23 var writer int
24 if err := app.db.Writer().QueryRow("PRAGMA secure_delete").Scan(&writer); err != nil {
25 t.Fatalf("read secure_delete on the writer: %v", err)
26 }
27 if writer != 1 {
28 t.Errorf("secure_delete on the writer = %d, want 1", writer)
29 }
30
31 var reader int
32 if err := app.db.G.Clauses(dbresolver.Read).Raw("PRAGMA secure_delete").Scan(&reader).Error; err != nil {
33 t.Fatalf("read secure_delete on a reader: %v", err)
34 }
35 if reader != 1 {
36 t.Errorf("secure_delete on a reader = %d, want 1", reader)
37 }
38}
39
40// The schema applied at boot must be the one the app declares, and it
41// must contain the sessions tables as well as this app's own. A merge
42// that silently dropped one would show up here and nowhere else until
43// the first sign-in.
44func TestBootSchemaCreatesBothTableSets(t *testing.T) {
45 app := newTestApp(t)
46
47 for _, table := range []string{"people", "sessions"} {
48 var name string
49 err := app.db.Writer().
50 QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).
51 Scan(&name)
52 if err != nil {
53 t.Errorf("table %q was not created: %v", table, err)
54 }
55 }
56}
57
58// There is no address column, and this asserts it against the database
59// that was actually built rather than against the migration text. Gate
60// 2's schema half reads the .sql files; this reads sqlite_master, so a
61// column arriving from a dependency's migrations — rastrillo/auth's, if
62// anybody ever merged its schema — is caught too.
63func TestNoAddressColumnInTheBuiltDatabase(t *testing.T) {
64 app := newTestApp(t)
65
66 rows, err := app.db.Writer().Query("SELECT name FROM sqlite_master WHERE type='table'")
67 if err != nil {
68 t.Fatal(err)
69 }
70 defer rows.Close()
71
72 var tables []string
73 for rows.Next() {
74 var name string
75 if err := rows.Scan(&name); err != nil {
76 t.Fatal(err)
77 }
78 tables = append(tables, name)
79 }
80 if err := rows.Err(); err != nil {
81 t.Fatal(err)
82 }
83
84 for _, table := range tables {
85 cols, err := app.db.Writer().Query("SELECT name FROM pragma_table_info(?)", table)
86 if err != nil {
87 t.Fatalf("pragma_table_info(%q): %v", table, err)
88 }
89 for cols.Next() {
90 var col string
91 if err := cols.Scan(&col); err != nil {
92 cols.Close()
93 t.Fatal(err)
94 }
95 if addressishColumn(col) {
96 t.Errorf("table %q has column %q, which names an address", table, col)
97 }
98 }
99 cols.Close()
100 }
101}
102
103// addressishColumn is the runtime twin of gate 2's static matcher. It is
104// deliberately a separate, simpler implementation: two independent
105// checks that agree are worth more than one shared helper that could be
106// wrong in one place. The cost of that is real — the two disagreed on
107// sealed_address the first time this ran, because only the static one
108// carried the exemption — so both exemptions are stated in both places.
109//
110// The exemption is the same one: a sealed blob is not an address, and
111// says so in its name. It is a whole word in the identifier, not a
112// substring, so a column called unsealed_address does not slip through.
113func addressishColumn(col string) bool {
114 parts := splitIdent(col)
115 for _, part := range parts {
116 if part == "sealed" {
117 return false
118 }
119 }
120 for _, part := range parts {
121 switch part {
122 // The same list as internal/guardstest's addressWords. They are
123 // separate implementations on purpose, but disagreeing about
124 // WHAT they forbid is not independence, it is a hole — and they
125 // did disagree: the static one had no bare "mail", so
126 // sender_mail passed it and was caught only here.
127 case "address", "addresses", "addr", "email", "emails", "mail", "recipient", "recipients":
128 return true
129 }
130 }
131 return false
132}
133
134func splitIdent(s string) []string {
135 var out []string
136 cur := ""
137 for _, r := range s {
138 if r == '_' || r == '-' {
139 if cur != "" {
140 out = append(out, cur)
141 }
142 cur = ""
143 continue
144 }
145 if r >= 'A' && r <= 'Z' {
146 r += 'a' - 'A'
147 }
148 cur += string(r)
149 }
150 if cur != "" {
151 out = append(out, cur)
152 }
153 return out
154}
155
156// The exemption is a whole word, not a substring: the one column that
157// carries an address in this schema is allowed, and a name that merely
158// contains the letters is not.
159func TestAddressishColumnExemptionIsAWholeWord(t *testing.T) {
160 for _, col := range []string{"sealed_address", "address_sealed"} {
161 if addressishColumn(col) {
162 t.Errorf("addressishColumn(%q) = true; a sealed blob is not an address", col)
163 }
164 }
165 for _, col := range []string{"address", "owner_email", "unsealed_address", "recipient", "mail_to"} {
166 if !addressishColumn(col) {
167 t.Errorf("addressishColumn(%q) = false", col)
168 }
169 }
170}
171