DigitalConsumer.FixMyGlass/src/router/methods/before-each-error-recovery.spec.js
Matt Sykes 76b4c35d59 Prevent error redirect loops with beforeEach circuit breaker
This adds a circuit breaker for uncaught failures in beforeEach. The first failure still goes through the normal ERROR/RESTART recovery path; a second consecutive failure redirects to the static error page instead of looping.
- New before-each-error-recovery module tracks recovery attempts in sessionStorage (with an in-memory fallback).
- ERROR/RESTART routes skip normal guard logic so the counter can accumulate across bounce-backs.
- The counter clears only after a fully successful navigation.
- redirectToStaticErrorPage centralizes hard-error bailout: logs, clears poisoned sessionStorage keys (submittedState, heritage redirect count, recovery count, externalParameterState), then navigates to /fmg/static/error.
- The static error page RESTART flow clears the same sessionStorage keys so bad state does not re-enter the funnel.
- Session storage key strings are centralized in session-storage.js.
- Unit tests cover recovery counting, bailout clearing, and navigation fallbacks.
Expected impact: Stops repeated ERROR → RESTART → ERROR cycles (including corrupt submittedState JSON.parse failures and recurring automation errors in us-east-1/us-east-2) by bailing out to the static error page after one failed recovery attempt.
2026-06-10 15:48:29 -04:00

80 lines
2.8 KiB
JavaScript

import {
clearBeforeEachErrorRecoveryCount,
getBeforeEachErrorRecoveryCount,
hasBeforeEachErrorRecoveryBeenAttempted,
incrementBeforeEachErrorRecoveryCount,
resetBeforeEachErrorRecoveryStateForTests,
} from "./before-each-error-recovery";
import { sessionStorageKeyConstants } from "@/constants/session-storage";
describe("router/methods/before-each-error-recovery", () => {
const recoveryKey = sessionStorageKeyConstants.BEFORE_EACH_ERROR_RECOVERY_COUNT;
let originalSessionStorage;
beforeEach(() => {
resetBeforeEachErrorRecoveryStateForTests();
originalSessionStorage = window.sessionStorage;
});
afterEach(() => {
Object.defineProperty(window, "sessionStorage", {
configurable: true,
value: originalSessionStorage,
});
resetBeforeEachErrorRecoveryStateForTests();
});
it("reads and writes recovery count via sessionStorage when available", () => {
incrementBeforeEachErrorRecoveryCount();
expect(window.sessionStorage.getItem(recoveryKey)).toBe("1");
expect(getBeforeEachErrorRecoveryCount()).toBe(1);
expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(true);
});
it("clears sessionStorage and in-memory count", () => {
incrementBeforeEachErrorRecoveryCount();
clearBeforeEachErrorRecoveryCount();
expect(window.sessionStorage.getItem(recoveryKey)).toBeNull();
expect(getBeforeEachErrorRecoveryCount()).toBe(0);
expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(false);
});
it("uses in-memory count when sessionStorage getItem throws", () => {
Object.defineProperty(window, "sessionStorage", {
configurable: true,
value: {
getItem: () => {
throw new Error("sessionStorage blocked");
},
setItem: jest.fn(),
removeItem: jest.fn(),
},
});
incrementBeforeEachErrorRecoveryCount();
incrementBeforeEachErrorRecoveryCount();
expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(true);
expect(getBeforeEachErrorRecoveryCount()).toBe(2);
});
it("uses in-memory count when sessionStorage setItem throws", () => {
Object.defineProperty(window, "sessionStorage", {
configurable: true,
value: {
getItem: () => null,
setItem: () => {
throw new Error("sessionStorage blocked");
},
removeItem: jest.fn(),
},
});
incrementBeforeEachErrorRecoveryCount();
expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(true);
expect(getBeforeEachErrorRecoveryCount()).toBe(1);
});
});