- 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.
270 lines
8.4 KiB
TypeScript
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");
|
|
});
|
|
});
|