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/helpers.tsmodified+22-27
Changes to tests/e2e/helpers.ts
@@ -1,11 +1,9 @@
11/**
22 * Shared plumbing for the end-to-end suite.
33 *
4 * These tests drive the app over HTTP against a running dev server. That is
5 * possible at all because every form in the app handles its own POST now
6 * (stx#1847, see resources/views/login.stx) — sign-in, sign-up, password reset
7 * and project creation are ordinary form submissions, so a browser is not
8 * needed to exercise them and this suite carries no browser dependency.
4 * These tests drive an explicitly opted-in, isolated local server over HTTP.
5 * See README.md for the disposable database requirement and the legacy auth
6 * expectations that still need migrating to the current JSON API.
97 *
108 * What that does NOT cover is anything whose behaviour only exists after
119 * hydration: SPA navigation, the theme toggle, clipboard buttons, the delete
@@ -13,7 +11,10 @@
1311 * deliberately out of scope here rather than faked.
1412 */
1513
16const BASE = Bun.env.E2E_BASE_URL || 'http://localhost:3000'
14import { cleanupE2EAccounts, e2eUrl, validateE2ETarget } from './safety'
15
16const target = validateE2ETarget(Bun.env)
17const createdEmails = new Set<string>()
1718
1819/** A password that satisfies the app's 8-character minimum. */
1920export const TEST_PASSWORD = 'e2e-test-password'
@@ -21,14 +22,16 @@ export const TEST_PASSWORD = 'e2e-test-password'
2122/**
2223 * Whether a server is reachable.
2324 *
24 * The suite SKIPS rather than fails when nothing is listening, so `bun test`
25 * The suite SKIPS unless explicitly enabled, and when nothing is listening, so `bun test`
2526 * stays green on a machine that has not started the app. A failing E2E suite
2627 * should mean the app is broken, not that the developer did not run `bun run
2728 * dev` — otherwise the signal gets ignored, which is worse than not having it.
2829 */
2930export async function serverIsUp(): Promise<boolean> {
31 if (!target)
32 return false
3033 try {
31 const res = await fetch(BASE, { signal: AbortSignal.timeout(3000) })
34 const res = await fetch(target.baseUrl, { redirect: 'manual', signal: AbortSignal.timeout(3000) })
3235 return res.ok
3336 }
3437 catch {
@@ -46,7 +49,7 @@ export async function serverIsUp(): Promise<boolean> {
4649export const SERVER_UP: boolean = await serverIsUp()
4750
4851export function url(path: string): string {
49 return BASE + path
52 return e2eUrl(target, path)
5053}
5154
5255/**
@@ -88,12 +91,13 @@ export function sessionCookie(res: Response): string {
8891/**
8992 * An email nobody else is using.
9093 *
91 * Unique per call so a rerun cannot collide with its own leftovers, and
92 * prefixed so the cleanup below can find every account this suite ever made —
93 * including from a run that crashed before its own teardown.
94 * Unique per call so a rerun cannot collide with its own leftovers. Track the
95 * exact address for this run's cleanup; the prefix is only a readable QA label.
9496 */
9597export function freshEmail(label: string): string {
96 return `e2e-${label}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}@loghq.test`
98 const email = `e2e-${label}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}@loghq.test`
99 createdEmails.add(email)
100 return email
97101}
98102
99103/** Register through the real form and return the session cookie it mints. */
@@ -109,20 +113,11 @@ export async function registerAndSignIn(label: string): Promise<{ email: string,
109113}
110114
111115/**
112 * Remove every account this suite has ever created, and anything hanging off
113 * one.
114 *
115 * Matches on the `e2e-%@loghq.test` prefix rather than tracking ids, so a run
116 * that died before teardown is cleaned up by the next one. Deletes children
117 * first — projects and tokens reference users.
116 * Remove only this process's generated accounts from the explicit disposable
117 * SQLite file. Never import the app's default database connection for cleanup.
118 * Failed-run leftovers remain confined to the disposable test database.
118119 */
119120export async function cleanupTestAccounts(): Promise<void> {
120 const { db } = await import('@stacksjs/database')
121 await db.unsafe(
122 `DELETE FROM projects WHERE owner_id IN (SELECT id FROM users WHERE email LIKE 'e2e-%@loghq.test')`,
123 ).execute()
124 await db.unsafe(
125 `DELETE FROM oauth_access_tokens WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'e2e-%@loghq.test')`,
126 ).execute()
127 await db.unsafe(`DELETE FROM users WHERE email LIKE 'e2e-%@loghq.test'`).execute()
121 cleanupE2EAccounts(target, createdEmails)
122 createdEmails.clear()
128123}