import { describe, test, expect, mock, beforeEach } from "bun:test"; // --------------------------------------------------------------------------- // Mocks // --------------------------------------------------------------------------- const postMock = mock(() => Promise.resolve({ data: { id: 9001 } })); const updateMock = mock(() => Promise.resolve({})); // --------------------------------------------------------------------------- // 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. // --------------------------------------------------------------------------- const stripMs = (d: string) => d.replace(/\.\d{3}Z$/, "Z"); 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 // --------------------------------------------------------------------------- describe("cw.opportunityService", () => { beforeEach(() => { postMock.mockReset(); postMock.mockImplementation(() => Promise.resolve({ data: { id: 9001 } }), ); updateMock.mockReset(); updateMock.mockImplementation(() => Promise.resolve({})); }); // ------------------------------------------------------------------- // submitTimeEntry // ------------------------------------------------------------------- describe("submitTimeEntry()", () => { test("submits time entry and returns success", async () => { const result = await submitTimeEntry({ activityId: 100, cwMemberId: 10, timeStart: "2026-03-01T09:00:00.000Z", timeEnd: "2026-03-01T10:00:00.000Z", notes: "Design review", }); expect(result.success).toBe(true); expect(result.cwTimeEntryId).toBe(9001); expect(result.message).toContain("9001"); }); test("strips milliseconds from ISO timestamps", async () => { await submitTimeEntry({ activityId: 100, cwMemberId: 10, timeStart: "2026-03-01T09:00:00.123Z", timeEnd: "2026-03-01T10:00:00.456Z", notes: "test", }); const body = postMock.mock.calls[0]?.[1]; expect(body.timeStart).toBe("2026-03-01T09:00:00Z"); expect(body.timeEnd).toBe("2026-03-01T10:00:00Z"); }); test("returns failure on API error", async () => { postMock.mockImplementation(() => Promise.reject(new Error("CW down")), ); const result = await submitTimeEntry({ activityId: 100, cwMemberId: 10, timeStart: "2026-03-01T09:00:00Z", timeEnd: "2026-03-01T10:00:00Z", notes: "test", }); expect(result.success).toBe(false); expect(result.cwTimeEntryId).toBeNull(); expect(result.message).toContain("Failed"); }); }); // ------------------------------------------------------------------- // syncOpportunityStatus // ------------------------------------------------------------------- describe("syncOpportunityStatus()", () => { test("syncs status to CW and returns success", async () => { const result = await syncOpportunityStatus({ opportunityId: 1001, statusCwId: 24, }); expect(result.success).toBe(true); expect(result.message).toContain("1001"); }); test("returns failure on API error", async () => { updateMock.mockImplementation(() => Promise.reject(new Error("API fail")), ); const result = await syncOpportunityStatus({ opportunityId: 1001, statusCwId: 24, }); expect(result.success).toBe(false); expect(result.message).toContain("Failed"); }); }); });