rename web/ to admin/, along with the web-named build and cli identifiers
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { ApiError, deleteGroup, getQueries, getStats, 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 getStats("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 getStats().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] }));
|
||||
});
|
||||
Reference in New Issue
Block a user