Files
nxdns/admin/src/lib/api.test.ts
T
mokhtar 6a0630c288
Gates / frontend (push) Successful in 2m6s
Gates / test (push) Successful in 2m57s
Gates / test-aarch64 (push) Successful in 8m31s
Gates / package (push) Successful in 4m19s
Gates / container (push) Failing after 2s
CI / gates (push) Failing after 26m21s
overview: one endpoint, live projections and a response cache (m36)
2026-08-27 17:48:20 +02:00

97 lines
4.0 KiB
TypeScript

import { ApiError, deleteGroup, getOverview, getQueries, listGroups, login, putGroupSources } from "@/lib/api";
function jsonResponse(payload: unknown, status = 200, headers: Record<string, string> = {}): Response {
return new Response(JSON.stringify(payload), {
status,
headers: { "content-type": "application/json", ...headers },
});
}
const fetchMock = vi.fn<typeof fetch>();
beforeEach(() => {
fetchMock.mockReset();
vi.stubGlobal("fetch", fetchMock);
});
afterEach(() => {
vi.unstubAllGlobals();
});
test("sends same-origin credentials and parses JSON", async () => {
fetchMock.mockResolvedValue(jsonResponse({ authenticated: true, auth_required: true }));
const response = await login({ password: "hunter2" });
expect(response.auth_required).toBe(true);
const [url, init] = fetchMock.mock.calls[0]!;
expect(url).toBe("/api/auth/login");
expect(init?.credentials).toBe("same-origin");
expect(init?.method).toBe("POST");
expect(init?.body).toBe(JSON.stringify({ password: "hunter2" }));
const headers = (init?.headers ?? {}) as Record<string, string>;
expect(headers["content-type"]).toBe("application/json");
});
test("throws ApiError with the {error} envelope message", async () => {
fetchMock.mockResolvedValue(jsonResponse({ error: "duplicate group name" }, 409));
const failure = await listGroups().catch((e: unknown) => e);
expect(failure).toBeInstanceOf(ApiError);
expect((failure as ApiError).status).toBe(409);
expect((failure as ApiError).message).toBe("duplicate group name");
expect((failure as ApiError).retryAfter).toBeUndefined();
});
test("falls back to a status message on a non-JSON error body", async () => {
fetchMock.mockResolvedValue(new Response("<html>bad gateway</html>", { status: 502 }));
const failure = await listGroups().catch((e: unknown) => e);
expect(failure).toBeInstanceOf(ApiError);
expect((failure as ApiError).message).toBe("HTTP 502");
});
test("parses Retry-After on 429", async () => {
fetchMock.mockResolvedValue(jsonResponse({ error: "rate limited" }, 429, { "Retry-After": "17" }));
const failure = await getOverview("1h").catch((e: unknown) => e);
expect(failure).toBeInstanceOf(ApiError);
expect((failure as ApiError).status).toBe(429);
expect((failure as ApiError).retryAfter).toBe(17);
});
test("ignores a malformed Retry-After header", async () => {
fetchMock.mockResolvedValue(jsonResponse({ error: "rate limited" }, 429, { "Retry-After": "soon" }));
const failure = await getOverview().catch((e: unknown) => e);
expect((failure as ApiError).retryAfter).toBeUndefined();
});
test("resolves void on 204", async () => {
fetchMock.mockResolvedValue(new Response(null, { status: 204 }));
await expect(deleteGroup(3)).resolves.toBeUndefined();
expect(fetchMock.mock.calls[0]![0]).toBe("/api/groups/3");
expect(fetchMock.mock.calls[0]![1]?.method).toBe("DELETE");
});
test("unwraps list envelopes", async () => {
fetchMock.mockResolvedValue(jsonResponse({ groups: [{ id: 1, name: "default", safe_search: false }] }));
const groups = await listGroups();
expect(groups).toEqual([{ id: 1, name: "default", safe_search: false }]);
});
test("serializes query filters, omitting undefined", async () => {
fetchMock.mockResolvedValue(jsonResponse({ queries: [], next_before: null }));
await getQueries({ limit: 50, blocked: true, domain: "ads.example", before: undefined });
expect(fetchMock.mock.calls[0]![0]).toBe("/api/queries?limit=50&blocked=true&domain=ads.example");
});
test("requests with no filters carry no query string", async () => {
fetchMock.mockResolvedValue(jsonResponse({ queries: [], next_before: null }));
await getQueries();
expect(fetchMock.mock.calls[0]![0]).toBe("/api/queries");
});
test("wraps and unwraps group sources", async () => {
fetchMock.mockResolvedValue(jsonResponse({ source_ids: [2, 5] }));
const stored = await putGroupSources(4, [5, 2]);
expect(stored).toEqual([2, 5]);
expect(fetchMock.mock.calls[0]![0]).toBe("/api/groups/4/sources");
expect(fetchMock.mock.calls[0]![1]?.body).toBe(JSON.stringify({ source_ids: [5, 2] }));
});