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/unit/e2e-safety.test.tsadded+82-0
Changes to tests/unit/e2e-safety.test.ts
@@ -0,0 +1,82 @@
1import { Database } from 'bun:sqlite'
2import { afterAll, describe, expect, test } from 'bun:test'
3import { mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
4import { tmpdir } from 'node:os'
5import { join } from 'node:path'
6import { cleanupE2EAccounts, e2eUrl, validateE2ETarget } from '../e2e/safety'
7
8const directory = mkdtempSync(join(tmpdir(), 'loghq-e2e-safety-'))
9const sqlitePath = join(directory, 'database.sqlite')
10const fixture = new Database(sqlitePath)
11fixture.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT); CREATE TABLE projects (owner_id INTEGER); CREATE TABLE oauth_access_tokens (user_id INTEGER)')
12fixture.close()
13afterAll(() => rmSync(directory, { recursive: true, force: true }))
14
15describe('mutating E2E target safety', () => {
16 test('is disabled by default, even when a server URL is present', () => {
17 expect(validateE2ETarget({})).toBeNull()
18 expect(validateE2ETarget({ E2E_BASE_URL: 'https://loghq.org' })).toBeNull()
19 })
20
21 test('requires an explicit local target and disposable database', () => {
22 expect(() => validateE2ETarget({ E2E_RUN: '1' })).toThrow()
23 expect(() => validateE2ETarget({ E2E_RUN: '1', E2E_BASE_URL: 'http://127.0.0.1:3100' })).toThrow()
24 })
25
26 test('rejects remote hosts, credentials, paths and non-HTTP targets', () => {
27 for (const target of [
28 'https://loghq.org',
29 'http://localhost.example.com:3100',
30 'http://localhost:3100',
31 'http://user:password@127.0.0.1:3100',
32 'http://127.0.0.1:3100/api',
33 'http://127.0.0.1:3100/?redirect=production',
34 'http://127.0.0.1:3100/#fragment',
35 'ftp://127.0.0.1:3100',
36 ]) {
37 expect(() => validateE2ETarget({ E2E_RUN: '1', E2E_BASE_URL: target, E2E_SQLITE_PATH: '/tmp/loghq-e2e-example/test.sqlite' })).toThrow()
38 }
39 })
40
41 test('accepts an explicit loopback origin with a real disposable file', () => {
42 for (const baseUrl of ['http://127.0.0.1:3100', 'http://[::1]:3100']) {
43 expect(validateE2ETarget({ E2E_RUN: '1', E2E_BASE_URL: baseUrl, E2E_SQLITE_PATH: sqlitePath }))
44 .toEqual({ baseUrl, sqlitePath: realpathSync(sqlitePath) })
45 }
46 })
47
48 test('rejects a symlink to a differently named file', () => {
49 const linkDirectory = mkdtempSync(join(tmpdir(), 'loghq-e2e-link-'))
50 const otherFile = join(directory, 'not-disposable.sqlite')
51 new Database(otherFile).close()
52 symlinkSync(otherFile, join(linkDirectory, 'database.sqlite'))
53 try {
54 expect(() => validateE2ETarget({ E2E_RUN: '1', E2E_BASE_URL: 'http://127.0.0.1:3100', E2E_SQLITE_PATH: join(linkDirectory, 'database.sqlite') })).toThrow()
55 }
56 finally {
57 rmSync(linkDirectory, { recursive: true, force: true })
58 }
59 })
60
61 test('cleans only this run in the disposable file and rejects cross-origin requests', () => {
62 const target = validateE2ETarget({ E2E_RUN: '1', E2E_BASE_URL: 'http://127.0.0.1:3100', E2E_SQLITE_PATH: sqlitePath })!
63 expect(() => e2eUrl(null, '/login')).toThrow()
64 expect(() => e2eUrl(target, 'https://loghq.org/logout')).toThrow()
65 expect(() => e2eUrl(target, '//example.com/logout')).toThrow()
66 expect(e2eUrl(target, '/login')).toBe('http://127.0.0.1:3100/login')
67 const email = 'e2e-current-run@loghq.test'
68 const db = new Database(sqlitePath)
69 try {
70 db.query('INSERT INTO users (id, email) VALUES (?, ?)').run(1, email)
71 db.query('INSERT INTO users (id, email) VALUES (?, ?)').run(2, 'e2e-previous-run@loghq.test')
72 db.exec('INSERT INTO projects VALUES (1), (2); INSERT INTO oauth_access_tokens VALUES (1), (2)')
73 cleanupE2EAccounts(target, new Set([email]))
74 expect(db.query('SELECT email FROM users').all()).toEqual([{ email: 'e2e-previous-run@loghq.test' }])
75 expect(db.query('SELECT owner_id FROM projects').all()).toEqual([{ owner_id: 2 }])
76 expect(db.query('SELECT user_id FROM oauth_access_tokens').all()).toEqual([{ user_id: 2 }])
77 }
78 finally {
79 db.close()
80 }
81 })
82})