import { describe, it, expect, vi, beforeEach } from "vitest"; import { Hono } from "hono"; // ─── Mocks ────────────────────────────────────────────────────────────────── // GRO-2294: the POST /clients/geocode-batch handler must clamp ?limit to the // documented maximum (500) before invoking the geocoding service. We mock the // service to capture the exact limit the route forwards. const geocodeUngeocodedClients = vi.fn(async () => ({ totalRemaining: 0, processed: 0, geocoded: 0, failed: 0, remaining: 0, })); vi.mock("../services/clientGeocoding.js", () => ({ geocodeUngeocodedClients, geocodeClient: vi.fn(), resolveClientGeocodingProvider: vi.fn(), })); vi.mock("@groombook/db", () => { const tableProxy = (name: string) => new Proxy( { _name: name }, { get: (_t, p) => (p === "_name" ? name : { table: name, column: p }) } ); return { getDb: () => ({}), clients: tableProxy("clients"), appointments: tableProxy("appointments"), and: vi.fn(), eq: vi.fn(), or: vi.fn(), exists: vi.fn(), }; }); const { clientsRouter } = await import("../routes/clients.js"); const app = new Hono(); app.route("/clients", clientsRouter); function postBatch(query: string) { return app.request(`/clients/geocode-batch${query}`, { method: "POST" }); } describe("POST /clients/geocode-batch — ?limit cap (GRO-2294)", () => { beforeEach(() => { geocodeUngeocodedClients.mockClear(); }); it("defaults to 50 when no ?limit is supplied", async () => { const res = await postBatch(""); expect(res.status).toBe(200); expect(geocodeUngeocodedClients).toHaveBeenCalledWith(expect.anything(), 50); }); it("passes through a value within the cap", async () => { const res = await postBatch("?limit=120"); expect(res.status).toBe(200); expect(geocodeUngeocodedClients).toHaveBeenCalledWith(expect.anything(), 120); }); it("clamps an over-cap value to 500", async () => { const res = await postBatch("?limit=100000"); expect(res.status).toBe(200); expect(geocodeUngeocodedClients).toHaveBeenCalledWith(expect.anything(), 500); }); it("floors a fractional value before clamping", async () => { const res = await postBatch("?limit=49.9"); expect(res.status).toBe(200); expect(geocodeUngeocodedClients).toHaveBeenCalledWith(expect.anything(), 49); }); it("rejects a non-positive limit with 400", async () => { const res = await postBatch("?limit=0"); expect(res.status).toBe(400); expect(geocodeUngeocodedClients).not.toHaveBeenCalled(); }); it("rejects a non-numeric limit with 400", async () => { const res = await postBatch("?limit=abc"); expect(res.status).toBe(400); expect(geocodeUngeocodedClients).not.toHaveBeenCalled(); }); });