Files
optima/api/tests/unit/opportunitiesManager.test.ts

336 lines
11 KiB
TypeScript

import { describe, test, expect, mock, beforeEach } from "bun:test";
import {
buildMockOpportunity,
buildMockCompany,
buildMockConstants,
} from "../setup";
// ---------------------------------------------------------------------------
// Stable mock factory
// ---------------------------------------------------------------------------
function createStablePrismaMock(
overrides: Record<string, Record<string, any>> = {},
) {
return new Proxy(
{},
{
get(_target, model: string) {
if (model === "$connect" || model === "$disconnect")
return mock(() => Promise.resolve());
if (overrides[model]) return overrides[model];
return new Proxy({}, { get: () => mock(() => Promise.resolve(null)) });
},
},
);
}
/**
* 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 {
// Key helpers — use real prefixes so cross-file mock leaks don't
// break opportunityCache.test.ts key assertions.
activityCacheKey: mock((id: number) => `opp:activities:${id}`),
companyCwCacheKey: mock((id: number) => `opp:company-cw:${id}`),
notesCacheKey: mock((id: number) => `opp:notes:${id}`),
contactsCacheKey: mock((id: number) => `opp:contacts:${id}`),
productsCacheKey: mock((id: number) => `opp:products:${id}`),
siteCacheKey: mock((a: number, b: number) => `opp:site:${a}:${b}`),
oppCwDataCacheKey: mock((id: number) => `opp:cw-data:${id}`),
// 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,
};
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("opportunities manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// fetchRecord (lightweight)
// -------------------------------------------------------------------
describe("fetchRecord()", () => {
test("returns OpportunityController by internal ID", async () => {
const oppData = {
...buildMockOpportunity(),
company: buildMockCompany(),
};
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
opportunity: {
findFirst: mock(() => Promise.resolve(oppData)),
},
}),
redis: {
get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve("OK")),
del: mock(() => Promise.resolve(1)),
},
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}),
);
mock.module(
"../../src/modules/cw-utils/opportunities/opportunities",
() => ({
opportunityCw: {
fetch: mock(() => Promise.resolve(null)),
create: mock(() => Promise.resolve({})),
delete: mock(() => Promise.resolve()),
},
}),
);
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
fetchByOpportunityDirect: mock(() => Promise.resolve([])),
},
}));
const { opportunities } =
await import("../../src/managers/opportunities");
const result = await opportunities.fetchRecord("opp-1");
expect(result).toBeDefined();
});
test("throws 404 when opportunity not found", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
opportunity: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
redis: {
get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve("OK")),
del: mock(() => Promise.resolve(1)),
},
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}),
);
mock.module(
"../../src/modules/cw-utils/opportunities/opportunities",
() => ({
opportunityCw: {
fetch: mock(() => Promise.resolve(null)),
},
}),
);
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
fetchByOpportunityDirect: mock(() => Promise.resolve([])),
},
}));
const { opportunities } =
await import("../../src/managers/opportunities");
try {
await opportunities.fetchRecord("nonexistent");
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("OpportunityNotFound");
expect(e.status).toBe(404);
}
});
test("uses numeric identifier as cwOpportunityId", async () => {
const oppData = { ...buildMockOpportunity(), company: null };
const findFirst = mock(() => Promise.resolve(oppData));
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
opportunity: { findFirst },
}),
redis: {
get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve("OK")),
del: mock(() => Promise.resolve(1)),
},
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}),
);
mock.module(
"../../src/modules/cw-utils/opportunities/opportunities",
() => ({
opportunityCw: {
fetch: mock(() => Promise.resolve(null)),
},
}),
);
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
fetchByOpportunityDirect: mock(() => Promise.resolve([])),
},
}));
const { opportunities } =
await import("../../src/managers/opportunities");
await opportunities.fetchRecord(1001);
const where = findFirst.mock.calls[0]?.[0]?.where;
expect(where).toHaveProperty("cwOpportunityId", 1001);
});
});
// -------------------------------------------------------------------
// count
// -------------------------------------------------------------------
describe("count()", () => {
test("returns total count", async () => {
const countMock = mock(() => Promise.resolve(15));
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
opportunity: {
countMock,
count: countMock,
findFirst: mock(() => Promise.resolve(null)),
},
}),
redis: {
get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve("OK")),
del: mock(() => Promise.resolve(1)),
},
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}),
);
mock.module(
"../../src/modules/cw-utils/opportunities/opportunities",
() => ({
opportunityCw: {},
}),
);
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {},
}));
const { opportunities } =
await import("../../src/managers/opportunities");
const result = await opportunities.count();
expect(result).toBe(15);
});
test("counts only open when openOnly is true", async () => {
const countMock = mock(() => Promise.resolve(8));
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
opportunity: {
count: countMock,
findFirst: mock(() => Promise.resolve(null)),
},
}),
redis: {
get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve("OK")),
del: mock(() => Promise.resolve(1)),
},
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}),
);
mock.module(
"../../src/modules/cw-utils/opportunities/opportunities",
() => ({
opportunityCw: {},
}),
);
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {},
}));
const { opportunities } =
await import("../../src/managers/opportunities");
const result = await opportunities.count({ openOnly: true });
expect(result).toBe(8);
});
});
// -------------------------------------------------------------------
// fetchPages
// -------------------------------------------------------------------
describe("fetchPages()", () => {
test("returns paginated opportunity controllers", async () => {
const items = [
{ ...buildMockOpportunity(), company: buildMockCompany() },
];
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
opportunity: {
findMany: mock(() => Promise.resolve(items)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
redis: {
get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve("OK")),
del: mock(() => Promise.resolve(1)),
},
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}),
);
mock.module(
"../../src/modules/cw-utils/opportunities/opportunities",
() => ({
opportunityCw: {},
}),
);
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
fetchByOpportunityDirect: mock(() => Promise.resolve([])),
},
}));
const { opportunities } =
await import("../../src/managers/opportunities");
const result = await opportunities.fetchPages(1, 10);
expect(result).toBeArrayOfSize(1);
});
});
});