UNPKG

syntropylog

Version:

An instance manager with observability for Node.js applications

1,174 lines (1,136 loc) 40.5 kB
'use strict'; var crypto = require('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 = crypto.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 = crypto.randomUUID(); const transactionId = crypto.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'; */ exports.BeaconRedisMock = BeaconRedisMock; exports.MockBrokerAdapter = MockBrokerAdapter; exports.MockContextManager = MockContextManager; exports.MockHttpClient = MockHttpClient; exports.MockSerializerRegistry = MockSerializerRegistry; exports.SpyTransport = SpyTransport; exports.createMockBrokerManager = createMockBrokerManager; exports.createMockContextManager = createMockContextManager; exports.createMockHttpManager = createMockHttpManager; exports.createMockLogger = createMockLogger; exports.createMockSerializationManager = createMockSerializationManager; exports.createServiceWithMock = createServiceWithMock; exports.createSyntropyLogMock = createSyntropyLogMock; exports.createTestHelper = createTestHelper; exports.getMockBrokerManager = getMockBrokerManager; exports.getMockContextManager = getMockContextManager; exports.getMockHttpManager = getMockHttpManager; exports.getMockLogger = getMockLogger; exports.getMockSerializationManager = getMockSerializationManager; exports.resetSyntropyLogMocks = resetSyntropyLogMocks; //# sourceMappingURL=index.cjs.map