import { render, screen, fireEvent, act } from "@testing-library/react"; /* eslint-disable react-x/component-hook-factories -- The vi.mock factory must expose a hook-named export (useAuth) to stand in for the real module; it's a test double, not a React hook. */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { AuthGuard } from "./auth-guard"; interface AuthState { isReady: boolean; isAuthenticated: boolean; } // Shared, mutable test doubles. Declared via vi.hoisted so they can be // referenced inside the (hoisted) vi.mock factory below. const h = vi.hoisted(() => { const state: { isReady: boolean; isAuthenticated: boolean } = { isReady: false, isAuthenticated: false, }; return { login: vi.fn(), logout: vi.fn(), state, }; }); vi.mock("@tailor-platform/app-shell", () => ({ useAuth: () => ({ login: h.login, logout: h.logout, isReady: h.state.isReady, isAuthenticated: h.state.isAuthenticated, }), })); // jsdom's navigator.onLine defaults to `true` and is writable, but we toggle // it via Object.defineProperty so it survives across tests and so the // snapshot read inside useSyncExternalStore picks up the change. function setOnline(value: boolean) { Object.defineProperty(navigator, "onLine", { configurable: true, get: () => value, }); } beforeEach(() => { h.login.mockClear(); h.logout.mockClear(); h.state = { isReady: false, isAuthenticated: false }; setOnline(true); }); const renderGuard = () => render(); const setAuth = (next: Partial) => { h.state = { ...h.state, ...next }; }; describe("AuthGuard", () => { it("shows a loading spinner while the auth bootstrap is in flight", () => { setAuth({ isReady: false, isAuthenticated: false }); renderGuard(); expect(screen.getByRole("status")).toBeInTheDocument(); expect(screen.getByText("Loading…")).toBeInTheDocument(); expect(screen.queryByText("Sign in required")).not.toBeInTheDocument(); }); it("shows the connectivity-error screen (not 'Sign in required') when the browser is offline", () => { // The case the user reported: disconnect WiFi -> app-shell can't reach the // IdP -> reports isAuthenticated=false. We must NOT mislead the user into // clicking Sign in; show a connectivity error instead. setOnline(false); setAuth({ isReady: true, isAuthenticated: false }); renderGuard(); expect(screen.getByText("Can't reach the server")).toBeInTheDocument(); expect(screen.queryByText("Sign in required")).not.toBeInTheDocument(); // Connectivity screen rendered without an onRetry, so no retry button. expect(screen.queryByRole("button", { name: "Retry" })).not.toBeInTheDocument(); expect(h.login).not.toHaveBeenCalled(); }); it("does not mention the VPN/exit-node anywhere on the connectivity screen", () => { // Prod is publicly reachable — VPN copy would be misleading there. setOnline(false); setAuth({ isReady: true, isAuthenticated: false }); renderGuard(); expect(screen.queryByText(/VPN/i)).not.toBeInTheDocument(); expect(screen.queryByText(/exit node/i)).not.toBeInTheDocument(); }); it("auto-recovers when the browser comes back online", () => { setOnline(false); setAuth({ isReady: true, isAuthenticated: false }); renderGuard(); expect(screen.getByText("Can't reach the server")).toBeInTheDocument(); // Browser regains connectivity -> hook re-renders -> guard falls through // to the sign-in branch on its own. No manual refresh required. act(() => { setOnline(true); window.dispatchEvent(new Event("online")); }); expect(screen.getByText("Sign in required")).toBeInTheDocument(); expect(screen.queryByText("Can't reach the server")).not.toBeInTheDocument(); }); it("shows 'Sign in required' when online and not authenticated", () => { setAuth({ isReady: true, isAuthenticated: false }); renderGuard(); expect(screen.getByText("Sign in required")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Sign in" })); expect(h.login).toHaveBeenCalledTimes(1); }); it("renders nothing (falls through) once the user is authenticated", () => { setAuth({ isReady: true, isAuthenticated: true }); const { container } = renderGuard(); // AuthProvider treats an empty/undefined guard return as "guard passes" // and renders its own children; here we just assert no guard UI shows. expect(container).toBeEmptyDOMElement(); expect(screen.queryByText("Sign in required")).not.toBeInTheDocument(); expect(screen.queryByText("Can't reach the server")).not.toBeInTheDocument(); }); });