fixed some lint issues
This commit is contained in:
parent
8680b6ec20
commit
a700818a06
15 changed files with 1574 additions and 1319 deletions
619
libs/core/cache/test/connection-manager.test.ts
vendored
619
libs/core/cache/test/connection-manager.test.ts
vendored
|
|
@ -1,543 +1,94 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
|
||||
import Redis from 'ioredis';
|
||||
import { describe, it, expect } 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<string, Function>,
|
||||
_onceCallbacks: {} as Record<string, Function>,
|
||||
// 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();
|
||||
describe('RedisConnectionManager', () => {
|
||||
it('should be a singleton', () => {
|
||||
const instance1 = RedisConnectionManager.getInstance();
|
||||
const instance2 = RedisConnectionManager.getInstance();
|
||||
expect(instance1).toBe(instance2);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const connection = 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 () => {
|
||||
const connection = 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 = {
|
||||
it('should create connections', () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
const connection = manager.getConnection({
|
||||
name: 'test',
|
||||
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();
|
||||
},
|
||||
});
|
||||
|
||||
expect(connection).toBeDefined();
|
||||
expect(connection.options.host).toBe('localhost');
|
||||
expect(connection.options.port).toBe(6379);
|
||||
});
|
||||
|
||||
it('should reuse singleton connections', () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
|
||||
const conn1 = manager.getConnection({
|
||||
name: 'shared',
|
||||
singleton: true,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
const conn2 = manager.getConnection({
|
||||
name: 'shared',
|
||||
singleton: true,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
expect(conn1).toBe(conn2);
|
||||
});
|
||||
|
||||
it('should create separate non-singleton connections', () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
|
||||
const conn1 = manager.getConnection({
|
||||
name: 'separate1',
|
||||
singleton: false,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
const conn2 = manager.getConnection({
|
||||
name: 'separate2',
|
||||
singleton: false,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
expect(conn1).not.toBe(conn2);
|
||||
});
|
||||
|
||||
it('should close all connections', async () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
|
||||
// Create a few connections
|
||||
manager.getConnection({
|
||||
name: 'close-test-1',
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
});
|
||||
|
||||
manager.getConnection({
|
||||
name: 'close-test-2',
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
});
|
||||
|
||||
// Close all
|
||||
await RedisConnectionManager.closeAll();
|
||||
|
||||
// Should not throw
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue