From 09f81d77de9d634ac31396b7ad01d505fff7e7f4 Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 8 Apr 2026 18:05:57 +0200 Subject: [PATCH] test(integration): assert security_events audit rows after register + login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two assertions to the auth-flow integration test that exercise the audit-log path: - after register: expect exactly 1 REGISTER row in auth.security_events for the new user - after login: expect exactly 1 LOGIN_SUCCESS row for the same user This locks in the fix from the previous commit (security.ts ?? null guard for optional fields) and catches any future regression where security.logEvent silently swallows a SQL error and the audit log goes into the void. Verified by reverting security.ts to the broken pre-fix version and re-running — the test fails with `Expected: 1, Received: 0` at the register-audit assert in 211ms instead of taking hours of production debugging. Also adds an explicit DELETE FROM auth.security_events to the afterAll cleanup. The FK from security_events.user_id to auth.users(id) is ON DELETE CASCADE so it would clean up implicitly anyway, but listing it explicitly makes the cleanup intent obvious from the test source. Net: 24 → 26 expects per run. Still ~22s end-to-end on a warm cache. --- tests/integration/auth-flow.test.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/integration/auth-flow.test.ts b/tests/integration/auth-flow.test.ts index ee73a4311..57084da4e 100644 --- a/tests/integration/auth-flow.test.ts +++ b/tests/integration/auth-flow.test.ts @@ -121,13 +121,27 @@ async function pgExec(sql: string): Promise { return out.trim(); } +// Returns the count of audit rows for (user, eventType). Used to assert +// that the security_events INSERT actually committed — historically this +// was silently broken because the SQL builder collapsed undefined params +// into literal nothing, which the catch swallowed as "(non-critical)". +async function countSecurityEvents(userId: string, eventType: string): Promise { + const out = await pgExec( + `SELECT COUNT(*) FROM auth.security_events WHERE user_id = '${userId}' AND event_type = '${eventType}';` + ); + return parseInt(out, 10); +} + // ─── Cleanup at the end so failed runs don't leak ──────────────────── afterAll(async () => { if (!createdUserId) return; try { + // security_events would also CASCADE on auth.users delete, but we + // drop it explicitly so the cleanup is obvious from reading. await pgExec( - `DELETE FROM auth.encryption_vault_audit WHERE user_id = '${createdUserId}'; + `DELETE FROM auth.security_events WHERE user_id = '${createdUserId}'; + DELETE FROM auth.encryption_vault_audit WHERE user_id = '${createdUserId}'; DELETE FROM auth.encryption_vaults WHERE user_id = '${createdUserId}'; DELETE FROM auth.users WHERE id = '${createdUserId}';` ); @@ -149,6 +163,13 @@ test('full register → verify → login → vault → logout flow', async () => expect(reg.json.user?.id).toBeTruthy(); createdUserId = reg.json.user!.id; + // 1a. Audit row was actually committed. + // Catches the bug class where security.logEvent silently swallows + // a SQL error (e.g. undefined params collapsing the values list) + // and the audit log goes into the void. + const registerAuditCount = await countSecurityEvents(createdUserId, 'REGISTER'); + expect(registerAuditCount).toBe(1); + // 2. Wait for the verification email to land in mailpit const mail = await waitForMail(TEST_EMAIL); expect(mail.html.length).toBeGreaterThan(0); @@ -188,6 +209,10 @@ test('full register → verify → login → vault → logout flow', async () => expect(login.json.refreshToken).toBeTruthy(); const jwt = login.json.accessToken!; + // 5a. Audit row for the successful login was committed too. + const loginAuditCount = await countSecurityEvents(createdUserId, 'LOGIN_SUCCESS'); + expect(loginAuditCount).toBe(1); + // 6. Validate the JWT against the same service that minted it const validate = await postJson<{ valid: boolean; payload?: { sub: string; email: string } }>( '/api/v1/auth/validate',