feat: add opportunity workflows, delete routes, company sites, algorithms, and expanded test coverage
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
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 — any unspecified export returns a mock fn. */
|
||||
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));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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/cache/opportunityCache", () =>
|
||||
buildCacheMock(),
|
||||
);
|
||||
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/cache/opportunityCache", () =>
|
||||
buildCacheMock(),
|
||||
);
|
||||
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/cache/opportunityCache", () =>
|
||||
buildCacheMock(),
|
||||
);
|
||||
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/cache/opportunityCache", () =>
|
||||
buildCacheMock(),
|
||||
);
|
||||
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/cache/opportunityCache", () =>
|
||||
buildCacheMock(),
|
||||
);
|
||||
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/cache/opportunityCache", () =>
|
||||
buildCacheMock(),
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user