syntropylog
Version:
An instance manager with observability for Node.js applications
242 lines (236 loc) ⢠8.44 kB
JavaScript
/**
* MockHttpClient - Framework Agnostic Mock
*
* This mock provides a testing-agnostic version of IHttpClientAdapter
* that can be used with both Vitest and Jest without conflicts.
*/
/**
* Creates a simple agnostic mock function without spy capabilities
*/
function createAgnosticMockFn(implementation) {
const mockFn = (...args) => {
if (implementation) {
return implementation(...args);
}
return undefined;
};
// Basic mock properties
mockFn.mockClear = () => { };
mockFn.mockReset = () => { };
mockFn.mockImplementation = (impl) => {
return createAgnosticMockFn(impl);
};
mockFn.mockReturnValue = (value) => {
return createAgnosticMockFn(() => value);
};
mockFn.mockResolvedValue = (value) => {
return createAgnosticMockFn(() => Promise.resolve(value));
};
mockFn.mockRejectedValue = (value) => {
return createAgnosticMockFn(() => Promise.reject(value));
};
return mockFn;
}
export class MockHttpClient {
spyFn = null;
timeouts = new Map();
// Core methods - will be initialized in constructor
request;
get;
post;
put;
delete;
patch;
setResponse;
setError;
setTimeout;
reset;
constructor(spyFn) {
this.spyFn = spyFn || null;
// Initialize mocks after spyFn is set
this.request = this.createMock().mockImplementation(async (request) => {
// Default successful response
return {
statusCode: 200,
data: { message: 'Mock response' },
headers: { 'content-type': 'application/json' },
};
});
this.get = this.createMock().mockImplementation(async (url, headers) => {
return this.request({
url,
method: 'GET',
headers: headers || {},
});
});
this.post = this.createMock().mockImplementation(async (url, body, headers) => {
return this.request({
url,
method: 'POST',
headers: headers || {},
body,
});
});
this.put = this.createMock().mockImplementation(async (url, body, headers) => {
return this.request({
url,
method: 'PUT',
headers: headers || {},
body,
});
});
this.delete = this.createMock().mockImplementation(async (url, headers) => {
return this.request({
url,
method: 'DELETE',
headers: headers || {},
});
});
this.patch = this.createMock().mockImplementation(async (url, body, headers) => {
return this.request({
url,
method: 'PATCH',
headers: headers || {},
body,
});
});
// Initialize method implementations
this.updateMethodImplementations();
this.setResponse = this.createMock().mockImplementation((method, response) => {
// Configure the request method to return the specified response
this.request.mockImplementation(async (req) => {
if (req.method.toUpperCase() === method.toUpperCase()) {
return response;
}
// Default response for other methods
return {
statusCode: 200,
data: { message: 'Mock response' },
headers: { 'content-type': 'application/json' },
};
});
// Also update individual method implementations
this.updateMethodImplementations();
});
this.setError = this.createMock().mockImplementation((method, error) => {
// Configure the request method to throw the specified error
this.request.mockImplementation(async (req) => {
if (req.method.toUpperCase() === method.toUpperCase()) {
const adapterError = {
name: error.name,
message: error.message,
stack: error.stack,
request: req,
isAdapterError: true,
};
throw adapterError;
}
// Default response for other methods
return {
statusCode: 200,
data: { message: 'Mock response' },
headers: { 'content-type': 'application/json' },
};
});
// Also update individual method implementations
this.updateMethodImplementations();
});
this.setTimeout = this.createMock().mockImplementation((method, timeoutMs) => {
this.timeouts.set(method, timeoutMs);
// Configure the request method to timeout
this.request.mockImplementation(async (req) => {
if (req.method.toUpperCase() === method.toUpperCase() &&
this.timeouts.has(method)) {
await new Promise((resolve) => setTimeout(resolve, this.timeouts.get(method) + 10));
throw new Error(`Mock HTTP client timed out after ${this.timeouts.get(method)}ms`);
}
// Default response for other methods
return {
statusCode: 200,
data: { message: 'Mock response' },
headers: { 'content-type': 'application/json' },
};
});
// Also update individual method implementations
this.updateMethodImplementations();
});
this.reset = this.createMock().mockImplementation(() => {
this.timeouts.clear();
this.request.mockReset();
this.get.mockReset();
this.post.mockReset();
this.put.mockReset();
this.delete.mockReset();
this.patch.mockReset();
// Restore default implementations
this.request.mockImplementation(async (request) => {
return {
statusCode: 200,
data: { message: 'Mock response' },
headers: { 'content-type': 'application/json' },
};
});
this.updateMethodImplementations();
});
}
updateMethodImplementations() {
this.get.mockImplementation(async (url, headers) => {
return this.request({
url,
method: 'GET',
headers: headers || {},
});
});
this.post.mockImplementation(async (url, body, headers) => {
return this.request({
url,
method: 'POST',
headers: headers || {},
body,
});
});
this.put.mockImplementation(async (url, body, headers) => {
return this.request({
url,
method: 'PUT',
headers: headers || {},
body,
});
});
this.delete.mockImplementation(async (url, headers) => {
return this.request({
url,
method: 'DELETE',
headers: headers || {},
});
});
this.patch.mockImplementation(async (url, body, headers) => {
return this.request({
url,
method: 'PATCH',
headers: headers || {},
body,
});
});
}
createMock(implementation) {
if (!this.spyFn) {
throw new Error(`
šØ SPY FUNCTION NOT INJECTED! š”
To use spy functions like toHaveBeenCalled(), toHaveBeenCalledWith(), etc.
YOU MUST inject your spy function in the constructor:
// For Vitest:
const mockHttp = new MockHttpClient(vi.fn);
// For Jest:
const mockHttp = new MockHttpClient(jest.fn);
// For Jasmine:
const mockHttp = new MockHttpClient(jasmine.createSpy);
// Without spy (basic functionality only):
const mockHttp = new MockHttpClient();
DON'T FORGET AGAIN! š¤
`);
}
return this.spyFn(implementation);
}
}
//# sourceMappingURL=MockHttpClient.js.map