/** * End-to-end tests for Auth procedures * * These tests cover ALL login scenarios from docs/initial-app.md: * * SIGNUP FLOWS: * - Signup with password * - Signup with passkey * * LOGIN FLOWS (from docs/initial-app.md Step 2: Authentication): * - Has passkey → full passkey authentication flow * - Has password + trusted device → immediate completion * - Has password + new device → requires email confirmation * - No password AND no passkey → polling stays pending * - Non-existent email → anti-enumeration (fake token, returns pending) * * OTHER FLOWS: * - Email verification flow * - Password reset flow (with session revocation) * - Logout * * Procedures tested: * - auth.signup - create account with password or passkey * - auth.createLoginRequest - first step of login flow * - auth.loginPassword - password verification * - auth.loginPasswordConfirm - email confirmation for untrusted devices * - auth.loginIfRequestIsCompleted - poll for login completion * - auth.webauthn.createRegistrationOptions - passkey registration * - auth.webauthn.verifyRegistration - passkey registration verification * - auth.webauthn.createAuthenticationOptions - passkey authentication * - auth.webauthn.verifyAuthentication - passkey authentication verification * - auth.verifyEmail - verify email token * - auth.resendVerificationEmail - resend verification * - auth.forgotPassword - request password reset * - auth.resetPassword - reset with token * - auth.logout - revoke session */ import type { Database } from "@reviq/db-schema"; import type { Kysely } from "kysely"; import type { APIContext } from "../../context.js"; import { beforeAll, describe, expect, test } from "bun:test"; import { call } from "@orpc/server"; import { createLoggingEmailClient } from "@reviq/emails"; import { createTestUser, describeE2E, getSharedDb, initTestDb, TEST_RP, uniqueTestId, withTestTransaction, } from "@reviq/test-helpers"; import { VirtualAuthenticator } from "@reviq/virtual-authenticator"; import { router } from "../../router.js"; import { COOKIE_NAMES } from "../../utils/cookies.js"; import { hashToken } from "../../utils/crypto.js"; import { hashPassword } from "../../utils/password.js"; /** Session expiry duration: 24 hours in milliseconds */ const SESSION_EXPIRY_MS = 24 * 60 * 60 * 1000; /** Login request expiry: 15 minutes */ const LOGIN_REQUEST_EXPIRY_MS = 15 * 60 * 1000; /** * Create an API context with optional cookies */ function createAPIContext( db: Kysely, options?: { sessionToken?: string; loginRequestToken?: string; deviceFingerprint?: string; }, ): APIContext { const reqHeaders = new Headers(); const cookies: string[] = []; if (options?.sessionToken) { cookies.push(`${COOKIE_NAMES.SESSION_TOKEN}=${options.sessionToken}`); } if (options?.loginRequestToken) { cookies.push( `${COOKIE_NAMES.LOGIN_REQUEST_TOKEN}=${options.loginRequestToken}`, ); } if (options?.deviceFingerprint) { cookies.push( `${COOKIE_NAMES.DEVICE_FINGERPRINT}=${options.deviceFingerprint}`, ); } if (cookies.length > 0) { reqHeaders.set("cookie", cookies.join("; ")); } return { db, origin: TEST_RP.origin, allowedOrigins: [...TEST_RP.allowedOrigins], rpName: TEST_RP.rpName, reqHeaders, resHeaders: new Headers(), email: { client: createLoggingEmailClient(), fromAddress: "test@example.com", baseUrl: TEST_RP.origin, }, }; } /** * Extract cookie value from response headers */ function getCookieFromResponse( headers: Headers, cookieName: string, ): string | null { const setCookies = headers.getSetCookie(); for (const cookie of setCookies) { if (cookie.startsWith(`${cookieName}=`)) { const parts = cookie.split(";")[0]?.split("="); const value = parts?.[1] ?? ""; // Check if it's a deletion (empty value or max-age=0) if (cookie.includes("Max-Age=0") || value === "") { return null; } return value; } } return null; } /** * Assert a value is not null/undefined and return it with narrowed type. * Use this instead of non-null assertions (!) to satisfy linter. */ function assertDefined( value: T | null | undefined, message = "Expected value to be defined", ): T { if (value == null) { throw new Error(message); } return value; } /** * Create a session for a user and return the token */ async function createSession( db: Kysely, userId: number, options?: { deviceId?: bigint }, ): Promise<{ token: string; sessionId: number }> { const token = `test-session-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); const result = await db .insertInto("sessions") .values({ user_id: userId, device_id: options?.deviceId ? options.deviceId.toString() : null, token_hash: tokenHashValue, trusted_mode: false, expires_at: expiresAt, }) .returning(["id"]) .executeTakeFirstOrThrow(); return { token, sessionId: Number(result.id) }; } /** * Create a login request for a user */ async function createLoginRequest( db: Kysely, userId: number, email: string, options?: { deviceFingerprint?: string; completedAt?: Date | null; expiresAt?: Date; }, ): Promise<{ token: string; id: number }> { const token = `login_test-${uniqueTestId()}`; const expiresAt = options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS); const result = await db .insertInto("login_requests") .values({ user_id: userId, email, token, device_fingerprint: options?.deviceFingerprint ?? "test-fingerprint", expires_at: expiresAt, completed_at: options?.completedAt ?? null, }) .returning(["id"]) .executeTakeFirstOrThrow(); return { token, id: Number(result.id) }; } /** * Create a trusted device for a user */ async function createTrustedDevice( db: Kysely, userId: number, fingerprint: string, ): Promise { const result = await db .insertInto("user_devices") .values({ user_id: userId, device_fingerprint: fingerprint, is_trusted: true, user_agent: "Test Browser", }) .returning(["id"]) .executeTakeFirstOrThrow(); return BigInt(result.id); } /** * Create an email verification token */ async function createEmailVerification( db: Kysely, userId: number, options?: { expiresAt?: Date }, ): Promise { const token = `verify-${uniqueTestId()}`; const expiresAt = options?.expiresAt ?? new Date(Date.now() + 24 * 60 * 60 * 1000); await db .insertInto("email_verifications") .values({ user_id: userId, token, expires_at: expiresAt, }) .execute(); return token; } /** * Create a password reset token */ async function createPasswordReset( db: Kysely, userId: number, options?: { expiresAt?: Date; usedAt?: Date | null }, ): Promise { const token = `reset-${uniqueTestId()}`; const expiresAt = options?.expiresAt ?? new Date(Date.now() + 60 * 60 * 1000); await db .insertInto("password_resets") .values({ user_id: userId, token, expires_at: expiresAt, used_at: options?.usedAt ?? null, }) .execute(); return token; } describeE2E("auth", () => { // Test setup beforeAll(async () => { await initTestDb(); }); // ============================================================================= // auth.signup tests // ============================================================================= describe("auth.signup", () => { test("creates user with valid password", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); const result = await call( router.auth.signup, { email: "newuser@example.com", password: "StrongP@ssw0rd123!" }, { context: ctx }, ); expect(result.success).toBe(true); // Verify user was created const user = await db .selectFrom("users") .selectAll() .where("email", "=", "newuser@example.com") .executeTakeFirst(); expect(user).toBeDefined(); expect(user?.password_hash).not.toBeNull(); expect(user?.email_verified_at).toBeNull(); // Verify session cookie was set const sessionToken = getCookieFromResponse( ctx.resHeaders, COOKIE_NAMES.SESSION_TOKEN, ); expect(sessionToken).not.toBeNull(); // Verify session was created in DB const sessions = await db .selectFrom("sessions") .selectAll() .where("user_id", "=", assertDefined(user).id) .execute(); expect(sessions.length).toBe(1); // Verify email verification token was created const verifications = await db .selectFrom("email_verifications") .selectAll() .where("user_id", "=", assertDefined(user).id) .execute(); expect(verifications.length).toBe(1); }); }); test("normalizes email to lowercase", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); await call( router.auth.signup, { email: "UPPERCASE@EXAMPLE.COM", password: "StrongP@ssw0rd123!" }, { context: ctx }, ); const user = await db .selectFrom("users") .select(["email"]) .where("email", "=", "uppercase@example.com") .executeTakeFirst(); expect(user).toBeDefined(); }); }); test("rejects weak password", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); await expect( call( router.auth.signup, { email: "weak@example.com", password: "password" }, { context: ctx }, ), ).rejects.toThrow(); }); }); test("rejects duplicate email (anti-enumeration)", async () => { await withTestTransaction(getSharedDb(), async (db) => { // Create existing user await createTestUser(db, { email: "existing@example.com" }); const ctx = createAPIContext(db); await expect( call( router.auth.signup, { email: "existing@example.com", password: "StrongP@ssw0rd123!" }, { context: ctx }, ), ).rejects.toThrow("Unable to create account"); }); }); test("rejects signup without password or passkey", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); await expect( call( router.auth.signup, { email: "noauth@example.com" }, { context: ctx }, ), ).rejects.toThrow(); }); }); // Note: This test uses getSharedDb() directly (not withTestTransaction) because // the signup procedure internally uses db.transaction(), and Kysely doesn't support // nested transactions. test("creates user with passkey", async () => { const db = getSharedDb(); const authenticator = new VirtualAuthenticator({ origin: TEST_RP.origin, }); const ctx = createAPIContext(db); // Step 1: Create registration options const { options, challengeId } = await call( router.auth.webauthn.createRegistrationOptions, { email: "passkeyuser@example.com" }, { context: ctx }, ); // Step 2: Create credential with virtual authenticator const response = authenticator.createCredential(options); // Step 3: Signup with passkey const signupCtx = createAPIContext(db); const result = await call( router.auth.signup, { email: "passkeyuser@example.com", passkeyInfo: { challengeId, response }, }, { context: signupCtx }, ); expect(result.success).toBe(true); // Verify user was created const user = await db .selectFrom("users") .selectAll() .where("email", "=", "passkeyuser@example.com") .executeTakeFirst(); expect(user).toBeDefined(); expect(user?.password_hash).toBeNull(); // No password for passkey signup expect(user?.email_verified_at).toBeNull(); // Verify passkey was stored const passkeys = await db .selectFrom("passkeys") .selectAll() .where("user_id", "=", assertDefined(user).id) .execute(); expect(passkeys.length).toBe(1); expect(passkeys[0]?.name).toBeDefined(); // Verify session cookie was set const sessionToken = getCookieFromResponse( signupCtx.resHeaders, COOKIE_NAMES.SESSION_TOKEN, ); expect(sessionToken).not.toBeNull(); // Verify webauthn challenge was deleted const challenges = await db .selectFrom("webauthn_challenges") .selectAll() .where("id", "=", challengeId.toString()) .execute(); expect(challenges.length).toBe(0); }); test("rejects passkey signup with expired challenge", async () => { await withTestTransaction(getSharedDb(), async (db) => { const authenticator = new VirtualAuthenticator({ origin: TEST_RP.origin, }); const ctx = createAPIContext(db); // Step 1: Create registration options const { options, challengeId } = await call( router.auth.webauthn.createRegistrationOptions, { email: "expiredchallenge@example.com" }, { context: ctx }, ); // Step 2: Create credential const response = authenticator.createCredential(options); // Step 3: Expire the challenge by updating created_at await db .updateTable("webauthn_challenges") .set({ created_at: new Date(Date.now() - 20 * 60 * 1000) }) // 20 minutes ago .where("id", "=", challengeId.toString()) .execute(); // Step 4: Try to signup with expired challenge const signupCtx = createAPIContext(db); await expect( call( router.auth.signup, { email: "expiredchallenge@example.com", passkeyInfo: { challengeId, response }, }, { context: signupCtx }, ), ).rejects.toThrow("Registration timed out"); }); }); test("rejects passkey signup with invalid response", async () => { await withTestTransaction(getSharedDb(), async (db) => { const authenticator = new VirtualAuthenticator({ origin: TEST_RP.origin, }); const ctx = createAPIContext(db); // Step 1: Create registration options const { options, challengeId } = await call( router.auth.webauthn.createRegistrationOptions, { email: "invalidresponse@example.com" }, { context: ctx }, ); // Step 2: Create credential const response = authenticator.createCredential(options); // Step 3: Tamper with the response response.response.clientDataJSON = "dGFtcGVyZWQ"; // "tampered" in base64 // Step 4: Try to signup with invalid response const signupCtx = createAPIContext(db); await expect( call( router.auth.signup, { email: "invalidresponse@example.com", passkeyInfo: { challengeId, response }, }, { context: signupCtx }, ), ).rejects.toThrow("Failed to register your device"); // Verify challenge was deleted (cleanup on error) const challenges = await db .selectFrom("webauthn_challenges") .selectAll() .where("id", "=", challengeId.toString()) .execute(); expect(challenges.length).toBe(0); }); }); }); // ============================================================================= // auth.createLoginRequest tests // ============================================================================= describe("auth.createLoginRequest", () => { test("returns auth methods for existing user with password", async () => { await withTestTransaction(getSharedDb(), async (db) => { await createTestUser(db, { email: "haspassword@example.com", passwordHash: await hashPassword("TestPassword123!"), }); const ctx = createAPIContext(db); const result = await call( router.auth.createLoginRequest, { email: "haspassword@example.com" }, { context: ctx }, ); expect(result.hasPassword).toBe(true); expect(result.hasPasskey).toBe(false); expect(result.isTrustedDevice).toBe(false); expect(result.email).toBe("haspassword@example.com"); // Verify login request was created const loginRequests = await db .selectFrom("login_requests") .selectAll() .execute(); expect(loginRequests.length).toBe(1); // Verify login request token cookie was set const token = getCookieFromResponse( ctx.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, ); expect(token).not.toBeNull(); expect(token).toStartWith("login_"); }); }); test("detects trusted device", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "trusted@example.com", passwordHash: await hashPassword("TestPassword123!"), }); const fingerprint = "trusted-device-fp"; await createTrustedDevice(db, user.id, fingerprint); const ctx = createAPIContext(db, { deviceFingerprint: fingerprint }); const result = await call( router.auth.createLoginRequest, { email: "trusted@example.com" }, { context: ctx }, ); expect(result.isTrustedDevice).toBe(true); }); }); test("returns fake response for non-existent user (anti-enumeration)", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); const result = await call( router.auth.createLoginRequest, { email: "nonexistent@example.com" }, { context: ctx }, ); // Should return all false (same as user without any auth methods) expect(result.hasPassword).toBe(false); expect(result.hasPasskey).toBe(false); expect(result.isTrustedDevice).toBe(false); // Should still set a login request token cookie (fake one) const token = getCookieFromResponse( ctx.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, ); expect(token).not.toBeNull(); // Should NOT create a login request in DB const loginRequests = await db .selectFrom("login_requests") .selectAll() .execute(); expect(loginRequests.length).toBe(0); }); }); test("normalizes email to lowercase", async () => { await withTestTransaction(getSharedDb(), async (db) => { await createTestUser(db, { email: "lowercase@example.com", passwordHash: await hashPassword("TestPassword123!"), }); const ctx = createAPIContext(db); const result = await call( router.auth.createLoginRequest, { email: "LOWERCASE@EXAMPLE.COM" }, { context: ctx }, ); expect(result.hasPassword).toBe(true); }); }); test("generates device fingerprint if not present", async () => { await withTestTransaction(getSharedDb(), async (db) => { await createTestUser(db, { email: "nofingerprint@example.com", passwordHash: await hashPassword("TestPassword123!"), }); const ctx = createAPIContext(db); // No device fingerprint await call( router.auth.createLoginRequest, { email: "nofingerprint@example.com" }, { context: ctx }, ); // Should set device fingerprint cookie const fingerprint = getCookieFromResponse( ctx.resHeaders, COOKIE_NAMES.DEVICE_FINGERPRINT, ); expect(fingerprint).not.toBeNull(); }); }); }); // ============================================================================= // auth.loginPassword tests // ============================================================================= describe("auth.loginPassword", () => { test("completes login immediately for trusted device", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "trustedlogin@example.com", passwordHash: await hashPassword("TestPassword123!"), }); const fingerprint = "trusted-login-fp"; await createTrustedDevice(db, user.id, fingerprint); const { token: loginToken } = await createLoginRequest( db, user.id, "trustedlogin@example.com", { deviceFingerprint: fingerprint }, ); const ctx = createAPIContext(db, { loginRequestToken: loginToken, deviceFingerprint: fingerprint, }); const result = await call( router.auth.loginPassword, { password: "TestPassword123!" }, { context: ctx }, ); expect(result.success).toBe(true); // Verify login request was marked as completed const loginRequest = await db .selectFrom("login_requests") .select(["completed_at"]) .where("token", "=", loginToken) .executeTakeFirst(); expect(loginRequest?.completed_at).not.toBeNull(); }); }); test("sends email for untrusted device (does not complete immediately)", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "untrustedlogin@example.com", passwordHash: await hashPassword("TestPassword123!"), }); const fingerprint = "untrusted-login-fp"; const { token: loginToken } = await createLoginRequest( db, user.id, "untrustedlogin@example.com", { deviceFingerprint: fingerprint }, ); const ctx = createAPIContext(db, { loginRequestToken: loginToken, deviceFingerprint: fingerprint, }); const result = await call( router.auth.loginPassword, { password: "TestPassword123!" }, { context: ctx }, ); expect(result.success).toBe(true); // Verify login request was NOT marked as completed (needs email confirmation) const loginRequest = await db .selectFrom("login_requests") .select(["completed_at"]) .where("token", "=", loginToken) .executeTakeFirst(); expect(loginRequest?.completed_at).toBeNull(); }); }); test("rejects invalid password", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "wrongpass@example.com", passwordHash: await hashPassword("CorrectPassword123!"), }); const { token: loginToken } = await createLoginRequest( db, user.id, "wrongpass@example.com", ); const ctx = createAPIContext(db, { loginRequestToken: loginToken }); await expect( call( router.auth.loginPassword, { password: "WrongPassword123!" }, { context: ctx }, ), ).rejects.toThrow("Invalid email or password"); }); }); test("rejects expired login request", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "expired@example.com", passwordHash: await hashPassword("TestPassword123!"), }); const { token: loginToken } = await createLoginRequest( db, user.id, "expired@example.com", { expiresAt: new Date(Date.now() - 1000) }, // Expired ); const ctx = createAPIContext(db, { loginRequestToken: loginToken }); await expect( call( router.auth.loginPassword, { password: "TestPassword123!" }, { context: ctx }, ), ).rejects.toThrow("Login request has expired"); }); }); test("rejects when no login request token cookie", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); // No login request token await expect( call( router.auth.loginPassword, { password: "TestPassword123!" }, { context: ctx }, ), ).rejects.toThrow("Invalid email or password"); }); }); test("rejects fake/invalid login request token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db, { loginRequestToken: "fake-token-12345", }); await expect( call( router.auth.loginPassword, { password: "TestPassword123!" }, { context: ctx }, ), ).rejects.toThrow("Invalid email or password"); }); }); test("rejects user without password set", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "nopassword@example.com", // No password hash }); const { token: loginToken } = await createLoginRequest( db, user.id, "nopassword@example.com", ); const ctx = createAPIContext(db, { loginRequestToken: loginToken }); await expect( call( router.auth.loginPassword, { password: "AnyPassword123!" }, { context: ctx }, ), ).rejects.toThrow("Invalid email or password"); }); }); }); // ============================================================================= // auth.loginPasswordConfirm tests // ============================================================================= describe("auth.loginPasswordConfirm", () => { test("marks login request as completed with valid token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "confirm@example.com", }); const { token: loginToken } = await createLoginRequest( db, user.id, "confirm@example.com", ); const ctx = createAPIContext(db); const result = await call( router.auth.loginPasswordConfirm, { token: loginToken }, { context: ctx }, ); expect(result.success).toBe(true); // Verify login request was marked as completed const loginRequest = await db .selectFrom("login_requests") .select(["completed_at"]) .where("token", "=", loginToken) .executeTakeFirst(); expect(loginRequest?.completed_at).not.toBeNull(); }); }); test("is idempotent for already completed requests", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "idempotent@example.com", }); const { token: loginToken } = await createLoginRequest( db, user.id, "idempotent@example.com", { completedAt: new Date() }, // Already completed ); const ctx = createAPIContext(db); const result = await call( router.auth.loginPasswordConfirm, { token: loginToken }, { context: ctx }, ); expect(result.success).toBe(true); }); }); test("rejects invalid token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); await expect( call( router.auth.loginPasswordConfirm, { token: "invalid-token" }, { context: ctx }, ), ).rejects.toThrow("Invalid or expired confirmation link"); }); }); test("rejects expired token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "expiredconfirm@example.com", }); const { token: loginToken } = await createLoginRequest( db, user.id, "expiredconfirm@example.com", { expiresAt: new Date(Date.now() - 1000) }, // Expired ); const ctx = createAPIContext(db); await expect( call( router.auth.loginPasswordConfirm, { token: loginToken }, { context: ctx }, ), ).rejects.toThrow("Invalid or expired confirmation link"); }); }); }); // ============================================================================= // auth.loginIfRequestIsCompleted tests // ============================================================================= describe("auth.loginIfRequestIsCompleted", () => { test("returns pending for incomplete login request", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "pending@example.com", }); const { token: loginToken } = await createLoginRequest( db, user.id, "pending@example.com", ); const ctx = createAPIContext(db, { loginRequestToken: loginToken }); const result = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx }, ); expect(result.status).toBe("pending"); }); }); test("returns expired for expired login request", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "expiredpoll@example.com", }); const { token: loginToken } = await createLoginRequest( db, user.id, "expiredpoll@example.com", { expiresAt: new Date(Date.now() - 1000) }, // Expired ); const ctx = createAPIContext(db, { loginRequestToken: loginToken }); const result = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx }, ); expect(result.status).toBe("expired"); }); }); test("creates session and returns completed for completed request", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "completed@example.com", }); const fingerprint = "completed-fp"; const { token: loginToken, id: loginRequestId } = await createLoginRequest(db, user.id, "completed@example.com", { deviceFingerprint: fingerprint, completedAt: new Date(), }); const ctx = createAPIContext(db, { loginRequestToken: loginToken, deviceFingerprint: fingerprint, }); const result = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx }, ); expect(result.status).toBe("completed"); expect(result.redirectTo).toBe("/auth/trust-device"); // Not trusted yet // Verify session was created const sessions = await db .selectFrom("sessions") .selectAll() .where("user_id", "=", user.id) .execute(); expect(sessions.length).toBe(1); expect(sessions[0]?.trusted_mode).toBe(true); // Verify session cookie was set const sessionToken = getCookieFromResponse( ctx.resHeaders, COOKIE_NAMES.SESSION_TOKEN, ); expect(sessionToken).not.toBeNull(); // Verify login request was deleted const loginRequest = await db .selectFrom("login_requests") .selectAll() .where("id", "=", loginRequestId.toString()) .executeTakeFirst(); expect(loginRequest).toBeUndefined(); // Verify user device was created const devices = await db .selectFrom("user_devices") .selectAll() .where("user_id", "=", user.id) .execute(); expect(devices.length).toBe(1); }); }); test("redirects to dashboard if device is already trusted", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "alreadytrusted@example.com", }); const fingerprint = "already-trusted-fp"; await createTrustedDevice(db, user.id, fingerprint); const { token: loginToken } = await createLoginRequest( db, user.id, "alreadytrusted@example.com", { deviceFingerprint: fingerprint, completedAt: new Date() }, ); const ctx = createAPIContext(db, { loginRequestToken: loginToken, deviceFingerprint: fingerprint, }); const result = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx }, ); expect(result.status).toBe("completed"); expect(result.redirectTo).toBe("/dashboard"); }); }); test("returns pending for fake/non-existent token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db, { loginRequestToken: "fake-token-xyz", }); const result = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx }, ); expect(result.status).toBe("pending"); }); }); test("returns pending when no cookie present", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); // No login request token const result = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx }, ); expect(result.status).toBe("pending"); }); }); test("returns pending when device fingerprint is missing", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "nofp@example.com", }); // Create login request without device fingerprint const token = `login_test-${uniqueTestId()}`; await db .insertInto("login_requests") .values({ user_id: user.id, email: "nofp@example.com", token, device_fingerprint: null, // No fingerprint expires_at: new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS), completed_at: new Date(), }) .execute(); const ctx = createAPIContext(db, { loginRequestToken: token }); const result = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx }, ); expect(result.status).toBe("pending"); }); }); }); // ============================================================================= // auth.verifyEmail tests // ============================================================================= describe("auth.verifyEmail", () => { test("verifies email with valid token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "verify@example.com", }); const token = await createEmailVerification(db, user.id); const ctx = createAPIContext(db); const result = await call( router.auth.verifyEmail, { token }, { context: ctx }, ); expect(result.success).toBe(true); // Verify user's email_verified_at was set const updatedUser = await db .selectFrom("users") .select(["email_verified_at"]) .where("id", "=", user.id) .executeTakeFirst(); expect(updatedUser?.email_verified_at).not.toBeNull(); // Verify verification record was deleted const verifications = await db .selectFrom("email_verifications") .selectAll() .where("user_id", "=", user.id) .execute(); expect(verifications.length).toBe(0); }); }); test("rejects invalid token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); await expect( call( router.auth.verifyEmail, { token: "invalid-token" }, { context: ctx }, ), ).rejects.toThrow("Invalid or expired token"); }); }); test("rejects expired token and cleans up", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "expiredverify@example.com", }); const token = await createEmailVerification(db, user.id, { expiresAt: new Date(Date.now() - 1000), // Expired }); const ctx = createAPIContext(db); await expect( call(router.auth.verifyEmail, { token }, { context: ctx }), ).rejects.toThrow("Invalid or expired token"); // Verify expired token was cleaned up const verifications = await db .selectFrom("email_verifications") .selectAll() .where("user_id", "=", user.id) .execute(); expect(verifications.length).toBe(0); }); }); }); // ============================================================================= // auth.resendVerificationEmail tests // ============================================================================= describe("auth.resendVerificationEmail", () => { test("creates new verification token for unverified user", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "resend@example.com", }); const { token: sessionToken } = await createSession(db, user.id); const ctx = createAPIContext(db, { sessionToken }); const result = await call( router.auth.resendVerificationEmail, undefined, { context: ctx, }, ); expect(result.success).toBe(true); // Verify new verification token was created const verifications = await db .selectFrom("email_verifications") .selectAll() .where("user_id", "=", user.id) .execute(); expect(verifications.length).toBe(1); }); }); test("deletes old verification tokens before creating new one", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "resendold@example.com", }); // Create existing verification await createEmailVerification(db, user.id); const { token: sessionToken } = await createSession(db, user.id); const ctx = createAPIContext(db, { sessionToken }); await call(router.auth.resendVerificationEmail, undefined, { context: ctx, }); // Should still have only 1 verification (old one deleted, new one created) const verifications = await db .selectFrom("email_verifications") .selectAll() .where("user_id", "=", user.id) .execute(); expect(verifications.length).toBe(1); }); }); test("returns success for already verified user (no-op)", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "alreadyverified@example.com", emailVerifiedAt: new Date(), }); const { token: sessionToken } = await createSession(db, user.id); const ctx = createAPIContext(db, { sessionToken }); const result = await call( router.auth.resendVerificationEmail, undefined, { context: ctx, }, ); expect(result.success).toBe(true); // No verification token should be created const verifications = await db .selectFrom("email_verifications") .selectAll() .where("user_id", "=", user.id) .execute(); expect(verifications.length).toBe(0); }); }); test("requires authentication", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); // No session await expect( call(router.auth.resendVerificationEmail, undefined, { context: ctx, }), ).rejects.toThrow(); }); }); }); // ============================================================================= // auth.forgotPassword tests // ============================================================================= describe("auth.forgotPassword", () => { test("creates password reset token for existing user", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "forgot@example.com", }); const ctx = createAPIContext(db); const result = await call( router.auth.forgotPassword, { email: "forgot@example.com" }, { context: ctx }, ); expect(result.success).toBe(true); // Verify password reset token was created const resets = await db .selectFrom("password_resets") .selectAll() .where("user_id", "=", user.id) .execute(); expect(resets.length).toBe(1); }); }); test("returns success for non-existent user (anti-enumeration)", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); const result = await call( router.auth.forgotPassword, { email: "nonexistent@example.com" }, { context: ctx }, ); // Should still return success (anti-enumeration) expect(result.success).toBe(true); // No password reset should be created const resets = await db .selectFrom("password_resets") .selectAll() .execute(); expect(resets.length).toBe(0); }); }); test("deletes existing password reset tokens before creating new one", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "forgotold@example.com", }); // Create existing reset token await createPasswordReset(db, user.id); const ctx = createAPIContext(db); await call( router.auth.forgotPassword, { email: "forgotold@example.com" }, { context: ctx }, ); // Should have only 1 reset token (old one deleted) const resets = await db .selectFrom("password_resets") .selectAll() .where("user_id", "=", user.id) .execute(); expect(resets.length).toBe(1); }); }); test("normalizes email to lowercase", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "forgotcase@example.com", }); const ctx = createAPIContext(db); await call( router.auth.forgotPassword, { email: "FORGOTCASE@EXAMPLE.COM" }, { context: ctx }, ); // Should find the user and create reset token const resets = await db .selectFrom("password_resets") .selectAll() .where("user_id", "=", user.id) .execute(); expect(resets.length).toBe(1); }); }); }); // ============================================================================= // auth.resetPassword tests // ============================================================================= describe("auth.resetPassword", () => { test("resets password with valid token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "reset@example.com", passwordHash: await hashPassword("OldPassword123!"), }); const token = await createPasswordReset(db, user.id); const ctx = createAPIContext(db); const result = await call( router.auth.resetPassword, { token, newPassword: "NewStrongP@ssw0rd!" }, { context: ctx }, ); expect(result.success).toBe(true); // Verify password was updated (can't directly verify hash, but check updated_at) const updatedUser = await db .selectFrom("users") .select(["password_hash", "updated_at"]) .where("id", "=", user.id) .executeTakeFirst(); expect(updatedUser?.password_hash).not.toBeNull(); // Verify reset token was marked as used const reset = await db .selectFrom("password_resets") .select(["used_at"]) .where("token", "=", token) .executeTakeFirst(); expect(reset?.used_at).not.toBeNull(); }); }); test("revokes all sessions after password reset", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "resetrevoke@example.com", passwordHash: await hashPassword("OldPassword123!"), }); // Create some sessions await createSession(db, user.id); const token = await createPasswordReset(db, user.id); const ctx = createAPIContext(db); await call( router.auth.resetPassword, { token, newPassword: "NewStrongP@ssw0rd!" }, { context: ctx }, ); // Verify all sessions were revoked const sessions = await db .selectFrom("sessions") .select(["revoked_at"]) .where("user_id", "=", user.id) .execute(); for (const session of sessions) { expect(session.revoked_at).not.toBeNull(); } }); }); test("rejects invalid token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); await expect( call( router.auth.resetPassword, { token: "invalid-token", newPassword: "NewStrongP@ssw0rd!" }, { context: ctx }, ), ).rejects.toThrow("Invalid or expired reset token"); }); }); test("rejects expired token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "resetexpired@example.com", }); const token = await createPasswordReset(db, user.id, { expiresAt: new Date(Date.now() - 1000), // Expired }); const ctx = createAPIContext(db); await expect( call( router.auth.resetPassword, { token, newPassword: "NewStrongP@ssw0rd!" }, { context: ctx }, ), ).rejects.toThrow("Reset token has expired"); }); }); test("rejects already used token", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "resetused@example.com", }); const token = await createPasswordReset(db, user.id, { usedAt: new Date(), // Already used }); const ctx = createAPIContext(db); await expect( call( router.auth.resetPassword, { token, newPassword: "NewStrongP@ssw0rd!" }, { context: ctx }, ), ).rejects.toThrow("Reset token has already been used"); }); }); test("rejects weak password", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "resetweak@example.com", }); const token = await createPasswordReset(db, user.id); const ctx = createAPIContext(db); await expect( call( router.auth.resetPassword, { token, newPassword: "weak" }, { context: ctx }, ), ).rejects.toThrow(); }); }); }); // ============================================================================= // auth.logout tests // ============================================================================= describe("auth.logout", () => { test("revokes current session", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "logout@example.com", }); const { token: sessionToken, sessionId } = await createSession( db, user.id, ); const ctx = createAPIContext(db, { sessionToken }); const result = await call(router.auth.logout, undefined, { context: ctx, }); expect(result.success).toBe(true); // Verify session was revoked const session = await db .selectFrom("sessions") .select(["revoked_at"]) .where("id", "=", sessionId.toString()) .executeTakeFirst(); expect(session?.revoked_at).not.toBeNull(); // Verify session cookie was deleted const setCookies = ctx.resHeaders.getSetCookie(); const sessionCookie = setCookies.find((c) => c.startsWith(`${COOKIE_NAMES.SESSION_TOKEN}=`), ); expect(sessionCookie).toContain("Max-Age=0"); }); }); test("requires authentication", async () => { await withTestTransaction(getSharedDb(), async (db) => { const ctx = createAPIContext(db); // No session await expect( call(router.auth.logout, undefined, { context: ctx }), ).rejects.toThrow(); }); }); }); // ============================================================================= // End-to-end login scenarios from docs/initial-app.md // ============================================================================= describe("End-to-end login scenarios", () => { test("Scenario: Password login with trusted device (immediate completion)", async () => { await withTestTransaction(getSharedDb(), async (db) => { // Setup: User with password and trusted device const user = await createTestUser(db, { email: "e2e-trusted@example.com", passwordHash: await hashPassword("TestPassword123!"), }); const fingerprint = "e2e-trusted-device"; await createTrustedDevice(db, user.id, fingerprint); // Step 1: Create login request const ctx1 = createAPIContext(db, { deviceFingerprint: fingerprint }); const loginRequestResult = await call( router.auth.createLoginRequest, { email: "e2e-trusted@example.com" }, { context: ctx1 }, ); expect(loginRequestResult.hasPassword).toBe(true); expect(loginRequestResult.isTrustedDevice).toBe(true); const loginToken = getCookieFromResponse( ctx1.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, ); // Step 2: Login with password (should complete immediately for trusted device) const ctx2 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), deviceFingerprint: fingerprint, }); await call( router.auth.loginPassword, { password: "TestPassword123!" }, { context: ctx2 }, ); // Step 3: Poll for completion const ctx3 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), deviceFingerprint: fingerprint, }); const completedResult = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx3 }, ); expect(completedResult.status).toBe("completed"); expect(completedResult.redirectTo).toBe("/dashboard"); // Already trusted // Verify session was created const sessionToken = getCookieFromResponse( ctx3.resHeaders, COOKIE_NAMES.SESSION_TOKEN, ); expect(sessionToken).not.toBeNull(); }); }); test("Scenario: Password login with untrusted device (requires email confirmation)", async () => { await withTestTransaction(getSharedDb(), async (db) => { // Setup: User with password but no trusted device await createTestUser(db, { email: "e2e-untrusted@example.com", passwordHash: await hashPassword("TestPassword123!"), }); const fingerprint = "e2e-untrusted-device"; // Step 1: Create login request const ctx1 = createAPIContext(db, { deviceFingerprint: fingerprint }); const loginRequestResult = await call( router.auth.createLoginRequest, { email: "e2e-untrusted@example.com" }, { context: ctx1 }, ); expect(loginRequestResult.hasPassword).toBe(true); expect(loginRequestResult.isTrustedDevice).toBe(false); const loginToken = getCookieFromResponse( ctx1.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, ); // Step 2: Login with password (should NOT complete - needs email confirmation) const ctx2 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), deviceFingerprint: fingerprint, }); await call( router.auth.loginPassword, { password: "TestPassword123!" }, { context: ctx2 }, ); // Step 3: Poll should return pending (email not confirmed yet) const ctx3 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), deviceFingerprint: fingerprint, }); const pendingResult = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx3 }, ); expect(pendingResult.status).toBe("pending"); // Step 4: User clicks email confirmation link const ctx4 = createAPIContext(db); await call( router.auth.loginPasswordConfirm, { token: assertDefined(loginToken) }, { context: ctx4 }, ); // Step 5: Poll should now return completed const ctx5 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), deviceFingerprint: fingerprint, }); const completedResult = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx5 }, ); expect(completedResult.status).toBe("completed"); expect(completedResult.redirectTo).toBe("/auth/trust-device"); // Not yet trusted }); }); test("Scenario: Login attempt with non-existent email (anti-enumeration)", async () => { await withTestTransaction(getSharedDb(), async (db) => { // Step 1: Create login request for non-existent email const ctx1 = createAPIContext(db); const result = await call( router.auth.createLoginRequest, { email: "doesnotexist@example.com" }, { context: ctx1 }, ); // Should return all false (indistinguishable from user without auth methods) expect(result.hasPassword).toBe(false); expect(result.hasPasskey).toBe(false); expect(result.isTrustedDevice).toBe(false); const loginToken = getCookieFromResponse( ctx1.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, ); expect(loginToken).not.toBeNull(); // Still get a token (fake) // Step 2: Trying to login with password should fail const ctx2 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), }); await expect( call( router.auth.loginPassword, { password: "AnyPassword123!" }, { context: ctx2 }, ), ).rejects.toThrow("Invalid email or password"); // Step 3: Polling should return pending until expired const ctx3 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), }); const pollResult = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx3 }, ); expect(pollResult.status).toBe("pending"); // Fake token - always pending }); }); test("Scenario: Complete password reset flow", async () => { await withTestTransaction(getSharedDb(), async (db) => { // Setup: User with existing password and sessions const user = await createTestUser(db, { email: "e2e-reset@example.com", passwordHash: await hashPassword("OldPassword123!"), }); await createSession(db, user.id); await createSession(db, user.id); // Step 1: Request password reset const ctx1 = createAPIContext(db); await call( router.auth.forgotPassword, { email: "e2e-reset@example.com" }, { context: ctx1 }, ); // Get the token from DB (in real flow, this would be from email) const reset = await db .selectFrom("password_resets") .select(["token"]) .where("user_id", "=", user.id) .executeTakeFirst(); // Step 2: Reset password const ctx2 = createAPIContext(db); await call( router.auth.resetPassword, { token: assertDefined(reset).token, newPassword: "NewSecureP@ss123!", }, { context: ctx2 }, ); // Verify all old sessions were revoked const sessions = await db .selectFrom("sessions") .select(["revoked_at"]) .where("user_id", "=", user.id) .execute(); for (const session of sessions) { expect(session.revoked_at).not.toBeNull(); } // Step 3: Login with new password should work const ctx3 = createAPIContext(db); await call( router.auth.createLoginRequest, { email: "e2e-reset@example.com" }, { context: ctx3 }, ); const loginToken = getCookieFromResponse( ctx3.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, ); // Mark login as completed (simulate trusted device or email confirmation) await db .updateTable("login_requests") .set({ completed_at: new Date() }) .where("token", "=", assertDefined(loginToken)) .execute(); const ctx4 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), }); const result = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx4 }, ); expect(result.status).toBe("completed"); }); }); test("Scenario: Passkey login flow (full e2e)", async () => { await withTestTransaction(getSharedDb(), async (db) => { // Setup: User with passkey const user = await createTestUser(db, { email: "e2e-passkey-login@example.com", }); const authenticator = new VirtualAuthenticator({ origin: TEST_RP.origin, }); const fingerprint = "e2e-passkey-device"; // Create a session for passkey registration (registration requires auth) const { token: regSessionToken, sessionId: regSessionId } = await createSession(db, user.id); // Create registration options const regOptionsCtx = createAPIContext(db, { sessionToken: regSessionToken, deviceFingerprint: fingerprint, }); const { options: regOptions, challengeId: regChallengeId } = await call( router.auth.webauthn.createRegistrationOptions, { email: user.email }, { context: regOptionsCtx }, ); // Create credential with virtual authenticator const regResponse = authenticator.createCredential(regOptions); // Verify registration const verifyRegCtx = createAPIContext(db, { sessionToken: regSessionToken, deviceFingerprint: fingerprint, }); await call( router.auth.webauthn.verifyRegistration, { challengeId: regChallengeId, response: regResponse }, { context: verifyRegCtx }, ); // Clean up registration session await db .deleteFrom("sessions") .where("id", "=", regSessionId.toString()) .execute(); // Step 1: Create login request const ctx1 = createAPIContext(db, { deviceFingerprint: fingerprint }); const loginRequestResult = await call( router.auth.createLoginRequest, { email: "e2e-passkey-login@example.com" }, { context: ctx1 }, ); expect(loginRequestResult.hasPasskey).toBe(true); const loginToken = getCookieFromResponse( ctx1.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, ); expect(loginToken).not.toBeNull(); // Step 2: Create authentication options const ctx2 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), deviceFingerprint: fingerprint, }); const { options: authOptions, challengeId: authChallengeId } = await call( router.auth.webauthn.createAuthenticationOptions, undefined, { context: ctx2 }, ); expect(authOptions.allowCredentials).toHaveLength(1); // Step 3: Authenticate with passkey const authResponse = authenticator.getAssertion(authOptions); const ctx3 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), deviceFingerprint: fingerprint, }); await call( router.auth.webauthn.verifyAuthentication, { challengeId: authChallengeId, response: authResponse }, { context: ctx3 }, ); // Step 4: Poll for completion - should be completed now const ctx4 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), deviceFingerprint: fingerprint, }); const completedResult = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx4 }, ); expect(completedResult.status).toBe("completed"); // Passkey login creates a trusted session, but device is not yet trusted // So user is redirected to trust-device screen expect(completedResult.redirectTo).toBe("/auth/trust-device"); // Verify session was created with trusted_mode = true const sessions = await db .selectFrom("sessions") .selectAll() .where("user_id", "=", user.id) .execute(); expect(sessions.length).toBe(1); expect(sessions[0]?.trusted_mode).toBe(true); // Verify session cookie was set const sessionToken = getCookieFromResponse( ctx4.resHeaders, COOKIE_NAMES.SESSION_TOKEN, ); expect(sessionToken).not.toBeNull(); }); }); test("Scenario: User with no auth methods (no password, no passkey)", async () => { await withTestTransaction(getSharedDb(), async (db) => { // Setup: User without any auth methods set up // This simulates a user who was created but never completed setup await createTestUser(db, { email: "e2e-no-auth@example.com", // No password hash }); const fingerprint = "e2e-no-auth-device"; // Step 1: Create login request const ctx1 = createAPIContext(db, { deviceFingerprint: fingerprint }); const loginRequestResult = await call( router.auth.createLoginRequest, { email: "e2e-no-auth@example.com" }, { context: ctx1 }, ); // Should indicate no auth methods available expect(loginRequestResult.hasPassword).toBe(false); expect(loginRequestResult.hasPasskey).toBe(false); expect(loginRequestResult.isTrustedDevice).toBe(false); const loginToken = getCookieFromResponse( ctx1.resHeaders, COOKIE_NAMES.LOGIN_REQUEST_TOKEN, ); expect(loginToken).not.toBeNull(); // Step 2: Poll should return pending (no way to complete login) const ctx2 = createAPIContext(db, { loginRequestToken: assertDefined(loginToken), deviceFingerprint: fingerprint, }); const pendingResult = await call( router.auth.loginIfRequestIsCompleted, undefined, { context: ctx2 }, ); expect(pendingResult.status).toBe("pending"); // According to docs: "Shows 'Check your email' but no email sent, polling will expire" // The login request exists but can never be completed since there's no auth method // Verify login request exists but is not completed const loginRequest = await db .selectFrom("login_requests") .selectAll() .where("token", "=", assertDefined(loginToken)) .executeTakeFirst(); expect(loginRequest).toBeDefined(); expect(loginRequest?.completed_at).toBeNull(); }); }); }); // ============================================================================= // loginRequestMiddleware tests (base.ts) // ============================================================================= describe("loginRequestMiddleware", () => { test("rejects request with no login request cookie", async () => { await withTestTransaction(getSharedDb(), async (db) => { // No login request token in context const ctx = createAPIContext(db); await expect( call(router.auth.webauthn.createAuthenticationOptions, undefined, { context: ctx, }), ).rejects.toThrow("No login request found"); }); }); test("rejects request with invalid login request token", async () => { await withTestTransaction(getSharedDb(), async (db) => { // Invalid token that doesn't exist in DB const ctx = createAPIContext(db, { loginRequestToken: "invalid-login-request-token", }); await expect( call(router.auth.webauthn.createAuthenticationOptions, undefined, { context: ctx, }), ).rejects.toThrow("Login request expired or not found"); }); }); test("rejects request with expired login request", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "expiredloginreq@example.com", }); // Create an expired login request const { token: loginToken } = await createLoginRequest( db, user.id, user.email, { expiresAt: new Date(Date.now() - 1000) }, // Expired ); const ctx = createAPIContext(db, { loginRequestToken: loginToken }); await expect( call(router.auth.webauthn.createAuthenticationOptions, undefined, { context: ctx, }), ).rejects.toThrow("Login request expired or not found"); }); }); }); }); // Close outer describeE2E