fix: eliminate cross-file mock.module pollution — complete exports for all mocked modules

This commit is contained in:
2026-03-09 03:26:22 -05:00
parent ad7507d133
commit 97ac4a2173
4 changed files with 84 additions and 45 deletions
+57 -27
View File
@@ -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