fix: resolve CI test failures — explicit cache mock exports, hoisted service mocks, pinned Bun 1.3.6

This commit is contained in:
2026-03-09 03:03:06 -05:00
parent f53b390e18
commit 15ef24eb3e
4 changed files with 84 additions and 106 deletions
+38 -96
View File
@@ -1,12 +1,45 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
// ---------------------------------------------------------------------------
// Top-level mocks — configured before the module is imported so the ESM
// linker always sees the mocked version, regardless of Bun's module cache.
// ---------------------------------------------------------------------------
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 },
}));
// Import AFTER mocks
import {
submitTimeEntry,
syncOpportunityStatus,
} from "../../src/services/cw.opportunityService";
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("cw.opportunityService", () => {
beforeEach(() => {
mock.restore();
postMock.mockReset();
postMock.mockImplementation(() =>
Promise.resolve({ data: { id: 9001 } }),
);
updateMock.mockReset();
updateMock.mockImplementation(() => Promise.resolve({}));
});
// -------------------------------------------------------------------
@@ -14,27 +47,6 @@ describe("cw.opportunityService", () => {
// -------------------------------------------------------------------
describe("submitTimeEntry()", () => {
test("submits time entry and returns success", async () => {
const postMock = mock(() => Promise.resolve({ data: { id: 9001 } }));
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: mock(() => Promise.resolve({})),
},
}),
);
const { submitTimeEntry } =
await import("../../src/services/cw.opportunityService");
const result = await submitTimeEntry({
activityId: 100,
cwMemberId: 10,
@@ -49,25 +61,6 @@ describe("cw.opportunityService", () => {
});
test("strips milliseconds from ISO timestamps", async () => {
const postMock = mock(() => Promise.resolve({ data: { id: 9001 } }));
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: mock(() => Promise.resolve({})) },
}),
);
const { submitTimeEntry } =
await import("../../src/services/cw.opportunityService");
await submitTimeEntry({
activityId: 100,
cwMemberId: 10,
@@ -82,26 +75,10 @@ describe("cw.opportunityService", () => {
});
test("returns failure on API error", async () => {
mock.module("../../src/constants", () => ({
connectWiseApi: {
post: mock(() => Promise.reject(new Error("CW down"))),
},
prisma: new Proxy(
{},
{
get: () => mock(() => Promise.resolve(null)),
},
),
}));
mock.module(
"../../src/modules/cw-utils/opportunities/opportunities",
() => ({
opportunityCw: { update: mock(() => Promise.resolve({})) },
}),
postMock.mockImplementation(() =>
Promise.reject(new Error("CW down")),
);
const { submitTimeEntry } =
await import("../../src/services/cw.opportunityService");
const result = await submitTimeEntry({
activityId: 100,
cwMemberId: 10,
@@ -121,25 +98,6 @@ describe("cw.opportunityService", () => {
// -------------------------------------------------------------------
describe("syncOpportunityStatus()", () => {
test("syncs status to CW and returns success", async () => {
const updateMock = mock(() => Promise.resolve({}));
mock.module("../../src/constants", () => ({
connectWiseApi: { post: mock(() => Promise.resolve({ data: {} })) },
prisma: new Proxy(
{},
{
get: () => mock(() => Promise.resolve(null)),
},
),
}));
mock.module(
"../../src/modules/cw-utils/opportunities/opportunities",
() => ({
opportunityCw: { update: updateMock },
}),
);
const { syncOpportunityStatus } =
await import("../../src/services/cw.opportunityService");
const result = await syncOpportunityStatus({
opportunityId: 1001,
statusCwId: 24,
@@ -150,26 +108,10 @@ describe("cw.opportunityService", () => {
});
test("returns failure on API error", async () => {
mock.module("../../src/constants", () => ({
connectWiseApi: { post: mock(() => Promise.resolve({ data: {} })) },
prisma: new Proxy(
{},
{
get: () => mock(() => Promise.resolve(null)),
},
),
}));
mock.module(
"../../src/modules/cw-utils/opportunities/opportunities",
() => ({
opportunityCw: {
update: mock(() => Promise.reject(new Error("API fail"))),
},
}),
updateMock.mockImplementation(() =>
Promise.reject(new Error("API fail")),
);
const { syncOpportunityStatus } =
await import("../../src/services/cw.opportunityService");
const result = await syncOpportunityStatus({
opportunityId: 1001,
statusCwId: 24,
+42 -10
View File
@@ -25,17 +25,49 @@ function createStablePrismaMock(
);
}
/** Build a complete cache mock — any unspecified export returns a mock fn. */
/**
* Build a complete cache mock with explicit named exports.
*
* Uses concrete properties instead of a Proxy so that Bun's ESM mock
* resolution can discover every named export at module-link time
* (some Bun versions do not enumerate Proxy keys for static imports).
*/
function buildCacheMock(overrides: Record<string, any> = {}) {
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));
},
});
const keyFn = (...args: any[]) => `mock:key:${args.join(":")}`;
return {
// Key helpers
activityCacheKey: mock(keyFn),
companyCwCacheKey: mock(keyFn),
notesCacheKey: mock(keyFn),
contactsCacheKey: mock(keyFn),
productsCacheKey: mock(keyFn),
siteCacheKey: mock(keyFn),
oppCwDataCacheKey: mock(keyFn),
// Read helpers
getCachedActivities: mock(() => Promise.resolve(null)),
getCachedCompanyCwData: mock(() => Promise.resolve(null)),
getCachedNotes: mock(() => Promise.resolve(null)),
getCachedContacts: mock(() => Promise.resolve(null)),
getCachedProducts: mock(() => Promise.resolve(null)),
getCachedSite: mock(() => Promise.resolve(null)),
getCachedOppCwData: mock(() => Promise.resolve(null)),
// Write / fetch helpers
fetchAndCacheActivities: mock(() => Promise.resolve(null)),
fetchAndCacheCompanyCwData: mock(() => Promise.resolve(null)),
fetchAndCacheNotes: mock(() => Promise.resolve(null)),
fetchAndCacheContacts: mock(() => Promise.resolve(null)),
fetchAndCacheProducts: mock(() => Promise.resolve(null)),
fetchAndCacheSite: mock(() => Promise.resolve(null)),
fetchAndCacheOppCwData: mock(() => Promise.resolve(null)),
// Invalidation helpers
invalidateNotesCache: mock(() => Promise.resolve()),
invalidateContactsCache: mock(() => Promise.resolve()),
invalidateProductsCache: mock(() => Promise.resolve()),
invalidateAllOpportunityCaches: mock(() => Promise.resolve()),
// Background refresh
refreshOpportunityCache: mock(() => Promise.resolve()),
...overrides,
};
}
// ---------------------------------------------------------------------------