syntropylog
Version:
An instance manager with observability for Node.js applications
1,153 lines (1,116 loc) • 39.9 kB
JavaScript
import { randomUUID } from 'crypto';
/*
* @file src/context/MockContextManager.ts
* @description Provides a mock implementation of the IContextManager interface,
* designed specifically for use in testing environments.
*/
/**
* @class MockContextManager
* @description A mock implementation of `IContextManager` for testing purposes.
* It uses a simple in-memory object instead of AsyncLocalStorage,
* making context management predictable and synchronous in tests.
* @implements {IContextManager}
*/
class MockContextManager {
constructor() {
/** @private The in-memory key-value store for the context. */
this.store = {};
/** @private The HTTP header name used for the correlation ID. */
this.correlationIdHeader = 'x-correlation-id';
/** @private The HTTP header name used for the transaction ID. */
this.transactionIdHeader = 'x-trace-id';
}
/**
* Configures the mock context manager.
* @param options The configuration options.
* @param options.correlationIdHeader The custom header name to use for the correlation ID.
* @param options.transactionIdHeader The custom header name for the transaction ID.
*/
configure(options) {
if (options?.correlationIdHeader) {
this.correlationIdHeader = options.correlationIdHeader;
}
if (options?.transactionIdHeader) {
this.transactionIdHeader = options.transactionIdHeader;
}
}
/**
* Simulates running a function within a new, isolated context.
* It saves the current context, creates a new one inheriting the parent's values,
* runs the callback, and then restores the original context. This process
* correctly handles both synchronous and asynchronous callbacks.
* @template T The return type of the callback.
* @param {() => T} callback The function to execute within the new context.
* @returns {T} The result of the callback.
*/
async run(fn) {
// Deep-clone the original store to ensure true isolation.
const originalStore = JSON.parse(JSON.stringify(this.store));
this.store = { ...this.store }; // Inherit from parent for the current run.
try {
// Await the callback, which might be sync or async.
await fn();
}
finally {
// Always restore the original, unmodified context.
this.store = originalStore;
}
}
/**
* Gets a value from the mock context by its key.
* @template T The expected type of the value.
* @param {string} key The key of the value to retrieve.
* @returns The value, or `undefined` if not found.
*/
get(key) {
return this.store[key];
}
/**
* Gets a shallow copy of the entire mock context store.
* @returns {ContextData} An object containing all context data.
*/
getAll() {
// Return a shallow copy to prevent direct mutation of the internal store.
return { ...this.store };
}
/**
* Sets a key-value pair in the mock context.
* @param {string} key The key for the value.
* @param {ContextValue} value The value to store.
* @returns {void}
*/
set(key, value) {
this.store[key] = value;
}
/**
* Clears the in-memory store.
* Useful for resetting state between tests (e.g., in a `beforeEach` hook).
* @returns {void}
*/
clear() {
this.store = {};
}
/**
* A convenience method to get the correlation ID from the mock context.
* @returns {string} The correlation ID, or a generated one if not set.
*/
getCorrelationId() {
// Return the value from the configured header name to avoid duplication in logs.
let correlationId = this.get(this.correlationIdHeader);
if (!correlationId || typeof correlationId !== 'string') {
correlationId = randomUUID();
this.set(this.correlationIdHeader, correlationId);
}
return correlationId;
}
/**
* A convenience method to get the transaction ID from the mock context.
* @returns {string | undefined} The transaction ID, or undefined if not set.
*/
getTransactionId() {
// The key in the context is always 'transactionId'.
return this.get('transactionId');
}
/**
* A convenience method to set the transaction ID in the mock context.
* @param {string} transactionId The transaction ID to set.
*/
setTransactionId(transactionId) {
// The key in the context is always 'transactionId'.
this.set('transactionId', transactionId);
}
/**
* Gets the configured HTTP header name used for the correlation ID.
* @returns {string} The header name.
*/
getCorrelationIdHeaderName() {
return this.correlationIdHeader;
}
/**
* Gets the configured HTTP header name used for the transaction ID.
* @returns {string} The header name.
*/
getTransactionIdHeaderName() {
return this.transactionIdHeader;
}
/**
* Mock implementation for getting trace context headers.
* In a real tracing scenario, this would be populated.
* @returns `undefined` as this mock does not implement tracing.
*/
getTraceContextHeaders() {
const headers = {};
// Only include headers if we have an active context (store is not empty)
if (Object.keys(this.store).length === 0) {
return headers; // Return empty object if no context is active
}
const correlationId = this.getCorrelationId();
const transactionId = this.getTransactionId();
if (correlationId) {
headers[this.getCorrelationIdHeaderName()] = correlationId;
}
if (transactionId) {
headers[this.getTransactionIdHeaderName()] = transactionId;
}
return headers;
}
getFilteredContext() {
return this.getAll();
}
/**
* Reconfigures the logging matrix dynamically.
* This method allows changing which context fields are included in logs
* without affecting security configurations like masking or log levels.
* @param newMatrix The new logging matrix configuration
*/
reconfigureLoggingMatrix(newMatrix) {
// Mock implementation - no actual logging matrix in mock
// This is just to satisfy the interface
}
}
/**
* @file src/logger/levels.ts
* @description Defines the available log levels, their names, and their severity weights.
*/
/**
* @description A mapping of log level names to their severity weights.
* Higher numbers indicate higher severity.
*/
const LOG_LEVEL_WEIGHTS = {
fatal: 60,
error: 50,
warn: 40,
info: 30,
debug: 20,
trace: 10,
silent: 0,
};
/**
* @description An array of the available log level names, derived from the weights object.
*/
const logLevels = Object.keys(LOG_LEVEL_WEIGHTS);
/**
* @file src/logger/transports/Transport.ts
* @description Defines the abstract base class for all log transports.
*/
/**
* @class Transport
* @description The abstract base class for all log transports. A transport is
* responsible for the final output of a log entry, whether it's to the console,
* a file, or a remote service.
*/
class Transport {
/**
* @constructor
* @param {TransportOptions} [options] - The configuration options for this transport.
*/
constructor(options = {}) {
this.level = options.level ?? 'info';
this.name = options.name ?? this.constructor.name;
this.formatter = options?.formatter;
this.sanitizationEngine = options?.sanitizationEngine;
}
/**
* Determines if the transport should process a log entry based on its log level.
* @param level - The level of the log entry to check.
* @returns {boolean} - True if the transport is enabled for this level, false otherwise.
*/
isLevelEnabled(level) {
return LOG_LEVEL_WEIGHTS[level] >= LOG_LEVEL_WEIGHTS[this.level];
}
/**
* A method to ensure all buffered logs are written before the application exits.
* Subclasses should override this if they perform I/O buffering.
* @returns {Promise<void>} A promise that resolves when flushing is complete.
*/
async flush() {
// Default implementation does nothing, assuming no buffering.
return Promise.resolve();
}
}
/**
* @class SpyTransport
* A transport designed for testing. It captures log entries in memory,
* allowing you to make assertions on what has been logged.
* @extends {Transport}
*/
class SpyTransport extends Transport {
/**
* @constructor
* @param {TransportOptions} [options] - Options for the transport, such as level.
*/
constructor(options) {
super(options);
this.entries = [];
}
/**
* Stores the log entry in an in-memory array.
* @param {LogEntry} entry - The log entry to capture.
* @returns {Promise<void>}
*/
async log(entry) {
this.entries.push(entry);
}
/**
* Returns all log entries captured by this transport.
* @returns {LogEntry[]} A copy of all captured log entries.
*/
getEntries() {
return [...this.entries];
}
/**
* Finds log entries where the properties match the given predicate.
* Note: This performs a shallow comparison on the entry's properties.
* @param {Partial<LogEntry> | ((entry: LogEntry) => boolean)} predicate - An object with properties to match or a function that returns true for matching entries.
* @returns {LogEntry[]} An array of matching log entries.
*/
findEntries(predicate) {
if (typeof predicate === 'function') {
// If the predicate is a function, use it directly with filter.
return this.entries.filter(predicate);
}
// If the predicate is an object, perform a shallow property comparison.
return this.entries.filter((entry) => {
return Object.keys(predicate).every((key) => {
const k = key;
return predicate[k] === entry[k];
});
});
}
/**
* Clears all captured log entries. Call this in your test setup
* (e.g., `beforeEach`) to ensure test isolation.
* @returns {void}
*/
clear() {
this.entries = [];
}
/**
* Returns the first log entry that was captured.
* @returns {LogEntry | undefined} The first entry, or undefined if none were captured.
*/
getFirstEntry() {
return this.entries[0];
}
/**
* Returns the most recent log entry that was captured.
* @returns {LogEntry | undefined} The last entry, or undefined if none were captured.
*/
getLastEntry() {
return this.entries[this.entries.length - 1];
}
}
/**
* Create a mock logger instance
*/
function createMockLogger() {
const logs = [];
return {
info: (message, metadata) => {
logs.push({ level: 'info', message, metadata });
},
warn: (message, metadata) => {
logs.push({ level: 'warn', message, metadata });
},
error: (message, metadata) => {
logs.push({ level: 'error', message, metadata });
},
debug: (message, metadata) => {
logs.push({ level: 'debug', message, metadata });
},
trace: (message, metadata) => {
logs.push({ level: 'trace', message, metadata });
},
fatal: (message, metadata) => {
logs.push({ level: 'fatal', message, metadata });
},
withSource: (source) => {
return createMockLogger(); // Return new instance with source context
},
};
}
/**
* Create a mock context manager instance
*/
function createMockContextManager() {
const context = {};
return {
run: async (fn) => {
// Simulate context execution
const correlationId = randomUUID();
const transactionId = randomUUID();
// Set default context values
context['x-correlation-id'] = correlationId;
context['x-transaction-id'] = transactionId;
context['x-correlation-id-test'] = correlationId;
// Execute the function with context
return await fn();
},
set: (key, value) => {
context[key] = value;
},
get: (key) => {
return context[key] || null;
},
getCorrelationIdHeaderName: () => 'x-correlation-id',
getTransactionIdHeaderName: () => 'x-transaction-id',
};
}
/**
* Create a mock HTTP manager instance
*/
function createMockHttpManager() {
return {
createClient: () => ({
get: async () => ({ data: {} }),
post: async () => ({ data: {} }),
put: async () => ({ data: {} }),
delete: async () => ({ data: {} }),
}),
};
}
/**
* Create a mock broker manager instance
*/
function createMockBrokerManager() {
return {
createClient: () => ({
publish: async () => undefined,
subscribe: async () => undefined,
}),
};
}
/**
* Create a mock serialization manager instance
*/
function createMockSerializationManager() {
return {
serialize: async () => '{}',
deserialize: async () => ({}),
};
}
// Global mock instances
let mockLogger;
let mockContextManager;
let mockHttpManager;
let mockBrokerManager;
let mockSerializationManager;
/**
* Get or create mock logger instance
*/
function getMockLogger() {
if (!mockLogger) {
mockLogger = createMockLogger();
}
return mockLogger;
}
/**
* Get or create mock context manager instance
*/
function getMockContextManager() {
if (!mockContextManager) {
mockContextManager = createMockContextManager();
}
return mockContextManager;
}
/**
* Get or create mock HTTP manager instance
*/
function getMockHttpManager() {
if (!mockHttpManager) {
mockHttpManager = createMockHttpManager();
}
return mockHttpManager;
}
/**
* Get or create mock broker manager instance
*/
function getMockBrokerManager() {
if (!mockBrokerManager) {
mockBrokerManager = createMockBrokerManager();
}
return mockBrokerManager;
}
/**
* Get or create mock serialization manager instance
*/
function getMockSerializationManager() {
if (!mockSerializationManager) {
mockSerializationManager = createMockSerializationManager();
}
return mockSerializationManager;
}
/**
* Create a complete mock of SyntropyLog
*
* @param spyFn - Optional spy function for framework compatibility (vi.fn, jest.fn, etc.)
*/
function createSyntropyLogMock(spyFn) {
const createMock = (implementation) => {
if (spyFn) {
return spyFn(implementation);
}
// Fallback to simple function if no spy provided
return implementation || (() => undefined);
};
return {
init: createMock(async () => undefined),
shutdown: createMock(async () => undefined),
getLogger: createMock(() => getMockLogger()),
getContextManager: createMock(() => getMockContextManager()),
getHttpManager: createMock(() => getMockHttpManager()),
getBrokerManager: createMock(() => getMockBrokerManager()),
getSerializationManager: createMock(() => getMockSerializationManager()),
};
}
/**
* Reset all mock instances
*/
function resetSyntropyLogMocks() {
mockLogger = undefined;
mockContextManager = undefined;
mockHttpManager = undefined;
mockBrokerManager = undefined;
mockSerializationManager = undefined;
}
/**
* Create a test helper for SyntropyLog testing
*
* @param spyFn - Optional spy function for framework compatibility (vi.fn, jest.fn, etc.)
*
* @example
* ```typescript
* // For Vitest
* const testHelper = createTestHelper(vi.fn);
*
* // For Jest
* const testHelper = createTestHelper(jest.fn);
*
* // For Jasmine
* const testHelper = createTestHelper(jasmine.createSpy);
*
* // Without spy (basic functionality only)
* const testHelper = createTestHelper();
*
* describe('MyService', () => {
* beforeEach(() => testHelper.beforeEach());
* afterEach(() => testHelper.afterEach());
*
* it('should work', () => {
* const service = new MyService(testHelper.mockSyntropyLog);
* // ... test logic
* });
* });
* ```
*/
function createTestHelper(spyFn) {
const mockSyntropyLog = createSyntropyLogMock(spyFn);
return {
mockSyntropyLog,
beforeEach: () => {
resetSyntropyLogMocks();
},
afterEach: () => {
// Clean up if needed
},
};
}
/**
* Create a service with SyntropyLog mock for testing
*
* @param ServiceClass - The service class to instantiate
* @param mockSyntropyLog - The mock SyntropyLog instance
* @returns Instance of the service with mock injected
*
* @example
* ```typescript
* const mockSyntropyLog = createSyntropyLogMock();
* const userService = createServiceWithMock(UserService, mockSyntropyLog);
* ```
*/
function createServiceWithMock(ServiceClass, mockSyntropyLog) {
return new ServiceClass(mockSyntropyLog);
}
/**
* MockBrokerAdapter - Framework Agnostic Mock
*
* This mock provides a testing-agnostic version of IBrokerAdapter
* that can be used with both Vitest and Jest without conflicts.
*/
/**
* Creates a simple agnostic mock function without spy capabilities
*/
function createAgnosticMockFn$3(implementation) {
const mockFn = (...args) => {
if (implementation) {
return implementation(...args);
}
return undefined;
};
// Basic mock properties
mockFn.mockClear = () => { };
mockFn.mockReset = () => { };
mockFn.mockImplementation = (impl) => {
return createAgnosticMockFn$3(impl);
};
mockFn.mockReturnValue = (value) => {
return createAgnosticMockFn$3(() => value);
};
mockFn.mockResolvedValue = (value) => {
return createAgnosticMockFn$3(() => Promise.resolve(value));
};
mockFn.mockRejectedValue = (value) => {
return createAgnosticMockFn$3(() => Promise.reject(value));
};
return mockFn;
}
class MockBrokerAdapter {
constructor(spyFn) {
this.spyFn = null;
this.errors = new Map();
this.timeouts = new Map();
this.spyFn = spyFn || null;
// Initialize mocks after spyFn is set
this.connect = this.createMock().mockImplementation(async () => {
// Check for timeout first
if (this.timeouts.has('connect')) {
await new Promise((resolve) => setTimeout(resolve, this.timeouts.get('connect') + 10));
throw new Error(`Mock broker timed out after ${this.timeouts.get('connect')}ms`);
}
// Check for error simulation
if (this.errors.has('connect')) {
throw this.errors.get('connect');
}
return undefined;
});
this.disconnect = this.createMock().mockImplementation(async () => {
if (this.timeouts.has('disconnect')) {
await new Promise((resolve) => setTimeout(resolve, this.timeouts.get('disconnect') + 10));
throw new Error(`Mock broker timed out after ${this.timeouts.get('disconnect')}ms`);
}
if (this.errors.has('disconnect')) {
throw this.errors.get('disconnect');
}
return undefined;
});
this.publish = this.createMock().mockImplementation(async (topic, message) => {
if (this.timeouts.has('publish')) {
await new Promise((resolve) => setTimeout(resolve, this.timeouts.get('publish') + 10));
throw new Error(`Mock broker timed out after ${this.timeouts.get('publish')}ms`);
}
if (this.errors.has('publish')) {
throw this.errors.get('publish');
}
return undefined;
});
this.subscribe = this.createMock().mockImplementation(async (topic, handler) => {
if (this.timeouts.has('subscribe')) {
await new Promise((resolve) => setTimeout(resolve, this.timeouts.get('subscribe') + 10));
throw new Error(`Mock broker timed out after ${this.timeouts.get('subscribe')}ms`);
}
if (this.errors.has('subscribe')) {
throw this.errors.get('subscribe');
}
return undefined;
});
this.setError = this.createMock().mockImplementation((method, error) => {
this.errors.set(method, error);
});
this.setTimeout = this.createMock().mockImplementation((method, timeoutMs) => {
this.timeouts.set(method, timeoutMs);
});
this.reset = this.createMock().mockImplementation(() => {
this.errors.clear();
this.timeouts.clear();
this.connect.mockReset();
this.disconnect.mockReset();
this.publish.mockReset();
this.subscribe.mockReset();
// Restore default implementations
this.connect.mockImplementation(async () => undefined);
this.disconnect.mockImplementation(async () => undefined);
this.publish.mockImplementation(async () => undefined);
this.subscribe.mockImplementation(async () => undefined);
});
}
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 mockBroker = new MockBrokerAdapter(vi.fn);
// For Jest:
const mockBroker = new MockBrokerAdapter(jest.fn);
// For Jasmine:
const mockBroker = new MockBrokerAdapter(jasmine.createSpy);
// Without spy (basic functionality only):
const mockBroker = new MockBrokerAdapter();
DON'T FORGET AGAIN! 😤
`);
}
return this.spyFn(implementation);
}
}
/**
* 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$2(implementation) {
const mockFn = (...args) => {
if (implementation) {
return implementation(...args);
}
return undefined;
};
// Basic mock properties
mockFn.mockClear = () => { };
mockFn.mockReset = () => { };
mockFn.mockImplementation = (impl) => {
return createAgnosticMockFn$2(impl);
};
mockFn.mockReturnValue = (value) => {
return createAgnosticMockFn$2(() => value);
};
mockFn.mockResolvedValue = (value) => {
return createAgnosticMockFn$2(() => Promise.resolve(value));
};
mockFn.mockRejectedValue = (value) => {
return createAgnosticMockFn$2(() => Promise.reject(value));
};
return mockFn;
}
class MockHttpClient {
constructor(spyFn) {
this.spyFn = null;
this.timeouts = new Map();
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);
}
}
/**
* MockSerializerRegistry - Framework Agnostic Mock
*
* This mock provides a testing-agnostic version of SerializerRegistry
* that can be used with both Vitest and Jest without conflicts.
*/
/**
* Creates a simple agnostic mock function without spy capabilities
*/
function createAgnosticMockFn$1(implementation) {
const mockFn = (...args) => {
if (implementation) {
return implementation(...args);
}
return undefined;
};
// Basic mock properties
mockFn.mockClear = () => { };
mockFn.mockReset = () => { };
mockFn.mockImplementation = (impl) => {
return createAgnosticMockFn$1(impl);
};
mockFn.mockReturnValue = (value) => {
return createAgnosticMockFn$1(() => value);
};
mockFn.mockResolvedValue = (value) => {
return createAgnosticMockFn$1(() => Promise.resolve(value));
};
mockFn.mockRejectedValue = (value) => {
return createAgnosticMockFn$1(() => Promise.reject(value));
};
return mockFn;
}
class MockSerializerRegistry {
constructor(spyFn) {
// Internal state to track configured serializers
this.serializers = new Map();
this.errorKeys = new Set();
this.timeoutMs = null;
this.spyFn = null;
this.spyFn = spyFn || null;
// Initialize mocks after spyFn is set
this.process = this.createMock().mockImplementation(async (meta, logger) => {
// Check for timeout first
if (this.timeoutMs !== null) {
await new Promise((resolve) => setTimeout(resolve, this.timeoutMs + 10));
throw new Error(`Mock serializer timed out after ${this.timeoutMs}ms.`);
}
const processedMeta = { ...meta };
// Process each field with its configured serializer
for (const [key, value] of Object.entries(processedMeta)) {
// Check for error simulation
if (this.errorKeys.has(key)) {
throw new Error(`Mock error for key '${key}'`);
}
// Check for serializer
const serializer = this.serializers.get(key);
if (serializer) {
try {
processedMeta[key] = serializer(value);
}
catch (error) {
logger.warn(`Mock serializer for key "${key}" failed.`, {
error: error instanceof Error ? error.message : String(error),
});
processedMeta[key] =
`[MOCK_SERIALIZER_ERROR: Failed to process key '${key}']`;
}
}
}
return processedMeta;
});
this.setSerializer = this.createMock().mockImplementation((key, serializer) => {
this.serializers.set(key, serializer);
});
this.setError = this.createMock().mockImplementation((key, error) => {
this.errorKeys.add(key);
});
this.setTimeout = this.createMock().mockImplementation((timeoutMs) => {
this.timeoutMs = timeoutMs;
});
this.reset = this.createMock().mockImplementation(() => {
this.serializers.clear();
this.errorKeys.clear();
this.timeoutMs = null;
this.process.mockReset();
this.process.mockImplementation(async (meta, logger) => {
return { ...meta };
});
});
}
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 mockSerializer = new MockSerializerRegistry(vi.fn);
// For Jest:
const mockSerializer = new MockSerializerRegistry(jest.fn);
// For Jasmine:
const mockSerializer = new MockSerializerRegistry(jasmine.createSpy);
// Without spy (basic functionality only):
const mockSerializer = new MockSerializerRegistry();
DON'T FORGET AGAIN! 😤
`);
}
return this.spyFn(implementation);
}
}
/**
* FILE: src/testing/BeaconRedisMock.ts
* DESCRIPTION: A mock implementation of IBeaconRedis for use in unit tests.
* This mock is framework agnostic and works with both Vitest and Jest.
*/
/**
* 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;
}
// Function that throws error for eval in transaction - outside of any mock context
const throwEvalError = () => {
throw new Error('EVAL not supported in transaction (mocked BeaconRedisMock)');
};
class BeaconRedisMock {
constructor(spyFn) {
this.spyFn = null;
this.spyFn = spyFn || null;
// Initialize mocks after spyFn is set
this.getInstanceName = this.createMock();
this.connect = this.createMock().mockResolvedValue(undefined);
this.disconnect = this.createMock().mockResolvedValue(undefined);
this.quit = this.createMock().mockResolvedValue(undefined);
this.updateConfig = this.createMock();
this.multi = this.createMock().mockReturnValue(this.createTransactionObject());
this.get = this.createMock();
this.set = this.createMock();
this.del = this.createMock();
this.exists = this.createMock();
this.expire = this.createMock();
this.ttl = this.createMock();
this.incr = this.createMock();
this.decr = this.createMock();
this.incrBy = this.createMock();
this.decrBy = this.createMock();
this.hGet = this.createMock();
this.hSet = this.createMock();
this.hGetAll = this.createMock();
this.hDel = this.createMock();
this.hExists = this.createMock();
this.hIncrBy = this.createMock();
this.lPush = this.createMock();
this.rPush = this.createMock();
this.lPop = this.createMock();
this.rPop = this.createMock();
this.lRange = this.createMock();
this.lLen = this.createMock();
this.lTrim = this.createMock();
this.sAdd = this.createMock();
this.sMembers = this.createMock();
this.sIsMember = this.createMock();
this.sRem = this.createMock();
this.sCard = this.createMock();
this.zAdd = this.createMock();
this.zRange = this.createMock();
this.zRangeWithScores = this.createMock();
this.zRem = this.createMock();
this.zCard = this.createMock();
this.zScore = this.createMock();
this.subscribe = this.createMock();
this.unsubscribe = this.createMock();
this.publish = this.createMock();
this.ping = this.createMock();
this.info = this.createMock();
this.eval = this.createMock();
}
// Create transaction object outside of mock to avoid hoisting issues
createTransactionObject() {
return {
exec: this.createMock().mockResolvedValue([]),
// eval is not implemented in transactions, so it should throw
eval: throwEvalError,
};
}
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 mockRedis = new BeaconRedisMock(vi.fn);
// For Jest:
const mockRedis = new BeaconRedisMock(jest.fn);
// For Jasmine:
const mockRedis = new BeaconRedisMock(jasmine.createSpy);
// Without spy (basic functionality only):
const mockRedis = new BeaconRedisMock();
DON'T FORGET AGAIN! 😤
`);
}
return this.spyFn(implementation);
}
}
/**
* @file src/testing/index.ts
* @description Public entry point for testing utilities.
* This allows users to import testing tools without polluting their production bundle.
*
* @example
* import { SyntropyLogTestHarness } from 'syntropylog/testing';
*/
export { BeaconRedisMock, MockBrokerAdapter, MockContextManager, MockHttpClient, MockSerializerRegistry, SpyTransport, createMockBrokerManager, createMockContextManager, createMockHttpManager, createMockLogger, createMockSerializationManager, createServiceWithMock, createSyntropyLogMock, createTestHelper, getMockBrokerManager, getMockContextManager, getMockHttpManager, getMockLogger, getMockSerializationManager, resetSyntropyLogMocks };
//# sourceMappingURL=index.mjs.map