Files
fusion/packages/dashboard/app/components/__tests__/AuthTokenRecoveryDialog.test.tsx
gsxdsm 0ff60a79d1 FN-7243: focus auth token recovery on unauthorized
Focus daemon authorization failures on auth-token recovery instead of engine remediation.

- Pass auth-token recovery state into dashboard banners so engine unavailable and status remediation banners stay hidden while recovery is open.
- Improve auth recovery dialog focus styling and tests for unauthorized daemon responses.
- Add release notes for focused auth-token recovery behavior.

Files changed:
 .changeset/auth-token-recovery-focus.md            |   7 ++
 .changeset/fn-7243-auth-recovery-banner.md         |   7 ++
 packages/dashboard/app/App.tsx                     |   1 +
 packages/dashboard/app/__tests__/auth.test.ts      |  31 ++++-
 .../app/components/AuthTokenRecoveryDialog.css     |   7 +-
 .../__tests__/AuthTokenRecoveryDialog.test.tsx     |  41 +++++-
 .../app/components/dashboard/DashboardBanners.tsx  |  14 ++-
 .../dashboard/__tests__/DashboardBanners.test.tsx  | 138 ++++++++++++++++++++-
 .../dashboard/app/components/dashboard/types.ts    |   1 +
 .../hooks/__tests__/useAuthTokenRecovery.test.ts   |   9 +-
 10 files changed, 238 insertions(+), 18 deletions(-)

Fusion-Task-Id: FN-7243

Fusion-Task-Lineage: 8292080d-a310-4fdc-9b5f-baa17a592bad

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-06-29 15:34:06 -07:00

121 lines
4.5 KiB
TypeScript

import { readFileSync } from "node:fs";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { AuthTokenRecoveryDialog } from "../AuthTokenRecoveryDialog";
import { clearAuthToken, setAuthToken } from "../../auth";
vi.mock("../../auth", () => ({
setAuthToken: vi.fn(),
clearAuthToken: vi.fn(),
}));
describe("AuthTokenRecoveryDialog", () => {
const originalLocation = window.location;
let reloadSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
reloadSpy = vi.fn();
Object.defineProperty(window, "location", {
configurable: true,
value: { ...originalLocation, reload: reloadSpy },
});
});
it("does not render when closed", () => {
render(<AuthTokenRecoveryDialog open={false} />);
expect(screen.queryByRole("dialog", { name: "Authentication token required" })).toBeNull();
});
it("renders a blocking shared-modal dialog, focuses the token input, and disables set until input is populated", () => {
render(<AuthTokenRecoveryDialog open={true} />);
const dialog = screen.getByRole("dialog", { name: "Authentication token required" });
expect(dialog).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /close/i })).toBeNull();
const overlay = dialog.closest(".auth-token-recovery-overlay");
expect(overlay).toBeTruthy();
if (!overlay) {
throw new Error("Expected auth token recovery overlay");
}
expect(overlay.classList.contains("modal-overlay")).toBe(true);
expect(overlay.classList.contains("open")).toBe(true);
expect(dialog.classList.contains("modal")).toBe(true);
expect(dialog.classList.contains("modal-md")).toBe(true);
const input = screen.getByLabelText("Replacement token");
expect(document.activeElement).toBe(input);
const setTokenButton = screen.getByRole("button", { name: "Set token and reload" });
expect(setTokenButton).toBeDisabled();
fireEvent.change(input, { target: { value: " " } });
expect(setTokenButton).toBeDisabled();
fireEvent.change(input, { target: { value: "abc123" } });
expect(setTokenButton).toBeEnabled();
});
it("focuses the token input when recovery opens after the app shell was already mounted", () => {
const { rerender } = render(<AuthTokenRecoveryDialog open={false} />);
rerender(<AuthTokenRecoveryDialog open={true} />);
const tokenInput = screen.getByLabelText("Replacement token");
expect(screen.getByRole("dialog", { name: "Authentication token required" })).toBeInTheDocument();
expect(tokenInput).toBe(document.activeElement);
});
it("keeps a single blocking dialog when duplicate open signals rerender it", () => {
const { rerender } = render(<AuthTokenRecoveryDialog open={true} />);
rerender(<AuthTokenRecoveryDialog open={true} />);
expect(screen.getAllByRole("dialog", { name: "Authentication token required" })).toHaveLength(1);
expect(document.querySelectorAll(".auth-token-recovery-overlay")).toHaveLength(1);
});
it("does not define auth-specific modal layering outside the shared modal classes", () => {
const css = readFileSync("app/components/AuthTokenRecoveryDialog.css", "utf8");
expect(css).not.toMatch(/auth-token-recovery-overlay\s*\{[^}]*z-index/s);
});
it("trims and stores replacement token before reloading", () => {
render(<AuthTokenRecoveryDialog open={true} />);
fireEvent.change(screen.getByLabelText("Replacement token"), { target: { value: " new-token " } });
fireEvent.click(screen.getByRole("button", { name: "Set token and reload" }));
expect(setAuthToken).toHaveBeenCalledWith("new-token");
expect(reloadSpy).toHaveBeenCalledTimes(1);
});
it("clears token and reloads when user retries without replacement token", () => {
render(<AuthTokenRecoveryDialog open={true} />);
fireEvent.click(screen.getByRole("button", { name: "Clear token and retry" }));
expect(clearAuthToken).toHaveBeenCalledTimes(1);
expect(reloadSpy).toHaveBeenCalledTimes(1);
});
it("does not dismiss on Escape key", () => {
render(<AuthTokenRecoveryDialog open={true} />);
const overlay = document.querySelector(".auth-token-recovery-overlay");
expect(overlay).toBeTruthy();
if (!overlay) {
throw new Error("Expected auth token recovery overlay");
}
fireEvent.keyDown(overlay, { key: "Escape" });
expect(screen.getByRole("dialog", { name: "Authentication token required" })).toBeInTheDocument();
});
});