diff --git a/apps/api/src/__tests__/mocks/db.ts b/apps/api/src/__tests__/mocks/db.ts new file mode 100644 index 0000000..79127eb --- /dev/null +++ b/apps/api/src/__tests__/mocks/db.ts @@ -0,0 +1,131 @@ +import { vi } from "vitest"; + +export const mockRows: Record = {}; + +export function resetMock() { + Object.keys(mockRows).forEach((key) => { + mockRows[key] = []; + }); +} + +function makeChainable(data: unknown[]): unknown { + const arr = [...data]; + const chain = new Proxy(arr, { + get(target, prop) { + if ( + prop === "where" || + prop === "orderBy" || + prop === "limit" || + prop === "leftJoin" || + prop === "rightJoin" || + prop === "innerJoin" + ) { + return () => chain; + } + return target[prop as keyof typeof target]; + }, + }); + return chain; +} + +function createTableProxy(tableName: string): unknown { + return new Proxy( + { _name: tableName }, + { + get: (target, prop) => + prop === "_name" ? tableName : { table: tableName, column: prop }, + } + ); +} + +const tables = [ + "user", + "session", + "account", + "verification", + "clients", + "pets", + "services", + "staff", + "recurringSeries", + "appointmentGroups", + "appointments", + "invoices", + "invoiceLineItems", + "invoiceTipSplits", + "refunds", + "reminderLogs", + "impersonationSessions", + "impersonationAuditLogs", + "conversations", + "messages", + "messageAttachments", + "messageConsentEvents", + "businessSettings", + "groomingVisitLogs", + "waitlistEntries", + "authProviderConfig", +] as const; + +type TableName = (typeof tables)[number]; + +const tableProxies: Record = {} as Record; + +tables.forEach((table) => { + tableProxies[table] = createTableProxy(table); +}); + +vi.mock("@groombook/db", () => ({ + getDb: () => ({ + select: () => ({ + from: (table: { _name: string }) => { + const tableName = table._name as TableName; + const rows = mockRows[tableName] || []; + return makeChainable(rows); + }, + }), + insert: () => ({ + values: (vals: Record) => ({ + returning: () => [{ ...vals, id: "mock-id" }], + }), + }), + update: () => ({ + set: (vals: Record) => ({ + where: () => ({ + returning: () => [{ ...vals, id: "mock-id" }], + }), + }), + }), + delete: () => ({ + where: () => ({ + returning: () => [{ id: "mock-id" }], + }), + }), + }), + ...tableProxies, + eq: vi.fn(), + and: vi.fn(), + or: vi.fn(), + ne: vi.fn(), + gt: vi.fn(), + gte: vi.fn(), + lt: vi.fn(), + lte: vi.fn(), + inArray: vi.fn(), + isNull: vi.fn(), + ilike: vi.fn(), + sql: vi.fn(), + exists: vi.fn(), + desc: vi.fn(), + asc: vi.fn(), + encryptSecret: vi.fn(), + decryptSecret: vi.fn(), + appointmentStatusEnum: ["scheduled", "confirmed", "in_progress", "completed", "cancelled", "no_show"], + staffRoleEnum: ["groomer", "receptionist", "manager"], + invoiceStatusEnum: ["draft", "pending", "paid", "void"], + paymentMethodEnum: ["cash", "card", "check", "other"], + clientStatusEnum: ["active", "disabled"], + messagingChannelEnum: ["sms", "mms"], + messageDirectionEnum: ["inbound", "outbound"], + messageStatusEnum: ["queued", "sent", "delivered", "failed"], +})); diff --git a/apps/api/src/__tests__/waitlist.test.ts b/apps/api/src/__tests__/waitlist.test.ts index 383bc80..b9b3421 100644 --- a/apps/api/src/__tests__/waitlist.test.ts +++ b/apps/api/src/__tests__/waitlist.test.ts @@ -41,12 +41,14 @@ let selectRows: Record[] = []; let selectSessionRow: Record | null = null; let insertedValues: Record[] = []; let updatedValues: Record[] = []; +let insertedAuditLogs: Record[] = []; function resetMock() { selectRows = []; selectSessionRow = null; insertedValues = []; updatedValues = []; + insertedAuditLogs = []; } vi.mock("@groombook/db", () => { @@ -94,6 +96,11 @@ vi.mock("@groombook/db", () => { { get: (t, p) => (p === "_name" ? "appointments" : { table: "appointments", column: p }) } ); + const impersonationAuditLogs = new Proxy( + { _name: "impersonationAuditLogs" }, + { get: (t, p) => (p === "_name" ? "impersonationAuditLogs" : { table: "impersonationAuditLogs", column: p }) } + ); + return { getDb: () => ({ select: () => ({ @@ -109,9 +116,18 @@ vi.mock("@groombook/db", () => { }), insert: () => ({ values: (vals: Record) => { - insertedValues.push(vals); + // Only count waitlist entry inserts, not audit log inserts from portalAudit middleware + if (vals.petId || vals.serviceId || vals.status !== undefined) { + insertedValues.push(vals); + } return { - returning: () => [{ ...WAITLIST_ENTRY, ...vals, id: "waitlist-uuid-new" }], + returning: () => { + if (vals.sessionId && !vals.petId) { + insertedAuditLogs.push(vals); + return [{ ...vals, id: "audit-log-uuid", createdAt: new Date() }]; + } + return [{ ...WAITLIST_ENTRY, ...vals, id: "waitlist-uuid-new" }]; + }, }; }, }), @@ -139,6 +155,7 @@ vi.mock("@groombook/db", () => { }), waitlistEntries, impersonationSessions, + impersonationAuditLogs, clients, pets, services,