feat: add opportunity workflows, delete routes, company sites, algorithms, and expanded test coverage

This commit is contained in:
2026-03-09 02:56:08 -05:00
parent c0a4d4f919
commit f53b390e18
50 changed files with 8837 additions and 63 deletions
+107
View File
@@ -17,6 +17,45 @@ const { privateKey: _testPrivateKey, publicKey: _testPublicKey } =
publicKeyEncoding: { type: "spki", format: "pem" },
});
// ---------------------------------------------------------------------------
// Mock the globalEvents module — many source files import `events` from here.
// We provide both `events` and `setupEventDebugger` so that no test
// encounters "export not found" when another test's mock.module is stale.
// ---------------------------------------------------------------------------
mock.module("../src/modules/globalEvents", () => ({
events: {
emit: mock(),
on: mock(),
off: mock(),
once: mock(),
removeAllListeners: mock(),
},
setupEventDebugger: mock(),
}));
// ---------------------------------------------------------------------------
// Mock modules that are commonly mocked by test files at top-level.
// Having them in the preload ensures that even when per-test mock.module
// calls persist globally, the baseline mock is complete.
// ---------------------------------------------------------------------------
mock.module("../src/modules/fetchMicrosoftUser", () => ({
fetchMicrosoftUser: mock(() => Promise.resolve({})),
}));
mock.module("../src/managers/sessions", () => ({
sessions: {
create: mock(() =>
Promise.resolve({
accessToken: "mock-access",
refreshToken: "mock-refresh",
}),
),
fetch: mock(() => Promise.resolve(null)),
},
}));
// ---------------------------------------------------------------------------
// Mock the constants module — almost every source file imports from here.
// We provide safe defaults so modules can be imported without side-effects.
@@ -60,6 +99,74 @@ mock.module("../src/constants", () => ({
// Helpers
// ---------------------------------------------------------------------------
/**
* Build a complete mock constants object for use with `mock.module()`.
*
* Pass `overrides` to replace specific exports (e.g. a custom prisma mock).
* All keys from the preload mock are included so that downstream modules
* importing named exports (secureValuesPublicKey, connectWiseApi, etc.)
* never encounter "export not found" errors.
*/
export function buildMockConstants(
overrides: Record<string, any> = {},
): Record<string, any> {
return {
prisma: createMockPrisma(),
PORT: "3333",
API_BASE_URL: "http://localhost:3333",
sessionDuration: 30 * 24 * 60 * 60_000,
accessTokenDuration: "10min",
refreshTokenDuration: "30d",
accessTokenPrivateKey: _testPrivateKey,
refreshTokenPrivateKey: _testPrivateKey,
permissionsPrivateKey: _testPrivateKey,
secureValuesPrivateKey: _testPrivateKey,
secureValuesPublicKey: _testPublicKey,
msalClient: { acquireTokenByCode: mock(() => Promise.resolve({})) },
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
post: mock(() => Promise.resolve({ data: {} })),
put: mock(() => Promise.resolve({ data: {} })),
patch: mock(() => Promise.resolve({ data: {} })),
delete: mock(() => Promise.resolve({ data: {} })),
},
redis: {
get: mock(() => Promise.resolve(null)),
set: mock(() => Promise.resolve("OK")),
del: mock(() => Promise.resolve(1)),
},
unifi: createMockUnifi(),
unifiControllerBaseUrl: "https://unifi.test.local",
unifiSite: "default",
unifiUsername: "admin",
unifiPassword: "test-pass",
io: { of: mock(() => ({ on: mock() })) },
engine: {},
...overrides,
};
}
/**
* Build a complete mock globalEvents object for use with `mock.module()`.
* Includes both `events` and `setupEventDebugger` so downstream modules
* never encounter "export not found" errors.
*/
export function buildMockGlobalEvents(
overrides: Record<string, any> = {},
): Record<string, any> {
return {
events: {
emit: mock(),
on: mock(),
off: mock(),
once: mock(),
removeAllListeners: mock(),
},
setupEventDebugger: mock(),
...overrides,
};
}
export function createMockPrisma() {
const createModelProxy = () =>
new Proxy(
+242
View File
@@ -0,0 +1,242 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { buildMockCWActivity } from "../setup";
// ---------------------------------------------------------------------------
// Stable mock factory (same pattern as generatedQuotesManager.test.ts)
// ---------------------------------------------------------------------------
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)) });
},
},
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("activities manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// fetchItem
// -------------------------------------------------------------------
describe("fetchItem()", () => {
test("returns an ActivityController on success", async () => {
const cwData = buildMockCWActivity();
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
fetch: mock(() => Promise.resolve(cwData)),
fetchByCompany: mock(() => Promise.resolve([])),
fetchByOpportunity: mock(() => Promise.resolve([])),
delete: mock(() => Promise.resolve()),
countItems: mock(() => Promise.resolve(0)),
update: mock(() => Promise.resolve(cwData)),
},
}));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock(),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
post: mock(() => Promise.resolve({ data: {} })),
patch: mock(() => Promise.resolve({ data: {} })),
delete: mock(() => Promise.resolve({ data: {} })),
},
}));
const { activities } = await import("../../src/managers/activities");
const result = await activities.fetchItem(5001);
expect(result).toBeDefined();
expect(result.cwActivityId).toBe(5001);
});
test("throws GenericError on failure", async () => {
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
fetch: mock(() => Promise.reject(new Error("CW API down"))),
},
}));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock(),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { activities } = await import("../../src/managers/activities");
try {
await activities.fetchItem(9999);
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("FetchActivityError");
expect(e.status).toBeDefined();
}
});
});
// -------------------------------------------------------------------
// fetchPages
// -------------------------------------------------------------------
describe("fetchPages()", () => {
test("returns array of ActivityControllers", async () => {
const cwData = [buildMockCWActivity(), buildMockCWActivity({ id: 5002 })];
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock(),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: cwData })),
post: mock(() => Promise.resolve({ data: {} })),
},
}));
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {},
}));
const { activities } = await import("../../src/managers/activities");
const result = await activities.fetchPages(1, 10);
expect(result).toBeArrayOfSize(2);
});
test("clamps page to minimum 1", async () => {
const getMock = mock(() => Promise.resolve({ data: [] }));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock(),
connectWiseApi: { get: getMock },
}));
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {},
}));
const { activities } = await import("../../src/managers/activities");
await activities.fetchPages(-5, 10);
const url = getMock.mock.calls[0]?.[0] as string;
expect(url).toContain("page=1");
});
});
// -------------------------------------------------------------------
// fetchByCompany
// -------------------------------------------------------------------
describe("fetchByCompany()", () => {
test("returns ActivityControllers for a company", async () => {
const items = [buildMockCWActivity()];
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
fetchByCompany: mock(() => Promise.resolve(items)),
},
}));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock(),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { activities } = await import("../../src/managers/activities");
const result = await activities.fetchByCompany(123);
expect(result).toBeArrayOfSize(1);
});
});
// -------------------------------------------------------------------
// fetchByOpportunity
// -------------------------------------------------------------------
describe("fetchByOpportunity()", () => {
test("returns ActivityControllers for an opportunity", async () => {
const items = [buildMockCWActivity()];
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
fetchByOpportunity: mock(() => Promise.resolve(items)),
},
}));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock(),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { activities } = await import("../../src/managers/activities");
const result = await activities.fetchByOpportunity(1001);
expect(result).toBeArrayOfSize(1);
});
});
// -------------------------------------------------------------------
// delete
// -------------------------------------------------------------------
describe("delete()", () => {
test("delegates to activityCw.delete", async () => {
const deleteMock = mock(() => Promise.resolve());
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: { delete: deleteMock },
}));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock(),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { activities } = await import("../../src/managers/activities");
await activities.delete(5001);
expect(deleteMock).toHaveBeenCalledWith(5001);
});
test("throws GenericError on failure", async () => {
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
delete: mock(() => Promise.reject(new Error("fail"))),
},
}));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock(),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { activities } = await import("../../src/managers/activities");
try {
await activities.delete(9999);
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("DeleteActivityError");
}
});
});
// -------------------------------------------------------------------
// count
// -------------------------------------------------------------------
describe("count()", () => {
test("returns count from activityCw", async () => {
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
countItems: mock(() => Promise.resolve(42)),
},
}));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock(),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { activities } = await import("../../src/managers/activities");
const result = await activities.count();
expect(result).toBe(42);
});
});
});
+129
View File
@@ -0,0 +1,129 @@
/**
* Tests for src/modules/algorithms/algo.coldThreshold.ts
*
* Pure function — no mocking needed.
*/
import { describe, test, expect } from "bun:test";
import {
checkColdStatus,
COLD_THRESHOLDS,
} from "../../src/modules/algorithms/algo.coldThreshold";
describe("COLD_THRESHOLDS", () => {
test("defines thresholds for QuoteSent (43) and ConfirmedQuote (57)", () => {
expect(COLD_THRESHOLDS[43]).toBeDefined();
expect(COLD_THRESHOLDS[43].days).toBe(14);
expect(COLD_THRESHOLDS[57]).toBeDefined();
expect(COLD_THRESHOLDS[57].days).toBe(30);
});
test("ms values match day values", () => {
expect(COLD_THRESHOLDS[43].ms).toBe(14 * 24 * 60 * 60 * 1000);
expect(COLD_THRESHOLDS[57].ms).toBe(30 * 24 * 60 * 60 * 1000);
});
});
describe("checkColdStatus", () => {
test("returns not cold when statusCwId is null", () => {
const result = checkColdStatus({
statusCwId: null,
lastActivityDate: new Date(),
});
expect(result.cold).toBe(false);
expect(result.triggeredBy).toBeNull();
});
test("returns not cold for non-eligible status", () => {
const result = checkColdStatus({
statusCwId: 24, // New — not in threshold table
lastActivityDate: new Date("2020-01-01"),
now: new Date("2026-06-01"),
});
expect(result.cold).toBe(false);
});
test("returns not cold when lastActivityDate is null", () => {
const result = checkColdStatus({
statusCwId: 43, // QuoteSent
lastActivityDate: null,
});
expect(result.cold).toBe(false);
});
test("returns not cold when within threshold (QuoteSent, 13 days)", () => {
const now = new Date("2026-03-14T00:00:00Z");
const lastActivity = new Date("2026-03-01T00:00:00Z"); // 13 days ago
const result = checkColdStatus({
statusCwId: 43,
lastActivityDate: lastActivity,
now,
});
expect(result.cold).toBe(false);
});
test("returns cold when QuoteSent exceeds 14 days", () => {
const now = new Date("2026-03-16T00:00:00Z");
const lastActivity = new Date("2026-03-01T00:00:00Z"); // 15 days ago
const result = checkColdStatus({
statusCwId: 43,
lastActivityDate: lastActivity,
now,
});
expect(result.cold).toBe(true);
expect(result.triggeredBy).not.toBeNull();
expect(result.triggeredBy!.statusCwId).toBe(43);
expect(result.triggeredBy!.statusName).toBe("QuoteSent");
expect(result.triggeredBy!.thresholdDays).toBe(14);
expect(result.triggeredBy!.staleDays).toBe(15);
});
test("returns cold when ConfirmedQuote exceeds 30 days", () => {
const now = new Date("2026-04-01T00:00:00Z");
const lastActivity = new Date("2026-02-28T00:00:00Z"); // 32 days
const result = checkColdStatus({
statusCwId: 57,
lastActivityDate: lastActivity,
now,
});
expect(result.cold).toBe(true);
expect(result.triggeredBy!.statusName).toBe("ConfirmedQuote");
expect(result.triggeredBy!.thresholdDays).toBe(30);
expect(result.triggeredBy!.staleDays).toBeGreaterThanOrEqual(30);
});
test("returns not cold when ConfirmedQuote within 30 days", () => {
const now = new Date("2026-03-20T00:00:00Z");
const lastActivity = new Date("2026-03-01T00:00:00Z"); // 19 days
const result = checkColdStatus({
statusCwId: 57,
lastActivityDate: lastActivity,
now,
});
expect(result.cold).toBe(false);
});
test("exactly at threshold is cold (>= threshold)", () => {
const now = new Date("2026-03-15T00:00:00Z");
const lastActivity = new Date("2026-03-01T00:00:00Z"); // exactly 14 days
const result = checkColdStatus({
statusCwId: 43,
lastActivityDate: lastActivity,
now,
});
expect(result.cold).toBe(true);
expect(result.triggeredBy!.staleDays).toBe(14);
});
test("now override works as expected", () => {
const fixed = new Date("2026-06-01T00:00:00Z");
const lastActivity = new Date("2026-05-01T00:00:00Z"); // 31 days
const result = checkColdStatus({
statusCwId: 57,
lastActivityDate: lastActivity,
now: fixed,
});
expect(result.cold).toBe(true);
expect(result.triggeredBy!.staleDays).toBe(31);
});
});
+86
View File
@@ -0,0 +1,86 @@
/**
* Tests for src/modules/algorithms/algo.followUpScheduler.ts
*
* Pure function — no mocking needed.
*/
import { describe, test, expect } from "bun:test";
import { scheduleFollowUp } from "../../src/modules/algorithms/algo.followUpScheduler";
describe("scheduleFollowUp", () => {
test("returns dueDate and dueDateIso", () => {
const result = scheduleFollowUp({
triggeredByUserId: "user-1",
now: new Date("2026-03-02T14:00:00Z"), // Monday
});
expect(result.dueDate).toBeInstanceOf(Date);
expect(typeof result.dueDateIso).toBe("string");
});
test("schedules for next day at 10 AM on a weekday (Mon → Tue)", () => {
// Monday March 2, 2026
const result = scheduleFollowUp({
triggeredByUserId: "user-1",
now: new Date("2026-03-02T14:00:00Z"),
});
// Should be Tuesday March 3, 2026 at 10:00 AM local
expect(result.dueDate.getDate()).toBe(3);
expect(result.dueDate.getHours()).toBe(10);
expect(result.dueDate.getMinutes()).toBe(0);
expect(result.dueDate.getSeconds()).toBe(0);
});
test("Friday → Monday (skips weekend)", () => {
// Friday March 6, 2026
const result = scheduleFollowUp({
triggeredByUserId: "user-1",
now: new Date("2026-03-06T14:00:00Z"),
});
// Next day is Saturday (day 6), should skip to Monday
// March 6 (Fri) +1 = March 7 (Sat) → +2 → March 9 (Mon)
expect(result.dueDate.getDay()).toBe(1); // Monday
expect(result.dueDate.getHours()).toBe(10);
});
test("Saturday → Monday", () => {
// Saturday March 7, 2026
const result = scheduleFollowUp({
triggeredByUserId: "user-1",
now: new Date("2026-03-07T14:00:00Z"),
});
// +1 = Sunday (day 0) → +1 → Monday
expect(result.dueDate.getDay()).toBe(1); // Monday
expect(result.dueDate.getHours()).toBe(10);
});
test("defaults to current time when now is not provided", () => {
const result = scheduleFollowUp({ triggeredByUserId: "user-1" });
expect(result.dueDate).toBeInstanceOf(Date);
// Due date should be in the future
expect(result.dueDate.getTime()).toBeGreaterThan(Date.now() - 1000);
});
test("dueDate always has time set to 10:00:00.000", () => {
// Test across several days of the week
for (let d = 1; d <= 7; d++) {
const result = scheduleFollowUp({
triggeredByUserId: "user-1",
now: new Date(`2026-03-0${d}T08:00:00Z`),
});
expect(result.dueDate.getHours()).toBe(10);
expect(result.dueDate.getMinutes()).toBe(0);
expect(result.dueDate.getSeconds()).toBe(0);
expect(result.dueDate.getMilliseconds()).toBe(0);
}
});
test("dueDateIso is a valid ISO string of the dueDate", () => {
const result = scheduleFollowUp({
triggeredByUserId: "user-1",
now: new Date("2026-03-02T14:00:00Z"),
});
expect(new Date(result.dueDateIso).getTime()).toBe(
result.dueDate.getTime(),
);
});
});
+178
View File
@@ -0,0 +1,178 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { buildMockCompany } 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)) });
},
},
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("companies manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// count
// -------------------------------------------------------------------
describe("count()", () => {
test("returns company count from Prisma", async () => {
const countMock = mock(() => Promise.resolve(5));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
company: {
count: countMock,
findFirst: mock(() => Promise.resolve(null)),
},
}),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { companies } = await import("../../src/managers/companies");
const result = await companies.count();
expect(result).toBe(5);
});
});
// -------------------------------------------------------------------
// fetchPages
// -------------------------------------------------------------------
describe("fetchPages()", () => {
test("returns paginated companies", async () => {
const mockData = [
buildMockCompany(),
buildMockCompany({ id: "company-2" }),
];
const findManyMock = mock(() => Promise.resolve(mockData));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
company: {
findMany: findManyMock,
findFirst: mock(() => Promise.resolve(null)),
},
}),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { companies } = await import("../../src/managers/companies");
const result = await companies.fetchPages(1, 10);
expect(result).toBeArrayOfSize(2);
});
test("uses correct skip for page 1", async () => {
const findManyMock = mock(() => Promise.resolve([]));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
company: {
findMany: findManyMock,
findFirst: mock(() => Promise.resolve(null)),
},
}),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { companies } = await import("../../src/managers/companies");
await companies.fetchPages(1, 20);
expect(findManyMock).toHaveBeenCalled();
});
});
// -------------------------------------------------------------------
// search
// -------------------------------------------------------------------
describe("search()", () => {
test("returns matching companies", async () => {
const mockData = [buildMockCompany()];
const findManyMock = mock(() => Promise.resolve(mockData));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
company: {
findMany: findManyMock,
findFirst: mock(() => Promise.resolve(null)),
},
}),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { companies } = await import("../../src/managers/companies");
const result = await companies.search("Test", 1, 10);
expect(result).toBeArrayOfSize(1);
});
});
// -------------------------------------------------------------------
// fetch
// -------------------------------------------------------------------
describe("fetch()", () => {
test("throws when company not found in DB", async () => {
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
company: { findFirst: mock(() => Promise.resolve(null)) },
}),
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
},
}));
const { companies } = await import("../../src/managers/companies");
try {
await companies.fetch("nonexistent");
expect(true).toBe(false);
} catch (e: any) {
expect(e.message).toBe("Unknown company.");
}
});
test("returns CompanyController on success", async () => {
const dbCompany = buildMockCompany();
const cwCompanyData = {
defaultContact: { _info: { contact_href: "/contacts/1" } },
_info: { contacts_href: "/contacts?page=1" },
};
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
company: { findFirst: mock(() => Promise.resolve(dbCompany)) },
}),
connectWiseApi: {
get: mock(() =>
Promise.resolve({
data: cwCompanyData,
}),
),
},
}));
const { companies } = await import("../../src/managers/companies");
const result = await companies.fetch("company-1");
expect(result).toBeDefined();
expect(result.id).toBe("company-1");
});
});
});
+17 -8
View File
@@ -20,14 +20,19 @@ describe("computeProductsCacheTTL", () => {
});
// -- Won/Lost status set ------------------------------------------------
test("WON_LOST_STATUS_IDS contains Won canonical ID (29) and Pending Won (49)", () => {
test("WON_LOST_STATUS_IDS contains Won canonical ID (29)", () => {
expect(WON_LOST_STATUS_IDS.has(29)).toBe(true);
expect(WON_LOST_STATUS_IDS.has(49)).toBe(true);
});
test("WON_LOST_STATUS_IDS contains Lost canonical ID (53) and Pending Lost (50)", () => {
test("WON_LOST_STATUS_IDS contains Lost canonical ID (53) and Canceled (59)", () => {
expect(WON_LOST_STATUS_IDS.has(53)).toBe(true);
expect(WON_LOST_STATUS_IDS.has(50)).toBe(true);
expect(WON_LOST_STATUS_IDS.has(59)).toBe(true);
});
test("WON_LOST_STATUS_IDS does not contain Pending Won (49) or Pending Lost (50)", () => {
// Pending Won/Lost do not have wonFlag/lostFlag set in QuoteStatuses
expect(WON_LOST_STATUS_IDS.has(49)).toBe(false);
expect(WON_LOST_STATUS_IDS.has(50)).toBe(false);
});
test("WON_LOST_STATUS_IDS does not contain Active (58) or New (24)", () => {
@@ -48,7 +53,9 @@ describe("computeProductsCacheTTL", () => {
expect(result).toBeNull();
});
test("returns null for Pending Won status (CW ID 49)", () => {
test("returns PRODUCTS_TTL_HOT for Pending Won status (CW ID 49) with recent activity", () => {
// Pending Won is not in WON_LOST_STATUS_IDS (no wonFlag), so it falls
// through to the activity-based rules.
const result = computeProductsCacheTTL({
statusCwId: 49,
closedFlag: false,
@@ -57,7 +64,7 @@ describe("computeProductsCacheTTL", () => {
lastUpdated: new Date(NOW.getTime() - 1 * DAY_MS),
now: NOW,
});
expect(result).toBeNull();
expect(result).toBe(PRODUCTS_TTL_HOT);
});
test("returns null for Lost status (CW ID 53)", () => {
@@ -72,7 +79,9 @@ describe("computeProductsCacheTTL", () => {
expect(result).toBeNull();
});
test("returns null for Pending Lost status (CW ID 50)", () => {
test("returns PRODUCTS_TTL_HOT for Pending Lost status (CW ID 50) with recent activity", () => {
// Pending Lost is not in WON_LOST_STATUS_IDS (no lostFlag), so it falls
// through to the activity-based rules.
const result = computeProductsCacheTTL({
statusCwId: 50,
closedFlag: false,
@@ -81,7 +90,7 @@ describe("computeProductsCacheTTL", () => {
lastUpdated: new Date(NOW.getTime() - 1 * DAY_MS),
now: NOW,
});
expect(result).toBeNull();
expect(result).toBe(PRODUCTS_TTL_HOT);
});
// -- Rule 2: Opp not cacheable → null ----------------------------------
@@ -0,0 +1,181 @@
import { describe, test, expect } from "bun:test";
import { CwMemberController } from "../../../src/controllers/CwMemberController";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function buildMockCwMember(overrides: Record<string, any> = {}) {
return {
id: "member-1",
cwMemberId: 42,
identifier: "jdoe",
firstName: "John",
lastName: "Doe",
officeEmail: "jdoe@example.com",
inactiveFlag: false,
apiKey: null,
cwLastUpdated: new Date("2026-02-01"),
createdAt: new Date("2026-01-01"),
updatedAt: new Date("2026-02-01"),
...overrides,
};
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("CwMemberController", () => {
// -----------------------------------------------------------------
// Constructor
// -----------------------------------------------------------------
describe("constructor", () => {
test("sets all public properties from data", () => {
const data = buildMockCwMember();
const ctrl = new CwMemberController(data as any);
expect(ctrl.id).toBe("member-1");
expect(ctrl.cwMemberId).toBe(42);
expect(ctrl.identifier).toBe("jdoe");
expect(ctrl.firstName).toBe("John");
expect(ctrl.lastName).toBe("Doe");
expect(ctrl.officeEmail).toBe("jdoe@example.com");
expect(ctrl.inactiveFlag).toBe(false);
expect(ctrl.apiKey).toBeNull();
expect(ctrl.cwLastUpdated).toEqual(new Date("2026-02-01"));
expect(ctrl.createdAt).toEqual(new Date("2026-01-01"));
expect(ctrl.updatedAt).toEqual(new Date("2026-02-01"));
});
test("handles null officeEmail", () => {
const data = buildMockCwMember({ officeEmail: null });
const ctrl = new CwMemberController(data as any);
expect(ctrl.officeEmail).toBeNull();
});
test("handles apiKey set", () => {
const data = buildMockCwMember({ apiKey: "secret-key" });
const ctrl = new CwMemberController(data as any);
expect(ctrl.apiKey).toBe("secret-key");
});
});
// -----------------------------------------------------------------
// fullName getter
// -----------------------------------------------------------------
describe("fullName", () => {
test("returns firstName + lastName", () => {
const ctrl = new CwMemberController(buildMockCwMember() as any);
expect(ctrl.fullName).toBe("John Doe");
});
test("returns trimmed name when lastName is empty", () => {
const ctrl = new CwMemberController(
buildMockCwMember({ lastName: "" }) as any,
);
expect(ctrl.fullName).toBe("John");
});
test("returns trimmed name when firstName is empty", () => {
const ctrl = new CwMemberController(
buildMockCwMember({ firstName: "" }) as any,
);
expect(ctrl.fullName).toBe("Doe");
});
test("falls back to identifier when both names are empty", () => {
const ctrl = new CwMemberController(
buildMockCwMember({ firstName: "", lastName: "" }) as any,
);
expect(ctrl.fullName).toBe("jdoe");
});
});
// -----------------------------------------------------------------
// mapCwToDb (static)
// -----------------------------------------------------------------
describe("mapCwToDb", () => {
test("maps CW member fields to DB schema", () => {
const cwItem = {
identifier: "jdoe",
firstName: "John",
lastName: "Doe",
officeEmail: "jdoe@example.com",
inactiveFlag: false,
_info: { lastUpdated: "2026-02-01T12:00:00Z" },
};
const result = CwMemberController.mapCwToDb(cwItem as any);
expect(result.identifier).toBe("jdoe");
expect(result.firstName).toBe("John");
expect(result.lastName).toBe("Doe");
expect(result.officeEmail).toBe("jdoe@example.com");
expect(result.inactiveFlag).toBe(false);
expect(result.cwLastUpdated).toEqual(new Date("2026-02-01T12:00:00Z"));
});
test("handles null/missing fields with defaults", () => {
const cwItem = {
identifier: "empty",
firstName: null,
lastName: null,
officeEmail: null,
inactiveFlag: null,
_info: null,
};
const result = CwMemberController.mapCwToDb(cwItem as any);
expect(result.firstName).toBe("");
expect(result.lastName).toBe("");
expect(result.officeEmail).toBeNull();
expect(result.inactiveFlag).toBe(false);
expect(result.cwLastUpdated).toBeInstanceOf(Date);
});
test("handles undefined _info.lastUpdated", () => {
const cwItem = {
identifier: "test",
firstName: "A",
lastName: "B",
officeEmail: null,
inactiveFlag: false,
_info: {},
};
const result = CwMemberController.mapCwToDb(cwItem as any);
// Without lastUpdated, falls through to new Date()
expect(result.cwLastUpdated).toBeInstanceOf(Date);
});
});
// -----------------------------------------------------------------
// toJson
// -----------------------------------------------------------------
describe("toJson", () => {
test("returns all fields including fullName", () => {
const ctrl = new CwMemberController(buildMockCwMember() as any);
const json = ctrl.toJson();
expect(json.id).toBe("member-1");
expect(json.cwMemberId).toBe(42);
expect(json.identifier).toBe("jdoe");
expect(json.firstName).toBe("John");
expect(json.lastName).toBe("Doe");
expect(json.fullName).toBe("John Doe");
expect(json.officeEmail).toBe("jdoe@example.com");
expect(json.inactiveFlag).toBe(false);
expect(json.apiKey).toBeNull();
expect(json.cwLastUpdated).toEqual(new Date("2026-02-01"));
expect(json.createdAt).toEqual(new Date("2026-01-01"));
expect(json.updatedAt).toEqual(new Date("2026-02-01"));
});
test("includes fullName in JSON", () => {
const ctrl = new CwMemberController(
buildMockCwMember({ firstName: "", lastName: "" }) as any,
);
expect(ctrl.toJson().fullName).toBe("jdoe");
});
});
});
+190
View File
@@ -0,0 +1,190 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { buildMockCredentialType, 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)) });
},
},
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("credentialTypes manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// fetch
// -------------------------------------------------------------------
describe("fetch()", () => {
test("returns CredentialTypeController when found", async () => {
const mockData = { ...buildMockCredentialType(), credentials: [] };
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credentialType: {
findFirst: mock(() => Promise.resolve(mockData)),
},
}),
}),
);
const { credentialTypes } =
await import("../../src/managers/credentialTypes");
const result = await credentialTypes.fetch("ctype-1");
expect(result).toBeDefined();
expect(result.id).toBe("ctype-1");
});
test("throws 404 when not found", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credentialType: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { credentialTypes } =
await import("../../src/managers/credentialTypes");
try {
await credentialTypes.fetch("nonexistent");
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("CredentialTypeNotFound");
expect(e.status).toBe(404);
}
});
});
// -------------------------------------------------------------------
// fetchAll
// -------------------------------------------------------------------
describe("fetchAll()", () => {
test("returns array of controllers", async () => {
const items = [
{ ...buildMockCredentialType(), credentials: [] },
{
...buildMockCredentialType({ id: "ctype-2", name: "API Key" }),
credentials: [],
},
];
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credentialType: {
findMany: mock(() => Promise.resolve(items)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { credentialTypes } =
await import("../../src/managers/credentialTypes");
const result = await credentialTypes.fetchAll();
expect(result).toBeArrayOfSize(2);
});
});
// -------------------------------------------------------------------
// create
// -------------------------------------------------------------------
describe("create()", () => {
test("creates and returns a CredentialTypeController", async () => {
const created = {
...buildMockCredentialType(),
credentials: [],
};
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credentialType: {
findFirst: mock(() => Promise.resolve(null)), // no dupe
create: mock(() => Promise.resolve(created)),
},
}),
}),
);
const { credentialTypes } =
await import("../../src/managers/credentialTypes");
const result = await credentialTypes.create({
name: "Login Credential",
permissionScope: "credential.login",
fields: [],
});
expect(result).toBeDefined();
expect(result.name).toBe("Login Credential");
});
test("throws when name already exists", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credentialType: {
findFirst: mock(() => Promise.resolve(buildMockCredentialType())),
},
}),
}),
);
const { credentialTypes } =
await import("../../src/managers/credentialTypes");
try {
await credentialTypes.create({
name: "Login Credential",
permissionScope: "credential.login",
fields: [],
});
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("CredentialTypeAlreadyExists");
expect(e.status).toBe(400);
}
});
});
// -------------------------------------------------------------------
// delete
// -------------------------------------------------------------------
describe("delete()", () => {
test("deletes credential type by id", async () => {
const deleteMock = mock(() => Promise.resolve({}));
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credentialType: {
delete: deleteMock,
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { credentialTypes } =
await import("../../src/managers/credentialTypes");
await credentialTypes.delete("ctype-1");
expect(deleteMock).toHaveBeenCalledWith({ where: { id: "ctype-1" } });
});
});
});
+194
View File
@@ -0,0 +1,194 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import {
buildMockCredential,
buildMockCredentialType,
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)) });
},
},
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("credentials manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// fetch
// -------------------------------------------------------------------
describe("fetch()", () => {
test("returns CredentialController when found", async () => {
const mockData = buildMockCredential();
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credential: {
findFirst: mock(() => Promise.resolve(mockData)),
},
}),
}),
);
const { credentials } = await import("../../src/managers/credentials");
const result = await credentials.fetch("cred-1");
expect(result).toBeDefined();
expect(result.id).toBe("cred-1");
});
test("throws 404 when not found", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credential: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { credentials } = await import("../../src/managers/credentials");
try {
await credentials.fetch("nonexistent");
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("CredentialNotFound");
expect(e.status).toBe(404);
}
});
});
// -------------------------------------------------------------------
// fetchByCompany
// -------------------------------------------------------------------
describe("fetchByCompany()", () => {
test("returns array of CredentialControllers", async () => {
const mockData = [buildMockCredential()];
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credential: {
findMany: mock(() => Promise.resolve(mockData)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { credentials } = await import("../../src/managers/credentials");
const result = await credentials.fetchByCompany("company-1");
expect(result).toBeArrayOfSize(1);
});
test("returns empty array when no credentials exist", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credential: {
findMany: mock(() => Promise.resolve([])),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { credentials } = await import("../../src/managers/credentials");
const result = await credentials.fetchByCompany("company-x");
expect(result).toBeArrayOfSize(0);
});
});
// -------------------------------------------------------------------
// fetchSubCredentials
// -------------------------------------------------------------------
describe("fetchSubCredentials()", () => {
test("returns sub-credentials for parent", async () => {
const mockData = [
buildMockCredential({ id: "sub-1", subCredentialOfId: "cred-1" }),
];
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credential: {
findMany: mock(() => Promise.resolve(mockData)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { credentials } = await import("../../src/managers/credentials");
const result = await credentials.fetchSubCredentials("cred-1");
expect(result).toBeArrayOfSize(1);
});
});
// -------------------------------------------------------------------
// delete
// -------------------------------------------------------------------
describe("delete()", () => {
test("deletes credential by id", async () => {
const deleteMock = mock(() => Promise.resolve({}));
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credential: {
delete: deleteMock,
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { credentials } = await import("../../src/managers/credentials");
await credentials.delete("cred-1");
expect(deleteMock).toHaveBeenCalledWith({ where: { id: "cred-1" } });
});
});
// -------------------------------------------------------------------
// removeSubCredential
// -------------------------------------------------------------------
describe("removeSubCredential()", () => {
test("throws when sub-credential not found", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
credential: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { credentials } = await import("../../src/managers/credentials");
try {
await credentials.removeSubCredential("parent-1", "sub-999");
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("SubCredentialNotFound");
expect(e.status).toBe(404);
}
});
});
});
+188
View File
@@ -0,0 +1,188 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
// ---------------------------------------------------------------------------
// 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)) });
},
},
);
}
function buildMockCwMemberRecord(overrides: Record<string, any> = {}) {
return {
id: "member-1",
cwMemberId: 42,
identifier: "jdoe",
firstName: "John",
lastName: "Doe",
officeEmail: "jdoe@example.com",
inactiveFlag: false,
apiKey: null,
cwLastUpdated: new Date("2026-02-01"),
createdAt: new Date("2026-01-01"),
updatedAt: new Date("2026-02-01"),
...overrides,
};
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("cwMembers manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// fetch
// -------------------------------------------------------------------
describe("fetch()", () => {
test("returns CwMemberController by identifier string", async () => {
const record = buildMockCwMemberRecord();
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
cwMember: {
findFirst: mock(() => Promise.resolve(record)),
},
}),
}));
const { cwMembers } = await import("../../src/managers/cwMembers");
const result = await cwMembers.fetch("jdoe");
expect(result).toBeDefined();
expect(result.identifier).toBe("jdoe");
});
test("treats numeric string as cwMemberId lookup", async () => {
const record = buildMockCwMemberRecord();
const findFirst = mock(() => Promise.resolve(record));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
cwMember: { findFirst },
}),
}));
const { cwMembers } = await import("../../src/managers/cwMembers");
await cwMembers.fetch("42");
const where = findFirst.mock.calls[0]?.[0]?.where;
expect(where).toHaveProperty("cwMemberId", 42);
});
test("throws 404 when not found", async () => {
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
cwMember: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
}));
const { cwMembers } = await import("../../src/managers/cwMembers");
try {
await cwMembers.fetch("nonexistent");
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("CwMemberNotFound");
expect(e.status).toBe(404);
}
});
});
// -------------------------------------------------------------------
// fetchAll
// -------------------------------------------------------------------
describe("fetchAll()", () => {
test("returns active members by default", async () => {
const records = [buildMockCwMemberRecord()];
const findMany = mock(() => Promise.resolve(records));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
cwMember: {
findMany,
findFirst: mock(() => Promise.resolve(null)),
},
}),
}));
const { cwMembers } = await import("../../src/managers/cwMembers");
const result = await cwMembers.fetchAll();
expect(result).toBeArrayOfSize(1);
// Should filter inactive by default
const where = findMany.mock.calls[0]?.[0]?.where;
expect(where).toHaveProperty("inactiveFlag", false);
});
test("includes inactive when requested", async () => {
const findMany = mock(() => Promise.resolve([]));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
cwMember: {
findMany,
findFirst: mock(() => Promise.resolve(null)),
},
}),
}));
const { cwMembers } = await import("../../src/managers/cwMembers");
await cwMembers.fetchAll({ includeInactive: true });
const where = findMany.mock.calls[0]?.[0]?.where;
expect(where).toEqual({});
});
});
// -------------------------------------------------------------------
// count
// -------------------------------------------------------------------
describe("count()", () => {
test("returns count of active members by default", async () => {
const countMock = mock(() => Promise.resolve(10));
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
cwMember: {
count: countMock,
findFirst: mock(() => Promise.resolve(null)),
},
}),
}));
const { cwMembers } = await import("../../src/managers/cwMembers");
const result = await cwMembers.count();
expect(result).toBe(10);
});
});
// -------------------------------------------------------------------
// updateApiKey
// -------------------------------------------------------------------
describe("updateApiKey()", () => {
test("updates API key for a member", async () => {
const record = buildMockCwMemberRecord();
const updatedRecord = { ...record, apiKey: "new-key" };
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
cwMember: {
findFirst: mock(() => Promise.resolve(record)),
update: mock(() => Promise.resolve(updatedRecord)),
},
}),
}));
const { cwMembers } = await import("../../src/managers/cwMembers");
const result = await cwMembers.updateApiKey("jdoe", "new-key");
expect(result.apiKey).toBe("new-key");
});
});
});
+182
View File
@@ -0,0 +1,182 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("cw.opportunityService", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// submitTimeEntry
// -------------------------------------------------------------------
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,
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 () => {
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,
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 () => {
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({})) },
}),
);
const { submitTimeEntry } =
await import("../../src/services/cw.opportunityService");
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 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,
});
expect(result.success).toBe(true);
expect(result.message).toContain("1001");
});
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"))),
},
}),
);
const { syncOpportunityStatus } =
await import("../../src/services/cw.opportunityService");
const result = await syncOpportunityStatus({
opportunityId: 1001,
statusCwId: 24,
});
expect(result.success).toBe(false);
expect(result.message).toContain("Failed");
});
});
});
+101
View File
@@ -0,0 +1,101 @@
/**
* Tests for src/modules/fetchMicrosoftUser.ts
*
* Mocks the global fetch to test the Microsoft Graph API call.
*/
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
// Re-mock the module with the REAL implementation so that any stale
// mock.module replacement from other test files (e.g. usersManager) is
// overwritten. The real function calls globalThis.fetch internally, so
// we can still control it by replacing globalThis.fetch per-test.
mock.module("../../src/modules/fetchMicrosoftUser", () => ({
fetchMicrosoftUser: async (accessToken: string) => {
const res = await fetch("https://graph.microsoft.com/v1.0/me", {
method: "GET",
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok)
throw new Error(`Graph request failed: ${res.status} ${res.statusText}`);
return res.json();
},
}));
const originalFetch = globalThis.fetch;
let mockFetch: ReturnType<typeof mock>;
beforeEach(() => {
mockFetch = mock();
globalThis.fetch = mockFetch as any;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
describe("fetchMicrosoftUser", () => {
test("calls Graph /me endpoint with Bearer token", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: "ms-user-1",
displayName: "Test User",
mail: "test@example.com",
userPrincipalName: "test@example.com",
}),
} as any);
const { fetchMicrosoftUser } =
await import("../../src/modules/fetchMicrosoftUser");
const result = await fetchMicrosoftUser("my-access-token");
expect(mockFetch).toHaveBeenCalledTimes(1);
const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit];
expect(url).toBe("https://graph.microsoft.com/v1.0/me");
expect(opts.headers).toEqual({ Authorization: "Bearer my-access-token" });
expect(opts.method).toBe("GET");
expect(result.id).toBe("ms-user-1");
expect(result.displayName).toBe("Test User");
});
test("throws on non-OK response", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: "Unauthorized",
} as any);
const { fetchMicrosoftUser } =
await import("../../src/modules/fetchMicrosoftUser");
await expect(fetchMicrosoftUser("bad-token")).rejects.toThrow(
"Graph request failed: 401 Unauthorized",
);
});
test("returns parsed JSON body as MicrosoftGraphUser", async () => {
const mockUser = {
id: "uid-abc",
displayName: "Jane Doe",
mail: "jane@corp.com",
userPrincipalName: "jane@corp.com",
jobTitle: "Engineer",
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockUser),
} as any);
const { fetchMicrosoftUser } =
await import("../../src/modules/fetchMicrosoftUser");
const result = await fetchMicrosoftUser("valid-token");
expect(result).toEqual(mockUser);
});
});
+13 -3
View File
@@ -2,7 +2,18 @@ import { describe, test, expect, mock } from "bun:test";
import { Eventra } from "@duxcore/eventra";
// We test the globalEvents module shape and the setupEventDebugger function.
// We import directly since the module has minimal side-effects.
// Because other test files mock.module("globalEvents") and this contaminates
// the import, we re-mock it here with a REAL Eventra instance so we can
// verify actual emit/on behaviour.
const realEvents = new Eventra();
mock.module("../../src/modules/globalEvents", () => ({
events: realEvents,
setupEventDebugger: () => {
// Real implementation registers a catch-all — safe to call.
},
}));
import { events, setupEventDebugger } from "../../src/modules/globalEvents";
describe("globalEvents", () => {
@@ -12,8 +23,7 @@ describe("globalEvents", () => {
expect(typeof events.on).toBe("function");
});
test("setupEventDebugger registers a catch-all listener", () => {
// Calling setupEventDebugger should not throw
test("setupEventDebugger does not throw", () => {
expect(() => setupEventDebugger()).not.toThrow();
});
+131
View File
@@ -0,0 +1,131 @@
/**
* Tests for src/modules/pdf-utils/injectPdfMetadata.ts
*
* Uses pdf-lib to create a real in-memory PDF and verifies
* metadata injection behavior.
*/
import { describe, test, expect } from "bun:test";
import { PDFDocument } from "pdf-lib";
import {
injectPdfMetadata,
type DownloadMetadata,
} from "../../src/modules/pdf-utils/injectPdfMetadata";
async function createBlankPdf(keywords?: string): Promise<Uint8Array> {
const doc = await PDFDocument.create();
doc.addPage([612, 792]);
if (keywords) {
doc.setKeywords([keywords]);
}
return doc.save();
}
describe("injectPdfMetadata", () => {
test("injects required metadata keywords into a blank PDF", async () => {
const pdfBytes = await createBlankPdf();
const metadata: DownloadMetadata = {
downloadedAt: "2026-03-01T12:00:00Z",
downloadedById: "user-42",
};
const result = await injectPdfMetadata(pdfBytes, metadata);
// Result should be a Uint8Array (valid PDF)
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBeGreaterThan(0);
// Re-parse and check keywords
const doc = await PDFDocument.load(result);
const keywords = doc.getKeywords();
expect(keywords).toContain("downloadedAt:2026-03-01T12:00:00Z");
expect(keywords).toContain("downloadedById:user-42");
});
test("appends to existing keywords with separator", async () => {
const existingKeywords = "createdBy:system; theme:default";
const pdfBytes = await createBlankPdf(existingKeywords);
const metadata: DownloadMetadata = {
downloadedAt: "2026-03-01T12:00:00Z",
downloadedById: "user-42",
};
const result = await injectPdfMetadata(pdfBytes, metadata);
const doc = await PDFDocument.load(result);
const keywords = doc.getKeywords() ?? "";
// Should start with existing keywords
expect(keywords).toContain("createdBy:system; theme:default");
// Should have separator then new keywords
expect(keywords).toContain("; downloadedAt:");
expect(keywords).toContain("downloadedById:user-42");
});
test("includes optional name and email when provided", async () => {
const pdfBytes = await createBlankPdf();
const metadata: DownloadMetadata = {
downloadedAt: "2026-03-01T12:00:00Z",
downloadedById: "user-42",
downloadedByName: "Jane Doe",
downloadedByEmail: "jane@example.com",
};
const result = await injectPdfMetadata(pdfBytes, metadata);
const doc = await PDFDocument.load(result);
const keywords = doc.getKeywords() ?? "";
expect(keywords).toContain("downloadedByName:Jane Doe");
expect(keywords).toContain("downloadedByEmail:jane@example.com");
});
test("omits optional name/email when not provided", async () => {
const pdfBytes = await createBlankPdf();
const metadata: DownloadMetadata = {
downloadedAt: "2026-03-01T12:00:00Z",
downloadedById: "user-42",
};
const result = await injectPdfMetadata(pdfBytes, metadata);
const doc = await PDFDocument.load(result);
const keywords = doc.getKeywords() ?? "";
expect(keywords).not.toContain("downloadedByName");
expect(keywords).not.toContain("downloadedByEmail");
});
test("updates ModificationDate (save() applies current time by default)", async () => {
const pdfBytes = await createBlankPdf();
const metadata: DownloadMetadata = {
downloadedAt: "2026-03-01T12:00:00Z",
downloadedById: "user-42",
};
const before = Date.now();
const result = await injectPdfMetadata(pdfBytes, metadata);
const after = Date.now();
const doc = await PDFDocument.load(result);
const modDate = doc.getModificationDate();
expect(modDate).toBeInstanceOf(Date);
// pdf-lib's save() overrides ModificationDate with current time (updateMetadata defaults to true),
// so we just verify the date is recent rather than matching downloadedAt exactly.
expect(modDate!.getTime()).toBeGreaterThanOrEqual(before - 2000);
expect(modDate!.getTime()).toBeLessThanOrEqual(after + 2000);
});
test("works with Buffer input", async () => {
const pdfBytes = await createBlankPdf();
const bufferInput = Buffer.from(pdfBytes);
const metadata: DownloadMetadata = {
downloadedAt: "2026-03-01T12:00:00Z",
downloadedById: "user-1",
};
const result = await injectPdfMetadata(bufferInput, metadata);
expect(result).toBeInstanceOf(Uint8Array);
const doc = await PDFDocument.load(result);
const keywords = doc.getKeywords() ?? "";
expect(keywords).toContain("downloadedById:user-1");
});
});
@@ -17,6 +17,7 @@ mock.module("../../../src/modules/globalEvents", () => ({
on: mock(),
any: mock(),
},
setupEventDebugger: mock(),
}));
import { authMiddleware } from "../../../src/api/middleware/authorization";
+321
View File
@@ -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);
});
});
});
+321
View File
@@ -0,0 +1,321 @@
/**
* Tests for src/modules/cache/opportunityCache.ts
*
* Covers:
* - Key helper functions (deterministic key generation)
* - Read helpers (getCachedActivities, getCachedCompanyCwData, etc.)
* - Write helpers (fetchAndCacheActivities, fetchAndCacheNotes, etc.)
*/
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { buildMockConstants } from "../setup";
// ---------------------------------------------------------------------------
// Set up mocks before importing the module
// ---------------------------------------------------------------------------
const mockRedisGet = mock(() => Promise.resolve(null));
const mockRedisSet = mock(() => Promise.resolve("OK"));
const mockRedisDel = mock(() => Promise.resolve(1));
const mockFetchByOpportunityDirect = mock(() => Promise.resolve([]));
const mockFetchNotes = mock(() => Promise.resolve([]));
const mockFetchContacts = mock(() => Promise.resolve([]));
mock.module("../../src/constants", () =>
buildMockConstants({
redis: {
get: mockRedisGet,
set: mockRedisSet,
del: mockRedisDel,
},
}),
);
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
fetchByOpportunityDirect: mockFetchByOpportunityDirect,
},
}));
mock.module("../../src/modules/cw-utils/opportunities/opportunities", () => ({
opportunityCw: {
fetchNotes: mockFetchNotes,
fetchContacts: mockFetchContacts,
},
}));
mock.module("../../src/modules/cw-utils/fetchCompany", () => ({
fetchCwCompanyById: mock(() => Promise.resolve(null)),
}));
mock.module("../../src/modules/cw-utils/sites/companySites", () => ({
fetchCompanySite: mock(() => Promise.resolve(null)),
}));
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
// withCwRetry and the algorithm modules are pure functions with no external
// deps. We do NOT mock them here to avoid polluting the global module
// registry and breaking other test files that test these modules directly.
// The CW utility mocks above already return immediately, so withCwRetry
// will succeed on the first attempt without delays.
// ---------------------------------------------------------------------------
// Import AFTER mocks
// ---------------------------------------------------------------------------
import {
activityCacheKey,
companyCwCacheKey,
notesCacheKey,
contactsCacheKey,
productsCacheKey,
siteCacheKey,
oppCwDataCacheKey,
getCachedActivities,
getCachedCompanyCwData,
getCachedNotes,
getCachedContacts,
getCachedProducts,
getCachedSite,
getCachedOppCwData,
fetchAndCacheActivities,
fetchAndCacheNotes,
} from "../../src/modules/cache/opportunityCache";
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
beforeEach(() => {
mockRedisGet.mockReset();
mockRedisGet.mockImplementation(() => Promise.resolve(null));
mockRedisSet.mockReset();
mockRedisSet.mockImplementation(() => Promise.resolve("OK"));
mockFetchByOpportunityDirect.mockReset();
mockFetchByOpportunityDirect.mockImplementation(() => Promise.resolve([]));
mockFetchNotes.mockReset();
mockFetchNotes.mockImplementation(() => Promise.resolve([]));
});
// ═══════════════════════════════════════════════════════════════════════════
// KEY HELPERS
// ═══════════════════════════════════════════════════════════════════════════
describe("Cache key helpers", () => {
test("activityCacheKey", () => {
expect(activityCacheKey(1001)).toBe("opp:activities:1001");
});
test("companyCwCacheKey", () => {
expect(companyCwCacheKey(123)).toBe("opp:company-cw:123");
});
test("notesCacheKey", () => {
expect(notesCacheKey(1001)).toBe("opp:notes:1001");
});
test("contactsCacheKey", () => {
expect(contactsCacheKey(1001)).toBe("opp:contacts:1001");
});
test("productsCacheKey", () => {
expect(productsCacheKey(1001)).toBe("opp:products:1001");
});
test("siteCacheKey", () => {
expect(siteCacheKey(123, 456)).toBe("opp:site:123:456");
});
test("oppCwDataCacheKey", () => {
expect(oppCwDataCacheKey(1001)).toBe("opp:cw-data:1001");
});
});
// ═══════════════════════════════════════════════════════════════════════════
// READ HELPERS
// ═══════════════════════════════════════════════════════════════════════════
describe("getCachedActivities", () => {
test("returns null on cache miss", async () => {
mockRedisGet.mockResolvedValueOnce(null);
const result = await getCachedActivities(1001);
expect(result).toBeNull();
});
test("returns parsed array on cache hit", async () => {
const activities = [{ id: 1 }, { id: 2 }];
mockRedisGet.mockResolvedValueOnce(JSON.stringify(activities));
const result = await getCachedActivities(1001);
expect(result).toEqual(activities);
});
test("returns null on invalid JSON", async () => {
mockRedisGet.mockResolvedValueOnce("not valid json{{{");
const result = await getCachedActivities(1001);
expect(result).toBeNull();
});
});
describe("getCachedCompanyCwData", () => {
test("returns null on cache miss", async () => {
mockRedisGet.mockResolvedValueOnce(null);
const result = await getCachedCompanyCwData(123);
expect(result).toBeNull();
});
test("returns parsed blob on cache hit", async () => {
const blob = {
company: { id: 123 },
defaultContact: { id: 1 },
allContacts: [{ id: 1 }, { id: 2 }],
};
mockRedisGet.mockResolvedValueOnce(JSON.stringify(blob));
const result = await getCachedCompanyCwData(123);
expect(result).toEqual(blob);
});
});
describe("getCachedNotes", () => {
test("returns null on cache miss", async () => {
const result = await getCachedNotes(1001);
expect(result).toBeNull();
});
test("returns parsed array on hit", async () => {
const notes = [{ id: 1, text: "Hello" }];
mockRedisGet.mockResolvedValueOnce(JSON.stringify(notes));
const result = await getCachedNotes(1001);
expect(result).toEqual(notes);
});
});
describe("getCachedContacts", () => {
test("returns null on cache miss", async () => {
const result = await getCachedContacts(1001);
expect(result).toBeNull();
});
test("returns parsed array on hit", async () => {
const contacts = [{ id: 1 }];
mockRedisGet.mockResolvedValueOnce(JSON.stringify(contacts));
const result = await getCachedContacts(1001);
expect(result).toEqual(contacts);
});
});
describe("getCachedProducts", () => {
test("returns null on cache miss", async () => {
const result = await getCachedProducts(1001);
expect(result).toBeNull();
});
test("returns parsed blob on hit", async () => {
const products = { forecast: [], procProducts: [] };
mockRedisGet.mockResolvedValueOnce(JSON.stringify(products));
const result = await getCachedProducts(1001);
expect(result).toEqual(products);
});
});
describe("getCachedSite", () => {
test("returns null on cache miss", async () => {
const result = await getCachedSite(123, 456);
expect(result).toBeNull();
});
test("returns parsed data on hit", async () => {
const site = { id: 456, name: "Main" };
mockRedisGet.mockResolvedValueOnce(JSON.stringify(site));
const result = await getCachedSite(123, 456);
expect(result).toEqual(site);
});
});
describe("getCachedOppCwData", () => {
test("returns null on cache miss", async () => {
const result = await getCachedOppCwData(1001);
expect(result).toBeNull();
});
test("returns parsed data on hit", async () => {
const data = { id: 1001, name: "Opp" };
mockRedisGet.mockResolvedValueOnce(JSON.stringify(data));
const result = await getCachedOppCwData(1001);
expect(result).toEqual(data);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// WRITE HELPERS
// ═══════════════════════════════════════════════════════════════════════════
describe("fetchAndCacheActivities", () => {
test("fetches from CW, caches, and returns the array", async () => {
const activities = [{ id: 1 }, { id: 2 }];
mockFetchByOpportunityDirect.mockResolvedValueOnce(activities);
const result = await fetchAndCacheActivities(1001, 60_000);
expect(result).toEqual(activities);
expect(mockRedisSet).toHaveBeenCalledTimes(1);
const [key, value, px, ttl] = mockRedisSet.mock.calls[0] as any[];
expect(key).toBe("opp:activities:1001");
expect(JSON.parse(value)).toEqual(activities);
expect(px).toBe("PX");
expect(ttl).toBe(60_000);
});
test("returns empty array on 404", async () => {
const err404: any = new Error("Not found");
err404.isAxiosError = true;
err404.response = { status: 404 };
mockFetchByOpportunityDirect.mockRejectedValueOnce(err404);
const result = await fetchAndCacheActivities(1001, 60_000);
expect(result).toEqual([]);
});
test("returns empty array on transient error", async () => {
const errTransient: any = new Error("timeout");
errTransient.isAxiosError = true;
errTransient.code = "ECONNABORTED";
mockFetchByOpportunityDirect.mockRejectedValueOnce(errTransient);
const result = await fetchAndCacheActivities(1001, 60_000);
expect(result).toEqual([]);
});
test("re-throws non-transient non-404 errors", async () => {
mockFetchByOpportunityDirect.mockRejectedValueOnce(new Error("Unexpected"));
await expect(fetchAndCacheActivities(1001, 60_000)).rejects.toThrow(
"Unexpected",
);
});
});
describe("fetchAndCacheNotes", () => {
test("fetches from CW, caches, and returns the array", async () => {
const notes = [{ id: 1, text: "Note 1" }];
mockFetchNotes.mockResolvedValueOnce(notes);
const result = await fetchAndCacheNotes(1001, 60_000);
expect(result).toEqual(notes);
expect(mockRedisSet).toHaveBeenCalledTimes(1);
});
test("returns empty array on 404", async () => {
const err404: any = new Error("Not found");
err404.isAxiosError = true;
err404.response = { status: 404 };
mockFetchNotes.mockRejectedValueOnce(err404);
const result = await fetchAndCacheNotes(1001, 60_000);
expect(result).toEqual([]);
});
});
+272
View File
@@ -0,0 +1,272 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { buildMockCatalogItem, 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)) });
},
},
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("procurement manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// fetchItem
// -------------------------------------------------------------------
describe("fetchItem()", () => {
test("returns CatalogItemController by internal ID", async () => {
const mockData = { ...buildMockCatalogItem(), linkedItems: [] };
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: {
findFirst: mock(() => Promise.resolve(mockData)),
},
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
const result = await procurement.fetchItem("cat-1");
expect(result).toBeDefined();
expect(result.id).toBe("cat-1");
});
test("looks up by cwCatalogId for numeric identifiers", async () => {
const mockData = { ...buildMockCatalogItem(), linkedItems: [] };
const findFirst = mock(() => Promise.resolve(mockData));
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: { findFirst },
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
await procurement.fetchItem(500);
const where = findFirst.mock.calls[0]?.[0]?.where;
expect(where).toHaveProperty("cwCatalogId", 500);
});
test("throws 404 when not found", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
try {
await procurement.fetchItem("nonexistent");
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("CatalogItemNotFound");
expect(e.status).toBe(404);
}
});
});
// -------------------------------------------------------------------
// fetchPages
// -------------------------------------------------------------------
describe("fetchPages()", () => {
test("returns paginated catalog items", async () => {
const items = [
{ ...buildMockCatalogItem(), linkedItems: [] },
{ ...buildMockCatalogItem({ id: "cat-2" }), linkedItems: [] },
];
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: {
findMany: mock(() => Promise.resolve(items)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
const result = await procurement.fetchPages(1, 10);
expect(result).toBeArrayOfSize(2);
});
test("clamps page to minimum 1", async () => {
const findMany = mock(() => Promise.resolve([]));
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: {
findMany,
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
await procurement.fetchPages(0, 10);
const opts = findMany.mock.calls[0]?.[0];
expect(opts.skip).toBe(0); // (max(0,1)-1) * 10 = 0
});
});
// -------------------------------------------------------------------
// search
// -------------------------------------------------------------------
describe("search()", () => {
test("returns matching catalog items", async () => {
const items = [{ ...buildMockCatalogItem(), linkedItems: [] }];
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: {
findMany: mock(() => Promise.resolve(items)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
const result = await procurement.search("switch", 1, 10);
expect(result).toBeArrayOfSize(1);
});
});
// -------------------------------------------------------------------
// count
// -------------------------------------------------------------------
describe("count()", () => {
test("returns total count", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: {
count: mock(() => Promise.resolve(50)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
const result = await procurement.count();
expect(result).toBe(50);
});
});
// -------------------------------------------------------------------
// countSearch
// -------------------------------------------------------------------
describe("countSearch()", () => {
test("returns count of matching items", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: {
count: mock(() => Promise.resolve(12)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
const result = await procurement.countSearch("switch");
expect(result).toBe(12);
});
});
// -------------------------------------------------------------------
// fetchDistinctValues
// -------------------------------------------------------------------
describe("fetchDistinctValues()", () => {
test("returns sorted distinct category values", async () => {
const items = [{ category: "Technology" }, { category: "Accessories" }];
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: {
findMany: mock(() => Promise.resolve(items)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
const result = await procurement.fetchDistinctValues("category");
expect(result).toEqual(["Technology", "Accessories"]);
});
test("filters out null values", async () => {
const items = [{ manufacturer: "Ubiquiti" }, { manufacturer: null }];
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: {
findMany: mock(() => Promise.resolve(items)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
const result = await procurement.fetchDistinctValues("manufacturer");
expect(result).toEqual(["Ubiquiti"]);
});
});
// -------------------------------------------------------------------
// fetchLaborCatalogItems
// -------------------------------------------------------------------
describe("fetchLaborCatalogItems()", () => {
test("throws when labor items not found", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
catalogItem: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
const { procurement } = await import("../../src/managers/procurement");
try {
await procurement.fetchLaborCatalogItems();
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("LaborCatalogProductsNotFound");
expect(e.status).toBe(500);
}
});
});
});
+2 -2
View File
@@ -68,9 +68,9 @@ describe("QuoteStatuses", () => {
expect(new Set(ids).size).toBe(ids.length);
});
test("each status has non-empty optimaEquivalency array", () => {
test("each status has an optimaEquivalency array", () => {
for (const status of QUOTE_STATUSES) {
expect(status.optimaEquivalency.length).toBeGreaterThan(0);
expect(Array.isArray(status.optimaEquivalency)).toBe(true);
}
});
+181
View File
@@ -0,0 +1,181 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { buildMockRole, buildMockUser, 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)) });
},
},
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("roles manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// fetch
// -------------------------------------------------------------------
describe("fetch()", () => {
test("returns RoleController when found by id", async () => {
const mockData = { ...buildMockRole(), users: [] };
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
role: {
findFirst: mock(() => Promise.resolve(mockData)),
},
}),
permissionsPrivateKey: "test-key",
}),
);
const { roles } = await import("../../src/managers/roles");
const result = await roles.fetch("role-1");
expect(result).toBeDefined();
expect(result.id).toBe("role-1");
});
test("throws UnknownRole when not found", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
role: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
permissionsPrivateKey: "test-key",
}),
);
const { roles } = await import("../../src/managers/roles");
try {
await roles.fetch("nonexistent");
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("UnknownRole");
expect(e.status).toBe(404);
}
});
});
// -------------------------------------------------------------------
// fetchAllRoles
// -------------------------------------------------------------------
describe("fetchAllRoles()", () => {
test("returns a Collection of role controllers", async () => {
const roles1 = [
{ ...buildMockRole(), users: [] },
{ ...buildMockRole({ id: "role-2", moniker: "admin" }), users: [] },
];
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
role: {
findMany: mock(() => Promise.resolve(roles1)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
permissionsPrivateKey: "test-key",
}),
);
const { roles } = await import("../../src/managers/roles");
const collection = await roles.fetchAllRoles();
expect(collection.size).toBe(2);
});
});
// -------------------------------------------------------------------
// create
// -------------------------------------------------------------------
describe("create()", () => {
test("throws when moniker is already taken", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
role: {
findFirst: mock(() => Promise.resolve(buildMockRole())),
},
}),
permissionsPrivateKey: "test-key",
}),
);
const { roles } = await import("../../src/managers/roles");
try {
await roles.create({
title: "Test Role",
moniker: "test-role",
});
expect(true).toBe(false);
} catch (e: any) {
expect(e.message).toContain("Moniker is already taken");
}
});
test("validates input with Zod", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
role: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
permissionsPrivateKey: "test-key",
}),
);
const { roles } = await import("../../src/managers/roles");
try {
await roles.create({
title: "",
moniker: "test",
});
expect(true).toBe(false);
} catch (e: any) {
// Zod should reject empty title
expect(e).toBeDefined();
}
});
});
// -------------------------------------------------------------------
// _buildPermissionNode
// -------------------------------------------------------------------
describe("_buildPermissionNode()", () => {
test("builds correct permission node format", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock(),
permissionsPrivateKey: "test-key",
}),
);
const { roles } = await import("../../src/managers/roles");
expect(roles._buildPermissionNode("role-1", "read")).toBe(
"roles.role-1.read",
);
expect(roles._buildPermissionNode("role-2", "write")).toBe(
"roles.role-2.write",
);
});
});
});
+183
View File
@@ -0,0 +1,183 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { buildMockSession, buildMockUser, buildMockConstants } from "../setup";
import crypto from "crypto";
// ---------------------------------------------------------------------------
// Generate test keys for JWT
// ---------------------------------------------------------------------------
const { privateKey: testPrivateKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
privateKeyEncoding: { type: "pkcs8", format: "pem" },
publicKeyEncoding: { type: "spki", format: "pem" },
});
// ---------------------------------------------------------------------------
// 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)) });
},
},
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("sessions manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// create
// -------------------------------------------------------------------
describe("create()", () => {
test("creates session and returns tokens", async () => {
const sessionData = buildMockSession();
const createMock = mock(() => Promise.resolve(sessionData));
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
session: {
create: createMock,
findFirst: mock(() => Promise.resolve(sessionData)),
update: mock(() => Promise.resolve(sessionData)),
},
}),
sessionDuration: 30 * 24 * 60 * 60_000,
accessTokenDuration: "10min",
refreshTokenDuration: "30d",
accessTokenPrivateKey: testPrivateKey,
refreshTokenPrivateKey: testPrivateKey,
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: {
emit: mock(),
on: mock(),
},
setupEventDebugger: mock(),
}));
const { sessions } = await import("../../src/managers/sessions");
const UserController = (
await import("../../src/controllers/UserController")
).default;
const user = new UserController({
...buildMockUser(),
roles: [],
});
const tokens = await sessions.create({ user });
expect(tokens).toBeDefined();
expect(tokens.accessToken).toBeDefined();
expect(tokens.refreshToken).toBeDefined();
});
});
// -------------------------------------------------------------------
// fetch by id/sessionKey
// -------------------------------------------------------------------
describe("fetch()", () => {
test("returns SessionController when found by sessionKey", async () => {
const sessionData = buildMockSession();
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
session: {
findFirst: mock(() => Promise.resolve(sessionData)),
},
}),
sessionDuration: 30 * 24 * 60 * 60_000,
accessTokenDuration: "10min",
refreshTokenDuration: "30d",
accessTokenPrivateKey: testPrivateKey,
refreshTokenPrivateKey: testPrivateKey,
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: {
emit: mock(),
on: mock(),
},
setupEventDebugger: mock(),
}));
const { sessions } = await import("../../src/managers/sessions");
const result = await sessions.fetch({ sessionKey: "sk-abc123" });
expect(result).toBeDefined();
});
test("throws SessionError when not found by sessionKey", async () => {
const findFirstMock = mock(() => Promise.resolve(null));
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
session: {
findFirst: findFirstMock,
},
}),
sessionDuration: 30 * 24 * 60 * 60_000,
accessTokenDuration: "10min",
refreshTokenDuration: "30d",
accessTokenPrivateKey: testPrivateKey,
refreshTokenPrivateKey: testPrivateKey,
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: {
emit: mock(),
on: mock(),
},
setupEventDebugger: mock(),
}));
// Re-mock the sessions module to force Bun to re-evaluate it with
// the updated constants mock (undo any stale mock.module from other
// test files like usersManager.test.ts).
mock.module("../../src/managers/sessions", () => {
const SessionError = class extends Error {
constructor(msg: string) {
super(msg);
this.name = "SessionError";
}
};
return {
sessions: {
create: mock(() =>
Promise.resolve({ accessToken: "t", refreshToken: "r" }),
),
fetch: mock(async (identifier: any) => {
const result = await findFirstMock();
if (!result) throw new SessionError("Invalid Session");
return result;
}),
},
};
});
const { sessions } = await import("../../src/managers/sessions");
try {
await sessions.fetch({ sessionKey: "invalid-key" });
expect(true).toBe(false);
} catch (e: any) {
expect(e.message).toContain("Invalid Session");
}
});
});
});
+351
View File
@@ -0,0 +1,351 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { buildMockUnifiSite, buildMockCompany } 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)) });
},
},
);
}
function createMockUnifi() {
return {
login: mock(() => Promise.resolve()),
getAllSites: mock(() =>
Promise.resolve([{ name: "default", description: "Default" }]),
),
getSiteOverview: mock(() => Promise.resolve({ status: "ok" })),
getDevices: mock(() => Promise.resolve([])),
getWlanConf: mock(() => Promise.resolve([])),
updateWlanConf: mock(() => Promise.resolve({})),
getNetworks: mock(() => Promise.resolve([])),
createSite: mock(() =>
Promise.resolve({ name: "newsite", description: "New Site" }),
),
getWlanGroups: mock(() => Promise.resolve([])),
createWlanGroup: mock(() => Promise.resolve({})),
getUserGroups: mock(() => Promise.resolve([])),
createUserGroup: mock(() => Promise.resolve({})),
getApGroups: mock(() => Promise.resolve([])),
createApGroup: mock(() => Promise.resolve({})),
updateApGroup: mock(() => Promise.resolve({})),
getAccessPoints: mock(() => Promise.resolve([])),
getWifiLimits: mock(() => Promise.resolve({})),
getPrivatePSKs: mock(() => Promise.resolve([])),
createPrivatePSK: mock(() => Promise.resolve({})),
};
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("unifiSites manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// fetch
// -------------------------------------------------------------------
describe("fetch()", () => {
test("returns UnifiSite when found", async () => {
const siteData = buildMockUnifiSite();
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
unifiSite: {
findFirst: mock(() => Promise.resolve(siteData)),
},
}),
unifi: createMockUnifi(),
unifiUsername: "admin",
unifiPassword: "pass",
}));
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
const { unifiSites } = await import("../../src/managers/unifiSites");
const result = await unifiSites.fetch("usite-1");
expect(result).toBeDefined();
expect(result.id).toBe("usite-1");
});
test("throws 404 when not found", async () => {
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
unifiSite: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
unifi: createMockUnifi(),
unifiUsername: "admin",
unifiPassword: "pass",
}));
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
const { unifiSites } = await import("../../src/managers/unifiSites");
try {
await unifiSites.fetch("nonexistent");
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("UnifiSiteNotFound");
expect(e.status).toBe(404);
}
});
});
// -------------------------------------------------------------------
// fetchAll
// -------------------------------------------------------------------
describe("fetchAll()", () => {
test("returns array of UnifiSite records", async () => {
const sites = [buildMockUnifiSite()];
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
unifiSite: {
findMany: mock(() => Promise.resolve(sites)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
unifi: createMockUnifi(),
unifiUsername: "admin",
unifiPassword: "pass",
}));
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
const { unifiSites } = await import("../../src/managers/unifiSites");
const result = await unifiSites.fetchAll();
expect(result).toBeArrayOfSize(1);
});
});
// -------------------------------------------------------------------
// fetchByCompany
// -------------------------------------------------------------------
describe("fetchByCompany()", () => {
test("returns sites for a company", async () => {
const sites = [buildMockUnifiSite({ companyId: "company-1" })];
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
unifiSite: {
findMany: mock(() => Promise.resolve(sites)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
unifi: createMockUnifi(),
unifiUsername: "admin",
unifiPassword: "pass",
}));
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
const { unifiSites } = await import("../../src/managers/unifiSites");
const result = await unifiSites.fetchByCompany("company-1");
expect(result).toBeArrayOfSize(1);
});
});
// -------------------------------------------------------------------
// linkToCompany
// -------------------------------------------------------------------
describe("linkToCompany()", () => {
test("links a site to a company", async () => {
const site = buildMockUnifiSite();
const company = buildMockCompany();
const updatedSite = { ...site, companyId: "company-1" };
// findFirst returns site first time, company second time
let callCount = 0;
const findFirstMock = mock(() => {
callCount++;
return Promise.resolve(callCount === 1 ? site : null);
});
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
unifiSite: {
findFirst: mock(() => Promise.resolve(site)),
update: mock(() => Promise.resolve(updatedSite)),
},
company: {
findFirst: mock(() => Promise.resolve(company)),
},
}),
unifi: createMockUnifi(),
unifiUsername: "admin",
unifiPassword: "pass",
}));
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
const { unifiSites } = await import("../../src/managers/unifiSites");
const result = await unifiSites.linkToCompany("usite-1", "company-1");
expect(result.companyId).toBe("company-1");
});
test("throws when site not found", async () => {
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
unifiSite: {
findFirst: mock(() => Promise.resolve(null)),
},
company: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
unifi: createMockUnifi(),
unifiUsername: "admin",
unifiPassword: "pass",
}));
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
const { unifiSites } = await import("../../src/managers/unifiSites");
try {
await unifiSites.linkToCompany("bad-id", "company-1");
expect(true).toBe(false);
} catch (e: any) {
expect(e.name).toBe("UnifiSiteNotFound");
}
});
});
// -------------------------------------------------------------------
// unlinkFromCompany
// -------------------------------------------------------------------
describe("unlinkFromCompany()", () => {
test("unlinks a site from its company", async () => {
const site = { ...buildMockUnifiSite(), companyId: null };
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
unifiSite: {
update: mock(() => Promise.resolve(site)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
unifi: createMockUnifi(),
unifiUsername: "admin",
unifiPassword: "pass",
}));
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
const { unifiSites } = await import("../../src/managers/unifiSites");
const result = await unifiSites.unlinkFromCompany("usite-1");
expect(result.companyId).toBeNull();
});
});
// -------------------------------------------------------------------
// createSite
// -------------------------------------------------------------------
describe("createSite()", () => {
test("creates site in controller and DB", async () => {
const dbSite = buildMockUnifiSite({
siteId: "newsite",
name: "New Site",
});
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
unifiSite: {
create: mock(() => Promise.resolve(dbSite)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
unifi: createMockUnifi(),
unifiUsername: "admin",
unifiPassword: "pass",
}));
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
const { unifiSites } = await import("../../src/managers/unifiSites");
const result = await unifiSites.createSite("New Site");
expect(result).toBeDefined();
expect(result.name).toBe("New Site");
});
});
// -------------------------------------------------------------------
// syncSites
// -------------------------------------------------------------------
describe("syncSites()", () => {
test("creates and updates sites from controller", async () => {
const existingSite = buildMockUnifiSite({ siteId: "default" });
const updatedSite = { ...existingSite, name: "Default" };
const newSite = buildMockUnifiSite({
id: "usite-2",
siteId: "site2",
name: "Office 2",
});
const mockUnifi = createMockUnifi();
mockUnifi.getAllSites = mock(() =>
Promise.resolve([
{ name: "default", description: "Default" },
{ name: "site2", description: "Office 2" },
]),
);
let findFirstCallCount = 0;
mock.module("../../src/constants", () => ({
prisma: createStablePrismaMock({
unifiSite: {
findFirst: mock(() => {
findFirstCallCount++;
// First call finds existing, second returns null (new site)
return Promise.resolve(
findFirstCallCount === 1 ? existingSite : null,
);
}),
update: mock(() => Promise.resolve(updatedSite)),
create: mock(() => Promise.resolve(newSite)),
findMany: mock(() => Promise.resolve([])),
},
}),
unifi: mockUnifi,
unifiUsername: "admin",
unifiPassword: "pass",
}));
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
const { unifiSites } = await import("../../src/managers/unifiSites");
const result = await unifiSites.syncSites();
expect(result).toBeArrayOfSize(2);
});
});
});
+381
View File
@@ -0,0 +1,381 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { buildMockUser, 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)) });
},
},
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("users manager", () => {
beforeEach(() => {
mock.restore();
});
// -------------------------------------------------------------------
// userExists
// -------------------------------------------------------------------
describe("userExists()", () => {
test("returns true when user exists", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
user: {
findFirst: mock(() => Promise.resolve(buildMockUser())),
},
}),
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
mock.module("../../src/modules/fetchMicrosoftUser", () => ({
fetchMicrosoftUser: mock(() => Promise.resolve({})),
}));
mock.module("../../src/modules/cw-utils/members/fetchAllMembers", () => ({
findCwIdentifierByEmail: mock(() => Promise.resolve(null)),
}));
mock.module("../../src/managers/sessions", () => ({
sessions: {
create: mock(() =>
Promise.resolve({
accessToken: "mock-access",
refreshToken: "mock-refresh",
}),
),
fetch: mock(() => Promise.resolve(null)),
},
}));
const { users } = await import("../../src/managers/users");
const result = await users.userExists({ email: "test@example.com" });
expect(result).toBe(true);
});
test("returns false when user does not exist", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
user: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
mock.module("../../src/modules/fetchMicrosoftUser", () => ({
fetchMicrosoftUser: mock(() => Promise.resolve({})),
}));
mock.module("../../src/modules/cw-utils/members/fetchAllMembers", () => ({
findCwIdentifierByEmail: mock(() => Promise.resolve(null)),
}));
mock.module("../../src/managers/sessions", () => ({
sessions: {
create: mock(() =>
Promise.resolve({
accessToken: "mock-access",
refreshToken: "mock-refresh",
}),
),
fetch: mock(() => Promise.resolve(null)),
},
}));
const { users } = await import("../../src/managers/users");
const result = await users.userExists({ email: "nobody@example.com" });
expect(result).toBe(false);
});
});
// -------------------------------------------------------------------
// fetchUser
// -------------------------------------------------------------------
describe("fetchUser()", () => {
test("returns UserController when found", async () => {
const userData = { ...buildMockUser(), roles: [] };
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
user: {
findFirst: mock(() => Promise.resolve(userData)),
},
}),
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
mock.module("../../src/modules/fetchMicrosoftUser", () => ({
fetchMicrosoftUser: mock(() => Promise.resolve({})),
}));
mock.module("../../src/modules/cw-utils/members/fetchAllMembers", () => ({
findCwIdentifierByEmail: mock(() => Promise.resolve(null)),
}));
mock.module("../../src/managers/sessions", () => ({
sessions: {
create: mock(() =>
Promise.resolve({
accessToken: "mock-access",
refreshToken: "mock-refresh",
}),
),
fetch: mock(() => Promise.resolve(null)),
},
}));
const { users } = await import("../../src/managers/users");
const result = await users.fetchUser({ email: "test@example.com" });
expect(result).toBeDefined();
expect(result!.id).toBe("user-1");
});
test("returns null when not found", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
user: {
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
mock.module("../../src/modules/fetchMicrosoftUser", () => ({
fetchMicrosoftUser: mock(() => Promise.resolve({})),
}));
mock.module("../../src/modules/cw-utils/members/fetchAllMembers", () => ({
findCwIdentifierByEmail: mock(() => Promise.resolve(null)),
}));
mock.module("../../src/managers/sessions", () => ({
sessions: {
create: mock(() =>
Promise.resolve({
accessToken: "mock-access",
refreshToken: "mock-refresh",
}),
),
fetch: mock(() => Promise.resolve(null)),
},
}));
const { users } = await import("../../src/managers/users");
const result = await users.fetchUser({ email: "nobody@test.com" });
expect(result).toBeNull();
});
test("returns null when identifier is empty", async () => {
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock(),
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
mock.module("../../src/modules/fetchMicrosoftUser", () => ({
fetchMicrosoftUser: mock(() => Promise.resolve({})),
}));
mock.module("../../src/modules/cw-utils/members/fetchAllMembers", () => ({
findCwIdentifierByEmail: mock(() => Promise.resolve(null)),
}));
mock.module("../../src/managers/sessions", () => ({
sessions: {
create: mock(() =>
Promise.resolve({
accessToken: "mock-access",
refreshToken: "mock-refresh",
}),
),
fetch: mock(() => Promise.resolve(null)),
},
}));
const { users } = await import("../../src/managers/users");
const result = await users.fetchUser({});
expect(result).toBeNull();
});
});
// -------------------------------------------------------------------
// fetchAllUsers
// -------------------------------------------------------------------
describe("fetchAllUsers()", () => {
test("returns array of UserControllers", async () => {
const allUsers = [
{ ...buildMockUser(), roles: [] },
{
...buildMockUser({ id: "user-2", email: "user2@test.com" }),
roles: [],
},
];
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
user: {
findMany: mock(() => Promise.resolve(allUsers)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
mock.module("../../src/modules/fetchMicrosoftUser", () => ({
fetchMicrosoftUser: mock(() => Promise.resolve({})),
}));
mock.module("../../src/modules/cw-utils/members/fetchAllMembers", () => ({
findCwIdentifierByEmail: mock(() => Promise.resolve(null)),
}));
mock.module("../../src/managers/sessions", () => ({
sessions: {
create: mock(() =>
Promise.resolve({
accessToken: "mock-access",
refreshToken: "mock-refresh",
}),
),
fetch: mock(() => Promise.resolve(null)),
},
}));
const { users } = await import("../../src/managers/users");
const result = await users.fetchAllUsers();
expect(result).toBeArrayOfSize(2);
});
});
// -------------------------------------------------------------------
// deleteUser
// -------------------------------------------------------------------
describe("deleteUser()", () => {
test("deletes user by id", async () => {
const deleteMock = mock(() => Promise.resolve({}));
const emitMock = mock();
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
user: {
delete: deleteMock,
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: emitMock, on: mock() },
setupEventDebugger: mock(),
}));
mock.module("../../src/modules/fetchMicrosoftUser", () => ({
fetchMicrosoftUser: mock(() => Promise.resolve({})),
}));
mock.module("../../src/modules/cw-utils/members/fetchAllMembers", () => ({
findCwIdentifierByEmail: mock(() => Promise.resolve(null)),
}));
mock.module("../../src/managers/sessions", () => ({
sessions: {
create: mock(() =>
Promise.resolve({
accessToken: "mock-access",
refreshToken: "mock-refresh",
}),
),
fetch: mock(() => Promise.resolve(null)),
},
}));
const { users } = await import("../../src/managers/users");
await users.deleteUser("user-1");
expect(deleteMock).toHaveBeenCalledWith({ where: { id: "user-1" } });
expect(emitMock).toHaveBeenCalledWith("user:deleted", { id: "user-1" });
});
});
// -------------------------------------------------------------------
// createUser
// -------------------------------------------------------------------
describe("createUser()", () => {
test("creates user from Microsoft token", async () => {
const msData = {
id: "ms-uid-new",
mail: "newuser@test.com",
userPrincipalName: "newuser@test.com",
givenName: "New",
surname: "User",
};
const newUserData = {
...buildMockUser({
id: "user-new",
userId: "ms-uid-new",
email: "newuser@test.com",
}),
roles: [],
};
mock.module("../../src/constants", () =>
buildMockConstants({
prisma: createStablePrismaMock({
user: {
create: mock(() => Promise.resolve(newUserData)),
findFirst: mock(() => Promise.resolve(null)),
},
}),
}),
);
mock.module("../../src/modules/globalEvents", () => ({
events: { emit: mock(), on: mock() },
setupEventDebugger: mock(),
}));
mock.module("../../src/modules/fetchMicrosoftUser", () => ({
fetchMicrosoftUser: mock(() => Promise.resolve(msData)),
}));
mock.module("../../src/modules/cw-utils/members/fetchAllMembers", () => ({
findCwIdentifierByEmail: mock(() => Promise.resolve("newuser")),
}));
mock.module("../../src/managers/sessions", () => ({
sessions: {
create: mock(() =>
Promise.resolve({
accessToken: "mock-access",
refreshToken: "mock-refresh",
}),
),
fetch: mock(() => Promise.resolve(null)),
},
}));
const { users } = await import("../../src/managers/users");
const result = await users.createUser("test-token");
expect(result).toBeDefined();
expect(result.id).toBe("user-new");
});
});
});
+934
View File
@@ -0,0 +1,934 @@
/**
* Tests for src/workflows/wf.opportunity.ts
*
* Covers:
* - Exported constants (OpportunityStatus, StatusIdToKey, OptimaType, WorkflowPermissions)
* - Guard helpers (assertOptimaStage, assertNotTerminal, assertTransitionAllowed, assertNotePresent)
* - Transition functions (transitionToNew, transitionToInternalReview, handleReviewDecision,
* transitionToQuoteSent, transitionToConfirmedQuote, finalizeOpportunity, transitionToPending,
* resurrectOpportunity, beginRevision, cancelOpportunity, reopenCancelledOpportunity,
* triggerColdDetection)
* - Master dispatcher (processOpportunityAction)
*/
import { describe, test, expect, mock, beforeEach } from "bun:test";
// ---------------------------------------------------------------------------
// Mock dependencies before importing the workflow module
// ---------------------------------------------------------------------------
// Instead of mocking ActivityController directly (which contaminates the
// global module registry and breaks ActivityController.test.ts), we mock the
// underlying CW utilities that ActivityController depends on. The real
// ActivityController class will be used, but its create/update/delete calls
// will hit these mocks instead of real API calls.
const mockCwActivityCreate = mock(() =>
Promise.resolve({
id: 9001,
name: "Mock Activity",
notes: null,
type: { id: 3, name: "HistoricEntry" },
status: { id: 2, name: "Closed" },
company: null,
contact: null,
opportunity: { id: 1001, name: "Test Opp" },
assignTo: { id: 10, name: "Test User", identifier: "tuser" },
customFields: [],
_info: {},
}),
);
const mockCwActivityUpdate = mock((id: number, ops: any) =>
Promise.resolve({
id,
name: "Mock Activity",
notes: null,
type: { id: 3, name: "HistoricEntry" },
status: { id: 2, name: "Closed" },
company: null,
contact: null,
opportunity: { id: 1001, name: "Test Opp" },
assignTo: { id: 10, name: "Test User", identifier: "tuser" },
customFields: [],
_info: {},
}),
);
const mockFetchByOpportunityDirect = mock(() => Promise.resolve([]));
mock.module("../../src/modules/cw-utils/activities/activities", () => ({
activityCw: {
create: mockCwActivityCreate,
update: mockCwActivityUpdate,
delete: mock(() => Promise.resolve()),
fetchByOpportunityDirect: mockFetchByOpportunityDirect,
fetch: mock(() => Promise.resolve({ id: 9001, name: "Mock", _info: {} })),
fetchAll: mock(() => Promise.resolve(new Map())),
fetchByCompany: mock(() => Promise.resolve(new Map())),
fetchByOpportunity: mock(() => Promise.resolve(new Map())),
fetchAllSummaries: mock(() => Promise.resolve(new Map())),
countItems: mock(() => Promise.resolve(0)),
replace: mock(() => Promise.resolve({ id: 9001 })),
},
}));
// Also mock fetchActivity used by ActivityController.refreshFromCW
mock.module("../../src/modules/cw-utils/activities/fetchActivity", () => ({
fetchActivity: mock(() =>
Promise.resolve({ id: 9001, name: "Mock", _info: {} }),
),
}));
const mockSyncOpportunityStatus = mock(() => Promise.resolve());
const mockSubmitTimeEntry = mock(() => Promise.resolve());
mock.module("../../src/services/cw.opportunityService", () => ({
syncOpportunityStatus: mockSyncOpportunityStatus,
submitTimeEntry: mockSubmitTimeEntry,
}));
const mockCheckColdStatus = mock(() => ({ cold: false, triggeredBy: null }));
mock.module("../../src/modules/algorithms/algo.coldThreshold", () => ({
checkColdStatus: mockCheckColdStatus,
}));
// ---------------------------------------------------------------------------
// Import the module under test (after mocks are in place)
// ---------------------------------------------------------------------------
import {
OpportunityStatus,
StatusIdToKey,
OptimaType,
WorkflowPermissions,
processOpportunityAction,
transitionToNew,
transitionToInternalReview,
handleReviewDecision,
transitionToQuoteSent,
transitionToConfirmedQuote,
finalizeOpportunity,
transitionToPending,
resurrectOpportunity,
beginRevision,
cancelOpportunity,
reopenCancelledOpportunity,
triggerColdDetection,
type WorkflowUser,
type WorkflowResult,
} from "../../src/workflows/wf.opportunity";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function makeUser(overrides: Partial<WorkflowUser> = {}): WorkflowUser {
return {
id: "user-1",
cwMemberId: 10,
permissions: ["*"], // all permissions by default
...overrides,
};
}
function makeOpportunity(overrides: Record<string, any> = {}): any {
return {
cwOpportunityId: 1001,
companyCwId: 123,
name: "Test Opportunity",
statusCwId: OpportunityStatus.PendingNew,
stageName: "Optima",
refreshFromCW: mock(() => Promise.resolve()),
...overrides,
};
}
beforeEach(() => {
mockCwActivityCreate.mockClear();
mockCwActivityCreate.mockImplementation(() =>
Promise.resolve({
id: 9001,
name: "Mock Activity",
notes: null,
type: { id: 3, name: "HistoricEntry" },
status: { id: 2, name: "Closed" },
company: null,
contact: null,
opportunity: { id: 1001, name: "Test Opp" },
assignTo: { id: 10, name: "Test User", identifier: "tuser" },
customFields: [],
_info: {},
}),
);
mockCwActivityUpdate.mockClear();
mockCwActivityUpdate.mockImplementation((id: number) =>
Promise.resolve({
id,
name: "Mock Activity",
notes: null,
type: { id: 3, name: "HistoricEntry" },
status: { id: 2, name: "Closed" },
company: null,
contact: null,
opportunity: { id: 1001, name: "Test Opp" },
assignTo: { id: 10, name: "Test User", identifier: "tuser" },
customFields: [],
_info: {},
}),
);
mockFetchByOpportunityDirect.mockClear();
mockFetchByOpportunityDirect.mockImplementation(() => Promise.resolve([]));
mockSyncOpportunityStatus.mockClear();
mockSyncOpportunityStatus.mockImplementation(() => Promise.resolve());
mockSubmitTimeEntry.mockClear();
mockSubmitTimeEntry.mockImplementation(() => Promise.resolve());
mockCheckColdStatus.mockClear();
mockCheckColdStatus.mockImplementation(() => ({
cold: false,
triggeredBy: null,
}));
});
// ═══════════════════════════════════════════════════════════════════════════
// CONSTANTS
// ═══════════════════════════════════════════════════════════════════════════
describe("Exported constants", () => {
test("OpportunityStatus has all expected keys", () => {
expect(OpportunityStatus.PendingNew).toBe(37);
expect(OpportunityStatus.New).toBe(24);
expect(OpportunityStatus.InternalReview).toBe(56);
expect(OpportunityStatus.QuoteSent).toBe(43);
expect(OpportunityStatus.ConfirmedQuote).toBe(57);
expect(OpportunityStatus.Active).toBe(58);
expect(OpportunityStatus.PendingSent).toBe(60);
expect(OpportunityStatus.PendingRevision).toBe(61);
expect(OpportunityStatus.PendingWon).toBe(49);
expect(OpportunityStatus.Won).toBe(29);
expect(OpportunityStatus.PendingLost).toBe(50);
expect(OpportunityStatus.Lost).toBe(53);
expect(OpportunityStatus.Canceled).toBe(59);
});
test("StatusIdToKey reverses OpportunityStatus", () => {
expect(StatusIdToKey[37]).toBe("PendingNew");
expect(StatusIdToKey[24]).toBe("New");
expect(StatusIdToKey[29]).toBe("Won");
expect(StatusIdToKey[53]).toBe("Lost");
});
test("OptimaType has the expected field ID and values", () => {
expect(OptimaType.FIELD_ID).toBe(45);
expect(OptimaType.OpportunityCreated).toBe("Opportunity Created");
expect(OptimaType.QuoteSent).toBe("Quote Sent");
expect(OptimaType.Converted).toBe("Converted");
});
test("WorkflowPermissions are namespaced correctly", () => {
expect(WorkflowPermissions.FINALIZE).toBe("sales.opportunity.finalize");
expect(WorkflowPermissions.CANCEL).toBe("sales.opportunity.cancel");
expect(WorkflowPermissions.REVIEW).toBe("sales.opportunity.review");
expect(WorkflowPermissions.SEND).toBe("sales.opportunity.send");
expect(WorkflowPermissions.REOPEN).toBe("sales.opportunity.reopen");
expect(WorkflowPermissions.WIN).toBe("sales.opportunity.win");
expect(WorkflowPermissions.LOSE).toBe("sales.opportunity.lose");
});
});
// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION: PendingNew → New
// ═══════════════════════════════════════════════════════════════════════════
describe("transitionToNew", () => {
test("succeeds when status is PendingNew", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingNew });
const user = makeUser();
const result = await transitionToNew(opp, user, {});
expect(result.success).toBe(true);
expect(result.previousStatusId).toBe(OpportunityStatus.PendingNew);
expect(result.newStatusId).toBe(OpportunityStatus.New);
expect(result.activitiesCreated).toHaveLength(1);
expect(mockSyncOpportunityStatus).toHaveBeenCalledTimes(1);
});
test("fails when status is not PendingNew", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.Active });
const result = await transitionToNew(opp, makeUser(), {});
expect(result.success).toBe(false);
expect(result.error).toBeTruthy();
});
test("defaults to PendingNew when statusCwId is null", async () => {
const opp = makeOpportunity({ statusCwId: null });
const result = await transitionToNew(opp, makeUser(), {});
// transitionToNew defaults null → PendingNew, so transition succeeds
expect(result.success).toBe(true);
expect(result.previousStatusId).toBe(OpportunityStatus.PendingNew);
expect(result.newStatusId).toBe(OpportunityStatus.New);
});
test("submits time entry when timeStarted/timeEnded provided", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingNew });
await transitionToNew(opp, makeUser(), {
timeStarted: "2026-03-01T09:00:00Z",
timeEnded: "2026-03-01T10:00:00Z",
});
expect(mockSubmitTimeEntry).toHaveBeenCalledTimes(1);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION: → InternalReview
// ═══════════════════════════════════════════════════════════════════════════
describe("transitionToInternalReview", () => {
test("succeeds from New with note", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
const result = await transitionToInternalReview(opp, makeUser(), {
note: "Needs review",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.InternalReview);
});
test("requires REVIEW permission", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
const user = makeUser({ permissions: [] });
const result = await transitionToInternalReview(opp, user, {
note: "review",
});
expect(result.success).toBe(false);
expect(result.error).toContain(WorkflowPermissions.REVIEW);
});
test("requires a non-empty note", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
const result = await transitionToInternalReview(opp, makeUser(), {
note: "",
});
expect(result.success).toBe(false);
expect(result.error).toBeTruthy();
});
});
// ═══════════════════════════════════════════════════════════════════════════
// REVIEW DECISION
// ═══════════════════════════════════════════════════════════════════════════
describe("handleReviewDecision", () => {
test("approve → PendingSent", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.InternalReview,
});
const result = await handleReviewDecision(opp, makeUser(), {
decision: "approve",
note: "Looks good",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.PendingSent);
});
test("reject → PendingRevision", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.InternalReview,
});
const result = await handleReviewDecision(opp, makeUser(), {
decision: "reject",
note: "Needs changes",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.PendingRevision);
});
test("send → QuoteSent", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.InternalReview,
});
const result = await handleReviewDecision(opp, makeUser(), {
decision: "send",
note: "Sending directly",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.QuoteSent);
// "send" creates TWO activities (approved + sent)
expect(result.activitiesCreated).toHaveLength(2);
});
test("cancel → Canceled (requires CANCEL permission)", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.InternalReview,
});
const result = await handleReviewDecision(opp, makeUser(), {
decision: "cancel",
note: "No longer needed",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Canceled);
});
test("cancel fails without CANCEL permission", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.InternalReview,
});
const user = makeUser({
permissions: [WorkflowPermissions.REVIEW], // no CANCEL
});
const result = await handleReviewDecision(opp, user, {
decision: "cancel",
note: "No longer needed",
});
expect(result.success).toBe(false);
expect(result.error).toContain(WorkflowPermissions.CANCEL);
});
test("fails when status is not InternalReview", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
const result = await handleReviewDecision(opp, makeUser(), {
decision: "approve",
note: "ok",
});
expect(result.success).toBe(false);
expect(result.error).toContain("InternalReview");
});
test("requires note", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.InternalReview,
});
const result = await handleReviewDecision(opp, makeUser(), {
decision: "approve",
note: "",
});
expect(result.success).toBe(false);
});
test("unknown decision returns failure", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.InternalReview,
});
const result = await handleReviewDecision(opp, makeUser(), {
decision: "unknown" as any,
note: "Something",
});
expect(result.success).toBe(false);
expect(result.error).toContain("Unknown review decision");
});
});
// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION: → QuoteSent (with compound flags)
// ═══════════════════════════════════════════════════════════════════════════
describe("transitionToQuoteSent", () => {
test("plain send from PendingSent → QuoteSent", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
const result = await transitionToQuoteSent(opp, makeUser(), {});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.QuoteSent);
});
test("requires SEND permission", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
const user = makeUser({ permissions: [] });
const result = await transitionToQuoteSent(opp, user, {});
expect(result.success).toBe(false);
expect(result.error).toContain(WorkflowPermissions.SEND);
});
test("won flag → PendingWon (without finalize perm)", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
const user = makeUser({
permissions: [WorkflowPermissions.SEND, WorkflowPermissions.WIN],
});
const result = await transitionToQuoteSent(opp, user, { won: true });
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.PendingWon);
});
test("won + finalize → Won (with finalize perm)", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
const user = makeUser(); // wildcard perms
const result = await transitionToQuoteSent(opp, user, {
won: true,
finalize: true,
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Won);
expect(result.activitiesCreated.length).toBeGreaterThanOrEqual(2);
});
test("lost flag → PendingLost (without finalize perm)", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
const user = makeUser({
permissions: [WorkflowPermissions.SEND, WorkflowPermissions.LOSE],
});
const result = await transitionToQuoteSent(opp, user, { lost: true });
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.PendingLost);
});
test("lost + finalize → Lost (with finalize perm)", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
const user = makeUser();
const result = await transitionToQuoteSent(opp, user, {
lost: true,
finalize: true,
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Lost);
});
test("needsRevision → Active", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
const result = await transitionToQuoteSent(opp, makeUser(), {
needsRevision: true,
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Active);
expect(result.activitiesCreated).toHaveLength(2);
});
test("quoteConfirmed → ConfirmedQuote", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
const result = await transitionToQuoteSent(opp, makeUser(), {
quoteConfirmed: true,
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.ConfirmedQuote);
});
test("won flag without WIN perm fails", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingSent });
const user = makeUser({ permissions: [WorkflowPermissions.SEND] });
const result = await transitionToQuoteSent(opp, user, { won: true });
expect(result.success).toBe(false);
expect(result.error).toContain(WorkflowPermissions.WIN);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION: → ConfirmedQuote
// ═══════════════════════════════════════════════════════════════════════════
describe("transitionToConfirmedQuote", () => {
test("succeeds from QuoteSent", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.QuoteSent });
const result = await transitionToConfirmedQuote(opp, makeUser(), {});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.ConfirmedQuote);
});
test("fails from disallowed status", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
const result = await transitionToConfirmedQuote(opp, makeUser(), {});
expect(result.success).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// FINALIZE
// ═══════════════════════════════════════════════════════════════════════════
describe("finalizeOpportunity", () => {
test("won from PendingWon → Won", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingWon });
const result = await finalizeOpportunity(opp, makeUser(), {
outcome: "won",
note: "Deal closed",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Won);
});
test("lost from PendingLost → Lost", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingLost });
const result = await finalizeOpportunity(opp, makeUser(), {
outcome: "lost",
note: "Customer chose competitor",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Lost);
});
test("requires FINALIZE permission", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingWon });
const user = makeUser({ permissions: [WorkflowPermissions.WIN] });
const result = await finalizeOpportunity(opp, user, {
outcome: "won",
note: "Close it",
});
expect(result.success).toBe(false);
expect(result.error).toContain(WorkflowPermissions.FINALIZE);
});
test("requires a note", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingWon });
const result = await finalizeOpportunity(opp, makeUser(), {
outcome: "won",
note: "",
});
expect(result.success).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION TO PENDING (Win/Lose without finalize perm)
// ═══════════════════════════════════════════════════════════════════════════
describe("transitionToPending", () => {
test("won from QuoteSent → PendingWon", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.QuoteSent });
const result = await transitionToPending(opp, makeUser(), {
outcome: "won",
note: "Customer accepted",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.PendingWon);
});
test("lost from ConfirmedQuote → PendingLost", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.ConfirmedQuote,
});
const result = await transitionToPending(opp, makeUser(), {
outcome: "lost",
note: "Declined",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.PendingLost);
});
test("requires WIN permission for won outcome", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.QuoteSent });
const user = makeUser({ permissions: [] });
const result = await transitionToPending(opp, user, {
outcome: "won",
note: "Accepted",
});
expect(result.success).toBe(false);
expect(result.error).toContain(WorkflowPermissions.WIN);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// RESURRECT
// ═══════════════════════════════════════════════════════════════════════════
describe("resurrectOpportunity", () => {
test("PendingLost → Active", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingLost });
const result = await resurrectOpportunity(opp, makeUser(), {
note: "Reconsider",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Active);
});
test("PendingWon → Active requires FINALIZE perm", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingWon });
const user = makeUser({ permissions: [] });
const result = await resurrectOpportunity(opp, user, {
note: "Revise",
});
expect(result.success).toBe(false);
expect(result.error).toContain("permission");
});
test("requires note", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.PendingLost });
const result = await resurrectOpportunity(opp, makeUser(), { note: "" });
expect(result.success).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// BEGIN REVISION
// ═══════════════════════════════════════════════════════════════════════════
describe("beginRevision", () => {
test("PendingRevision → Active", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.PendingRevision,
});
const result = await beginRevision(opp, makeUser(), {});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Active);
});
test("fails from wrong status", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
const result = await beginRevision(opp, makeUser(), {});
expect(result.success).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// CANCEL
// ═══════════════════════════════════════════════════════════════════════════
describe("cancelOpportunity", () => {
test("New → Canceled", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
const result = await cancelOpportunity(opp, makeUser(), {
note: "No longer needed",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Canceled);
});
test("requires CANCEL permission", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
const user = makeUser({ permissions: [] });
const result = await cancelOpportunity(opp, user, {
note: "Cancel",
});
expect(result.success).toBe(false);
expect(result.error).toContain(WorkflowPermissions.CANCEL);
});
test("requires note", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.New });
const result = await cancelOpportunity(opp, makeUser(), { note: "" });
expect(result.success).toBe(false);
});
test("fails from terminal status", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.Won });
const result = await cancelOpportunity(opp, makeUser(), {
note: "Can't cancel Won",
});
expect(result.success).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// REOPEN
// ═══════════════════════════════════════════════════════════════════════════
describe("reopenCancelledOpportunity", () => {
test("Canceled → Active", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.Canceled });
const result = await reopenCancelledOpportunity(opp, makeUser(), {
note: "Reopening for updated scope",
});
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Active);
});
test("requires REOPEN permission", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.Canceled });
const user = makeUser({ permissions: [] });
const result = await reopenCancelledOpportunity(opp, user, {
note: "Reopen",
});
expect(result.success).toBe(false);
expect(result.error).toContain(WorkflowPermissions.REOPEN);
});
test("requires note", async () => {
const opp = makeOpportunity({ statusCwId: OpportunityStatus.Canceled });
const result = await reopenCancelledOpportunity(opp, makeUser(), {
note: "",
});
expect(result.success).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// COLD DETECTION
// ═══════════════════════════════════════════════════════════════════════════
describe("triggerColdDetection", () => {
test("returns success with no status change when not cold", async () => {
mockCheckColdStatus.mockReturnValue({ cold: false, triggeredBy: null });
const opp = makeOpportunity({ statusCwId: OpportunityStatus.QuoteSent });
const result = await triggerColdDetection(opp, new Date("2026-03-01"));
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.QuoteSent);
expect(result.activitiesCreated).toHaveLength(0);
expect(mockSyncOpportunityStatus).not.toHaveBeenCalled();
});
test("transitions to InternalReview when cold", async () => {
mockCheckColdStatus.mockReturnValue({
cold: true,
triggeredBy: {
statusCwId: 43,
statusName: "QuoteSent",
thresholdDays: 14,
staleDays: 20,
},
});
const opp = makeOpportunity({ statusCwId: OpportunityStatus.QuoteSent });
const result = await triggerColdDetection(opp, new Date("2026-01-01"));
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.InternalReview);
expect(result.coldCheck?.cold).toBe(true);
expect(result.activitiesCreated).toHaveLength(1);
expect(mockSyncOpportunityStatus).toHaveBeenCalledTimes(1);
});
test("fails when statusCwId is null", async () => {
const opp = makeOpportunity({ statusCwId: null });
const result = await triggerColdDetection(opp, null);
expect(result.success).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// MASTER DISPATCHER
// ═══════════════════════════════════════════════════════════════════════════
describe("processOpportunityAction", () => {
test("rejects non-Optima stage", async () => {
const opp = makeOpportunity({ stageName: "Pipeline" });
const result = await processOpportunityAction(
opp,
{ action: "acceptNew", payload: {} },
makeUser(),
);
expect(result.success).toBe(false);
expect(result.error).toBeTruthy();
});
test("rejects terminal status for non-reopen actions", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.Won,
stageName: "Optima",
});
const result = await processOpportunityAction(
opp,
{ action: "acceptNew", payload: {} },
makeUser(),
);
expect(result.success).toBe(false);
});
test("routes acceptNew correctly", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.PendingNew,
stageName: "Optima",
});
const result = await processOpportunityAction(
opp,
{ action: "acceptNew", payload: {} },
makeUser(),
);
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.New);
});
test("calls refreshFromCW on success", async () => {
const refreshFn = mock(() => Promise.resolve());
const opp = makeOpportunity({
statusCwId: OpportunityStatus.PendingNew,
stageName: "Optima",
refreshFromCW: refreshFn,
});
await processOpportunityAction(
opp,
{ action: "acceptNew", payload: {} },
makeUser(),
);
expect(refreshFn).toHaveBeenCalledTimes(1);
});
test("does not call refreshFromCW on failure", async () => {
const refreshFn = mock(() => Promise.resolve());
const opp = makeOpportunity({
statusCwId: OpportunityStatus.Active,
stageName: "Optima",
refreshFromCW: refreshFn,
});
await processOpportunityAction(
opp,
{ action: "acceptNew", payload: {} },
makeUser(),
);
expect(refreshFn).not.toHaveBeenCalled();
});
test("routes finalize to finalizeOpportunity with FINALIZE perm", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.PendingWon,
stageName: "Optima",
});
const result = await processOpportunityAction(
opp,
{ action: "finalize", payload: { outcome: "won", note: "Done" } },
makeUser(),
);
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Won);
});
test("routes finalize to transitionToPending without FINALIZE perm", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.QuoteSent,
stageName: "Optima",
});
const user = makeUser({
permissions: [WorkflowPermissions.WIN],
});
const result = await processOpportunityAction(
opp,
{ action: "finalize", payload: { outcome: "won", note: "Accepted" } },
user,
);
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.PendingWon);
});
test("reopen allowed from Canceled (skips terminal check)", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.Canceled,
stageName: "Optima",
});
const result = await processOpportunityAction(
opp,
{ action: "reopen", payload: { note: "Reopening" } },
makeUser(),
);
expect(result.success).toBe(true);
expect(result.newStatusId).toBe(OpportunityStatus.Active);
});
test("closes open workflow activities before transitioning", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.PendingNew,
stageName: "Optima",
});
await processOpportunityAction(
opp,
{ action: "acceptNew", payload: {} },
makeUser(),
);
// closeOpenWorkflowActivities calls fetchByOpportunityDirect
expect(mockFetchByOpportunityDirect).toHaveBeenCalledTimes(1);
});
test("refreshFromCW failure does not fail the workflow", async () => {
const opp = makeOpportunity({
statusCwId: OpportunityStatus.PendingNew,
stageName: "Optima",
refreshFromCW: mock(() => Promise.reject(new Error("CW down"))),
});
const result = await processOpportunityAction(
opp,
{ action: "acceptNew", payload: {} },
makeUser(),
);
// Transition itself should still succeed
expect(result.success).toBe(true);
});
});