525 lines
18 KiB
TypeScript
525 lines
18 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi } from "./support/network.js";
|
|
|
|
const PASSWORD_RESET_MESSAGE = "If the customer exists, a password reset email has been sent.";
|
|
const INVALID_CREDENTIALS = "Invalid credentials";
|
|
const INVALID_RESET_TOKEN = "Invalid or expired token";
|
|
const SUBUSER_PASSWORD_POLICY_MESSAGE =
|
|
"Password must contain at least one uppercase letter, one lowercase letter, and one number.";
|
|
const USER_TOKEN = "mock-user-session-token";
|
|
const SUBUSER_TOKEN = "mock-subuser-session-token";
|
|
const OPERATOR_TOKEN = "mock-operator-session-token";
|
|
const AUTH_FORM_TIMEOUT = 15_000;
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
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 openAuthPage(page, path, selector) {
|
|
const field = page.locator(selector);
|
|
await page.goto(path);
|
|
|
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
if (await field.isVisible().catch(() => false)) {
|
|
return field;
|
|
}
|
|
|
|
if (attempt === 0) {
|
|
try {
|
|
await page.goto(path, { waitUntil: "domcontentloaded" });
|
|
} catch (error) {
|
|
if (!isBenignNavigationError(error)) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
await expect(field).toBeVisible({ timeout: AUTH_FORM_TIMEOUT });
|
|
return field;
|
|
}
|
|
|
|
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 installUserLoginRoute(
|
|
page,
|
|
{ outcome = "success", token = USER_TOKEN, errorMessage = INVALID_CREDENTIALS } = {}
|
|
) {
|
|
await page.route("**/auth/login", async (route) => {
|
|
if (outcome === "error") {
|
|
await json(route, { data: { message: errorMessage } }, 401);
|
|
return;
|
|
}
|
|
|
|
await json(route, {
|
|
data: {
|
|
token,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async function installSubuserLoginRoute(
|
|
page,
|
|
{ outcome = "success", token = SUBUSER_TOKEN, errorMessage = "Subuser not found" } = {}
|
|
) {
|
|
await page.route("**/subusers/auth/password", async (route) => {
|
|
if (outcome === "error") {
|
|
await json(route, { data: { message: errorMessage } }, 401);
|
|
return;
|
|
}
|
|
|
|
await json(route, {
|
|
data: {
|
|
session: token,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async function installOperatorLoginRoute(
|
|
page,
|
|
{ outcome = "success", token = OPERATOR_TOKEN, errorMessage = INVALID_CREDENTIALS } = {}
|
|
) {
|
|
await page.route("**/auth/employee/login", async (route) => {
|
|
if (outcome === "error") {
|
|
await json(route, { data: { message: errorMessage } }, 401);
|
|
return;
|
|
}
|
|
|
|
await json(route, {
|
|
data: {
|
|
token,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async function installPasswordResetRequestRoute(page, message = PASSWORD_RESET_MESSAGE) {
|
|
await page.route("**/auth/password-reset/request", async (route) => {
|
|
await json(route, {
|
|
data: {
|
|
message,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async function installPasswordResetValidationRoute(page, { invalidToken = "invalidtoken" } = {}) {
|
|
await page.route("**/auth/password-reset/validate*", async (route) => {
|
|
const url = new URL(route.request().url());
|
|
if (url.searchParams.get("token") === invalidToken) {
|
|
await json(route, { data: { message: INVALID_RESET_TOKEN } }, 422);
|
|
return;
|
|
}
|
|
|
|
await json(route, { data: { valid: true } });
|
|
});
|
|
}
|
|
|
|
async function installCustomerRegistrationRoute(page, { status = 422, message }) {
|
|
await page.route("**/auth/register/cvr", async (route) => {
|
|
if (status >= 400) {
|
|
await json(route, { data: { message } }, status);
|
|
return;
|
|
}
|
|
|
|
await json(route, { data: { success: true } }, status);
|
|
});
|
|
}
|
|
|
|
async function installDriverRegistrationRoute(page, { status = 422, message }) {
|
|
await page.route(/\/subusers(?:\?.*)?$/, async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
if (status >= 400) {
|
|
await json(route, { data: { message } }, status);
|
|
return;
|
|
}
|
|
|
|
await json(route, { data: { success: true } }, status);
|
|
});
|
|
}
|
|
|
|
async function preparePublicAuthFlow(page) {
|
|
await mockApi(page, { authenticated: false });
|
|
}
|
|
|
|
async function loginAsMockUser(page) {
|
|
await preparePublicAuthFlow(page);
|
|
await installSessionRoute(page, { token: USER_TOKEN, sessionData: { permissions: ["user"] } });
|
|
await installUserLoginRoute(page);
|
|
|
|
await openAuthPage(page, "/login", 'input[name="customer_number"]');
|
|
await page.fill('input[name="customer_number"]', "12345679");
|
|
await page.fill('input[name="password"]', "5679");
|
|
await page.click("#login-button");
|
|
await settleAuthenticatedNavigation(page, USER_TOKEN, "/user", /\/user(?:\/)?(?:[?#].*)?$/);
|
|
}
|
|
|
|
async function loginAsMockSubuserByPhone(page) {
|
|
await preparePublicAuthFlow(page);
|
|
await installSessionRoute(page, {
|
|
token: SUBUSER_TOKEN,
|
|
sessionData: {
|
|
permissions: ["user"],
|
|
display_name: "E2E Driver",
|
|
username: "driver-user",
|
|
name: "E2E Driver",
|
|
},
|
|
});
|
|
await installSubuserLoginRoute(page);
|
|
|
|
await openAuthPage(page, "/login/driver", 'input[name="phone_country_code"]');
|
|
await page.fill('input[name="phone_country_code"]', "45");
|
|
await page.fill('input[name="phone"]', "42331128");
|
|
await page.fill('input[name="password"]', "Test1234");
|
|
await page.click("#subuser-login-button");
|
|
await settleAuthenticatedNavigation(page, SUBUSER_TOKEN, "/user", /\/user(?:\/)?(?:[?#].*)?$/);
|
|
}
|
|
|
|
async function loginAsMockSubuserByUsername(page) {
|
|
await preparePublicAuthFlow(page);
|
|
await installSessionRoute(page, {
|
|
token: SUBUSER_TOKEN,
|
|
sessionData: {
|
|
permissions: ["user"],
|
|
display_name: "E2E Driver",
|
|
username: "testsubuser",
|
|
name: "E2E Driver",
|
|
},
|
|
});
|
|
await installSubuserLoginRoute(page);
|
|
|
|
await openAuthPage(page, "/login/driver", 'button[id="subuser_login_method_username_button"]');
|
|
await page.click("#subuser_login_method_username_button");
|
|
await expect(page.locator('input[name="username"]')).toBeVisible({ timeout: AUTH_FORM_TIMEOUT });
|
|
await page.fill('input[name="username"]', "testsubuser");
|
|
await page.fill('input[name="password"]', "Test1234");
|
|
await page.click("#subuser-login-button");
|
|
await settleAuthenticatedNavigation(page, SUBUSER_TOKEN, "/user", /\/user(?:\/)?(?:[?#].*)?$/);
|
|
}
|
|
|
|
async function loginAsMockOperator(page) {
|
|
await preparePublicAuthFlow(page);
|
|
await installSessionRoute(page, {
|
|
token: OPERATOR_TOKEN,
|
|
sessionData: {
|
|
permissions: ["admin"],
|
|
display_name: "E2E Operator",
|
|
},
|
|
});
|
|
await installOperatorLoginRoute(page);
|
|
|
|
await openAuthPage(page, "/admin/login", 'input[name="user_id"]');
|
|
await page.fill('input[name="user_id"]', "11");
|
|
await page.fill('input[name="password"]', "aef18KHAPGiu90");
|
|
await page.click("#operator_login_button");
|
|
await settleAuthenticatedNavigation(page, OPERATOR_TOKEN, "/admin", /\/admin(?:\/)?(?:[?#].*)?$/);
|
|
}
|
|
|
|
test("[AUTH][User][Customer number] should login successfully", async ({ page }) => {
|
|
await loginAsMockUser(page);
|
|
});
|
|
|
|
test("[AUTH][User][Customer number] should fail to login using invalid password", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installUserLoginRoute(page, { outcome: "error" });
|
|
|
|
await openAuthPage(page, "/login", 'input[name="customer_number"]');
|
|
await page.fill('input[name="customer_number"]', "12345679");
|
|
await page.fill('input[name="password"]', "invalidpassword");
|
|
await page.click("#login-button");
|
|
await expect(page.locator("#user_auth_alert_error")).toHaveText(INVALID_CREDENTIALS);
|
|
});
|
|
|
|
test("[AUTH][User][Customer number] should fail to login using invalid customer number", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installUserLoginRoute(page, { outcome: "error" });
|
|
|
|
await openAuthPage(page, "/login", 'input[name="customer_number"]');
|
|
await page.fill('input[name="customer_number"]', "99999999999");
|
|
await page.fill('input[name="password"]', "5679");
|
|
await page.click("#login-button");
|
|
await expect(page.locator("#user_auth_alert_error")).toHaveText(INVALID_CREDENTIALS);
|
|
});
|
|
|
|
test("[AUTH][User][Customer number] should request password reset successfully", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installPasswordResetRequestRoute(page);
|
|
|
|
await openAuthPage(page, "/login", 'input[name="customer_number"]');
|
|
await page.click("#forgot-password-button");
|
|
await expect(page).toHaveURL("/auth/password-reset");
|
|
await page.fill('input[name="customer_number"]', "12345679");
|
|
await page.click("#password-reset-button");
|
|
await expect(page.locator("#password_reset_alert_success")).toHaveText(PASSWORD_RESET_MESSAGE);
|
|
});
|
|
|
|
test("[AUTH][User][Customer number] should not show failure to request password reset using invalid customer number", async ({
|
|
page,
|
|
}) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installPasswordResetRequestRoute(page);
|
|
|
|
await openAuthPage(page, "/login", 'input[name="customer_number"]');
|
|
await page.click("#forgot-password-button");
|
|
await expect(page).toHaveURL("/auth/password-reset");
|
|
await page.fill('input[name="customer_number"]', "99999999999");
|
|
await page.click("#password-reset-button");
|
|
await expect(page.locator("#password_reset_alert_success")).toHaveText(PASSWORD_RESET_MESSAGE);
|
|
});
|
|
|
|
test("[AUTH][User][Customer number] should fail to reset password using invalid token", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installPasswordResetValidationRoute(page);
|
|
|
|
await page.goto("/auth/password-reset/invalidtoken");
|
|
await expect(page.locator("#password_reset_alert_error")).toHaveText(INVALID_RESET_TOKEN);
|
|
});
|
|
|
|
test("[AUTH][User][Customer number] should fail to register already existing cvr", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installCustomerRegistrationRoute(page, {
|
|
message:
|
|
"CVR already registered under customer number 42331128. The submitted phone number must match the customer id. Manual cleanup or reassignment is required before retrying.",
|
|
});
|
|
|
|
await page.goto("/customer-creation");
|
|
await page.fill("#customer_cvr", "41004355");
|
|
await page.fill("#email", "2jepp9350@gmail.com");
|
|
await page.fill("#customer_phone", "42331129");
|
|
await page.click("#customer-register-button");
|
|
await expect(page.locator("#customer_creation_alert_error")).toHaveText(
|
|
"CVR already registered under customer number 42331128. The submitted phone number must match the customer id. Manual cleanup or reassignment is required before retrying."
|
|
);
|
|
});
|
|
|
|
test("[AUTH][User][Customer number] should fail to register already existing company phone number", async ({
|
|
page,
|
|
}) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installCustomerRegistrationRoute(page, {
|
|
message: "Company phone number already registered",
|
|
});
|
|
|
|
await page.goto("/customer-creation");
|
|
await page.fill("#customer_cvr", "43423010");
|
|
await page.fill("#email", "2jepp9350@gmail.com");
|
|
await page.fill("#customer_phone", "42331128");
|
|
await page.click("#customer-register-button");
|
|
await expect(page.locator("#customer_creation_alert_error")).toHaveText("Company phone number already registered");
|
|
});
|
|
|
|
test("[AUTH][Subuser][Phone number] should login successfully", async ({ page }) => {
|
|
await loginAsMockSubuserByPhone(page);
|
|
});
|
|
|
|
test("[AUTH][Subuser][Phone number] should fail to login using invalid phone number", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installSubuserLoginRoute(page, { outcome: "error", errorMessage: "Subuser not found" });
|
|
|
|
await page.goto("/login/driver");
|
|
await page.fill('input[name="phone_country_code"]', "45");
|
|
await page.fill('input[name="phone"]', "123456789");
|
|
await page.fill('input[name="password"]', "Test1234");
|
|
await page.click("#subuser-login-button");
|
|
await expect(page.locator("#subuser_auth_alert_error")).toHaveText("Subuser not found");
|
|
});
|
|
|
|
test("[AUTH][Subuser][Phone number] should fail to login using invalid password", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installSubuserLoginRoute(page, { outcome: "error", errorMessage: "Invalid password" });
|
|
|
|
await page.goto("/login/driver");
|
|
await page.fill('input[name="phone_country_code"]', "45");
|
|
await page.fill('input[name="phone"]', "42331128");
|
|
await page.fill('input[name="password"]', "Wrong1234");
|
|
await page.click("#subuser-login-button");
|
|
await expect(page.locator("#subuser_auth_alert_error")).toHaveText("Invalid password");
|
|
});
|
|
|
|
test("[AUTH][Subuser][Phone number] should reject passwords that fail the registration policy", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
|
|
await page.goto("/login/driver");
|
|
await page.fill('input[name="phone_country_code"]', "45");
|
|
await page.fill('input[name="phone"]', "42331128");
|
|
await page.fill('input[name="password"]', "invalidpassword");
|
|
await page.click("#subuser-login-button");
|
|
await expect(page.locator("#subuser_auth_alert_error")).toHaveText(SUBUSER_PASSWORD_POLICY_MESSAGE);
|
|
});
|
|
|
|
test("[AUTH][Subuser][Username] should login successfully", async ({ page }) => {
|
|
await loginAsMockSubuserByUsername(page);
|
|
});
|
|
|
|
test("[AUTH][Subuser][Username] should fail to login using invalid username", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installSubuserLoginRoute(page, { outcome: "error", errorMessage: "Subuser not found" });
|
|
|
|
await page.goto("/login/driver");
|
|
await page.click("#subuser_login_method_username_button");
|
|
await page.fill('input[name="username"]', "invalidusername");
|
|
await page.fill('input[name="password"]', "Test1234");
|
|
await page.click("#subuser-login-button");
|
|
await expect(page.locator("#subuser_auth_alert_error")).toHaveText("Subuser not found");
|
|
});
|
|
|
|
test("[AUTH][Subuser][Username] should fail to login using invalid password", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installSubuserLoginRoute(page, { outcome: "error", errorMessage: "Invalid password" });
|
|
|
|
await page.goto("/login/driver");
|
|
await page.click("#subuser_login_method_username_button");
|
|
await page.fill('input[name="username"]', "testsubuser");
|
|
await page.fill('input[name="password"]', "Wrong1234");
|
|
await page.click("#subuser-login-button");
|
|
await expect(page.locator("#subuser_auth_alert_error")).toHaveText("Invalid password");
|
|
});
|
|
|
|
test("[AUTH][Subuser][Username] should fail to register with already existing phone number", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installDriverRegistrationRoute(page, {
|
|
message: "Account already exists with this phone number",
|
|
});
|
|
|
|
await page.goto("/customer-creation");
|
|
await page.fill("#driver_cvr", "41004355");
|
|
await page.fill("#driver_phone", "42331128");
|
|
await page.click("#driver-register-button");
|
|
await expect(page.locator("#driver_creation_alert_error")).toHaveText(
|
|
"Account already exists with this phone number"
|
|
);
|
|
});
|
|
|
|
test("[AUTH][Subuser][Username] should fail to register with invalid (7-char) CVR number", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installDriverRegistrationRoute(page, {
|
|
message: "Parameter cvr must be at least 8 characters long",
|
|
});
|
|
|
|
await page.goto("/customer-creation");
|
|
await page.fill("#driver_cvr", "1234567");
|
|
await page.fill("#driver_phone", "42331128");
|
|
await page.click("#driver-register-button");
|
|
await expect(page.locator("#driver_creation_alert_error")).toHaveText(
|
|
"Parameter cvr must be at least 8 characters long"
|
|
);
|
|
});
|
|
|
|
test("[AUTH][Subuser][Username] should fail to register with invalid (9-char) CVR number", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installDriverRegistrationRoute(page, {
|
|
message: "Parameter cvr must be at most 8 characters long",
|
|
});
|
|
|
|
await page.goto("/customer-creation");
|
|
await page.fill("#driver_cvr", "1234567890");
|
|
await page.fill("#driver_phone", "42331128");
|
|
await page.click("#driver-register-button");
|
|
await expect(page.locator("#driver_creation_alert_error")).toHaveText(
|
|
"Parameter cvr must be at most 8 characters long"
|
|
);
|
|
});
|
|
|
|
test("[AUTH][Operator][User ID] should login successfully", async ({ page }) => {
|
|
await loginAsMockOperator(page);
|
|
});
|
|
|
|
test("[AUTH][Operator][User ID] should fail to login using invalid user ID", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installOperatorLoginRoute(page, { outcome: "error" });
|
|
|
|
await page.goto("/admin/login");
|
|
await page.fill('input[name="user_id"]', "99999999999");
|
|
await page.fill('input[name="password"]', "aef18KHAPGiu90");
|
|
await page.click("#operator_login_button");
|
|
await expect(page.locator("#user_auth_alert_error")).toHaveText(INVALID_CREDENTIALS);
|
|
});
|
|
|
|
test("[AUTH][Operator][User ID] should fail to login using invalid password", async ({ page }) => {
|
|
await preparePublicAuthFlow(page);
|
|
await installOperatorLoginRoute(page, { outcome: "error" });
|
|
|
|
await page.goto("/admin/login");
|
|
await page.fill('input[name="user_id"]', "11");
|
|
await page.fill('input[name="password"]', "0000");
|
|
await page.click("#operator_login_button");
|
|
await expect(page.locator("#user_auth_alert_error")).toHaveText(INVALID_CREDENTIALS);
|
|
});
|