394 lines
14 KiB
JavaScript
394 lines
14 KiB
JavaScript
// @vitest-environment jsdom
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
authenticatedRequest: vi.fn(),
|
|
parseError: vi.fn(),
|
|
pingApiServer: vi.fn(),
|
|
fetchReleaseRuntime: vi.fn(() => Promise.resolve({})),
|
|
swalFire: vi.fn(() => Promise.resolve({ isConfirmed: true })),
|
|
loginRedirect: vi.fn(),
|
|
subuserLoginRedirect: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/components/session/authenticatedRequest.vue", () => ({
|
|
authenticatedRequest: mocks.authenticatedRequest,
|
|
}));
|
|
|
|
vi.mock("@/components/request/HandleGlobalError.vue", () => ({
|
|
parseError: mocks.parseError,
|
|
}));
|
|
|
|
vi.mock("@/services/apiHealth.js", () => ({
|
|
pingApiServer: mocks.pingApiServer,
|
|
}));
|
|
|
|
vi.mock("@/services/releaseBootstrap.js", () => ({
|
|
fetchReleaseRuntime: mocks.fetchReleaseRuntime,
|
|
}));
|
|
|
|
vi.mock("sweetalert2", () => ({
|
|
default: {
|
|
fire: mocks.swalFire,
|
|
},
|
|
}));
|
|
|
|
import { getSessionData, getSubuserSessionData, SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
import { EDGE_GATEWAY_WORKSPACE_CACHE_KEY } from "@/services/edgeGateways.js";
|
|
import {
|
|
buildPeriodCacheKey,
|
|
getCachedPeriodPage,
|
|
setCachedPeriodPage,
|
|
} from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportPaging.js";
|
|
import {
|
|
getCachedXlvaskUsageAmount,
|
|
setCachedXlvaskUsageAmount,
|
|
} from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
|
|
|
|
const seedStoredSession = (token = "stored-token") => {
|
|
localStorage.setItem("token", token);
|
|
SessionUser.token.value = token;
|
|
SessionUser.authenticated.value = true;
|
|
SessionUser.isSubuser.value = false;
|
|
SessionUser.initiated.value = false;
|
|
};
|
|
|
|
const invalidSessionError = () => ({
|
|
response: {
|
|
status: 401,
|
|
data: {
|
|
message: "Unauthenticated",
|
|
},
|
|
},
|
|
});
|
|
|
|
const userNotFoundSessionError = () => ({
|
|
response: {
|
|
status: 400,
|
|
data: {
|
|
data: {
|
|
message: "User not found",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const subuserSessionPayload = () => ({
|
|
data: {
|
|
data: {
|
|
id: 77,
|
|
username: "driver-77",
|
|
name: "Driver 77",
|
|
email: "driver@example.test",
|
|
email_verified: true,
|
|
email_verified_at: "2026-07-13 10:00:00",
|
|
phone_country_code: 45,
|
|
phone: 12345678,
|
|
phone_verified: true,
|
|
phone_verified_at: "2026-07-13 10:00:00",
|
|
verification_state: "verified",
|
|
verification: { state: "verified" },
|
|
grants: [
|
|
{
|
|
grant_id: 5,
|
|
billing_customer_number: 12345678,
|
|
permissions: ["BOOKINGS_LIST", "VEHICLES_LIST"],
|
|
},
|
|
],
|
|
created_at: "2026-07-13 09:00:00",
|
|
updated_at: "2026-07-13 09:30:00",
|
|
suspended_at: null,
|
|
},
|
|
},
|
|
});
|
|
|
|
describe("session bootstrap failure handling", () => {
|
|
beforeEach(() => {
|
|
SessionUser.auth.forceClearSession();
|
|
SessionUser.functions.redirectTo.pages.login = mocks.loginRedirect;
|
|
SessionUser.functions.redirectTo.pages.subuserLogin = mocks.subuserLoginRedirect;
|
|
mocks.authenticatedRequest.mockReset();
|
|
mocks.parseError.mockReset();
|
|
mocks.pingApiServer.mockReset();
|
|
mocks.fetchReleaseRuntime.mockReset();
|
|
mocks.fetchReleaseRuntime.mockResolvedValue({});
|
|
mocks.swalFire.mockClear();
|
|
mocks.loginRedirect.mockReset();
|
|
mocks.subuserLoginRedirect.mockReset();
|
|
window.history.replaceState({}, "", "/user");
|
|
});
|
|
|
|
it("clears privileged session caches when forcing the session closed", () => {
|
|
seedStoredSession("cached-session-token");
|
|
const periodCacheKey = buildPeriodCacheKey({
|
|
dateFrom: "2026-05-01",
|
|
dateTo: "2026-05-31",
|
|
periodView: "all",
|
|
page: 1,
|
|
limit: 100,
|
|
search: "",
|
|
flagTab: "all",
|
|
includeRequiresAction: true,
|
|
includeBooked: true,
|
|
});
|
|
|
|
setCachedPeriodPage(
|
|
periodCacheKey,
|
|
{ types: { all: [{ customer_name: "Sensitive Customer" }] } },
|
|
{ page: 1, per_page: 100, total: 1 }
|
|
);
|
|
setCachedXlvaskUsageAmount(8101, {
|
|
order_items: [{ license_plate: "PII-123" }],
|
|
potential_duplicates: [],
|
|
});
|
|
window.sessionStorage.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify({ details: { 1: "gateway" } }));
|
|
window.localStorage.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify({ details: { 1: "legacy" } }));
|
|
|
|
expect(getCachedPeriodPage(periodCacheKey)?.data?.types?.all?.[0]?.customer_name).toBe("Sensitive Customer");
|
|
expect(getCachedXlvaskUsageAmount(8101)?.order_items?.[0]?.license_plate).toBe("PII-123");
|
|
expect(window.sessionStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toContain("gateway");
|
|
expect(window.localStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toContain("legacy");
|
|
window.localStorage.setItem("selected_customer_number", "12345678");
|
|
|
|
SessionUser.auth.forceClearSession();
|
|
|
|
expect(getCachedPeriodPage(periodCacheKey)).toBeNull();
|
|
expect(getCachedXlvaskUsageAmount(8101)).toBeNull();
|
|
expect(Object.keys(window.sessionStorage).filter((key) => key.startsWith("invoicing-period-page:")).length).toBe(0);
|
|
expect(Object.keys(window.sessionStorage).filter((key) => key.startsWith("xlvask-usage-amount:")).length).toBe(0);
|
|
expect(window.sessionStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toBeNull();
|
|
expect(window.localStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toBeNull();
|
|
expect(window.localStorage.getItem("selected_customer_number")).toBeNull();
|
|
});
|
|
|
|
it("clears the local session when the logout request stalls", async () => {
|
|
seedStoredSession("logout-token");
|
|
mocks.authenticatedRequest.mockReturnValueOnce(new Promise(() => {}));
|
|
|
|
await SessionUser.auth.logout({ timeoutMs: 1 });
|
|
|
|
expect(mocks.authenticatedRequest).toHaveBeenCalledWith("/auth/logout", "GET");
|
|
expect(localStorage.getItem("token")).toBeNull();
|
|
expect(SessionUser.authenticated.value).toBe(false);
|
|
});
|
|
|
|
it("redirects after a confirmed logout even when the API logout request fails", async () => {
|
|
seedStoredSession("logout-token");
|
|
mocks.authenticatedRequest.mockRejectedValueOnce(new Error("Network unavailable"));
|
|
|
|
await SessionUser.functions.showConfirmLogoutDialog();
|
|
|
|
expect(localStorage.getItem("token")).toBeNull();
|
|
expect(mocks.loginRedirect).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("clears an invalid stored session after ping confirms the API is reachable", async () => {
|
|
seedStoredSession("invalid-token");
|
|
mocks.authenticatedRequest.mockRejectedValueOnce(invalidSessionError());
|
|
mocks.pingApiServer.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
url: "/api/ping",
|
|
error: null,
|
|
});
|
|
|
|
const result = await getSessionData();
|
|
|
|
expect(mocks.pingApiServer).toHaveBeenCalledTimes(1);
|
|
expect(localStorage.getItem("token")).toBeNull();
|
|
expect(SessionUser.authenticated.value).toBe(false);
|
|
expect(mocks.loginRedirect).toHaveBeenCalledTimes(1);
|
|
expect(result).toMatchObject({
|
|
cleared: true,
|
|
apiReachable: true,
|
|
redirectedTo: "/login",
|
|
});
|
|
});
|
|
|
|
it("does not expose subuser grant permissions before a valid grant is selected", () => {
|
|
SessionUser.auth.forceClearSession();
|
|
localStorage.setItem("token", "subuser-token");
|
|
localStorage.setItem("is_subuser", "true");
|
|
SessionUser.token.value = "subuser-token";
|
|
SessionUser.authenticated.value = true;
|
|
SessionUser.isSubuser.value = true;
|
|
SessionUser.permissions.value = ["SUBUSERS_LIST"];
|
|
SessionUser.subuser.grants.value = [
|
|
{
|
|
billing_customer_number: 12345678,
|
|
permissions: ["SUBUSERS_LIST"],
|
|
},
|
|
];
|
|
SessionUser.subuser.selectGrant(null);
|
|
|
|
expect(SessionUser.hasPermission("SUBUSERS_LIST")).toBe(false);
|
|
|
|
SessionUser.subuser.selectGrant(12345678);
|
|
|
|
expect(SessionUser.hasPermission("SUBUSERS_LIST")).toBe(true);
|
|
});
|
|
|
|
it("maps regular customer feature actions through legacy customer permissions", () => {
|
|
SessionUser.auth.forceClearSession();
|
|
SessionUser.isSubuser.value = false;
|
|
SessionUser.permissions.value = ["user"];
|
|
|
|
expect(SessionUser.canAccessCustomerFeature("vehicles", "list")).toBe(true);
|
|
expect(SessionUser.canAccessCustomerFeature("vehicles", "add")).toBe(true);
|
|
expect(SessionUser.canAccessCustomerFeature("bookings", "add")).toBe(true);
|
|
expect(SessionUser.canAccessCustomerFeature("orders", "list")).toBe(true);
|
|
expect(SessionUser.canAccessCustomerFeature("vehicles", "delete")).toBe(false);
|
|
});
|
|
|
|
it("maps subuser feature actions through the selected grant only", () => {
|
|
SessionUser.auth.forceClearSession();
|
|
SessionUser.isSubuser.value = true;
|
|
SessionUser.permissions.value = ["BOOKINGS_ADD", "VEHICLES_DELETE"];
|
|
SessionUser.subuser.grants.value = [
|
|
{
|
|
billing_customer_number: 12345678,
|
|
permissions: ["BOOKINGS_ADD", "VEHICLES_LIST"],
|
|
},
|
|
{
|
|
billing_customer_number: 87654321,
|
|
permissions: ["VEHICLES_DELETE"],
|
|
},
|
|
];
|
|
|
|
SessionUser.subuser.selectGrant(12345678);
|
|
|
|
expect(SessionUser.canAccessCustomerFeature("bookings", "add")).toBe(true);
|
|
expect(SessionUser.canAccessCustomerFeature("vehicles", "list")).toBe(true);
|
|
expect(SessionUser.canAccessCustomerFeature("vehicles", "delete")).toBe(false);
|
|
|
|
SessionUser.subuser.selectGrant(87654321);
|
|
|
|
expect(SessionUser.canAccessCustomerFeature("bookings", "add")).toBe(false);
|
|
expect(SessionUser.canAccessCustomerFeature("vehicles", "delete")).toBe(true);
|
|
});
|
|
|
|
it("redirects invalid stored subuser sessions to driver login", async () => {
|
|
seedStoredSession("invalid-subuser-token");
|
|
localStorage.setItem("is_subuser", "true");
|
|
SessionUser.isSubuser.value = true;
|
|
mocks.authenticatedRequest.mockRejectedValueOnce(invalidSessionError());
|
|
mocks.pingApiServer.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
url: "/api/ping",
|
|
error: null,
|
|
});
|
|
|
|
const result = await getSubuserSessionData();
|
|
|
|
expect(localStorage.getItem("token")).toBeNull();
|
|
expect(localStorage.getItem("is_subuser")).toBeNull();
|
|
expect(mocks.loginRedirect).not.toHaveBeenCalled();
|
|
expect(mocks.subuserLoginRedirect).toHaveBeenCalledTimes(1);
|
|
expect(result).toMatchObject({
|
|
cleared: true,
|
|
apiReachable: true,
|
|
redirectedTo: "/login/driver",
|
|
});
|
|
});
|
|
|
|
it("recovers a stored subuser token that was missing the subuser localStorage marker", async () => {
|
|
seedStoredSession("subuser-token-without-marker");
|
|
mocks.authenticatedRequest.mockRejectedValueOnce(userNotFoundSessionError());
|
|
mocks.authenticatedRequest.mockResolvedValueOnce(subuserSessionPayload());
|
|
|
|
const result = await getSessionData();
|
|
|
|
expect(mocks.authenticatedRequest).toHaveBeenCalledTimes(2);
|
|
expect(mocks.authenticatedRequest.mock.calls[0][0]).toBe("/auth/session");
|
|
expect(mocks.authenticatedRequest.mock.calls[1][0]).toBe("/subusers/me");
|
|
expect(result).toBeUndefined();
|
|
expect(localStorage.getItem("token")).toBe("subuser-token-without-marker");
|
|
expect(localStorage.getItem("is_subuser")).toBe("true");
|
|
expect(localStorage.getItem("selected_customer_number")).toBe("12345678");
|
|
expect(SessionUser.isSubuser.value).toBe(true);
|
|
expect(SessionUser.initiated.value).toBe(true);
|
|
expect(SessionUser.subuser.id.value).toBe(77);
|
|
expect(SessionUser.permissions.value).toEqual(["BOOKINGS_LIST", "VEHICLES_LIST"]);
|
|
expect(mocks.loginRedirect).not.toHaveBeenCalled();
|
|
expect(mocks.subuserLoginRedirect).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("redirects to driver login when subuser-token recovery also proves invalid", async () => {
|
|
seedStoredSession("stale-subuser-token-without-marker");
|
|
mocks.authenticatedRequest.mockRejectedValueOnce(userNotFoundSessionError());
|
|
mocks.authenticatedRequest.mockRejectedValueOnce(invalidSessionError());
|
|
mocks.pingApiServer.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
url: "/api/ping",
|
|
error: null,
|
|
});
|
|
|
|
const result = await getSessionData();
|
|
|
|
expect(mocks.authenticatedRequest).toHaveBeenCalledTimes(2);
|
|
expect(mocks.authenticatedRequest.mock.calls[1][0]).toBe("/subusers/me");
|
|
expect(localStorage.getItem("token")).toBeNull();
|
|
expect(localStorage.getItem("is_subuser")).toBeNull();
|
|
expect(mocks.loginRedirect).not.toHaveBeenCalled();
|
|
expect(mocks.subuserLoginRedirect).toHaveBeenCalledTimes(1);
|
|
expect(result).toMatchObject({
|
|
cleared: true,
|
|
apiReachable: true,
|
|
redirectedTo: "/login/driver",
|
|
});
|
|
});
|
|
|
|
it("preserves an invalid stored session and routes to connectivity when ping fails", async () => {
|
|
seedStoredSession("invalid-token");
|
|
mocks.authenticatedRequest.mockRejectedValueOnce(invalidSessionError());
|
|
mocks.pingApiServer.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: null,
|
|
url: "/api/ping",
|
|
error: new Error("Network unavailable"),
|
|
});
|
|
|
|
const result = await getSessionData();
|
|
|
|
expect(mocks.pingApiServer).toHaveBeenCalledTimes(1);
|
|
expect(localStorage.getItem("token")).toBe("invalid-token");
|
|
expect(SessionUser.authenticated.value).toBe(true);
|
|
expect(mocks.loginRedirect).not.toHaveBeenCalled();
|
|
expect(result).toMatchObject({
|
|
cleared: false,
|
|
apiReachable: false,
|
|
redirectedTo: "/connectivity-issue",
|
|
});
|
|
});
|
|
|
|
it("does not clear the stored session for generic server bootstrap errors", async () => {
|
|
seedStoredSession("still-valid-token");
|
|
const serverError = {
|
|
response: {
|
|
status: 500,
|
|
data: {
|
|
message: "Internal server error",
|
|
},
|
|
},
|
|
};
|
|
mocks.authenticatedRequest.mockRejectedValueOnce(serverError);
|
|
|
|
const result = await getSessionData();
|
|
|
|
expect(mocks.pingApiServer).not.toHaveBeenCalled();
|
|
expect(localStorage.getItem("token")).toBe("still-valid-token");
|
|
expect(SessionUser.authenticated.value).toBe(true);
|
|
expect(mocks.loginRedirect).not.toHaveBeenCalled();
|
|
expect(mocks.parseError).toHaveBeenCalledWith(serverError, "auth");
|
|
expect(mocks.swalFire).toHaveBeenCalledTimes(1);
|
|
expect(result).toMatchObject({
|
|
cleared: false,
|
|
apiReachable: null,
|
|
redirectedTo: null,
|
|
});
|
|
});
|
|
});
|