/**
* Mock Attio Client for unit and integration tests
*/
import type { AttioClient } from '../../src/attio-client.js';
/**
* Mock response configuration for a specific request
*/
export interface MockResponse<T = any> {
data?: T;
error?: {
message: string;
statusCode?: number;
response?: unknown;
};
delay?: number; // Simulate network delay in ms
}
/**
* Mock Attio Client that allows tests to configure responses
*/
export class MockAttioClient implements Pick<AttioClient, 'get' | 'post' | 'put' | 'patch' | 'delete'> {
// Track all calls for assertions
public calls: {
method: string;
path: string;
body?: unknown;
params?: Record<string, string | number | boolean>;
}[] = [];
// Response configurations by method and path
private responses: Map<string, MockResponse> = new Map();
// Default response if no specific mock is configured
private defaultResponse: MockResponse = { data: {} };
/**
* Configure a mock response for a specific request
*
* @param method - HTTP method
* @param path - Request path (supports wildcards with *)
* @param response - Mock response configuration
*
* @example
* client.mockResponse('GET', '/objects/companies/records/*', {
* data: { id: { record_id: '123' }, values: { name: 'Acme' } }
* });
*/
mockResponse(method: string, path: string, response: MockResponse): void {
const key = this.makeKey(method, path);
this.responses.set(key, response);
}
/**
* Set default response for any unmocked request
*/
setDefaultResponse(response: MockResponse): void {
this.defaultResponse = response;
}
/**
* Clear all mocked responses and call history
*/
reset(): void {
this.calls = [];
this.responses.clear();
this.defaultResponse = { data: {} };
}
/**
* Get call history for a specific method and path
*/
getCallsFor(method: string, path?: string): typeof this.calls {
return this.calls.filter(
(call) => call.method === method && (!path || call.path === path)
);
}
/**
* Assert that a specific request was made
*/
assertCalled(method: string, path: string, times?: number): void {
const calls = this.getCallsFor(method, path);
if (times !== undefined && calls.length !== times) {
throw new Error(
`Expected ${method} ${path} to be called ${times} times, but was called ${calls.length} times`
);
}
if (times === undefined && calls.length === 0) {
throw new Error(`Expected ${method} ${path} to be called, but it was not`);
}
}
// Implement AttioClient methods
async get<T>(path: string, params?: Record<string, string | number | boolean>): Promise<T> {
return this.handleRequest<T>('GET', path, undefined, params);
}
async post<T>(path: string, body: unknown): Promise<T> {
return this.handleRequest<T>('POST', path, body);
}
async put<T>(path: string, body: unknown): Promise<T> {
return this.handleRequest<T>('PUT', path, body);
}
async patch<T>(path: string, body: unknown): Promise<T> {
return this.handleRequest<T>('PATCH', path, body);
}
async delete<T>(path: string): Promise<T> {
return this.handleRequest<T>('DELETE', path);
}
// Private helpers
private makeKey(method: string, path: string): string {
return `${method}:${path}`;
}
private findResponse(method: string, path: string): MockResponse {
// Try exact match first
const exactKey = this.makeKey(method, path);
if (this.responses.has(exactKey)) {
return this.responses.get(exactKey)!;
}
// Try wildcard matches
for (const [key, response] of this.responses.entries()) {
const [mockMethod, mockPath] = key.split(':');
if (mockMethod === method && this.matchesWildcard(path, mockPath)) {
return response;
}
}
// Return default
return this.defaultResponse;
}
private matchesWildcard(path: string, pattern: string): boolean {
// Convert wildcard pattern to regex
const regexPattern = pattern
.replace(/\*/g, '.*') // * matches any characters
.replace(/\//g, '\\/'); // Escape forward slashes
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
private async handleRequest<T>(
method: string,
path: string,
body?: unknown,
params?: Record<string, string | number | boolean>
): Promise<T> {
// Record the call
this.calls.push({ method, path, body, params });
// Find response configuration
const response = this.findResponse(method, path);
// Simulate network delay if configured
if (response.delay) {
await new Promise((resolve) => setTimeout(resolve, response.delay));
}
// Return error or data
if (response.error) {
const error = new Error(response.error.message) as any;
error.statusCode = response.error.statusCode;
error.response = response.error.response;
throw error;
}
return response.data as T;
}
}
/**
* Create a new mock Attio client
*
* @example
* const client = createMockAttioClient();
* client.mockResponse('GET', '/workspace', { data: { id: '123' } });
* const result = await client.get('/workspace');
*/
export function createMockAttioClient(): MockAttioClient {
return new MockAttioClient();
}