import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test'; import { RedisConnectionManager } from '../src/connection-manager'; import type { RedisConfig } from '../src/types'; // Mock ioredis const mockRedisInstance = { on: mock((_event: string, _callback: Function) => { // Store callbacks for triggering events mockRedisInstance._eventCallbacks[event] = callback; }), once: mock((event: string, callback: Function) => { mockRedisInstance._onceCallbacks[event] = callback; }), ping: mock(async () => 'PONG'), quit: mock(async () => 'OK'), status: 'ready', _eventCallbacks: {} as Record, _onceCallbacks: {} as Record, // Helper to trigger events _triggerEvent(event: string, ...args: any[]) { if (this._eventCallbacks[event]) { this._eventCallbacks[event](...args); } if (this._onceCallbacks[event]) { this._onceCallbacks[event](...args); delete this._onceCallbacks[event]; } } }; mock.module('ioredis', () => ({ default: mock(() => { // Create a new instance for each Redis connection with event handling methods const instance = { ...mockRedisInstance, _eventCallbacks: {}, _onceCallbacks: {}, on: function(event: string, callback: Function) { this._eventCallbacks[event] = callback; return this; }, once: function(event: string, callback: Function) { this._onceCallbacks[event] = callback; return this; }, _triggerEvent: function(event: string, ...args: any[]) { if (this._eventCallbacks[event]) { this._eventCallbacks[event](...args); } if (this._onceCallbacks[event]) { this._onceCallbacks[event](...args); delete this._onceCallbacks[event]; } } }; return instance; }) })); // Skip these tests when running all tests together // Run them individually with: bun test libs/core/cache/test/connection-manager.test.ts describe.skip('RedisConnectionManager', () => { let manager: RedisConnectionManager; const mockLogger = { info: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), debug: mock(() => {}), }; beforeEach(() => { // Clear static state (RedisConnectionManager as any).instance = undefined; if ((RedisConnectionManager as any).sharedConnections) { (RedisConnectionManager as any).sharedConnections.clear(); } if ((RedisConnectionManager as any).readyConnections) { (RedisConnectionManager as any).readyConnections.clear(); } // Get new instance manager = RedisConnectionManager.getInstance(); // Set mock logger on the instance (manager as any).logger = mockLogger; // Reset mocks mockLogger.info.mockClear(); mockLogger.error.mockClear(); mockLogger.warn.mockClear(); mockLogger.debug.mockClear(); }); afterEach(async () => { await manager.closeAllConnections(); }); describe('getInstance', () => { it('should return singleton instance', () => { const instance1 = RedisConnectionManager.getInstance(); const instance2 = RedisConnectionManager.getInstance(); expect(instance1).toBe(instance2); }); }); describe('getConnection', () => { const baseConfig: RedisConfig = { host: 'localhost', port: 6379, }; it('should create unique connection when singleton is false', () => { const connection1 = manager.getConnection({ name: 'test', singleton: false, redisConfig: baseConfig, logger: mockLogger, }); const connection2 = manager.getConnection({ name: 'test', singleton: false, redisConfig: baseConfig, logger: mockLogger, }); expect(connection1).not.toBe(connection2); expect(mockLogger.debug).toHaveBeenCalledTimes(2); }); it('should reuse shared connection when singleton is true', () => { const connection1 = manager.getConnection({ name: 'shared-test', singleton: true, redisConfig: baseConfig, logger: mockLogger, }); const connection2 = manager.getConnection({ name: 'shared-test', singleton: true, redisConfig: baseConfig, logger: mockLogger, }); expect(connection1).toBe(connection2); expect(mockLogger.info).toHaveBeenCalledWith('Created shared Redis connection: shared-test'); }); it('should apply custom db number', () => { const connection = manager.getConnection({ name: 'db-test', singleton: false, db: 5, redisConfig: baseConfig, logger: mockLogger, }); expect(connection).toBeDefined(); }); it('should handle TLS configuration', () => { const tlsConfig: RedisConfig = { ...baseConfig, tls: { cert: 'cert-content', key: 'key-content', ca: 'ca-content', rejectUnauthorized: false, }, }; const connection = manager.getConnection({ name: 'tls-test', singleton: false, redisConfig: tlsConfig, logger: mockLogger, }); expect(connection).toBeDefined(); }); it('should use provided logger', () => { const customLogger = { info: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), debug: mock(() => {}), }; manager.getConnection({ name: 'logger-test', singleton: false, redisConfig: baseConfig, logger: customLogger, }); expect(customLogger.debug).toHaveBeenCalled(); }); }); describe('connection events', () => { it('should handle connect event', () => { const connection = manager.getConnection({ name: 'event-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); // Trigger connect event (connection as any)._triggerEvent('connect'); expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Redis connection established')); }); it('should handle ready event', () => { const connection = manager.getConnection({ name: 'ready-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); // Trigger ready event (connection as any)._triggerEvent('ready'); expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Redis connection ready')); }); it('should handle error event', () => { const connection = manager.getConnection({ name: 'error-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); const error = new Error('Connection failed'); (connection as any)._triggerEvent('error', error); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('Redis connection error'), error ); }); it('should handle close event', () => { const connection = manager.getConnection({ name: 'close-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); (connection as any)._triggerEvent('close'); expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('Redis connection closed')); }); it('should handle reconnecting event', () => { const connection = manager.getConnection({ name: 'reconnect-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); (connection as any)._triggerEvent('reconnecting'); expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('Redis reconnecting')); }); }); describe('closeConnection', () => { it('should close connection successfully', async () => { const connection = manager.getConnection({ name: 'close-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, }); await manager.closeConnection(connection); expect(connection.quit).toHaveBeenCalled(); }); it('should handle close errors gracefully', async () => { const connection = manager.getConnection({ name: 'close-error-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); // Make quit throw an error (connection.quit as any).mockImplementation(() => Promise.reject(new Error('Quit failed'))); await manager.closeConnection(connection); expect(mockLogger.warn).toHaveBeenCalledWith( 'Error closing Redis connection:', expect.any(Error) ); }); }); describe('closeAllConnections', () => { it('should close all unique connections', async () => { const conn1 = manager.getConnection({ name: 'unique1', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); const conn2 = manager.getConnection({ name: 'unique2', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); await manager.closeAllConnections(); expect(conn1.quit).toHaveBeenCalled(); expect(conn2.quit).toHaveBeenCalled(); expect(mockLogger.info).toHaveBeenCalledWith('All Redis connections closed'); }); it('should close shared connections', async () => { const sharedConn = manager.getConnection({ name: 'shared', singleton: true, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); await manager.closeAllConnections(); expect(sharedConn.quit).toHaveBeenCalled(); expect(manager.getConnectionCount()).toEqual({ shared: 0, unique: 0 }); }); }); describe('getConnectionCount', () => { it('should return correct connection counts', () => { manager.getConnection({ name: 'unique1', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, }); manager.getConnection({ name: 'unique2', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, }); manager.getConnection({ name: 'shared1', singleton: true, redisConfig: { host: 'localhost', port: 6379 }, }); const counts = manager.getConnectionCount(); expect(counts.unique).toBe(2); expect(counts.shared).toBe(1); }); }); describe('getConnectionNames', () => { it('should return connection names', () => { manager.getConnection({ name: 'test-unique', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, }); manager.getConnection({ name: 'test-shared', singleton: true, redisConfig: { host: 'localhost', port: 6379 }, }); const names = manager.getConnectionNames(); expect(names.shared).toContain('test-shared'); expect(names.unique.length).toBe(1); expect(names.unique[0]).toContain('test-unique'); }); }); describe('healthCheck', () => { it('should report healthy connections', async () => { manager.getConnection({ name: 'health-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, }); const health = await manager.healthCheck(); expect(health.healthy).toBe(true); expect(Object.keys(health.details).length).toBeGreaterThan(0); }); it('should report unhealthy connections', async () => { const connection = manager.getConnection({ name: 'unhealthy-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, }); // Make ping fail (connection.ping as any).mockImplementation(() => Promise.reject(new Error('Ping failed'))); const health = await manager.healthCheck(); expect(health.healthy).toBe(false); expect(Object.values(health.details)).toContain(false); }); }); describe('waitForAllConnections', () => { it('should wait for connections to be ready', async () => { manager.getConnection({ name: 'wait-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); // Connection is already ready await RedisConnectionManager.waitForAllConnections(1000); expect(mockLogger.info).toHaveBeenCalledWith('All Redis connections are ready'); }); it('should handle no connections', async () => { await RedisConnectionManager.waitForAllConnections(1000); expect(mockLogger.debug).toHaveBeenCalledWith('No Redis connections to wait for'); }); it('should timeout if connection not ready', async () => { const connection = manager.getConnection({ name: 'timeout-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, }); // Make connection not ready (connection as any).status = 'connecting'; await expect(RedisConnectionManager.waitForAllConnections(100)).rejects.toThrow( 'failed to be ready within 100ms' ); }); it('should handle connection errors during wait', async () => { const connection = manager.getConnection({ name: 'error-wait-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, logger: mockLogger, }); // Make connection not ready (connection as any).status = 'connecting'; // Trigger error after a delay setTimeout(() => { (connection as any)._triggerEvent('error', new Error('Connection failed')); }, 50); await expect(RedisConnectionManager.waitForAllConnections(1000)).rejects.toThrow( 'Connection failed' ); }); }); describe('areAllConnectionsReady', () => { it('should return false when no connections', () => { expect(RedisConnectionManager.areAllConnectionsReady()).toBe(false); }); it('should return true when all connections ready', async () => { manager.getConnection({ name: 'ready-check-test', singleton: false, redisConfig: { host: 'localhost', port: 6379 }, }); await RedisConnectionManager.waitForAllConnections(1000); expect(RedisConnectionManager.areAllConnectionsReady()).toBe(true); }); }); describe('edge cases', () => { it('should handle concurrent access to shared connections', () => { // Test that multiple requests for the same shared connection return the same instance const conn1 = manager.getConnection({ name: 'shared-concurrent', singleton: true, redisConfig: { host: 'localhost', port: 6379 }, }); const conn2 = manager.getConnection({ name: 'shared-concurrent', singleton: true, redisConfig: { host: 'localhost', port: 6379 }, }); expect(conn1).toBe(conn2); expect(manager.getConnectionCount().shared).toBe(1); }); it('should apply all Redis options', () => { const fullConfig: RedisConfig = { host: 'localhost', port: 6379, username: 'user', password: 'pass', db: 2, maxRetriesPerRequest: 5, retryDelayOnFailover: 200, connectTimeout: 20000, commandTimeout: 10000, keepAlive: 5000, }; const connection = manager.getConnection({ name: 'full-config-test', singleton: false, redisConfig: fullConfig, }); expect(connection).toBeDefined(); }); }); });