Files
pleno-vue/tests/e2e/subuser-management.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

517 lines
16 KiB
TypeScript

import { expect, test, type Page, type Route } from "@playwright/test";
type MockSubuser = {
id: number;
username: string | null;
name: string | null;
email: string | null;
phone_country_code: number | null;
phone: number | null;
created_at: string;
updated_at: string;
suspended_at: string | null;
two_factor_enabled: boolean;
setup_required: boolean;
invite_accepted: boolean;
can_resend_invite: boolean;
profile_editable_by_manager: boolean;
grant_id: number;
grant_enabled: boolean;
grant_note: string | null;
grant_permissions: string[];
access_state: "active" | "pending_setup" | "disabled" | "inactive";
};
const API_PATTERN = /https:\/\/api\.truckwash\.io(?::4433)?\//;
const baseUserSession = {
id: 12,
customer_number: 12345678,
group_id: 3,
email: "demo@truckwash.test",
phone: {
number: "11111111",
country_code: "45",
},
notifications: {
wash_certificate_email: null,
email_notifications_enabled: true,
sms_notifications_enabled: false,
},
created_at: "2026-04-14 09:00:00",
updated_at: "2026-04-14 09:00:00",
display_name: "Demo Company",
economic_customer: [],
};
const baseSubuserSession = {
id: 91,
username: "manager",
name: "Subuser Manager",
email: "manager@example.com",
phone_country_code: 45,
phone: 12345678,
created_at: "2026-04-14 09:00:00",
updated_at: "2026-04-14 09:00:00",
suspended_at: null,
two_factor_enabled: false,
grants: [
{
name: "Demo Company",
billing_customer_number: 12345678,
permissions: ["SUBUSERS_LIST", "SUBUSERS_ADD", "SUBUSERS_EDIT", "SUBUSERS_DELETE"],
},
],
};
const initialSubusers = (): MockSubuser[] => [
{
id: 1,
username: null,
name: "Pending Driver",
email: null,
phone_country_code: 45,
phone: 11111111,
created_at: "2026-04-14 08:00:00",
updated_at: "2026-04-14 08:00:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: true,
invite_accepted: false,
can_resend_invite: true,
profile_editable_by_manager: false,
grant_id: 11,
grant_enabled: true,
grant_note: null,
grant_permissions: ["BOOKINGS_LIST"],
access_state: "pending_setup",
},
{
id: 2,
username: "driver.active",
name: "Active Driver",
email: "active@example.com",
phone_country_code: 45,
phone: 22222222,
created_at: "2026-04-14 07:30:00",
updated_at: "2026-04-14 07:30:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: false,
invite_accepted: true,
can_resend_invite: false,
profile_editable_by_manager: false,
grant_id: 12,
grant_enabled: true,
grant_note: "Morgenhold",
grant_permissions: ["BOOKINGS_LIST", "VEHICLES_LIST"],
access_state: "active",
},
{
id: 3,
username: "driver.disabled",
name: "Disabled Driver",
email: "disabled@example.com",
phone_country_code: 45,
phone: 33333333,
created_at: "2026-04-14 07:00:00",
updated_at: "2026-04-14 07:00:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: false,
invite_accepted: true,
can_resend_invite: false,
profile_editable_by_manager: false,
grant_id: 13,
grant_enabled: false,
grant_note: "Sat på pause",
grant_permissions: ["BOOKINGS_LIST"],
access_state: "disabled",
},
];
const permissionNodePayload = [
{
group: "Subusers",
description: "Subuser management",
nodes: [
{
key: "SUBUSERS_LIST",
name: "List chauffeurs",
description: "Can view chauffeurs",
type: "LIST",
default: false,
},
{
key: "SUBUSERS_EDIT",
name: "Edit grants",
description: "Can edit grants",
type: "EDIT",
default: false,
},
],
},
];
const jsonResponse = (route: Route, data: unknown, meta: Record<string, unknown> = {}) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
success: true,
data,
meta,
includes: [],
}),
});
const recalculateAccessState = (subuser: MockSubuser) => {
subuser.can_resend_invite = subuser.setup_required;
subuser.invite_accepted = !subuser.setup_required;
if (subuser.grant_enabled) {
subuser.access_state = subuser.setup_required ? "pending_setup" : "active";
return;
}
subuser.access_state = "disabled";
};
async function primeSession(
page: Page,
options: { token: string; isSubuser?: boolean; selectedCustomerNumber?: number }
) {
await page.addInitScript((session) => {
window.localStorage.setItem("token", session.token);
if (session.isSubuser) {
window.localStorage.setItem("is_subuser", "true");
} else {
window.localStorage.removeItem("is_subuser");
}
if (session.selectedCustomerNumber) {
window.localStorage.setItem("selected_customer_number", String(session.selectedCustomerNumber));
} else {
window.localStorage.removeItem("selected_customer_number");
}
}, options);
}
async function mockManagementApi(
page: Page,
{
userPermissions = ["user"],
subuserPermissions = ["SUBUSERS_LIST", "SUBUSERS_ADD", "SUBUSERS_EDIT", "SUBUSERS_DELETE"],
isSubuser = false,
}: {
userPermissions?: string[];
subuserPermissions?: string[];
isSubuser?: boolean;
}
) {
const subusers = initialSubusers();
let nextId = 100;
let nextGrantId = 1000;
await page.route(API_PATTERN, async (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.pathname === "/auth/session" && request.method() === "GET") {
return jsonResponse(route, {
...baseUserSession,
permissions: userPermissions,
});
}
if (url.pathname === "/subusers/me" && request.method() === "GET") {
return jsonResponse(route, {
...baseSubuserSession,
grants: baseSubuserSession.grants.map((grant) => ({
...grant,
permissions: subuserPermissions,
})),
});
}
if (url.pathname === "/subusers" && request.method() === "GET") {
return jsonResponse(route, subusers, {
pagination: {
page: 1,
per_page: 100,
total: subusers.length,
},
});
}
if (url.pathname === "/subusers" && request.method() === "PUT") {
return route.fulfill({
status: 403,
contentType: "application/json",
body: JSON.stringify({
success: false,
data: {
message: "Customers can only manage subuser grants. Drivers own their account profile.",
},
meta: {},
includes: [],
}),
});
}
if (url.pathname === "/subusers/invite" && request.method() === "POST") {
const payload = request.postDataJSON() as Record<string, string | number | null>;
const created: MockSubuser = {
id: nextId++,
username: null,
name: payload.name as string,
email: null,
phone_country_code: Number(payload.phone_country_code),
phone: Number(payload.phone),
created_at: "2026-04-14 10:00:00",
updated_at: "2026-04-14 10:00:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: true,
invite_accepted: false,
can_resend_invite: true,
profile_editable_by_manager: false,
grant_id: nextGrantId++,
grant_enabled: true,
grant_note: null,
grant_permissions: [],
access_state: "pending_setup",
};
subusers.unshift(created);
return jsonResponse(route, {
subuser: created,
grant: { id: created.grant_id },
invite: {
setup_link: `https://truckwash.io/complete-registration?token=subuser-${created.id}`,
delivery: {
status: "unavailable",
message: "SMS blev ikke sendt i testmiljøet.",
},
},
});
}
if (url.pathname === "/subusers/invite/resend" && request.method() === "POST") {
const payload = request.postDataJSON() as Record<string, number>;
const target = subusers.find((subuser) => subuser.id === Number(payload.id));
if (!target) {
return route.fulfill({ status: 404, body: JSON.stringify({ success: false }) });
}
if (!target.setup_required) {
return route.fulfill({
status: 409,
contentType: "application/json",
body: JSON.stringify({
success: false,
data: {
message: "Driver account already accepted the invitation.",
},
meta: {},
includes: [],
}),
});
}
return jsonResponse(route, {
subuser: target,
grant: { id: target.grant_id },
invite: {
setup_link: `https://truckwash.io/complete-registration?token=resend-${target.id}`,
delivery: {
status: "sent",
message: "Invitation sendt igen.",
},
},
});
}
if (url.pathname === "/subusers/grants" && request.method() === "PUT") {
const payload = request.postDataJSON() as Record<string, unknown>;
const target = subusers.find((subuser) => subuser.grant_id === Number(payload.id));
if (!target) {
return route.fulfill({ status: 404, body: JSON.stringify({ success: false }) });
}
if (Array.isArray(payload.permissions)) {
target.grant_permissions = payload.permissions as string[];
}
if (Object.prototype.hasOwnProperty.call(payload, "note")) {
target.grant_note = (payload.note as string) || null;
}
if (Object.prototype.hasOwnProperty.call(payload, "enabled")) {
target.grant_enabled = Boolean(payload.enabled);
}
recalculateAccessState(target);
target.updated_at = "2026-04-14 10:05:00";
return jsonResponse(route, {
id: target.grant_id,
enabled: target.grant_enabled,
note: target.grant_note,
permissions: target.grant_permissions,
});
}
if (url.pathname === "/subusers/permission-nodes" && request.method() === "GET") {
return jsonResponse(route, permissionNodePayload);
}
if (url.pathname === "/subusers/me" && request.method() === "PUT") {
const payload = request.postDataJSON() as Record<string, unknown>;
return jsonResponse(route, {
...baseSubuserSession,
name: (payload.name as string) ?? baseSubuserSession.name,
email: (payload.email as string) ?? baseSubuserSession.email,
});
}
if (url.pathname === "/ping" && request.method() === "GET") {
return jsonResponse(route, { ok: true });
}
return route.fulfill({
status: 404,
contentType: "application/json",
body: JSON.stringify({
success: false,
data: {
message: `Unhandled API route in test: ${request.method()} ${url.pathname}`,
},
meta: {},
includes: [],
}),
});
});
}
test("customer user can invite and manage grant access from /user/subusers without editing the driver account", async ({
page,
}) => {
await primeSession(page, { token: "user-token" });
await mockManagementApi(page, { userPermissions: ["user"] });
await page.goto("/user/subusers");
await expect(page.getByRole("heading", { name: /underbrugere|chauffører/i })).toBeVisible();
await expect(page.getByRole("button", { name: /Invit.*chauff/i })).toBeVisible();
await expect(page.getByText("Pending Driver")).toBeVisible();
await expect(page.getByText("Disabled Driver")).toBeVisible();
await expect(page.getByTestId("subuser-email-1")).toHaveText("E-mail oplyses ved accept");
await expect(page.getByTestId("subuser-username-1")).toHaveText("Brugernavn oplyses ved accept");
await expect(page.getByTestId("subuser-email-2")).toHaveText("active@example.com");
await expect(page.getByTestId("subuser-resend-2")).toHaveCount(0);
await page.getByRole("button", { name: /Invit.*chauff/i }).click();
await page.fill("#subuser-form-name", "Invited Driver");
await page.fill("#subuser-form-phone-country-code", "45");
await page.fill("#subuser-form-phone", "44444444");
await page.getByRole("button", { name: "Send invitation" }).click();
await expect(page.getByRole("heading", { name: "Chauffør oprettet" })).toBeVisible();
await page.getByRole("button", { name: "Luk" }).click();
await expect(page.getByText("Invited Driver")).toBeVisible();
await expect(page.getByTestId("subuser-edit-100")).toHaveCount(0);
await expect(page.getByTestId("subuser-email-100")).toHaveText("E-mail oplyses ved accept");
await expect(page.getByTestId("subuser-username-100")).toHaveText("Brugernavn oplyses ved accept");
await page.getByTestId("subuser-permissions-100").click();
await expect(page.getByText("Tilladelsesnoder for Invited Driver")).toBeVisible();
await page.getByTestId("permission-node-checkbox-SUBUSERS_LIST").click();
await page.getByRole("button", { name: "Gem" }).click();
await expect(page.getByText(/SUBUSERS_LIST/)).toBeVisible();
await page.getByTestId("subuser-toggle-100").click();
await page.getByRole("button", { name: "Deaktivér" }).click();
await expect(page.getByTestId("subuser-status-100")).toContainText("Deaktiveret");
await page.getByTestId("subuser-toggle-100").click();
await page.getByRole("button", { name: "Genaktivér" }).click();
await expect(page.getByTestId("subuser-status-100")).toContainText("Afventer opsætning");
await page.getByTestId("subuser-resend-100").click();
await expect(page.getByRole("heading", { name: "Chauffør oprettet" })).toBeVisible();
await page.getByRole("button", { name: "Luk" }).click();
});
test("authorized subuser managers can access the chauffør page and the legacy grants route redirects", async ({
page,
}) => {
await primeSession(page, {
token: "subuser-manager-token",
isSubuser: true,
selectedCustomerNumber: 12345678,
});
await mockManagementApi(page, {
isSubuser: true,
subuserPermissions: ["SUBUSERS_LIST", "SUBUSERS_ADD", "SUBUSERS_EDIT", "SUBUSERS_DELETE"],
});
await page.goto("/user/subusers/grants");
await expect(page).toHaveURL("/user/subusers");
await expect(page.locator("label.label", { hasText: "Vælg kunde" })).toBeVisible();
await expect(page.getByRole("button", { name: /Invit.*chauff/i })).toBeVisible();
});
test("subuser self-service profile edits still save through /subusers/me", async ({ page }) => {
await primeSession(page, {
token: "subuser-profile-token",
isSubuser: true,
selectedCustomerNumber: 12345678,
});
await mockManagementApi(page, {
isSubuser: true,
});
await page.goto("/user/profile");
await expect(
page
.locator(".card-header")
.filter({ has: page.locator(".fa-user") })
.first()
).toBeVisible();
await page
.locator(".card-header")
.filter({ has: page.locator(".fa-user") })
.first()
.click();
await page.locator('button:has-text("Rediger navn")').click();
await page.fill(".swal2-input", "Profile Driver Updated");
await page.click(".swal2-confirm");
await expect(page.locator(".swal2-title")).toContainText("Navn gemt");
await page.click(".swal2-confirm");
await page
.locator(".card-header")
.filter({ has: page.locator(".fa-address-card") })
.first()
.click();
await page.locator('button:has-text("Rediger e-mail")').click();
await page.fill(".swal2-input", "profile.updated@example.com");
await page.click(".swal2-confirm");
await expect(page.locator(".swal2-title")).toContainText("E-mail gemt");
await page.click(".swal2-confirm");
});
test("subusers without SUBUSERS_LIST cannot access the chauffør page", async ({ page }) => {
await primeSession(page, {
token: "subuser-no-access-token",
isSubuser: true,
selectedCustomerNumber: 12345678,
});
await mockManagementApi(page, {
isSubuser: true,
subuserPermissions: ["BOOKINGS_LIST"],
});
await page.goto("/user/subusers");
await expect(page.getByRole("heading", { name: "403 Forbidden" }).first()).toBeVisible();
await expect(page.getByRole("heading", { name: "You do not have permission to view this page." })).toBeVisible();
});