import { render, screen, fireEvent } from "@testing-library/react"; /* eslint-disable react-x/component-hook-factories -- The vi.mock factories below must expose hook-named exports (useAuth/useQuery) to stand in for the real modules; these are test doubles, not React hooks. */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { ActiveUserGuard } from "./active-user-guard"; interface GuardState { data?: { me?: { status: string } | null }; fetching: boolean; error?: { graphQLErrors?: { extensions?: { code?: string } }[] }; } // Shared, mutable test doubles. Declared via vi.hoisted so they can be // referenced inside the (hoisted) vi.mock factories below. const h = vi.hoisted(() => { const state: GuardState = { data: undefined, fetching: false, error: undefined, }; return { login: vi.fn(), logout: vi.fn(), refetch: vi.fn(), state, }; }); vi.mock("@tailor-platform/app-shell", () => ({ useAuth: () => ({ login: h.login, logout: h.logout }), })); vi.mock("urql", () => ({ useQuery: () => [h.state, h.refetch], })); const renderGuard = () => render(
app content
, ); beforeEach(() => { h.login.mockClear(); h.logout.mockClear(); h.refetch.mockClear(); h.state = { data: undefined, fetching: false, error: undefined }; }); describe("ActiveUserGuard", () => { it("renders the app when the account status is ACTIVE", () => { h.state = { data: { me: { status: "ACTIVE" } }, fetching: false }; renderGuard(); expect(screen.getByText("app content")).toBeInTheDocument(); expect(h.login).not.toHaveBeenCalled(); }); it("shows a spinner while the status query is in flight (no blank screen)", () => { h.state = { data: undefined, fetching: true }; renderGuard(); expect(screen.getByRole("status")).toBeInTheDocument(); expect(screen.queryByText("app content")).not.toBeInTheDocument(); }); it("keeps the cached app visible while a revalidation is in flight (no spinner flash)", () => { // urql's cache-and-network policy sets `fetching=true` on re-execution // while the previous result is still cached. The guard must NOT replace // the rendered app with a full-screen spinner in that window. h.state = { data: { me: { status: "ACTIVE" } }, fetching: true }; renderGuard(); expect(screen.getByText("app content")).toBeInTheDocument(); expect(screen.queryByRole("status")).not.toBeInTheDocument(); }); it("shows 'Account deactivated' only for a confirmed non-ACTIVE status", () => { h.state = { data: { me: { status: "INACTIVE" } }, fetching: false }; renderGuard(); expect(screen.getByText("Account deactivated")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Sign out" })); expect(h.logout).toHaveBeenCalledTimes(1); }); it("shows a connectivity error (not deactivation) on a network error", () => { // Network failure: error present, no auth code, no data — e.g. the VPN // exit node is not connected. h.state = { data: undefined, fetching: false, error: { graphQLErrors: [] }, }; renderGuard(); expect(screen.getByText("Can't reach the server")).toBeInTheDocument(); expect(screen.queryByText("Account deactivated")).not.toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Retry" })); expect(h.refetch).toHaveBeenCalledWith({ requestPolicy: "network-only" }); }); it("redirects to login on an auth error", () => { h.state = { data: undefined, fetching: false, error: { graphQLErrors: [{ extensions: { code: "UNAUTHORIZED" } }] }, }; renderGuard(); expect(h.login).toHaveBeenCalledTimes(1); expect(screen.queryByText("app content")).not.toBeInTheDocument(); expect(screen.queryByText("Account deactivated")).not.toBeInTheDocument(); }); it("re-logs in (does not render the app) when an auth error accompanies cached data", () => { // cache-and-network: stale ACTIVE data + a FORBIDDEN revalidation error. // Auth errors must take precedence so we never render with a bad token. h.state = { data: { me: { status: "ACTIVE" } }, fetching: false, error: { graphQLErrors: [{ extensions: { code: "FORBIDDEN" } }] }, }; renderGuard(); expect(h.login).toHaveBeenCalledTimes(1); expect(screen.queryByText("app content")).not.toBeInTheDocument(); }); });