also looking at this
test(e2e): isolate mutating runs and account cleanup
#12
4 files
+209
-27
| @@ -0,0 +1,82 @@ | ||
| 1 | import { Database } from 'bun:sqlite' | |
| 2 | import { afterAll, describe, expect, test } from 'bun:test' | |
| 3 | import { mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs' | |
| 4 | import { tmpdir } from 'node:os' | |
| 5 | import { join } from 'node:path' | |
| 6 | import { cleanupE2EAccounts, e2eUrl, validateE2ETarget } from '../e2e/safety' | |
| 7 | ||
| 8 | const directory = mkdtempSync(join(tmpdir(), 'loghq-e2e-safety-')) | |
| 9 | const sqlitePath = join(directory, 'database.sqlite') | |
| 10 | const fixture = new Database(sqlitePath) | |
| 11 | fixture.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT); CREATE TABLE projects (owner_id INTEGER); CREATE TABLE oauth_access_tokens (user_id INTEGER)') | |
| 12 | fixture.close() | |
| 13 | afterAll(() => rmSync(directory, { recursive: true, force: true })) | |
| 14 | ||
| 15 | describe('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 | }) | |