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
97 lines
4.0 KiB
TypeScript
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] }));
|
|
});
|