ReviewOS

also looking at this

loghqorg/loghq

test(e2e): isolate mutating runs and account cleanup

#12
Merged glennmichael123 wants to merge test/loghq-e2e-safety into main
4 files +209 -27
tests/e2e/safety.tsadded+76-0
Changes to tests/e2e/safety.ts
@@ -0,0 +1,76 @@
1import { Database } from 'bun:sqlite'
2import { realpathSync, statSync } from 'node:fs'
3import { tmpdir } from 'node:os'
4import { basename, dirname, isAbsolute } from 'node:path'
5
6interface E2EEnvironment {
7 E2E_RUN?: string
8 E2E_BASE_URL?: string
9 E2E_SQLITE_PATH?: string
10}
11
12export interface E2ETarget {
13 baseUrl: string
14 sqlitePath: string
15}
16
17export function e2eUrl(target: E2ETarget | null, path: string): string {
18 if (!target)
19 throw new Error('Mutating E2E tests are disabled. See tests/e2e/README.md.')
20 const resolved = new URL(path, target.baseUrl)
21 if (resolved.origin !== target.baseUrl)
22 throw new Error('E2E requests cannot leave the configured loopback origin.')
23 return resolved.href
24}
25
26export function cleanupE2EAccounts(target: E2ETarget | null, emails: ReadonlySet<string>): void {
27 if (!target || emails.size === 0)
28 return
29 const checked = validateE2ETarget({ E2E_RUN: '1', E2E_BASE_URL: target.baseUrl, E2E_SQLITE_PATH: target.sqlitePath })!
30 const db = new Database(checked.sqlitePath, { readwrite: true, create: false })
31 try {
32 db.transaction(() => {
33 for (const email of emails) {
34 db.query('DELETE FROM projects WHERE owner_id IN (SELECT id FROM users WHERE email = ?)').run(email)
35 db.query('DELETE FROM oauth_access_tokens WHERE user_id IN (SELECT id FROM users WHERE email = ?)').run(email)
36 db.query('DELETE FROM users WHERE email = ?').run(email)
37 }
38 })()
39 }
40 finally {
41 db.close()
42 }
43}
44
45/** Never infer permission to mutate from an unrelated server being reachable. */
46export function validateE2ETarget(env: E2EEnvironment): E2ETarget | null {
47 if (env.E2E_RUN !== '1')
48 return null
49
50 let base: URL
51 try {
52 base = new URL(env.E2E_BASE_URL || '')
53 }
54 catch {
55 throw new Error('E2E_RUN requires an explicit numeric-loopback HTTP origin.')
56 }
57 if (base.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(base.hostname)
58 || !base.port || base.username || base.password || base.pathname !== '/' || base.search || base.hash) {
59 throw new Error('Mutating E2E tests only accept a numeric-loopback HTTP origin with an explicit port.')
60 }
61
62 const path = env.E2E_SQLITE_PATH || ''
63 if (!isAbsolute(path) || basename(path) !== 'database.sqlite')
64 throw new Error('E2E_SQLITE_PATH must name an existing disposable database.sqlite file.')
65
66 // Resolve symlinks before checking containment. /tmp is /private/tmp on macOS.
67 const sqlitePath = realpathSync(path)
68 const directory = realpathSync(dirname(path))
69 const roots = new Set([realpathSync(tmpdir()), realpathSync('/tmp')])
70 if (dirname(sqlitePath) !== directory || basename(sqlitePath) !== 'database.sqlite'
71 || !basename(directory).startsWith('loghq-e2e-')
72 || !roots.has(dirname(directory)) || !statSync(sqlitePath).isFile()) {
73 throw new Error('E2E database must be a regular file inside a loghq-e2e-* directory directly under the system temp directory.')
74 }
75 return { baseUrl: base.origin, sqlitePath }
76}