336 lines
11 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|