Harden edge gateway workspace cache

This commit is contained in:
Jeppe B
2026-06-02 00:08:20 +02:00
parent f613d58727
commit 9450c7902d
12 changed files with 255 additions and 30 deletions
@@ -10,6 +10,7 @@ import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue"
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const employees = ref([]);
const { t } = useI18n();
@@ -52,6 +53,7 @@ const login = async () => {
}
// Save the token in the local storage
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', data.token);
// Redirect to the dashboard
window.location.href = '/admin';
@@ -72,6 +74,7 @@ const loginWithPasskey = async () => {
const result = await authenticateWithPasskey('employee', null, recaptchaToken);
if (result.token) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', result.token);
window.location.href = '/admin';
return;
+3
View File
@@ -9,6 +9,7 @@ import { parseError, clearErrors, addError, getError } from "@/components/reques
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const { t } = useI18n();
@@ -68,6 +69,7 @@ const login = async () => {
}
// Save the token in the local storage
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', data.token);
// Set the success message
successMessage.value = "Du er nu logget ind!"
@@ -103,6 +105,7 @@ const loginWithPasskey = async () => {
const result = await authenticateWithPasskey('user', customerNum, recaptchaToken);
console.log('Passkey authentication result:', result);
if (result.token) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', result.token);
successMessage.value = "Du er nu logget ind!";
setTimeout(() => {
@@ -9,6 +9,7 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
import { getSubuserPasswordPolicyError } from "@/services/subuserPasswordPolicy.js";
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const { t } = useI18n();
@@ -69,6 +70,7 @@ const login = async () => {
// Save the session token
if (data.session) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', data.session);
localStorage.setItem('is_subuser', 'true');
window.location.reload();
@@ -97,6 +99,7 @@ const loginWithPasskey = async () => {
// Subuser login returns 'session' token, user login returns 'token'
const sessionToken = result.session || result.token;
if (sessionToken) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', sessionToken);
localStorage.setItem('is_subuser', 'true');
// Reload the page to update the UI
@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n';
import { verify2FA } from "@/services/TwoFactorAuthService.js";
import { parseError, getError, clearErrors } from "@/components/request/HandleGlobalError.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const { t } = useI18n();
const router = useRouter();
@@ -43,9 +44,11 @@ const verify = async () => {
// Handle the response based on user type
if (props.userType === 'subuser' && result.session) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', result.session);
localStorage.setItem('is_subuser', 'true');
} else if (result.token) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', result.token);
localStorage.removeItem('is_subuser');
}
+2
View File
@@ -2,6 +2,7 @@
import { ref } from 'vue'
import axios from 'axios'
import {API_URL} from "@/config.js";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
export const currentDepartment = ref(null);
export const selectDepartment = (department) => {
@@ -31,6 +32,7 @@ export const exitSession = () => {
}
});
clearEdgeGatewayWorkspaceCache();
localStorage.removeItem('token');
return result;
};
@@ -59,6 +59,7 @@ import {
setReleaseChannelSwitchNoticePrincipal,
} from "@/services/releaseChannelAvailability.js";
import { clearCachedXlvaskUsageAmount } from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
import {
clearPeriodCache,
clearSelfWashCountsCache,
@@ -107,6 +108,7 @@ const clearPrivilegedSessionCaches = () => {
clearPeriodCache();
clearSelfWashCountsCache();
clearCachedXlvaskUsageAmount();
clearEdgeGatewayWorkspaceCache();
};
const clearStoredSession = () => {
@@ -685,6 +687,7 @@ export const SessionUser = {
* @param {number|null} customerNumber - The billing_customer_number to select, or null to clear
*/
selectGrant: (customerNumber) => {
clearEdgeGatewayWorkspaceCache();
if (customerNumber === null) {
localStorage.removeItem("selected_customer_number");
SessionUser.subuser.selectedGrantCustomerNumber.value = null;
@@ -1,6 +1,7 @@
<script>
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import { parseError } from "@/components/request/HandleGlobalError.vue";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
import { DatabaseSystemObject } from "@/components/session/token/superUser/systemDatabase.vue";
import { Cron } from "@/components/session/token/superUser/cron.vue";
import { Economic } from "@/components/session/token/superUser/modules/Economic/Economic.vue";
@@ -106,6 +107,7 @@ export const SuperUserObject = {
const currentToken = localStorage.getItem("token");
localStorage.setItem("superuser_token", currentToken);
// Set the token in the local storage
clearEdgeGatewayWorkspaceCache();
localStorage.setItem("token", response.data.data.token);
// Redirect the user to the dashboard
window.location = "/";
@@ -143,6 +145,7 @@ export const SuperUserObject = {
// Get the superuser token
const superuserToken = localStorage.getItem("superuser_token");
// Set the token in the local storage
clearEdgeGatewayWorkspaceCache();
localStorage.setItem("token", superuserToken);
// Clear the superuser token
localStorage.removeItem("superuser_token");
+146 -28
View File
@@ -1,6 +1,7 @@
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
export const EDGE_GATEWAY_WORKSPACE_CACHE_KEY = "truckwash.edgeGatewayWorkspace.cache.v1";
export const EDGE_GATEWAY_WORKSPACE_CACHE_TTL_MS = 15 * 60 * 1000;
const EDGE_GATEWAY_BASE = "/edge-gateways";
const EDGE_GATEWAY_CONFIG_BASE = "/edgegateway/config";
@@ -91,53 +92,167 @@ const toConfigPayload = (entries) => {
return payload;
};
const createEmptyWorkspaceCache = () => ({
const createEmptyWorkspaceCache = (scope = null) => ({
scope,
cachedAt: Date.now(),
departments: null,
lists: {},
details: {},
});
const storage = () => {
if (typeof window === "undefined" || !window.sessionStorage) {
return null;
}
return window.sessionStorage;
};
const removeLegacyWorkspaceCache = () => {
if (typeof window === "undefined" || !window.localStorage) {
return;
}
try {
window.localStorage.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
} catch (_error) {
// Browser storage can be unavailable in private browsing or tests.
}
};
const getStoredValue = (key) => {
if (typeof window === "undefined" || !window.localStorage) {
return null;
}
return window.localStorage;
try {
return window.localStorage.getItem(key);
} catch (_error) {
return null;
}
};
const hashScopePart = (value) => {
let hash = 0;
const input = String(value || "");
for (let index = 0; index < input.length; index += 1) {
hash = (hash << 5) - hash + input.charCodeAt(index);
hash |= 0;
}
return hash.toString(36);
};
const currentWorkspaceCacheScope = () => {
const token = getStoredValue("token");
if (!token) {
return null;
}
return JSON.stringify({
token: hashScopePart(token),
isSubuser: getStoredValue("is_subuser") === "true",
selectedCustomerNumber: getStoredValue("selected_customer_number") || null,
hasSuperuserToken: Boolean(getStoredValue("superuser_token")),
});
};
const isFreshCachedAt = (cachedAt) => Date.now() - Number(cachedAt || 0) <= EDGE_GATEWAY_WORKSPACE_CACHE_TTL_MS;
const isFreshCacheEntry = (entry) => Boolean(entry && typeof entry === "object" && isFreshCachedAt(entry.cachedAt));
const isSensitiveCacheField = (fieldName) =>
/(^|[_-])(token|secret|password|credential|authorization|api[_-]?key|private[_-]?key)($|[_-])/i.test(
String(fieldName || "")
) || String(fieldName || "").toLowerCase() === "config_json";
const sanitizeCacheValue = (value) => {
if (Array.isArray(value)) {
return value.map((item) => sanitizeCacheValue(item));
}
if (!value || typeof value !== "object") {
return value;
}
return Object.entries(value).reduce((sanitized, [key, childValue]) => {
if (!isSensitiveCacheField(key)) {
sanitized[key] = sanitizeCacheValue(childValue);
}
return sanitized;
}, {});
};
const sanitizeGatewayCachePayload = (value) => cloneJson(sanitizeCacheValue(value));
const loadWorkspaceCache = () => {
if (edgeGatewayWorkspaceCache !== null) {
removeLegacyWorkspaceCache();
const scope = currentWorkspaceCacheScope();
if (!scope) {
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache(null);
return edgeGatewayWorkspaceCache;
}
const localStorageHandle = storage();
if (!localStorageHandle) {
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache();
if (
edgeGatewayWorkspaceCache !== null &&
edgeGatewayWorkspaceCache.scope === scope &&
isFreshCachedAt(edgeGatewayWorkspaceCache.cachedAt)
) {
return edgeGatewayWorkspaceCache;
}
const storageHandle = storage();
if (!storageHandle) {
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache(scope);
return edgeGatewayWorkspaceCache;
}
try {
const decoded = JSON.parse(localStorageHandle.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY) || "null");
const decoded = JSON.parse(storageHandle.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY) || "null");
if (!decoded || typeof decoded !== "object" || decoded.scope !== scope || !isFreshCachedAt(decoded.cachedAt)) {
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache(scope);
storageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
return edgeGatewayWorkspaceCache;
}
edgeGatewayWorkspaceCache = {
...createEmptyWorkspaceCache(),
...(decoded && typeof decoded === "object" ? decoded : {}),
lists: decoded?.lists && typeof decoded.lists === "object" ? decoded.lists : {},
details: decoded?.details && typeof decoded.details === "object" ? decoded.details : {},
...createEmptyWorkspaceCache(scope),
cachedAt: Number(decoded.cachedAt || Date.now()),
departments: isFreshCacheEntry(decoded.departments) ? decoded.departments : null,
lists:
decoded?.lists && typeof decoded.lists === "object"
? Object.fromEntries(Object.entries(decoded.lists).filter(([, entry]) => isFreshCacheEntry(entry)))
: {},
details:
decoded?.details && typeof decoded.details === "object"
? Object.fromEntries(Object.entries(decoded.details).filter(([, entry]) => isFreshCacheEntry(entry)))
: {},
};
} catch (_error) {
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache();
edgeGatewayWorkspaceCache = createEmptyWorkspaceCache(scope);
storageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
}
return edgeGatewayWorkspaceCache;
};
const persistWorkspaceCache = () => {
const localStorageHandle = storage();
if (!localStorageHandle || edgeGatewayWorkspaceCache === null) {
const storageHandle = storage();
const scope = currentWorkspaceCacheScope();
if (!storageHandle || edgeGatewayWorkspaceCache === null || !scope || edgeGatewayWorkspaceCache.scope !== scope) {
return;
}
localStorageHandle.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify(edgeGatewayWorkspaceCache));
edgeGatewayWorkspaceCache.cachedAt = Date.now();
storageHandle.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify(edgeGatewayWorkspaceCache));
};
export const clearEdgeGatewayWorkspaceCache = () => {
edgeGatewayWorkspaceCache = null;
removeLegacyWorkspaceCache();
const storageHandle = storage();
if (!storageHandle) {
return;
}
storageHandle.removeItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY);
};
const mergeGatewaySnapshots = (currentGateway, nextGateway) => {
@@ -181,8 +296,9 @@ const cacheGatewaySnapshot = (gateway) => {
}
const cache = loadWorkspaceCache();
const gatewayId = String(gateway.id);
const mergedGateway = mergeGatewaySnapshots(cache.details[gatewayId]?.data || null, gateway);
const sanitizedGateway = sanitizeGatewayCachePayload(gateway);
const gatewayId = String(sanitizedGateway.id);
const mergedGateway = mergeGatewaySnapshots(cache.details[gatewayId]?.data || null, sanitizedGateway);
cache.details[gatewayId] = {
cachedAt: Date.now(),
data: mergedGateway,
@@ -198,9 +314,9 @@ const cacheGatewaySnapshot = (gateway) => {
const existingIndex = rows.findIndex((item) => Number(item?.id || 0) === Number(gateway.id));
if (existingIndex >= 0) {
rows[existingIndex] = mergeGatewaySnapshots(rows[existingIndex], gateway);
rows[existingIndex] = mergeGatewaySnapshots(rows[existingIndex], sanitizedGateway);
} else if (matchesDepartment) {
rows.unshift(cloneJson(gateway));
rows.unshift(cloneJson(sanitizedGateway));
}
cache.lists[cacheKey] = {
@@ -216,18 +332,19 @@ const cacheGatewaySnapshot = (gateway) => {
const cacheFleetSnapshot = ({ departmentId = null, view = "summary", rows = [], meta = {} } = {}) => {
const cache = loadWorkspaceCache();
const sanitizedRows = sanitizeGatewayCachePayload(Array.isArray(rows) ? rows : []);
const key = listCacheKey({ departmentId, view });
cache.lists[key] = {
params: {
departmentId: normalizeDepartmentId(departmentId),
view: String(view || "summary"),
},
rows: cloneJson(Array.isArray(rows) ? rows : []),
meta: cloneJson(meta || {}),
rows: sanitizedRows,
meta: sanitizeGatewayCachePayload(meta || {}),
cachedAt: Date.now(),
};
rows.forEach((gateway) => {
sanitizedRows.forEach((gateway) => {
if (!gateway?.id) {
return;
}
@@ -236,7 +353,7 @@ const cacheFleetSnapshot = ({ departmentId = null, view = "summary", rows = [],
if (!cache.details[gatewayId]) {
cache.details[gatewayId] = {
cachedAt: Date.now(),
data: cloneJson(gateway),
data: sanitizeGatewayCachePayload(gateway),
};
return;
}
@@ -254,7 +371,7 @@ const cacheDepartmentsSnapshot = (departments) => {
const cache = loadWorkspaceCache();
cache.departments = {
cachedAt: Date.now(),
data: cloneJson(Array.isArray(departments) ? departments : []),
data: sanitizeGatewayCachePayload(Array.isArray(departments) ? departments : []),
};
persistWorkspaceCache();
};
@@ -296,13 +413,14 @@ export const unwrapEdgeGatewayResponse = (response, fallback = null) => response
export const unwrapEdgeGatewayMeta = (response) => response?.data?.meta ?? {};
export const peekCachedEdgeGatewayDepartments = () => {
const departments = loadWorkspaceCache().departments?.data;
return Array.isArray(departments) ? cloneJson(departments) : null;
const entry = loadWorkspaceCache().departments;
const departments = entry?.data;
return isFreshCacheEntry(entry) && Array.isArray(departments) ? cloneJson(departments) : null;
};
export const peekCachedEdgeGatewayList = ({ departmentId = null, view = "summary" } = {}) => {
const entry = loadWorkspaceCache().lists[listCacheKey({ departmentId, view })];
if (!entry || !Array.isArray(entry.rows)) {
if (!isFreshCacheEntry(entry) || !Array.isArray(entry.rows)) {
return null;
}
@@ -320,7 +438,7 @@ export const peekCachedEdgeGateway = (gatewayId) => {
}
const entry = loadWorkspaceCache().details[normalizedGatewayId];
return entry?.data ? cloneJson(entry.data) : null;
return isFreshCacheEntry(entry) && entry?.data ? cloneJson(entry.data) : null;
};
export const patchEdgeGatewayCache = (gatewayId, patch) => {
+2
View File
@@ -12,6 +12,7 @@ import { QrcodeStream, QrcodeDropZone, QrcodeCapture } from 'vue-qrcode-reader'
import { sounds } from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
import PageLoader from "@/components/global/PageLoader.vue";
import Swal from "sweetalert2";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const isProcessingQRCode = ref(false); // State to indicate if QR code is being processed
const playCaptureSound = () => {
@@ -20,6 +21,7 @@ const playCaptureSound = () => {
const applyToken = (token: string) => {
isProcessingQRCode.value = true; // Start processing
clearEdgeGatewayWorkspaceCache();
localStorage.setItem("token", token);
window.location.href = "/";
}
+2
View File
@@ -1,12 +1,14 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, computed } from "vue";
import PageLoader from "@/components/global/PageLoader.vue";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const isProcessing = ref(false);
const hasToken = computed(() => localStorage.getItem("superuser_token") !== null);
const applyToken = (token: string) => {
isProcessing.value = true; // Start processing
clearEdgeGatewayWorkspaceCache();
localStorage.setItem("token", token);
localStorage.removeItem("superuser_token");
window.location.href = "/";
+78 -2
View File
@@ -1,3 +1,5 @@
// @vitest-environment jsdom
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -10,7 +12,12 @@ vi.mock("@/components/session/authenticatedRequest.vue", () => ({
import {
EDGE_GATEWAY_WORKSPACE_CACHE_KEY,
EDGE_GATEWAY_WORKSPACE_CACHE_TTL_MS,
clearEdgeGatewayWorkspaceCache,
getEdgeGatewayInstallTokenStatus,
listEdgeGateways,
peekCachedEdgeGateway,
peekCachedEdgeGatewayList,
getEdgeGatewayModuleConfig,
setEdgeGatewayModuleConfig,
} from "@/services/edgeGateways.js";
@@ -18,6 +25,9 @@ import {
describe("edge gateway service", () => {
beforeEach(() => {
authenticatedRequestMock.mockReset();
localStorage.clear();
sessionStorage.clear();
clearEdgeGatewayWorkspaceCache();
});
it("loads module config entries and derives a keyed config object", async () => {
@@ -76,11 +86,77 @@ describe("edge gateway service", () => {
expect(authenticatedRequestMock).toHaveBeenCalledWith("/edge-gateways/install-token/9001/status", "GET", {});
});
it("keeps a stable browser cache namespace for workspace snapshots", () => {
it("uses a stable browser cache namespace for session-scoped workspace snapshots", () => {
const source = readFileSync(join(process.cwd(), "src/services/edgeGateways.js"), "utf8");
expect(EDGE_GATEWAY_WORKSPACE_CACHE_KEY).toBe("truckwash.edgeGatewayWorkspace.cache.v1");
expect(source).toContain("localStorage");
expect(source).toContain("sessionStorage");
expect(source).toContain("EDGE_GATEWAY_WORKSPACE_CACHE_KEY");
});
it("stores workspace cache entries in session storage scoped to the active token", async () => {
localStorage.setItem("token", "operator-a-token");
authenticatedRequestMock.mockResolvedValueOnce({
data: {
data: [
{
id: 101,
department_id: 55,
name: "Privileged gateway",
metadata: { agent_token: "secret-agent-token", health: "ok" },
bindings: [{ relay_id: 7, credential_secret: "binding-secret" }],
},
],
meta: { fleet_usage: { total: 1 }, api_key: "fleet-api-key" },
},
});
await listEdgeGateways({ departmentId: 55, view: "summary" });
expect(localStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toBeNull();
const persisted = JSON.parse(sessionStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY));
expect(persisted.scope).toEqual(expect.any(String));
expect(persisted.scope).not.toContain("operator-a-token");
expect(persisted.lists).toBeTruthy();
expect(JSON.stringify(persisted)).not.toContain("secret-agent-token");
expect(JSON.stringify(persisted)).not.toContain("binding-secret");
expect(JSON.stringify(persisted)).not.toContain("fleet-api-key");
expect(peekCachedEdgeGateway(101)?.metadata?.health).toBe("ok");
localStorage.setItem("token", "operator-b-token");
expect(peekCachedEdgeGatewayList({ departmentId: 55, view: "summary" })).toBeNull();
expect(peekCachedEdgeGateway(101)).toBeNull();
});
it("expires stale workspace cache entries", async () => {
localStorage.setItem("token", "operator-a-token");
authenticatedRequestMock.mockResolvedValueOnce({
data: {
data: [{ id: 202, department_id: 55, name: "Stale gateway" }],
meta: {},
},
});
await listEdgeGateways({ departmentId: 55, view: "summary" });
const persisted = JSON.parse(sessionStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY));
persisted.cachedAt = Date.now() - EDGE_GATEWAY_WORKSPACE_CACHE_TTL_MS - 1;
Object.values(persisted.lists).forEach((entry) => {
entry.cachedAt = persisted.cachedAt;
});
Object.values(persisted.details).forEach((entry) => {
entry.cachedAt = persisted.cachedAt;
});
sessionStorage.setItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY, JSON.stringify(persisted));
vi.resetModules();
const { peekCachedEdgeGateway: peekGateway, peekCachedEdgeGatewayList: peekList } = await import(
"@/services/edgeGateways.js"
);
expect(peekList({ departmentId: 55, view: "summary" })).toBeNull();
expect(peekGateway(202)).toBeNull();
expect(sessionStorage.getItem(EDGE_GATEWAY_WORKSPACE_CACHE_KEY)).toBeNull();
});
});
+7
View File
@@ -29,6 +29,7 @@ vi.mock("sweetalert2", () => ({
}));
import { getSessionData, SessionUser } from "@/components/session/token/SessionUser.vue";
import { EDGE_GATEWAY_WORKSPACE_CACHE_KEY } from "@/services/edgeGateways.js";
import {
buildPeriodCacheKey,
getCachedPeriodPage,
@@ -91,9 +92,13 @@ describe("session bootstrap failure handling", () => {
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");
SessionUser.auth.forceClearSession();
@@ -101,6 +106,8 @@ describe("session bootstrap failure handling", () => {
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();
});
it("clears an invalid stored session after ping confirms the API is reachable", async () => {