90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
import { act } from "react";
|
|
import { renderHook, waitFor } from "@testing-library/react";
|
|
import type { ReactNode } from "react";
|
|
import { AuthProvider, resetAuthProbeForTests, useAuth } from "@/auth/store";
|
|
|
|
const STORAGE_KEY = "nxdns_auth_required";
|
|
|
|
function wrapper({ children }: { children: ReactNode }) {
|
|
return <AuthProvider>{children}</AuthProvider>;
|
|
}
|
|
|
|
function jsonResponse(payload: unknown, status = 200, headers: Record<string, string> = {}): Response {
|
|
return new Response(JSON.stringify(payload), {
|
|
status,
|
|
headers: { "content-type": "application/json", ...headers },
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
sessionStorage.clear();
|
|
resetAuthProbeForTests();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
test("mounting with nothing stored probes and settles authRequired true on 401", async () => {
|
|
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
if (String(input) === "/api/auth/login") return jsonResponse({ error: "password required" }, 401);
|
|
return jsonResponse({ error: "not stubbed" }, 404);
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const { result } = renderHook(() => useAuth(), { wrapper });
|
|
expect(result.current.authRequired).toBeNull();
|
|
|
|
await waitFor(() => expect(result.current.authRequired).toBe(true));
|
|
expect(sessionStorage.getItem(STORAGE_KEY)).toBe("true");
|
|
});
|
|
|
|
test("mounting with nothing stored probes and settles authRequired false when auth is off", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => jsonResponse({ authenticated: true, auth_required: false })),
|
|
);
|
|
|
|
const { result } = renderHook(() => useAuth(), { wrapper });
|
|
await waitFor(() => expect(result.current.authRequired).toBe(false));
|
|
expect(sessionStorage.getItem(STORAGE_KEY)).toBe("false");
|
|
});
|
|
|
|
test("a stored value is the fast path: no probe fires on mount", async () => {
|
|
sessionStorage.setItem(STORAGE_KEY, "true");
|
|
const fetchMock = vi.fn();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const { result } = renderHook(() => useAuth(), { wrapper });
|
|
expect(result.current.authRequired).toBe(true);
|
|
|
|
await act(async () => {});
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("logout swallows a 401 from an already-dead session", async () => {
|
|
sessionStorage.setItem(STORAGE_KEY, "true");
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => jsonResponse({ error: "unauthorized" }, 401)),
|
|
);
|
|
|
|
const { result } = renderHook(() => useAuth(), { wrapper });
|
|
await expect(result.current.logout()).resolves.toBeUndefined();
|
|
});
|
|
|
|
test("logout rethrows non-401 errors such as 429", async () => {
|
|
sessionStorage.setItem(STORAGE_KEY, "true");
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => jsonResponse({ error: "rate limited" }, 429, { "retry-after": "7" })),
|
|
);
|
|
|
|
const { result } = renderHook(() => useAuth(), { wrapper });
|
|
await expect(result.current.logout()).rejects.toMatchObject({
|
|
name: "ApiError",
|
|
status: 429,
|
|
retryAfter: 7,
|
|
});
|
|
});
|