From 15ef24eb3e6c527a27036e85ee7676cd35d721e6 Mon Sep 17 00:00:00 2001 From: Jackson Roberts Date: Mon, 9 Mar 2026 03:03:06 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20resolve=20CI=20test=20failures=20?= =?UTF-8?q?=E2=80=94=20explicit=20cache=20mock=20exports,=20hoisted=20serv?= =?UTF-8?q?ice=20mocks,=20pinned=20Bun=201.3.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-and-publish.yaml | 2 + .github/workflows/tests.yaml | 2 + tests/unit/cwOpportunityService.test.ts | 134 +++++++---------------- tests/unit/opportunitiesManager.test.ts | 52 +++++++-- 4 files changed, 84 insertions(+), 106 deletions(-) diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml index 7c5a46a..62c6b0a 100644 --- a/.github/workflows/build-and-publish.yaml +++ b/.github/workflows/build-and-publish.yaml @@ -14,6 +14,8 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.6" - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 706e3af..844952c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -14,6 +14,8 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.6" - name: Install dependencies run: bun install --frozen-lockfile diff --git a/tests/unit/cwOpportunityService.test.ts b/tests/unit/cwOpportunityService.test.ts index c90a5fc..8daa589 100644 --- a/tests/unit/cwOpportunityService.test.ts +++ b/tests/unit/cwOpportunityService.test.ts @@ -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, diff --git a/tests/unit/opportunitiesManager.test.ts b/tests/unit/opportunitiesManager.test.ts index 8c46088..b101071 100644 --- a/tests/unit/opportunitiesManager.test.ts +++ b/tests/unit/opportunitiesManager.test.ts @@ -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 = {}) { - 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, + }; } // ---------------------------------------------------------------------------