From dee3bb098a1e728cbd8f3452fd15054bd95709de Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Wed, 27 May 2026 21:52:05 +0200 Subject: [PATCH] e2e: fix 2fa leakage in next tests --- e2e/0002-backup-restore.spec.ts | 11 ++--- e2e/0004-mirror-sync.spec.ts | 4 +- e2e/0005-two-factor.spec.ts | 79 +++++++++++++++++++++++---------- 3 files changed, 64 insertions(+), 30 deletions(-) diff --git a/e2e/0002-backup-restore.spec.ts b/e2e/0002-backup-restore.spec.ts index 018c1381..80e3ad57 100644 --- a/e2e/0002-backup-restore.spec.ts +++ b/e2e/0002-backup-restore.spec.ts @@ -576,15 +576,16 @@ test("deleting a volume cascades and removes its backup schedule", async ({ page await volumeLink.click(); await expect(page).toHaveURL(/\/volumes\/[^/?#]+/); await expect(page.getByRole("button", { name: "Actions" })).toBeVisible(); - const deleteVolumeMenuItem = page.getByRole("menuitem", { name: "Delete" }); - await expect(async () => { await page.getByRole("button", { name: "Actions" }).click(); + const deleteVolumeMenuItem = page.getByRole("menuitem", { name: "Delete" }); await expect(deleteVolumeMenuItem).toBeVisible(); + await deleteVolumeMenuItem.click(); + await expect(page.getByRole("heading", { name: "Delete volume?" })).toBeVisible(); }).toPass({ timeout: 10000 }); - await deleteVolumeMenuItem.click({ force: true }); - await expect(page.getByRole("heading", { name: "Delete volume?" })).toBeVisible(); - await expect(page.getByText("All backup schedules associated with this volume will also be removed.")).toBeVisible(); + await expect( + page.getByText("All backup schedules associated with this volume will also be removed."), + ).toBeVisible(); await page.getByRole("button", { name: "Delete volume" }).click(); await expect(page.getByText("Volume deleted successfully")).toBeVisible(); diff --git a/e2e/0004-mirror-sync.spec.ts b/e2e/0004-mirror-sync.spec.ts index 18caddba..5616abfd 100644 --- a/e2e/0004-mirror-sync.spec.ts +++ b/e2e/0004-mirror-sync.spec.ts @@ -52,8 +52,8 @@ async function getRepositoryShortId(page: Page, name: string) { } async function createRepository(page: Page, name: string) { - await gotoAndWaitForAppReady(page, "/repositories"); - await page.getByRole("button", { name: "Create repository" }).click(); + await gotoAndWaitForAppReady(page, "/repositories/create"); + await expect(page.getByRole("textbox", { name: "Name" })).toBeVisible(); await page.getByRole("textbox", { name: "Name" }).fill(name); await page.getByRole("combobox", { name: "Backend" }).click(); await page.getByRole("option", { name: "Local" }).click(); diff --git a/e2e/0005-two-factor.spec.ts b/e2e/0005-two-factor.spec.ts index ec0a39a5..8b10b4c6 100644 --- a/e2e/0005-two-factor.spec.ts +++ b/e2e/0005-two-factor.spec.ts @@ -1,10 +1,36 @@ -import type { Page } from "@playwright/test"; +import type { Browser, Page } from "@playwright/test"; import { base32 } from "@better-auth/utils/base32"; import { createOTP } from "@better-auth/utils/otp"; +import path from "node:path"; import { expect, test } from "./test"; import { gotoAndWaitForAppReady } from "./helpers/page"; -const workerPassword = "password123"; +const twoFactorPassword = "password123"; + +async function createTwoFactorUser(browser: Browser, username: string) { + const context = await browser.newContext({ + baseURL: `http://${process.env.SERVER_IP}:4096`, + storageState: { cookies: [], origins: [] }, + }); + const page = await context.newPage(); + + await gotoAndWaitForAppReady(page, "/onboarding"); + await page.getByRole("textbox", { name: "Email" }).fill(`${username}@example.com`); + await page.getByRole("textbox", { name: "Username" }).fill(username); + await page.getByRole("textbox", { name: "Password", exact: true }).fill(twoFactorPassword); + await page.getByRole("textbox", { name: "Confirm Password" }).fill(twoFactorPassword); + await page.getByRole("button", { name: "Create admin user" }).click(); + await expect(page.getByText("Download Your Recovery Key")).toBeVisible(); + + await page.getByRole("textbox", { name: "Confirm Your Password" }).fill(twoFactorPassword); + const downloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "Download Recovery Key" }).click(); + const download = await downloadPromise; + await download.saveAs(path.join(process.cwd(), "playwright", `restic-${username}.pass`)); + await expect(page).toHaveURL("/volumes"); + + return { context, page }; +} async function generateTotp(secret: string) { const decodedSecret = new TextDecoder().decode(base32.decode(secret)); @@ -16,36 +42,43 @@ async function fillOtp(page: Page, code: string) { await page.keyboard.type(code); } -test("user can enable 2FA and sign in with a TOTP code", async ({ page }) => { - await gotoAndWaitForAppReady(page, "/settings"); +test("user can enable 2FA and sign in with a TOTP code", async ({ browser }, testInfo) => { + const username = `e2e-2fa-${testInfo.parallelIndex}-${testInfo.retry}`; + const { context, page: twoFactorPage } = await createTwoFactorUser(browser, username); - const username = await page.locator("#username").inputValue(); + try { + await gotoAndWaitForAppReady(twoFactorPage, "/settings"); - await page.getByRole("button", { name: "Enable 2FA" }).click(); - await page.getByRole("textbox", { name: "Password" }).fill(workerPassword); - await page.getByRole("button", { name: "Continue" }).click(); + await twoFactorPage.getByRole("button", { name: "Enable 2FA" }).click(); + await twoFactorPage.getByRole("textbox", { name: "Password" }).fill(twoFactorPassword); + await twoFactorPage.getByRole("button", { name: "Continue" }).click(); - await expect(page.getByRole("heading", { name: "Scan QR Code" })).toBeVisible(); - const secret = await page.locator("input[readonly]").inputValue(); + await expect(twoFactorPage.getByRole("heading", { name: "Scan QR Code" })).toBeVisible(); + const secret = await twoFactorPage.locator("input[readonly]").inputValue(); - await page.getByRole("button", { name: "Continue" }).click(); - await expect(page.getByRole("heading", { name: "Verify setup" })).toBeVisible(); + await twoFactorPage.getByRole("button", { name: "Continue" }).click(); + await expect(twoFactorPage.getByRole("heading", { name: "Verify setup" })).toBeVisible(); - await fillOtp(page, await generateTotp(secret)); + await fillOtp(twoFactorPage, await generateTotp(secret)); - await expect(page.getByText(/Status:\s*Enabled/)).toBeVisible(); + await expect(twoFactorPage.getByText(/Status:\s*Enabled/)).toBeVisible(); - await page.context().clearCookies(); - await gotoAndWaitForAppReady(page, "/login"); + await context.clearCookies(); + await gotoAndWaitForAppReady(twoFactorPage, "/login"); - await page.getByRole("textbox", { name: "Username" }).fill(username); - await page.getByRole("textbox", { name: "Password" }).fill(workerPassword); - await page.getByRole("button", { name: "Login" }).click(); + const usernameInput = twoFactorPage.getByRole("textbox", { name: "Username" }); + await usernameInput.fill(username); + await expect(usernameInput).toHaveValue(username); + await twoFactorPage.getByRole("textbox", { name: "Password" }).fill(twoFactorPassword); + await twoFactorPage.getByRole("button", { name: "Login" }).click(); - await expect(page.getByRole("heading", { name: "Two-Factor Authentication" })).toBeVisible(); + await expect(twoFactorPage.getByRole("heading", { name: "Two-Factor Authentication" })).toBeVisible(); - await fillOtp(page, await generateTotp(secret)); + await fillOtp(twoFactorPage, await generateTotp(secret)); - await expect(page).toHaveURL("/volumes"); - await expect(page.getByRole("button", { name: "Create Volume" })).toBeVisible(); + await expect(twoFactorPage).toHaveURL("/volumes"); + await expect(twoFactorPage.getByRole("button", { name: "Create Volume" })).toBeVisible(); + } finally { + await context.close(); + } });