import { Client } from 'pg'; /** * Direct database access for the tests, for the one thing the API deliberately * will not give them. * * The password-reset token is only ever delivered by email, which these tests * cannot read. It is read from the database rather than through a helper * endpoint because an endpoint that returns a reset token for an arbitrary * address is account takeover for every customer if it is ever reachable, and * an environment gate is a thin thing to stand between that and production. * Doing it here keeps the capability entirely inside the test process. * * That reasoning is unchanged from when it lived inline in * password-reset.spec.ts. What has changed is that it is no longer sitting in a * spec where it can be copied into the next one that wants a shortcut. */ /** * The default port is 55500, matching scripts/start-local.ps1. * * It used to be 55432, which is the integration suite's disposable Postgres — * a different database, with different credentials, that the app under test is * not connected to. Worse, 55432 is reserved by Hyper-V on at least one machine * here, so the spec failed with a bare ECONNREFUSED naming a port nobody had * chosen. TEST_PGPORT still overrides, for CI and for anyone running the stack * somewhere else. */ function connectionSettings() { return { host: process.env.TEST_PGHOST || 'localhost', port: parseInt(process.env.TEST_PGPORT || '55500', 10), user: process.env.TEST_PGUSER || 'redefined_local', password: process.env.TEST_PGPASSWORD || 'redefined_local', database: process.env.TEST_PGDATABASE || 'redefined_local' }; } /** Opens a connection, runs the query, and closes it whatever happens. */ async function withClient(run: (client: Client) => Promise): Promise { const client = new Client(connectionSettings()); await client.connect(); try { return await run(client); } finally { await client.end(); } } /** * The most recent password-reset token issued to an address. * * Throws rather than returning null: every caller is about to build a URL from * it, and a missing token means the request under test did not do what it said, * which is worth failing loudly at the point it happened. */ export async function readPasswordResetToken(email: string): Promise { return withClient(async (client) => { const { rows } = await client.query( `SELECT t.token FROM customer_tokens t JOIN customers c ON c.id = t.customer_id WHERE c.email = $1 AND t.kind = 'password_reset' ORDER BY t.created_at DESC LIMIT 1`, [email] ); if (!rows.length) throw new Error(`no password_reset token issued for ${email}`); return rows[0].token as string; }); } /** * Puts a passkey on an account, for the tests that are about what happens to it * (#42). * * Registering one for real needs an authenticator, which Playwright does not * have and which no fixture can supply. The credential is a row, and a password * reset acts on the row rather than on the hardware behind it, so a row is * enough to test the interaction — nothing here ever tries to sign in with it. * * Here rather than in a spec for the same reason as the token read above: the * next spec that wants a shortcut should have to come and ask for one. */ export async function giveCustomerAPasskey(email: string, name = 'Test key'): Promise { await withClient(async (client) => { const { rowCount } = await client.query( `INSERT INTO customer_credentials (customer_id, credential_id, public_key, name) SELECT id, 'e2e-' || id || '-' || floor(random() * 1e9)::text, 'not-a-real-key', $2 FROM customers WHERE email = $1`, [email, name] ); if (rowCount === 0) throw new Error(`no customer to give a passkey to: ${email}`); }); }