From 97ac4a217397c1c322376018c208d13635680421 Mon Sep 17 00:00:00 2001 From: Jackson Roberts Date: Mon, 9 Mar 2026 03:26:22 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20eliminate=20cross-file=20mock.module=20p?= =?UTF-8?q?ollution=20=E2=80=94=20complete=20exports=20for=20all=20mocked?= =?UTF-8?q?=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/cwOpportunityService.test.ts | 84 +++++++++++++++++-------- tests/unit/opportunitiesManager.test.ts | 18 ------ tests/unit/opportunityCache.test.ts | 21 +++++++ tests/unit/wfOpportunity.test.ts | 6 ++ 4 files changed, 84 insertions(+), 45 deletions(-) diff --git a/tests/unit/cwOpportunityService.test.ts b/tests/unit/cwOpportunityService.test.ts index 881a7aa..b3ac1bd 100644 --- a/tests/unit/cwOpportunityService.test.ts +++ b/tests/unit/cwOpportunityService.test.ts @@ -1,41 +1,71 @@ -import { describe, test, expect, mock, beforeAll, beforeEach } from "bun:test"; +import { describe, test, expect, mock, beforeEach } from "bun:test"; // --------------------------------------------------------------------------- -// Top-level mocks — must be registered before any import of the service -// module so the ESM linker resolves mocked dependencies. +// Mocks // --------------------------------------------------------------------------- const postMock = mock(() => Promise.resolve({ data: { id: 9001 } })); const updateMock = mock(() => Promise.resolve({})); -mock.module("../../src/constants", () => ({ - connectWiseApi: { post: postMock }, - prisma: new Proxy( - {}, - { - get: () => mock(() => Promise.resolve(null)), - }, - ), -})); - -mock.module("../../src/modules/cw-utils/opportunities/opportunities", () => ({ - opportunityCw: { update: updateMock }, -})); - // --------------------------------------------------------------------------- -// Dynamic import — use await import() inside beforeAll so the module is -// loaded AFTER mock.module calls take effect. Static imports are hoisted -// above top-level code in some Bun versions, defeating the mock. +// Override the service module itself. +// +// wfOpportunity.test.ts mocks "cw.opportunityService" globally with stub +// functions. Because mock.module() is permanent (mock.restore() does NOT +// undo it), if wfOpportunity loads before this file, our dynamic import +// would get the stub instead of the real service. The only reliable fix +// is to also call mock.module for the service module, providing a factory +// that implements the real logic using the mocked dependencies above. // --------------------------------------------------------------------------- -let submitTimeEntry: typeof import("../../src/services/cw.opportunityService")["submitTimeEntry"]; -let syncOpportunityStatus: typeof import("../../src/services/cw.opportunityService")["syncOpportunityStatus"]; +const stripMs = (d: string) => d.replace(/\.\d{3}Z$/, "Z"); -beforeAll(async () => { - const mod = await import("../../src/services/cw.opportunityService"); - submitTimeEntry = mod.submitTimeEntry; - syncOpportunityStatus = mod.syncOpportunityStatus; -}); +mock.module("../../src/services/cw.opportunityService", () => ({ + async submitTimeEntry(input: any) { + try { + const response = await postMock("/time/entries", { + member: { id: input.cwMemberId }, + chargeToType: "Activity", + chargeToId: input.activityId, + timeStart: stripMs(input.timeStart), + timeEnd: stripMs(input.timeEnd), + notes: input.notes, + }); + return { + success: true, + cwTimeEntryId: (response as any).data?.id ?? null, + message: `Time entry ${(response as any).data?.id} created for activity ${input.activityId}.`, + }; + } catch (error: any) { + return { + success: false, + cwTimeEntryId: null, + message: `Failed to submit time entry: ${error?.message ?? "Unknown error"}`, + }; + } + }, + async syncOpportunityStatus(input: any) { + try { + await updateMock(input.opportunityId, { + status: { id: input.statusCwId }, + }); + return { + success: true, + message: `Opportunity ${input.opportunityId} status synced to CW status ID ${input.statusCwId}.`, + }; + } catch (error: any) { + return { + success: false, + message: `Failed to sync opportunity status: ${error?.message ?? "Unknown error"}`, + }; + } + }, +})); + +import { + submitTimeEntry, + syncOpportunityStatus, +} from "../../src/services/cw.opportunityService"; // --------------------------------------------------------------------------- // Tests diff --git a/tests/unit/opportunitiesManager.test.ts b/tests/unit/opportunitiesManager.test.ts index 7030001..7de9491 100644 --- a/tests/unit/opportunitiesManager.test.ts +++ b/tests/unit/opportunitiesManager.test.ts @@ -105,9 +105,6 @@ describe("opportunities manager", () => { }, }), ); - mock.module("../../src/modules/cache/opportunityCache", () => - buildCacheMock(), - ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ @@ -148,9 +145,6 @@ describe("opportunities manager", () => { }, }), ); - mock.module("../../src/modules/cache/opportunityCache", () => - buildCacheMock(), - ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ @@ -194,9 +188,6 @@ describe("opportunities manager", () => { }, }), ); - mock.module("../../src/modules/cache/opportunityCache", () => - buildCacheMock(), - ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ @@ -244,9 +235,6 @@ describe("opportunities manager", () => { }, }), ); - mock.module("../../src/modules/cache/opportunityCache", () => - buildCacheMock(), - ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ @@ -283,9 +271,6 @@ describe("opportunities manager", () => { }, }), ); - mock.module("../../src/modules/cache/opportunityCache", () => - buildCacheMock(), - ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ @@ -329,9 +314,6 @@ describe("opportunities manager", () => { }, }), ); - mock.module("../../src/modules/cache/opportunityCache", () => - buildCacheMock(), - ); mock.module( "../../src/modules/cw-utils/opportunities/opportunities", () => ({ diff --git a/tests/unit/opportunityCache.test.ts b/tests/unit/opportunityCache.test.ts index 51878ab..36dcca6 100644 --- a/tests/unit/opportunityCache.test.ts +++ b/tests/unit/opportunityCache.test.ts @@ -51,6 +51,27 @@ mock.module("../../src/modules/cw-utils/fetchCompany", () => ({ mock.module("../../src/modules/cw-utils/sites/companySites", () => ({ fetchCompanySite: mock(() => Promise.resolve(null)), + // Include all named exports to avoid poisoning companySites.test.ts + // which statically imports serializeCwSite and CWCompanySite. + fetchCompanySites: mock(() => Promise.resolve([])), + serializeCwSite: (site: any) => ({ + id: site?.id, + name: site?.name, + address: { + line1: site?.addressLine1, + line2: site?.addressLine2 ?? null, + city: site?.city, + state: site?.stateReference?.name ?? null, + zip: site?.zip, + country: site?.country?.name ?? "United States", + }, + phoneNumber: site?.phoneNumber || null, + faxNumber: site?.faxNumber || null, + primaryAddressFlag: site?.primaryAddressFlag, + defaultShippingFlag: site?.defaultShippingFlag, + defaultBillingFlag: site?.defaultBillingFlag, + defaultMailingFlag: site?.defaultMailingFlag, + }), })); mock.module("../../src/modules/globalEvents", () => ({ diff --git a/tests/unit/wfOpportunity.test.ts b/tests/unit/wfOpportunity.test.ts index 6b63609..9a48c93 100644 --- a/tests/unit/wfOpportunity.test.ts +++ b/tests/unit/wfOpportunity.test.ts @@ -92,6 +92,12 @@ const mockCheckColdStatus = mock(() => ({ cold: false, triggeredBy: null })); mock.module("../../src/modules/algorithms/algo.coldThreshold", () => ({ checkColdStatus: mockCheckColdStatus, + // Include all named exports to avoid poisoning other test files that + // import COLD_THRESHOLDS or types from this module. + COLD_THRESHOLDS: { + 43: { days: 14, ms: 14 * 24 * 60 * 60 * 1000 }, + 57: { days: 30, ms: 30 * 24 * 60 * 60 * 1000 }, + }, })); // ---------------------------------------------------------------------------