import { describe, it, expect, vi, beforeEach } from "vitest"; import { Hono } from "hono"; // ─── Mocks ────────────────────────────────────────────────────────────────── // GRO-2294: GET /api/admin/settings must not return the encrypted // googleMapsApiKey ciphertext, on either the existing-row or auto-create branch. let selectRows: Record[] = []; let insertReturning: Record[] = []; let updateReturning: Record[] = []; function makeChainable(data: unknown[]): unknown { const arr = [...data]; const chain = new Proxy(arr, { get(target, prop) { if (prop === "where" || prop === "orderBy" || prop === "limit") { return () => chain; } // @ts-expect-error proxy passthrough return target[prop]; }, }); return chain; } vi.mock("@groombook/db", () => { const businessSettings = new Proxy( { _name: "business_settings" }, { get: (_t, p) => (p === "_name" ? "business_settings" : { column: p }) } ); return { getDb: () => ({ select: () => ({ from: () => makeChainable(selectRows) }), insert: () => ({ values: () => ({ returning: () => insertReturning }), }), update: () => ({ set: () => ({ where: () => ({ returning: () => updateReturning }) }), }), }), businessSettings, eq: vi.fn(), }; }); vi.mock("../lib/s3.js", () => ({ getPresignedUploadUrl: vi.fn(), deleteObject: vi.fn(), putObject: vi.fn(), getObject: vi.fn(), })); const { settingsRouter } = await import("../routes/settings.js"); const app = new Hono(); app.route("/settings", settingsRouter); // PATCH /settings is guarded by requireSuperUser(), which reads the staff record // from context. Inject a super-user staff row so the handler runs. const patchApp = new Hono<{ Variables: { staff: { id: string; isSuperUser: boolean } }; }>(); patchApp.use("*", async (c, next) => { c.set("staff", { id: "staff-1", isSuperUser: true }); await next(); }); patchApp.route("/settings", settingsRouter); const FULL_ROW = { id: "settings-uuid-1", businessName: "GroomBook", primaryColor: "#4f8a6f", accentColor: "#8b7355", routeOptimizationProvider: "google", googleMapsApiKey: "ENCRYPTED::super-secret-ciphertext", createdAt: new Date(), updatedAt: new Date(), }; describe("GET /settings — googleMapsApiKey redaction (GRO-2294)", () => { beforeEach(() => { selectRows = []; insertReturning = []; }); it("omits googleMapsApiKey from an existing settings row", async () => { selectRows = [{ ...FULL_ROW }]; const res = await app.request("/settings", { method: "GET" }); expect(res.status).toBe(200); const body = (await res.json()) as Record; expect(body).not.toHaveProperty("googleMapsApiKey"); // Non-secret fields are still returned. expect(body.businessName).toBe("GroomBook"); expect(body.routeOptimizationProvider).toBe("google"); }); it("omits googleMapsApiKey from the auto-create branch", async () => { selectRows = []; insertReturning = [{ ...FULL_ROW, id: "settings-uuid-new" }]; const res = await app.request("/settings", { method: "GET" }); expect(res.status).toBe(200); const body = (await res.json()) as Record; expect(body).not.toHaveProperty("googleMapsApiKey"); expect(body.id).toBe("settings-uuid-new"); }); }); describe("PATCH /settings — googleMapsApiKey redaction (GRO-2299)", () => { beforeEach(() => { selectRows = []; insertReturning = []; updateReturning = []; }); function patchRequest(body: Record) { return patchApp.request("/settings", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); } it("omits googleMapsApiKey from the PATCH response", async () => { selectRows = [{ ...FULL_ROW }]; updateReturning = [{ ...FULL_ROW, businessName: "Updated Name" }]; const res = await patchRequest({ businessName: "Updated Name" }); expect(res.status).toBe(200); const body = (await res.json()) as Record; expect(body).not.toHaveProperty("googleMapsApiKey"); // Non-secret updated fields are still returned. expect(body.businessName).toBe("Updated Name"); expect(body.routeOptimizationProvider).toBe("google"); }); it("omits googleMapsApiKey on the auto-create-then-update branch", async () => { selectRows = []; insertReturning = [{ ...FULL_ROW, id: "settings-uuid-new" }]; updateReturning = [{ ...FULL_ROW, id: "settings-uuid-new" }]; const res = await patchRequest({ primaryColor: "#123456" }); expect(res.status).toBe(200); const body = (await res.json()) as Record; expect(body).not.toHaveProperty("googleMapsApiKey"); expect(body.id).toBe("settings-uuid-new"); }); });