Files
fusion/packages/dashboard/app/hooks/__tests__/useAuthTokenRecovery.test.ts
gsxdsm 2f0f7c652f FN-8301: restore PWA token recovery after cold starts
Restore the auth-token dialog when an installed PWA receives a daemon 401 before React mounts.

- Latch daemon authentication failures for mount-time recovery.
- Clear the latch when the authentication token changes.
- Cover cold-start recovery and successful-response behavior with dashboard tests.

Files changed:
 .changeset/fn-8301-pwa-auth-recovery.md            |  7 +++
 packages/dashboard/app/__tests__/auth.test.ts      | 40 ++++++++++++++
 packages/dashboard/app/auth.ts                     |  8 +++
 .../hooks/__tests__/useAuthTokenRecovery.test.ts   | 61 +++++++++++++++++++++-
 .../dashboard/app/hooks/useAuthTokenRecovery.ts    | 10 ++--
 5 files changed, 121 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-8301

Fusion-Task-Lineage: b35236f4-af3d-4313-8b7d-6cd59a237144

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-18 11:02:45 -07:00

101 lines
3.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { renderHook, act } from "@testing-library/react";
import {
AUTH_TOKEN_RECOVERY_REQUIRED_EVENT,
clearAuthToken,
hasDaemonAuthFailure,
installAuthFetch,
setAuthToken,
} from "../../auth";
import { useAuthTokenRecovery } from "../useAuthTokenRecovery";
const originalFetch = window.fetch;
function waitForDaemonAuthRecoveryEvent(): Promise<void> {
return new Promise((resolve) => {
const handleRecovery = () => {
window.removeEventListener(AUTH_TOKEN_RECOVERY_REQUIRED_EVENT, handleRecovery);
resolve();
};
window.addEventListener(AUTH_TOKEN_RECOVERY_REQUIRED_EVENT, handleRecovery);
});
}
describe("useAuthTokenRecovery", () => {
beforeEach(() => {
clearAuthToken();
window.localStorage.clear();
window.history.replaceState({}, "", "/");
window.fetch = originalFetch;
delete (window as Window & { __fnAuthFetchInstalled?: boolean }).__fnAuthFetchInstalled;
});
afterEach(() => {
vi.restoreAllMocks();
});
it("opens on daemon auth-failure events and stays open for duplicate signals", () => {
const { result } = renderHook(() => useAuthTokenRecovery());
expect(result.current.open).toBe(false);
act(() => {
window.dispatchEvent(new Event(AUTH_TOKEN_RECOVERY_REQUIRED_EVENT));
});
expect(result.current.open).toBe(true);
act(() => {
window.dispatchEvent(new Event(AUTH_TOKEN_RECOVERY_REQUIRED_EVENT));
window.dispatchEvent(new Event(AUTH_TOKEN_RECOVERY_REQUIRED_EVENT));
});
expect(result.current.open).toBe(true);
});
it("opens when a daemon-auth 401 latched before the hook mounts", async () => {
window.fetch = vi.fn(async () => new Response(
JSON.stringify({ error: "Unauthorized", message: "Valid bearer token required" }),
{ status: 401, headers: { "content-type": "application/json" } },
)) as unknown as typeof window.fetch;
installAuthFetch();
const recoveryEvent = waitForDaemonAuthRecoveryEvent();
await fetch("/api/tasks");
await recoveryEvent;
expect(hasDaemonAuthFailure()).toBe(true);
const { result } = renderHook(() => useAuthTokenRecovery());
expect(result.current.open).toBe(true);
});
it("does not open for a successful API response with a valid token", async () => {
setAuthToken("valid-token");
window.fetch = vi.fn(async () => new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "content-type": "application/json" },
})) as unknown as typeof window.fetch;
installAuthFetch();
await fetch("/api/tasks");
expect(hasDaemonAuthFailure()).toBe(false);
const { result } = renderHook(() => useAuthTokenRecovery());
expect(result.current.open).toBe(false);
});
it("removes the daemon auth-failure listener on unmount", () => {
const addSpy = vi.spyOn(window, "addEventListener");
const removeSpy = vi.spyOn(window, "removeEventListener");
const { unmount } = renderHook(() => useAuthTokenRecovery());
const addedCall = addSpy.mock.calls.find(
([type]) => type === AUTH_TOKEN_RECOVERY_REQUIRED_EVENT,
);
expect(addedCall).toBeTruthy();
const addedHandler = addedCall![1] as EventListener;
unmount();
expect(removeSpy).toHaveBeenCalledWith(AUTH_TOKEN_RECOVERY_REQUIRED_EVENT, addedHandler);
});
});