/** * Regression tests for `src/actions/time-tracking/route-summary.ts` → * `getFleetRouteSummary`. * * Why this exists: the original implementation referenced * `fw.display_name` and `fw.pin` in its SQL, but the unified * `field_workers` table (cycle 10) uses `name` and `pin_hash`. The * wrong-column reference crashed the `/admin/time-tracking` overview * page on render with `column "fw.display_name" does not exist`. No * test caught it because nothing exercised this code path. * * These tests mock the bare minimum (admin user + pg pool) and assert: * 1. The SQL passed to `pool.query` references the correct columns * (`fw.name`, `fw.pin_hash`) and NOT the legacy names. * 2. The row mapper projects `r.name` / `r.pin_hash` (not the * legacy keys). * 3. Empty result set yields `[]` without crashing. * 4. Boundary: an unknown date still produces a valid SQL query * and a typed return shape. */ import { describe, it, expect, vi, beforeEach } from "vitest"; const { mockGetAdminUser, mockQuery } = vi.hoisted(() => { return { mockGetAdminUser: vi.fn(), mockQuery: vi.fn(), }; }); vi.mock("server-only", () => ({})); vi.mock("@/lib/admin-permissions", () => ({ getAdminUser: mockGetAdminUser, })); vi.mock("@/lib/db", () => ({ getPool: () => ({ query: mockQuery }), })); // Stub the withBrand call inside `getRouteTimeSummary` so importing the // module doesn't try to resolve drizzle at test time. vi.mock("@/db/client", () => ({ withBrand: vi.fn(async (_brandId: string, fn: (db: unknown) => unknown) => fn({ select: () => ({ from: () => ({ where: () => ({ limit: () => [] }) }) }) }), ), })); // Stub the cross-module call into the admin time-tracking settings. vi.mock("@/actions/time-tracking", () => ({ getTimeTrackingSettings: vi.fn(async () => null), })); // Imported AFTER all mocks are wired. import { getFleetRouteSummary } from "@/actions/time-tracking/route-summary"; const BRAND_ID = "11111111-2222-3333-4444-555555555555"; const TODAY = "2026-07-06"; beforeEach(() => { mockGetAdminUser.mockReset(); mockQuery.mockReset(); mockGetAdminUser.mockResolvedValue({ id: "admin-1", role: "platform_admin" }); }); describe("getFleetRouteSummary — SQL column contract", () => { it("references fw.name and fw.pin_hash (NOT the legacy fw.display_name / fw.pin)", async () => { mockQuery.mockResolvedValueOnce({ rows: [] }); await getFleetRouteSummary(BRAND_ID, TODAY); expect(mockQuery).toHaveBeenCalledTimes(1); const [sql] = mockQuery.mock.calls[0]; expect(sql).toContain("fw.name"); expect(sql).toContain("fw.pin_hash"); // The legacy columns must NOT appear anywhere in the query — // catching them in the SELECT, JOIN, or ORDER BY is the whole // point of this test. expect(sql).not.toMatch(/fw\.display_name/); expect(sql).not.toMatch(/\bfw\.pin\b/); }); it("queries with the brand id and ISO date bounds (regression: off-by-one date math)", async () => { mockQuery.mockResolvedValueOnce({ rows: [] }); await getFleetRouteSummary(BRAND_ID, TODAY); const [, params] = mockQuery.mock.calls[0]; expect(params).toHaveLength(3); expect(params[0]).toBe(BRAND_ID); // Lower bound is the requested date at UTC midnight; upper bound // is the NEXT day at UTC midnight (24-hour window, half-open). expect(params[1]).toBe(`${TODAY}T00:00:00.000Z`); const upper = new Date(params[2] as string); const lower = new Date(params[1] as string); expect(upper.getTime() - lower.getTime()).toBe(24 * 60 * 60 * 1000); }); }); describe("getFleetRouteSummary — row projection", () => { it("maps the row's `name` field onto workerName and falls back when name is null", async () => { mockQuery.mockResolvedValueOnce({ rows: [ { id: "worker-1", name: "Tyler", pin_hash: "scrypt$abc123$xyz", active: true, water_entries: "3", water_gallons: "120", worked_minutes: "240", is_clocked_in: true, }, ], }); const rows = await getFleetRouteSummary(BRAND_ID, TODAY); expect(rows).toEqual([ { workerId: "worker-1", workerName: "Tyler", active: true, water: { entryCount: 3, totalGallons: 120 }, time: { workedMinutesToday: 240, isClockedIn: true }, }, ]); }); it("falls back to a 6-char pin_hash fingerprint when name is null (not the legacy pin column)", async () => { mockQuery.mockResolvedValueOnce({ rows: [ { id: "worker-2", name: null, pin_hash: "scrypt$ABCDEF$rest", active: false, water_entries: "0", water_gallons: null, worked_minutes: "0", is_clocked_in: false, }, ], }); const rows = await getFleetRouteSummary(BRAND_ID, TODAY); expect(rows).toHaveLength(1); expect(rows[0].workerName).toBe("scrypt"); expect(rows[0].active).toBe(false); expect(rows[0].water.totalGallons).toBe(0); expect(rows[0].time.workedMinutesToday).toBe(0); expect(rows[0].time.isClockedIn).toBe(false); }); }); describe("getFleetRouteSummary — boundaries + failure modes", () => { it("returns [] when there are no active workers in the brand", async () => { mockQuery.mockResolvedValueOnce({ rows: [] }); const rows = await getFleetRouteSummary(BRAND_ID, TODAY); expect(rows).toEqual([]); }); it("returns [] when the caller is unauthenticated (admin gate)", async () => { mockGetAdminUser.mockResolvedValueOnce(null); const rows = await getFleetRouteSummary(BRAND_ID, TODAY); expect(rows).toEqual([]); // Critical: we must not have hit the pool if auth failed. expect(mockQuery).not.toHaveBeenCalled(); }); it("defaults to today's UTC date when no date argument is supplied", async () => { mockQuery.mockResolvedValueOnce({ rows: [] }); await getFleetRouteSummary(BRAND_ID); expect(mockQuery).toHaveBeenCalledTimes(1); const [, params] = mockQuery.mock.calls[0]; // The bounds must form a one-day window; lower < upper, and // lower must be UTC midnight on today's date. const lower = new Date(params[1] as string); const upper = new Date(params[2] as string); expect(lower.getUTCHours()).toBe(0); expect(upper.getTime() - lower.getTime()).toBe(24 * 60 * 60 * 1000); }); it("coerces null numeric aggregates to 0 (water_gallons / worked_minutes can be null)", async () => { mockQuery.mockResolvedValueOnce({ rows: [ { id: "worker-3", name: "Pat", pin_hash: "scrypt$XXX", active: true, water_entries: "0", water_gallons: null, worked_minutes: null, is_clocked_in: false, }, ], }); const rows = await getFleetRouteSummary(BRAND_ID, TODAY); expect(rows[0].water.totalGallons).toBe(0); expect(rows[0].water.entryCount).toBe(0); expect(rows[0].time.workedMinutesToday).toBe(0); }); });