PSA/server/test-utils/testMocks.ts
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

265 lines
8.4 KiB
TypeScript

import { vi } from 'vitest';
import { IUserWithRoles } from '../src/interfaces/auth.interfaces';
import { getCurrentUser, hasPermission } from '@alga-psa/auth';
const currentUserRef = vi.hoisted(() => ({
user: {
user_id: 'mock-user-id',
tenant: '11111111-1111-1111-1111-111111111111',
username: 'mock-user',
first_name: 'Mock',
last_name: 'User',
email: 'mock.user@example.com',
hashed_password: 'hashed_password_here',
is_inactive: false,
user_type: 'internal',
roles: []
} as IUserWithRoles
}));
const sessionUserRef = vi.hoisted(() => ({
user: {
id: 'mock-user-id',
tenant: '11111111-1111-1111-1111-111111111111'
}
}));
const permissionRef = vi.hoisted(() => ({
value: ['user_schedule:update', 'user_schedule:read'] as string[]
}));
const permissionCheckRef = vi.hoisted(() => ({
fn: (user: IUserWithRoles, resource?: string, action?: string) =>
user.roles?.some(role => role.role_name.toLowerCase() === 'admin') ?? true
}));
// Hoisted so the vi.mock('next/headers') factory below can safely reference it.
// (vi.mock factories are hoisted to the top of the module; referencing a
// function-local variable from a factory causes "mockHeaders is not defined".)
const nextHeadersRef = vi.hoisted(() => {
const buildHeaders = (tenantId: string) => ({
get: (key: string) => (key === 'x-tenant-id' ? tenantId : null),
append: () => {},
delete: () => {},
entries: () => [][Symbol.iterator](),
forEach: () => {},
has: () => false,
keys: () => [][Symbol.iterator](),
set: () => {},
values: () => [][Symbol.iterator](),
});
return {
tenantId: '11111111-1111-1111-1111-111111111111',
current: buildHeaders('11111111-1111-1111-1111-111111111111') as ReturnType<typeof buildHeaders> | null,
buildHeaders
};
});
vi.mock('next/headers', () => ({
headers: vi.fn(() =>
nextHeadersRef.current ?? nextHeadersRef.buildHeaders(nextHeadersRef.tenantId)
)
}));
vi.mock('@alga-psa/users/actions', () => ({
getCurrentUser: vi.fn(() => Promise.resolve(currentUserRef.user)),
getCurrentUserPermissions: vi.fn(() => Promise.resolve(permissionRef.value))
}));
// Note: '@alga-psa/users/actions' mock above covers the canonical import path
vi.mock('server/src/lib/auth/getSession', () => ({
getSession: vi.fn(() => Promise.resolve({ user: sessionUserRef.user }))
}));
vi.mock('@/lib/auth/getSession', () => ({
getSession: vi.fn(() => Promise.resolve({ user: sessionUserRef.user }))
}));
vi.mock('server/src/lib/auth/rbac', () => ({
hasPermission: vi.fn((user: IUserWithRoles, resource: string, action: string) =>
Promise.resolve(permissionCheckRef.fn(user, resource, action))
)
}));
vi.mock('@/lib/auth/rbac', () => ({
hasPermission: vi.fn((user: IUserWithRoles, resource: string, action: string) =>
Promise.resolve(permissionCheckRef.fn(user, resource, action))
)
}));
// Package actions (billing, integrations, clients, …) import rbac from
// @alga-psa/auth; route it through the same permissionCheck as the legacy paths.
vi.mock('@alga-psa/auth/rbac', () => ({
hasPermission: vi.fn((user: IUserWithRoles, resource: string, action: string) =>
Promise.resolve(permissionCheckRef.fn(user, resource, action))
)
}));
// Integration tests have no Redis (none in CI; local creds differ); the real
// publishers would block on connection retries until tests time out. Tests
// that assert on publishes can vi.spyOn these mocked functions as usual.
vi.mock('@alga-psa/event-bus/publishers', () => ({
publishEvent: vi.fn(async () => undefined),
publishWorkflowEvent: vi.fn(async () => undefined),
}));
/**
* Creates a mock Headers object with tenant context
* @param tenantId Optional tenant ID (defaults to a test UUID)
* @returns Mock Headers object
*/
export const createMockHeaders = (tenantId: string = '11111111-1111-1111-1111-111111111111') => ({
get: vi.fn((key: string) => {
if (key === 'x-tenant-id') {
return tenantId;
}
return null;
}),
append: vi.fn(),
delete: vi.fn(),
entries: vi.fn(),
forEach: vi.fn(),
has: vi.fn(),
keys: vi.fn(),
set: vi.fn(),
values: vi.fn(),
});
/**
* Sets up next/headers mock
* @param tenantId Optional tenant ID for the headers
*/
export function mockNextHeaders(tenantId?: string) {
const mockHeaders = createMockHeaders(tenantId);
// The next/headers module mock is registered once at module scope (see
// nextHeadersRef above); here we just swap in the headers instance it returns.
nextHeadersRef.tenantId = tenantId ?? '11111111-1111-1111-1111-111111111111';
nextHeadersRef.current = mockHeaders as unknown as typeof nextHeadersRef.current;
return mockHeaders;
}
/**
* Sets up next-auth session mock
* @param userId Optional user ID for the session
* @param tenantId Optional tenant ID for the session
*/
export function mockNextAuth(userId: string = 'mock-user-id', tenantId: string = '11111111-1111-1111-1111-111111111111') {
vi.mock('next-auth/next', () => ({
getServerSession: vi.fn(() => Promise.resolve({
user: {
id: userId,
tenant: tenantId
},
})),
}));
vi.mock('@/app/api/auth/[...nextauth]/options', () => ({
options: {},
}));
}
/**
* Sets up next/cache mock
*/
export function mockNextCache() {
vi.mock('next/cache', () => ({
revalidatePath: vi.fn(),
revalidateTag: vi.fn(),
// Pass-through: callers get the wrapped function without Next's cache layer.
unstable_cache: vi.fn(
(fn: (...args: unknown[]) => unknown) =>
(...args: unknown[]) =>
fn(...args)
),
}));
}
/**
* Sets up RBAC mock with custom permission logic
* @param permissionCheck Function to determine if a user has permission
*/
export function mockRBAC(
permissionCheck: (user: IUserWithRoles, resource?: string, action?: string) => boolean =
(user) => user.roles?.some(role => role.role_name.toLowerCase() === 'admin') ?? true
) {
permissionCheckRef.fn = permissionCheck;
vi.mocked(hasPermission).mockImplementation((user: IUserWithRoles, resource?: string, action?: string) =>
Promise.resolve(permissionCheckRef.fn(user, resource, action))
);
}
/**
* Sets up getCurrentUser mock
* @param mockUser The user object to return
*/
export function mockGetCurrentUser(mockUser: IUserWithRoles) {
currentUserRef.user = mockUser;
vi.mocked(getCurrentUser).mockResolvedValue(mockUser);
}
export function setMockPermissions(permissions: string[]) {
permissionRef.value = permissions;
}
export function setMockUser(
user: IUserWithRoles,
permissions: string[] = ['user_schedule:update', 'user_schedule:read']
) {
mockGetCurrentUser(user);
permissionRef.value = permissions;
}
/**
* Helper to create a mock user with roles
* @param type User type ('internal' or 'client')
* @param overrides Optional user property overrides
* @returns Mock user object
*/
export function createMockUser(
type: 'internal' | 'client' = 'internal',
overrides: Partial<IUserWithRoles> & Record<string, any> = {}
): IUserWithRoles {
return {
user_id: overrides.user_id || 'mock-user-id',
tenant: overrides.tenant || '11111111-1111-1111-1111-111111111111',
username: overrides.username || `mock-${type}`,
first_name: overrides.first_name || 'Mock',
last_name: overrides.last_name || type === 'internal' ? 'Internal' : 'Client',
email: overrides.email || `mock.${type}@example.com`,
hashed_password: overrides.hashed_password || 'hashed_password_here',
is_inactive: overrides.is_inactive || false,
user_type: type,
roles: overrides.roles || [],
...overrides
} as IUserWithRoles;
}
/**
* Sets up all common mocks with default configuration
* @param options Optional configuration for the mocks
*/
export function setupCommonMocks(options: {
tenantId?: string;
userId?: string;
user?: IUserWithRoles;
permissionCheck?: (user: IUserWithRoles, resource?: string, action?: string) => boolean;
permissions?: string[];
} = {}) {
const tenantId = options.tenantId || '11111111-1111-1111-1111-111111111111';
const userId = options.userId || 'mock-user-id';
const user = options.user || createMockUser('internal', { user_id: userId, tenant: tenantId });
mockNextHeaders(tenantId);
mockNextAuth(userId, tenantId);
mockNextCache();
mockRBAC(options.permissionCheck);
setMockUser(user, options.permissions ?? permissionRef.value);
sessionUserRef.user = {
id: user.user_id,
tenant: tenantId
};
return { tenantId, userId, user };
}