forked from koshikraj/ottopus
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathschema.test.ts
More file actions
237 lines (211 loc) · 8.66 KB
/
Copy pathschema.test.ts
File metadata and controls
237 lines (211 loc) · 8.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import { PGlite } from '@electric-sql/pglite'
import { beforeAll, describe, expect, it } from 'vitest'
import { migrationFiles, statementsIn } from './migrate.js'
/**
* These run the real migrations against real Postgres (PGlite is Postgres
* compiled to WASM), so they test the database's behaviour rather than our
* beliefs about it. The append-only guarantees in particular cannot be checked
* any other way — they are triggers, not application code.
*/
let db: PGlite
const USER = '11111111-1111-1111-1111-111111111111'
const WALLET = '22222222-2222-2222-2222-222222222222'
const PLAN = '33333333-3333-3333-3333-333333333333'
async function seed(): Promise<void> {
await db.exec(`
insert into users (id, privy_did) values ('${USER}', 'did:privy:test');
insert into linked_wallets (id, user_id, address, wallet_type, ownership_proof)
values ('${WALLET}', '${USER}', '0xabc', 'metamask', '{"sig":"0x1"}'::jsonb);
insert into plans (id, version, plan_hash, user_id, wallet_id, intent, payload, expires_at)
values ('${PLAN}', 1, '0xhash', '${USER}', '${WALLET}', '{}'::jsonb, '{}'::jsonb, now() + interval '10 min');
insert into plan_events (plan_id, plan_version, status) values ('${PLAN}', 1, 'draft');
`)
}
beforeAll(async () => {
db = await PGlite.create()
// Supabase ships these roles; PGlite does not.
await db.exec(`create role anon; create role authenticated; create role service_role;`)
for (const file of await migrationFiles(new URL('../../drizzle', import.meta.url).pathname)) {
for (const stmt of await statementsIn(file)) await db.exec(stmt)
}
await seed()
}, 60_000)
describe('plans are append-only', () => {
it('rejects UPDATE', async () => {
await expect(db.exec(`update plans set reason = 'tampered' where id = '${PLAN}'`)).rejects.toThrow(
/append-only/,
)
})
it('rejects DELETE', async () => {
await expect(db.exec(`delete from plans where id = '${PLAN}'`)).rejects.toThrow(/append-only/)
})
it('allows a new version instead', async () => {
await db.exec(`
insert into plans (id, version, plan_hash, user_id, intent, payload, expires_at)
values ('${PLAN}', 2, '0xhash2', '${USER}', '{}'::jsonb, '{}'::jsonb, now() + interval '10 min')
`)
const r = await db.query(`select count(*)::int as n from plans where id = '${PLAN}'`)
expect((r.rows[0] as { n: number }).n).toBe(2)
})
})
describe('plan_events and simulations are append-only', () => {
it('rejects UPDATE on plan_events', async () => {
await expect(
db.exec(`update plan_events set status = 'confirmed' where plan_id = '${PLAN}'`),
).rejects.toThrow(/append-only/)
})
it('rejects DELETE on plan_events', async () => {
await expect(db.exec(`delete from plan_events where plan_id = '${PLAN}'`)).rejects.toThrow(
/append-only/,
)
})
it('rejects UPDATE on simulations', async () => {
await db.exec(`
insert into simulations (plan_id, plan_version, provider, ok)
values ('${PLAN}', 1, 'tenderly', true)
`)
await expect(db.exec(`update simulations set ok = false`)).rejects.toThrow(/append-only/)
})
})
describe('referential integrity', () => {
it('rejects a plan_event for an unknown plan', async () => {
await expect(
db.exec(
`insert into plan_events (plan_id, plan_version, status) values ('44444444-4444-4444-4444-444444444444', 1, 'draft')`,
),
).rejects.toThrow(/foreign key|violates/i)
})
it('rejects a plan_event for a version that does not exist', async () => {
await expect(
db.exec(`insert into plan_events (plan_id, plan_version, status) values ('${PLAN}', 99, 'draft')`),
).rejects.toThrow(/foreign key|violates/i)
})
it('rejects a simulation for an unknown plan', async () => {
await expect(
db.exec(
`insert into simulations (plan_id, plan_version, provider, ok) values ('44444444-4444-4444-4444-444444444444', 1, 'tenderly', true)`,
),
).rejects.toThrow(/foreign key|violates/i)
})
})
describe('status vocabulary is frozen', () => {
it('rejects a status outside the vocabulary', async () => {
await expect(
db.exec(`insert into plan_events (plan_id, plan_version, status) values ('${PLAN}', 1, 'simulated')`),
).rejects.toThrow(/plan_events_status|violates check/i)
})
it('accepts every frozen status', async () => {
for (const s of [
'draft',
'awaiting_review',
'awaiting_signature',
'submitted',
'confirmed',
'failed',
'expired',
'blocked',
'superseded',
'cancelled',
]) {
await db.exec(
`insert into plan_events (plan_id, plan_version, status) values ('${PLAN}', 1, '${s}')`,
)
}
})
})
describe('wallets are unlinked, never deleted', () => {
it('refuses to delete a wallet a plan is bound to', async () => {
// Must fail as a foreign key error, not as a trigger deadlock from the
// database trying to null the column on an append-only row.
await expect(db.exec(`delete from linked_wallets where id = '${WALLET}'`)).rejects.toThrow(
/foreign key|violates/i,
)
})
it('allows re-linking an address after it is unlinked', async () => {
await db.exec(`update linked_wallets set unlinked_at = now() where id = '${WALLET}'`)
await db.exec(`
insert into linked_wallets (user_id, address, wallet_type, ownership_proof)
values ('${USER}', '0xabc', 'rabby', '{"sig":"0x2"}'::jsonb)
`)
const r = await db.query(
`select count(*)::int as n from linked_wallets where user_id = '${USER}' and address = '0xabc'`,
)
expect((r.rows[0] as { n: number }).n).toBe(2)
})
it('rejects two active links for the same address', async () => {
await expect(
db.exec(`
insert into linked_wallets (user_id, address, wallet_type, ownership_proof)
values ('${USER}', '0xabc', 'metamask', '{"sig":"0x3"}'::jsonb)
`),
).rejects.toThrow(/unique|duplicate/i)
})
})
describe('wallet constraints', () => {
it('rejects a non-lowercase address', async () => {
await expect(
db.exec(`
insert into linked_wallets (user_id, address, wallet_type, ownership_proof)
values ('${USER}', '0xABCDEF', 'metamask', '{"sig":"0x4"}'::jsonb)
`),
).rejects.toThrow(/lowercase|violates check/i)
})
it('rejects a signing wallet with no ownership proof', async () => {
await expect(
db.exec(`
insert into linked_wallets (user_id, address, wallet_type)
values ('${USER}', '0xdef', 'metamask')
`),
).rejects.toThrow(/proof_required|violates check/i)
})
it('allows a watch-only wallet with no proof', async () => {
await db.exec(`
insert into linked_wallets (user_id, address, wallet_type, is_watch_only)
values ('${USER}', '0xfeed', 'watch_only', true)
`)
})
})
/**
* Nothing in the application should ever attempt this, which is precisely why
* the guard is in the database: the threat left is our own code, and the
* service bypasses RLS.
*/
describe('watch-only is permanent', () => {
it('refuses to promote a watch-only wallet to a signer', async () => {
await expect(
db.exec(`
update linked_wallets set is_watch_only = false, ownership_proof = '{"via":"privy"}'::jsonb
where address = '0xfeed'
`),
).rejects.toThrow(/watch-only/i)
})
it('still allows unlinking one', async () => {
await db.exec(`update linked_wallets set unlinked_at = now() where address = '0xfeed'`)
const r = await db.query(`select unlinked_at from linked_wallets where address = '0xfeed'`)
expect((r.rows[0] as { unlinked_at: Date | null }).unlinked_at).not.toBeNull()
})
/** The supported route back to signing: unlink, then link again with proof. */
it('lets the same address return as a proven signer', async () => {
await db.exec(`
insert into linked_wallets (user_id, address, wallet_type, ownership_proof)
values ('${USER}', '0xfeed', 'metamask', '{"via":"privy_identity_token"}'::jsonb)
`)
const r = await db.query(
`select is_watch_only from linked_wallets where address = '0xfeed' and unlinked_at is null`,
)
expect(r.rows).toHaveLength(1)
expect((r.rows[0] as { is_watch_only: boolean }).is_watch_only).toBe(false)
})
})
describe('RLS denies client roles', () => {
it('gives anon no access to plans', async () => {
await db.exec(`set role anon`)
await expect(db.query(`select * from plans`)).rejects.toThrow(/permission denied/i)
await db.exec(`reset role`)
})
it('gives authenticated no access to linked_wallets', async () => {
await db.exec(`set role authenticated`)
await expect(db.query(`select * from linked_wallets`)).rejects.toThrow(/permission denied/i)
await db.exec(`reset role`)
})
})