Files
XQKqueue/web/src/api.test.ts
2026-07-12 15:53:24 +08:00

84 lines
2.9 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { api } from "./api";
function jsonResponse(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
afterEach(() => {
vi.unstubAllGlobals();
});
describe("mutating queue requests", () => {
it("sends an idempotency key when creating a ticket", async () => {
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({
ticket: { id: "ticket-1", display_number: "00001", status: "WAITING" },
revision: 2,
}));
vi.stubGlobal("fetch", fetchMock);
const response = await api.createTicket("project-1", { phone: "13800138000" }, "ticket-intent-key");
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
const headers = init.headers as Headers;
expect(headers.get("Idempotency-Key")).toBe("ticket-intent-key");
expect(response.ticket.ticket_number).toBe("00001");
});
it("sends the current revision and an idempotency key when calling next", async () => {
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({
batch: { id: "batch-1", batch_number: 3, status: "ACTIVE", tickets: [], called_at: null },
revision: 8,
device_results: [],
}));
vi.stubGlobal("fetch", fetchMock);
await api.callNext("project-1", 7, 3, "call-intent-key");
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
const headers = init.headers as Headers;
expect(headers.get("Idempotency-Key")).toBe("call-intent-key");
expect(JSON.parse(String(init.body))).toEqual({ expected_revision: 7, count: 3 });
});
it("exposes nested API error code and details for explicit duplicate confirmation", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({
error: {
code: "DUPLICATE_PHONE",
message: "该手机号已有活动号码",
details: { existing_ticket: "00001" },
},
}), {
status: 409,
headers: { "Content-Type": "application/json" },
}));
vi.stubGlobal("fetch", fetchMock);
await expect(api.createTicket("project-1", { phone: "13800138000" })).rejects.toMatchObject({
code: "DUPLICATE_PHONE",
status: 409,
details: { existing_ticket: "00001" },
});
});
});
describe("isolated portal authentication", () => {
it("uses different endpoints for staff and admin sessions", async () => {
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ user: { id: "u1", role: "STAFF" }, projects: [] }));
vi.stubGlobal("fetch", fetchMock);
await api.login("staff", "employee", "secret");
await api.me("admin");
await api.logout("staff");
expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([
"/api/staff/auth/login",
"/api/admin/auth/me",
"/api/staff/auth/logout",
]);
});
});