From 01fe007f3550be6b85f872b60b96099ab0b8bf47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 10 Sep 2026 04:52:53 +0200 Subject: [PATCH] test(e2e): take the TOTP parameters and the field hooks from the source The spec re-declared the server's algorithm, digit count and period as literals, so a change to any of them would leave the spec generating codes the server no longer verifies and blaming the product. It imports the three exported constants instead; the recovery-batch size stays a literal, because its module pulls in the database client and would drag the generated client into the Playwright process. The two credential fields on the login form are addressed by test id now, like the submit button beside them, and the recovery toggle at the challenge is renamed to the house's noun-verb order. --- e2e/mfa-step-up-journey.spec.ts | 19 ++++++++++++------- src/app/auth/login/page.tsx | 2 ++ src/components/auth/mfa-login-step.tsx | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/e2e/mfa-step-up-journey.spec.ts b/e2e/mfa-step-up-journey.spec.ts index bc9ed52db..7a42acb95 100644 --- a/e2e/mfa-step-up-journey.spec.ts +++ b/e2e/mfa-step-up-journey.spec.ts @@ -6,6 +6,12 @@ import type { } from "@playwright/test"; import * as OTPAuth from "otpauth"; +import { + TOTP_ALGORITHM, + TOTP_DIGITS, + TOTP_PERIOD_SECONDS, +} from "@/lib/auth/mfa/totp"; + import { expect, test } from "./setup/test"; import { clearAuthRateLimits, @@ -77,7 +83,6 @@ import { * current step has too little left — instead of hoping the wall clock is kind. */ -const TOTP_PERIOD_SECONDS = 30; const RECOVERY_CODE_COUNT = 10; const REGENERATE_ENDPOINT = "/api/auth/me/mfa/recovery-codes/regenerate"; /** `/api/auth/login`, per IP per 15 minutes (`src/lib/rate-limit.ts`). */ @@ -96,8 +101,8 @@ function totpCodeAt(secretBase32: string, atMs: number): string { return new OTPAuth.TOTP({ issuer: "HealthLog", label: "HealthLog", - algorithm: "SHA1", - digits: 6, + algorithm: TOTP_ALGORITHM, + digits: TOTP_DIGITS, period: TOTP_PERIOD_SECONDS, secret: OTPAuth.Secret.fromBase32(secretBase32), }).generate({ timestamp: atMs }); @@ -141,8 +146,8 @@ async function submitPassword( await clearAuthRateLimits(); await page.goto("/auth/login"); await page.getByTestId("login-use-password").click(); - await page.locator("#email").fill(account.username); - await page.locator("#password").fill(account.password); + await page.getByTestId("login-email").fill(account.username); + await page.getByTestId("login-password").fill(account.password); const answer = page.waitForResponse( (res) => res.url().includes("/api/auth/login") && @@ -394,7 +399,7 @@ test.describe("second factor", () => { }); await test.step("spends a recovery code once", async () => { - await page.getByTestId("mfa-toggle-recovery").click(); + await page.getByTestId("mfa-recovery-toggle").click(); const answer = await submitFactor(page, liveRecoveryCodes[0]); expect(answer.status()).toBe(200); await page.waitForURL("/"); @@ -406,7 +411,7 @@ test.describe("second factor", () => { await submitPassword(page, account); await expect(page.getByTestId("mfa-login-step")).toBeVisible(); - await page.getByTestId("mfa-toggle-recovery").click(); + await page.getByTestId("mfa-recovery-toggle").click(); const answer = await submitFactor(page, liveRecoveryCodes[0]); expect(answer.status()).toBe(401); await expect(page.getByTestId("mfa-error")).toBeVisible(); diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 778fe26b2..2ece6eabc 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -438,6 +438,7 @@ export default function LoginPage() { setEmail(e.target.value)} @@ -459,6 +460,7 @@ export default function LoginPage() { setPassword(e.target.value)} diff --git a/src/components/auth/mfa-login-step.tsx b/src/components/auth/mfa-login-step.tsx index 1b0900fe6..2e6f893e3 100644 --- a/src/components/auth/mfa-login-step.tsx +++ b/src/components/auth/mfa-login-step.tsx @@ -198,7 +198,7 @@ export function MfaLoginStep({ {hasTotp && (