import { describe, test, expect, mock, beforeEach } from "bun:test"; import { buildMockOpportunity, buildMockCompany, buildMockConstants, } from "../setup"; // --------------------------------------------------------------------------- // Stable mock factory // --------------------------------------------------------------------------- function createStablePrismaMock( overrides: Record> = {}, ) { return new Proxy( {}, { get(_target, model: string) { if (model === "$connect" || model === "$disconnect") return mock(() => Promise.resolve()); if (overrides[model]) return overrides[model]; return new Proxy({}, { get: () => mock(() => Promise.resolve(null)) }); }, }, ); } /** Build a complete cache mock — any unspecified export returns a mock fn. */ function buildCacheMock(overrides: Record = {}) { return new Proxy(overrides, { get(target, prop: string) { if (prop in target) return target[prop]; // Key helpers return strings; everything else returns a mock fn if (prop.endsWith("CacheKey") || prop.endsWith("DataCacheKey")) return mock((...args: any[]) => `mock:${prop}:${args.join(":")}`); return mock(() => Promise.resolve(null)); }, }); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("opportunities manager", () => { beforeEach(() => { mock.restore(); }); // ------------------------------------------------------------------- // fetchRecord (lightweight) // ------------------------------------------------------------------- describe("fetchRecord()", () => { test("returns OpportunityController by internal ID", async () => { const oppData = { ...buildMockOpportunity(), company: buildMockCompany(), }; mock.module("../../src/constants", () => buildMockConstants({ prisma: createStablePrismaMock({ opportunity: { findFirst: mock(() => Promise.resolve(oppData)), }, }), redis: { get: mock(() => Promise.resolve(null)), set: mock(() => Promise.resolve("OK")), del: mock(() => Promise.resolve(1)), }, connectWiseApi: { get: mock(() => Promise.resolve({ data: {} })), }, }), ); mock.module("../../src/modules/cache/opportunityCache", () => buildCacheMock(), ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ opportunityCw: { fetch: mock(() => Promise.resolve(null)), create: mock(() => Promise.resolve({})), delete: mock(() => Promise.resolve()), }, }), ); mock.module("../../src/modules/cw-utils/activities/activities", () => ({ activityCw: { fetchByOpportunityDirect: mock(() => Promise.resolve([])), }, })); const { opportunities } = await import("../../src/managers/opportunities"); const result = await opportunities.fetchRecord("opp-1"); expect(result).toBeDefined(); }); test("throws 404 when opportunity not found", async () => { mock.module("../../src/constants", () => buildMockConstants({ prisma: createStablePrismaMock({ opportunity: { findFirst: mock(() => Promise.resolve(null)), }, }), redis: { get: mock(() => Promise.resolve(null)), set: mock(() => Promise.resolve("OK")), del: mock(() => Promise.resolve(1)), }, connectWiseApi: { get: mock(() => Promise.resolve({ data: {} })), }, }), ); mock.module("../../src/modules/cache/opportunityCache", () => buildCacheMock(), ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ opportunityCw: { fetch: mock(() => Promise.resolve(null)), }, }), ); mock.module("../../src/modules/cw-utils/activities/activities", () => ({ activityCw: { fetchByOpportunityDirect: mock(() => Promise.resolve([])), }, })); const { opportunities } = await import("../../src/managers/opportunities"); try { await opportunities.fetchRecord("nonexistent"); expect(true).toBe(false); } catch (e: any) { expect(e.name).toBe("OpportunityNotFound"); expect(e.status).toBe(404); } }); test("uses numeric identifier as cwOpportunityId", async () => { const oppData = { ...buildMockOpportunity(), company: null }; const findFirst = mock(() => Promise.resolve(oppData)); mock.module("../../src/constants", () => buildMockConstants({ prisma: createStablePrismaMock({ opportunity: { findFirst }, }), redis: { get: mock(() => Promise.resolve(null)), set: mock(() => Promise.resolve("OK")), del: mock(() => Promise.resolve(1)), }, connectWiseApi: { get: mock(() => Promise.resolve({ data: {} })), }, }), ); mock.module("../../src/modules/cache/opportunityCache", () => buildCacheMock(), ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ opportunityCw: { fetch: mock(() => Promise.resolve(null)), }, }), ); mock.module("../../src/modules/cw-utils/activities/activities", () => ({ activityCw: { fetchByOpportunityDirect: mock(() => Promise.resolve([])), }, })); const { opportunities } = await import("../../src/managers/opportunities"); await opportunities.fetchRecord(1001); const where = findFirst.mock.calls[0]?.[0]?.where; expect(where).toHaveProperty("cwOpportunityId", 1001); }); }); // ------------------------------------------------------------------- // count // ------------------------------------------------------------------- describe("count()", () => { test("returns total count", async () => { const countMock = mock(() => Promise.resolve(15)); mock.module("../../src/constants", () => buildMockConstants({ prisma: createStablePrismaMock({ opportunity: { countMock, count: countMock, findFirst: mock(() => Promise.resolve(null)), }, }), redis: { get: mock(() => Promise.resolve(null)), set: mock(() => Promise.resolve("OK")), del: mock(() => Promise.resolve(1)), }, connectWiseApi: { get: mock(() => Promise.resolve({ data: {} })), }, }), ); mock.module("../../src/modules/cache/opportunityCache", () => buildCacheMock(), ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ opportunityCw: {}, }), ); mock.module("../../src/modules/cw-utils/activities/activities", () => ({ activityCw: {}, })); const { opportunities } = await import("../../src/managers/opportunities"); const result = await opportunities.count(); expect(result).toBe(15); }); test("counts only open when openOnly is true", async () => { const countMock = mock(() => Promise.resolve(8)); mock.module("../../src/constants", () => buildMockConstants({ prisma: createStablePrismaMock({ opportunity: { count: countMock, findFirst: mock(() => Promise.resolve(null)), }, }), redis: { get: mock(() => Promise.resolve(null)), set: mock(() => Promise.resolve("OK")), del: mock(() => Promise.resolve(1)), }, connectWiseApi: { get: mock(() => Promise.resolve({ data: {} })), }, }), ); mock.module("../../src/modules/cache/opportunityCache", () => buildCacheMock(), ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ opportunityCw: {}, }), ); mock.module("../../src/modules/cw-utils/activities/activities", () => ({ activityCw: {}, })); const { opportunities } = await import("../../src/managers/opportunities"); const result = await opportunities.count({ openOnly: true }); expect(result).toBe(8); }); }); // ------------------------------------------------------------------- // fetchPages // ------------------------------------------------------------------- describe("fetchPages()", () => { test("returns paginated opportunity controllers", async () => { const items = [ { ...buildMockOpportunity(), company: buildMockCompany() }, ]; mock.module("../../src/constants", () => buildMockConstants({ prisma: createStablePrismaMock({ opportunity: { findMany: mock(() => Promise.resolve(items)), findFirst: mock(() => Promise.resolve(null)), }, }), redis: { get: mock(() => Promise.resolve(null)), set: mock(() => Promise.resolve("OK")), del: mock(() => Promise.resolve(1)), }, connectWiseApi: { get: mock(() => Promise.resolve({ data: {} })), }, }), ); mock.module("../../src/modules/cache/opportunityCache", () => buildCacheMock(), ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ opportunityCw: {}, }), ); mock.module("../../src/modules/cw-utils/activities/activities", () => ({ activityCw: { fetchByOpportunityDirect: mock(() => Promise.resolve([])), }, })); const { opportunities } = await import("../../src/managers/opportunities"); const result = await opportunities.fetchPages(1, 10); expect(result).toBeArrayOfSize(1); }); }); });