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", party_size: 3 }, revision: 2, })); vi.stubGlobal("fetch", fetchMock); const response = await api.createTicket("project-1", { phone: "13800138000", party_size: 3 }, "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"); expect(response.ticket.party_size).toBe(3); }); 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, "PEOPLE", 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, mode: "PEOPLE", 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", party_size: 2 })).rejects.toMatchObject({ code: "DUPLICATE_PHONE", status: 409, details: { existing_ticket: "00001" }, }); }); }); describe("public visitor lookup", () => { it("posts the phone number and normalizes returned ticket numbers", async () => { const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ tickets: [{ display_number: "00012", status: "WAITING" }], })); vi.stubGlobal("fetch", fetchMock); const response = await api.publicStatusByPhone("13800138000"); const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe("/api/public/status/search"); expect(init.method).toBe("POST"); expect(JSON.parse(String(init.body))).toEqual({ phone: "13800138000" }); expect(response.tickets[0].ticket_number).toBe("00012"); }); it("loads public projects and creates a ticket through the public endpoint", async () => { const fetchMock = vi.fn() .mockResolvedValueOnce(jsonResponse({ projects: [{ id: "project-1", name: "云岭漂流", status: "RUNNING", min_party_size: 1, max_party_size: 8 }] })) .mockResolvedValueOnce(jsonResponse({ ticket: { display_number: "00013", status: "WAITING", phone_last4: "8000", party_size: 4 }, public_token: "visitor-token", public_url: "/visitor/visitor-token", revision: 3, })); vi.stubGlobal("fetch", fetchMock); await expect(api.publicProjects()).resolves.toEqual({ projects: [{ id: "project-1", name: "云岭漂流", status: "RUNNING", min_party_size: 1, max_party_size: 8 }], }); const response = await api.publicCreateTicket("project-1", { phone: "13800138000", party_size: 4 }, "visitor-ticket-key"); const [url, init] = fetchMock.mock.calls[1] as [string, RequestInit]; expect(url).toBe("/api/public/projects/project-1/tickets"); expect(init.method).toBe("POST"); expect((init.headers as Headers).get("Idempotency-Key")).toBe("visitor-ticket-key"); expect(response.ticket.ticket_number).toBe("00013"); expect(response.ticket.party_size).toBe(4); expect(response.public_token).toBe("visitor-token"); }); }); 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", ]); }); });