Files
pleno-vue/tests/e2e/twoFactorAuth.spec.ts
T
Jeppe Bundgaard 983585ede7 Refactor and cleanup:
- Migrate postinstall script to `postinstall-sync-playwright-root-links.mjs` for streamlined path resolution.
- Replace `axios` v1.15.0 with v1.13.5 and downgrade `vite` from v8.0.5 to v7.1.11.
- Update testing code for consistent formatting and enhanced readability (e.g., `poll` and `catch` calls).
- Remove unused or redundant dependency flags and align `package-lock.json` with new configuration.
2026-04-14 14:17:28 +02:00

270 lines
8.4 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { mockApi } from "./support/network.js";
const USER_TOKEN = "mock-user-2fa-session-token";
const SUBUSER_TOKEN = "mock-subuser-2fa-session-token";
const USER_TWO_FACTOR_TOKEN = "mock-user-2fa-token";
const SUBUSER_TWO_FACTOR_TOKEN = "mock-subuser-2fa-token";
const VALID_CODE = "123456";
const INVALID_CODE_MESSAGE = "Invalid verification code";
const VERIFY_BUTTON = '[id="2fa-verify-button"]';
const ERROR_ALERT = '[id="2fa_auth_alert_error"]';
const SUCCESS_ALERT = '[id="2fa_auth_alert_success"]';
function json(route, body, status = 200) {
return route.fulfill({
status,
contentType: "application/json",
body: JSON.stringify(body),
});
}
function createSessionData(overrides = {}) {
return {
id: 1,
customer_number: 12345679,
group_id: 1,
email: "user@example.com",
phone: {
number: "12345678",
country_code: 45,
},
notifications: {
wash_certificate_email: null,
email_notifications_enabled: true,
sms_notifications_enabled: false,
},
created_at: "2026-01-01T00:00:00.000Z",
updated_at: "2026-01-01T00:00:00.000Z",
display_name: "E2E User",
permissions: ["user"],
economic_customer: [],
...overrides,
};
}
async function installSessionRoute(page, { token, sessionData }) {
await page.route("**/auth/session", async (route) => {
const authorization = route.request().headers().authorization;
if (authorization !== `Bearer ${token}`) {
await json(route, { message: "Unauthenticated" }, 401);
return;
}
await json(route, {
data: createSessionData(sessionData),
});
});
}
async function installUser2FALoginRoute(page) {
await page.route("**/auth/login", async (route) => {
await json(route, {
data: {
two_fa_required: true,
two_fa_token: USER_TWO_FACTOR_TOKEN,
},
});
});
}
async function installSubuser2FALoginRoute(page) {
await page.route("**/subusers/auth/password", async (route) => {
await json(route, {
data: {
two_fa_required: true,
two_fa_token: SUBUSER_TWO_FACTOR_TOKEN,
},
});
});
}
async function install2FAVerifyRoute(page) {
await page.route("**/auth/2fa/verify", async (route) => {
const body = route.request().postDataJSON();
if (body.code !== VALID_CODE) {
await json(route, { data: { message: INVALID_CODE_MESSAGE } }, 422);
return;
}
if (body["2fa_token"] === USER_TWO_FACTOR_TOKEN) {
await json(route, {
data: {
token: USER_TOKEN,
},
});
return;
}
if (body["2fa_token"] === SUBUSER_TWO_FACTOR_TOKEN) {
await json(route, {
data: {
session: SUBUSER_TOKEN,
},
});
return;
}
await json(route, { data: { message: INVALID_CODE_MESSAGE } }, 422);
});
}
async function preparePublic2FAFlow(page) {
await mockApi(page, { authenticated: false });
await install2FAVerifyRoute(page);
}
function isBenignNavigationError(error) {
const message = error instanceof Error ? error.message : String(error);
return (
message.includes("interrupted by another navigation") ||
message.includes("ERR_ABORTED") ||
message.includes("Frame load interrupted")
);
}
async function readStoredToken(page) {
try {
return await page.evaluate(() => window.localStorage.getItem("token"));
} catch {
return null;
}
}
async function settleAuthenticatedNavigation(page, token, targetPath, targetUrl) {
await expect.poll(() => readStoredToken(page)).toBe(token);
if (!targetUrl.test(page.url())) {
try {
await page.goto(targetPath, { waitUntil: "domcontentloaded" });
} catch (error) {
if (!isBenignNavigationError(error)) {
throw error;
}
}
}
await expect.poll(() => page.url(), { timeout: 15_000 }).toMatch(targetUrl);
}
async function goToUser2FAScreen(page) {
await preparePublic2FAFlow(page);
await installSessionRoute(page, { token: USER_TOKEN, sessionData: { permissions: ["user"] } });
await installUser2FALoginRoute(page);
await page.goto("/login");
await page.fill('input[name="customer_number"]', "12345680");
await page.fill('input[name="password"]', "5680");
await page.click("#login-button");
await expect(page.locator('input[name="2fa_code"]')).toBeVisible();
}
async function goToSubuser2FAScreen(page) {
await preparePublic2FAFlow(page);
await installSessionRoute(page, {
token: SUBUSER_TOKEN,
sessionData: {
permissions: ["user"],
display_name: "E2E Driver",
username: "driver-user",
name: "E2E Driver",
},
});
await installSubuser2FALoginRoute(page);
await page.goto("/login/driver");
await page.fill('input[name="phone_country_code"]', "45");
await page.fill('input[name="phone"]', "42331129");
await page.fill('input[name="password"]', "Test1234");
await page.click("#subuser-login-button");
await expect(page.locator('input[name="2fa_code"]')).toBeVisible();
}
test.describe("[AUTH][2FA][User]", () => {
test("should display 2FA verification screen after login for 2FA-enabled user", async ({ page }) => {
await goToUser2FAScreen(page);
await expect(page.locator(".fa-shield-alt")).toBeVisible();
await expect(page.locator('input[name="2fa_code"]')).toBeVisible();
await expect(page.locator(VERIFY_BUTTON)).toBeDisabled();
});
test("should keep verify button disabled until the code has 6 characters", async ({ page }) => {
await goToUser2FAScreen(page);
await page.fill('input[name="2fa_code"]', "123");
await expect(page.locator(VERIFY_BUTTON)).toBeDisabled();
await page.fill('input[name="2fa_code"]', VALID_CODE);
await expect(page.locator(VERIFY_BUTTON)).toBeEnabled();
});
test("should show error for invalid 2FA code", async ({ page }) => {
await goToUser2FAScreen(page);
await page.fill('input[name="2fa_code"]', "000000");
await page.click(VERIFY_BUTTON);
await expect(page.locator(ERROR_ALERT)).toHaveText(INVALID_CODE_MESSAGE);
});
test("should return to login form and clear the password when cancelling 2FA", async ({ page }) => {
await goToUser2FAScreen(page);
await page.click(".cancel-btn");
await expect(page.locator('input[name="customer_number"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toHaveValue("");
});
test("should login successfully with a valid 2FA code", async ({ page }) => {
await goToUser2FAScreen(page);
await page.fill('input[name="2fa_code"]', VALID_CODE);
await page.click(VERIFY_BUTTON);
await expect(page.locator(SUCCESS_ALERT)).toBeVisible();
await settleAuthenticatedNavigation(page, USER_TOKEN, "/user", /\/user(?:\/)?(?:[?#].*)?$/);
});
test("should limit the 2FA input to 6 characters", async ({ page }) => {
await goToUser2FAScreen(page);
const codeInput = page.locator('input[name="2fa_code"]');
await codeInput.fill("1234567890");
await expect(codeInput).toHaveValue(VALID_CODE);
});
});
test.describe("[AUTH][2FA][Subuser]", () => {
test("should display 2FA verification screen after login for 2FA-enabled subuser", async ({ page }) => {
await goToSubuser2FAScreen(page);
await expect(page.locator(".fa-shield-alt")).toBeVisible();
await expect(page.locator('input[name="2fa_code"]')).toBeVisible();
await expect(page.locator(VERIFY_BUTTON)).toBeDisabled();
});
test("should show error for invalid 2FA code", async ({ page }) => {
await goToSubuser2FAScreen(page);
await page.fill('input[name="2fa_code"]', "000000");
await page.click(VERIFY_BUTTON);
await expect(page.locator(ERROR_ALERT)).toHaveText(INVALID_CODE_MESSAGE);
});
test("should return to login form when cancelling 2FA", async ({ page }) => {
await goToSubuser2FAScreen(page);
await page.click(".cancel-btn");
await expect(page.locator('input[name="phone"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toHaveValue("");
});
test("should login successfully with a valid 2FA code", async ({ page }) => {
await goToSubuser2FAScreen(page);
await page.fill('input[name="2fa_code"]', VALID_CODE);
await page.click(VERIFY_BUTTON);
await expect(page.locator(SUCCESS_ALERT)).toBeVisible();
await expect.poll(() => page.evaluate(() => window.localStorage.getItem("token"))).toBe(SUBUSER_TOKEN);
await expect.poll(() => page.evaluate(() => window.localStorage.getItem("is_subuser"))).toBe("true");
});
});