549 lines
16 KiB
TypeScript
549 lines
16 KiB
TypeScript
/**
|
|
* Global test setup — mock heavy external dependencies so unit tests
|
|
* never touch real databases, APIs, or file-system keys.
|
|
*/
|
|
|
|
import { mock } from "bun:test";
|
|
import crypto from "crypto";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Generate a real RSA key pair for modules that call crypto.createPrivateKey()
|
|
// at import time (e.g. readSecureValue.ts).
|
|
// ---------------------------------------------------------------------------
|
|
const { privateKey: _testPrivateKey, publicKey: _testPublicKey } =
|
|
crypto.generateKeyPairSync("rsa", {
|
|
modulusLength: 2048,
|
|
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
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.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
mock.module("../src/constants", () => ({
|
|
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: {},
|
|
collectorSocket: {
|
|
connected: false,
|
|
connect: mock(),
|
|
disconnect: mock(),
|
|
on: mock(),
|
|
emit: mock(),
|
|
},
|
|
connectCollectorSocket: mock(),
|
|
}));
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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: {},
|
|
collectorSocket: {
|
|
connected: false,
|
|
connect: mock(),
|
|
disconnect: mock(),
|
|
on: mock(),
|
|
emit: mock(),
|
|
},
|
|
connectCollectorSocket: mock(),
|
|
...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(
|
|
{},
|
|
{
|
|
get(_target, prop) {
|
|
return mock(() => Promise.resolve(null));
|
|
},
|
|
}
|
|
);
|
|
|
|
return new Proxy(
|
|
{},
|
|
{
|
|
get(_target, prop) {
|
|
if (prop === "$connect" || prop === "$disconnect")
|
|
return mock(() => Promise.resolve());
|
|
return createModelProxy();
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export function createMockUnifi() {
|
|
return {
|
|
login: mock(() => Promise.resolve()),
|
|
getAllSites: mock(() => Promise.resolve([])),
|
|
getSiteOverview: mock(() => Promise.resolve({})),
|
|
getDevices: mock(() => Promise.resolve([])),
|
|
getWlanConf: mock(() => Promise.resolve([])),
|
|
updateWlanConf: mock(() => Promise.resolve({})),
|
|
getNetworks: mock(() => Promise.resolve([])),
|
|
createSite: mock(() =>
|
|
Promise.resolve({ name: "default", description: "Default" })
|
|
),
|
|
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({})),
|
|
};
|
|
}
|
|
|
|
/** Build a minimal Prisma-shaped User row for controller tests. */
|
|
export function buildMockUser(overrides: Record<string, any> = {}) {
|
|
return {
|
|
id: "user-1",
|
|
userId: "ms-uid-1",
|
|
name: "Test User",
|
|
login: "test@example.com",
|
|
email: "test@example.com",
|
|
emailVerified: null,
|
|
image: null,
|
|
token: "ms-token",
|
|
permissions: null,
|
|
createdAt: new Date("2025-01-01"),
|
|
updatedAt: new Date("2025-01-01"),
|
|
roles: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal Prisma-shaped Role row. */
|
|
export function buildMockRole(overrides: Record<string, any> = {}) {
|
|
return {
|
|
id: "role-1",
|
|
title: "Test Role",
|
|
moniker: "test-role",
|
|
permissions: "mock-permissions-token",
|
|
createdAt: new Date("2025-01-01"),
|
|
updatedAt: new Date("2025-01-01"),
|
|
users: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal Prisma-shaped Company row. */
|
|
export function buildMockCompany(overrides: Record<string, any> = {}) {
|
|
return {
|
|
id: 123,
|
|
uid: "company-1",
|
|
name: "Test Company",
|
|
phone: "555-1234",
|
|
website: "https://test.com",
|
|
contacts: [],
|
|
companyAddresses: [],
|
|
createdAt: new Date("2025-01-01"),
|
|
updatedAt: new Date("2025-01-01"),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal Session row. */
|
|
export function buildMockSession(overrides: Record<string, any> = {}) {
|
|
return {
|
|
id: "session-1",
|
|
sessionKey: "sk-abc123",
|
|
userId: "user-1",
|
|
expires: new Date(Date.now() + 30 * 24 * 60 * 60_000),
|
|
refreshedAt: null,
|
|
invalidatedAt: null,
|
|
refreshTokenGenerated: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal CredentialType row. */
|
|
export function buildMockCredentialType(overrides: Record<string, any> = {}) {
|
|
return {
|
|
id: "ctype-1",
|
|
name: "Login Credential",
|
|
permissionScope: "credential.login",
|
|
icon: null,
|
|
fields: [
|
|
{
|
|
id: "username",
|
|
name: "Username",
|
|
required: true,
|
|
secure: false,
|
|
valueType: "plain_text",
|
|
},
|
|
{
|
|
id: "password",
|
|
name: "Password",
|
|
required: true,
|
|
secure: true,
|
|
valueType: "password",
|
|
},
|
|
],
|
|
credentials: [],
|
|
createdAt: new Date("2025-01-01"),
|
|
updatedAt: new Date("2025-01-01"),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal Credential row. */
|
|
export function buildMockCredential(overrides: Record<string, any> = {}) {
|
|
const ctype = buildMockCredentialType();
|
|
const company = buildMockCompany();
|
|
return {
|
|
id: "cred-1",
|
|
name: "Test Credential",
|
|
notes: null,
|
|
typeId: ctype.id,
|
|
companyId: company.id,
|
|
subCredentialOfId: null,
|
|
fields: { username: "admin" },
|
|
type: ctype,
|
|
company,
|
|
securevalues: [
|
|
{
|
|
id: "sv-1",
|
|
name: "password",
|
|
content: "encrypted-data",
|
|
hash: "BLAKE2s$abc$salt",
|
|
credentialId: "cred-1",
|
|
createdAt: new Date("2025-01-01"),
|
|
updatedAt: new Date("2025-01-01"),
|
|
},
|
|
],
|
|
subCredentials: [],
|
|
createdAt: new Date("2025-01-01"),
|
|
updatedAt: new Date("2025-01-01"),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal UnifiSite row. */
|
|
export function buildMockUnifiSite(overrides: Record<string, any> = {}) {
|
|
return {
|
|
id: "usite-1",
|
|
name: "Main Office",
|
|
siteId: "default",
|
|
companyId: null,
|
|
createdAt: new Date("2025-01-01"),
|
|
updatedAt: new Date("2025-01-01"),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal Prisma-shaped Opportunity row. */
|
|
export function buildMockOpportunity(overrides: Record<string, any> = {}) {
|
|
return {
|
|
uid: "opp-1",
|
|
id: 1001,
|
|
name: "Test Opportunity",
|
|
notes: "Some notes",
|
|
typeId: 1,
|
|
type: { id: 1, name: "New Business" },
|
|
stageName: "Proposal",
|
|
stageCwId: 2,
|
|
statusId: 3,
|
|
status: { id: 3, name: "Active" },
|
|
priorityName: "High",
|
|
priorityCwId: 4,
|
|
ratingName: "Hot",
|
|
ratingCwId: 5,
|
|
source: "Referral",
|
|
campaignName: null,
|
|
campaignCwId: null,
|
|
primarySalesRepName: "John",
|
|
primarySalesRepIdentifier: "jroberts",
|
|
primarySalesRepCwId: 10,
|
|
secondarySalesRepName: null,
|
|
secondarySalesRepIdentifier: null,
|
|
secondarySalesRepCwId: null,
|
|
companyCwId: 123,
|
|
companyName: "Test Company",
|
|
companyId: "company-1",
|
|
contactCwId: 200,
|
|
contactName: "Jane Doe",
|
|
siteCwId: 300,
|
|
siteName: "Main Office",
|
|
customerPO: "PO-12345",
|
|
totalSalesTax: 50.0,
|
|
probability: 80,
|
|
locationName: "HQ",
|
|
locationCwId: 400,
|
|
departmentName: "Sales",
|
|
departmentCwId: 500,
|
|
expectedCloseDate: new Date("2026-04-01"),
|
|
pipelineChangeDate: new Date("2026-02-15"),
|
|
dateBecameLead: new Date("2026-01-01"),
|
|
closedDate: null,
|
|
closedFlag: false,
|
|
closedByName: null,
|
|
closedByCwId: null,
|
|
cwLastUpdated: new Date("2026-02-28"),
|
|
cwDateEntered: new Date("2026-01-01"),
|
|
productSequence: [],
|
|
createdAt: new Date("2026-01-01"),
|
|
updatedAt: new Date("2026-02-28"),
|
|
company: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal CW Activity object for ActivityController tests. */
|
|
export function buildMockCWActivity(overrides: Record<string, any> = {}) {
|
|
return {
|
|
id: 5001,
|
|
name: "Test Activity",
|
|
notes: "Activity notes",
|
|
type: { id: 1, name: "Call" },
|
|
status: { id: 2, name: "Open" },
|
|
company: { id: 123, identifier: "TestCo", name: "Test Company" },
|
|
contact: { id: 200, name: "Jane Doe" },
|
|
phoneNumber: "555-1234",
|
|
email: "jane@test.com",
|
|
opportunity: { id: 1001, name: "Test Opportunity" },
|
|
ticket: { id: 0, name: "" },
|
|
agreement: { id: 0, name: "" },
|
|
campaign: { id: 0, name: "" },
|
|
assignTo: { id: 10, identifier: "jroberts", name: "John Roberts" },
|
|
scheduleStatus: { id: 1, name: "Firm" },
|
|
reminder: { id: 1, name: "15 Minutes" },
|
|
where: { id: 1, name: "Office" },
|
|
dateStart: "2026-03-01T09:00:00Z",
|
|
dateEnd: "2026-03-01T10:00:00Z",
|
|
notifyFlag: false,
|
|
mobileGuid: "guid-abc123",
|
|
currency: { id: 1, name: "USD" },
|
|
customFields: [],
|
|
_info: {
|
|
lastUpdated: "2026-02-28T12:00:00Z",
|
|
updatedBy: "jroberts",
|
|
dateEntered: "2026-01-15T08:00:00Z",
|
|
enteredBy: "jroberts",
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal CW Forecast Item for ForecastProductController tests. */
|
|
export function buildMockCWForecastItem(overrides: Record<string, any> = {}) {
|
|
return {
|
|
id: 7001,
|
|
forecastDescription: "Network Switch",
|
|
opportunity: { id: 1001, name: "Test Opportunity" },
|
|
quantity: 5,
|
|
status: { id: 1, name: "Won" },
|
|
catalogItem: { id: 500, identifier: "USW-Pro-24" },
|
|
productDescription: "UniFi Switch Pro 24",
|
|
productClass: "Product",
|
|
forecastType: "Product",
|
|
revenue: 2500.0,
|
|
cost: 1800.0,
|
|
margin: 700.0,
|
|
percentage: 100,
|
|
includeFlag: true,
|
|
linkFlag: false,
|
|
recurringFlag: false,
|
|
taxableFlag: true,
|
|
recurringRevenue: 0,
|
|
recurringCost: 0,
|
|
cycles: 0,
|
|
sequenceNumber: 1,
|
|
subNumber: 0,
|
|
quoteWerksQuantity: 0,
|
|
_info: {
|
|
lastUpdated: "2026-02-28T12:00:00Z",
|
|
updatedBy: "jroberts",
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal Prisma-shaped GeneratedQuotes row. */
|
|
export function buildMockGeneratedQuote(overrides: Record<string, any> = {}) {
|
|
return {
|
|
id: "quote-1",
|
|
quoteRegenData: { theme: "default" },
|
|
quoteFile: new Uint8Array([0x25, 0x50, 0x44, 0x46]),
|
|
quoteFileName: "Quote-TestOpp.pdf",
|
|
opportunityId: "opp-1",
|
|
createdById: "user-1",
|
|
createdAt: new Date("2026-03-01"),
|
|
updatedAt: new Date("2026-03-01"),
|
|
opportunity: null,
|
|
createdBy: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Build a minimal Prisma-shaped CatalogItem row. */
|
|
export function buildMockCatalogItem(overrides: Record<string, any> = {}) {
|
|
return {
|
|
id: "cat-1",
|
|
cwCatalogId: 500,
|
|
identifier: "USW-Pro-24",
|
|
name: "UniFi Switch Pro 24",
|
|
description: "24-port managed switch",
|
|
customerDescription: "Enterprise switch",
|
|
internalNotes: null,
|
|
category: "Technology",
|
|
categoryCwId: 18,
|
|
subcategory: "Network-Switch",
|
|
subcategoryCwId: 112,
|
|
manufacturer: "Ubiquiti",
|
|
manufactureCwId: 248,
|
|
partNumber: "USW-Pro-24",
|
|
vendorName: "Ubiquiti Inc",
|
|
vendorSku: "USW-Pro-24",
|
|
vendorCwId: 100,
|
|
price: 500.0,
|
|
cost: 360.0,
|
|
inactive: false,
|
|
salesTaxable: true,
|
|
onHand: 10,
|
|
cwLastUpdated: new Date("2026-02-28"),
|
|
linkedItems: [],
|
|
createdAt: new Date("2026-01-01"),
|
|
updatedAt: new Date("2026-02-28"),
|
|
...overrides,
|
|
};
|
|
}
|