import { NetaClawDataSourceEntity } from '../src/modules/netaclaw/entity/data_source.js'; import { MysqlPoolManager } from '../src/modules/netaclaw/service/mysql_pool.js'; function makeDataSource(overrides: Partial = {}) { return Object.assign(new NetaClawDataSourceEntity(), { id: 7, name: 'orders', label: 'Orders DB', type: 'mysql', host: 'db.internal', port: 3307, database: 'orders_prod', username: 'readonly', passwordEncrypted: 'ciphertext', readonly: true, status: 1, allowedAgentIds: [9], extra: { poolConnectionLimit: 20, connectTimeout: 500, ssl: { rejectUnauthorized: true }, }, ...overrides, }); } describe('MysqlPoolManager', () => { it('reuses pools by data source id, clamps connection limits, and decrypts passwords', async () => { const pool = { end: jest.fn() }; const createPool = jest.fn().mockReturnValue(pool); const manager = new MysqlPoolManager(createPool as any); manager.secretCrypto = { decryptText: jest.fn().mockReturnValue('plain-password'), } as any; const first = await manager.getPool(makeDataSource()); const second = await manager.getPool(makeDataSource({ host: 'other.internal' })); expect(first).toBe(pool); expect(second).toBe(pool); expect(createPool).toHaveBeenCalledTimes(1); expect(createPool).toHaveBeenCalledWith({ host: 'db.internal', port: 3307, database: 'orders_prod', user: 'readonly', password: 'plain-password', waitForConnections: true, connectionLimit: 10, connectTimeout: 1000, ssl: { rejectUnauthorized: true }, }); }); it('closes and forgets cached pools', async () => { const firstPool = { end: jest.fn().mockResolvedValue(undefined) }; const secondPool = { end: jest.fn().mockResolvedValue(undefined) }; const createPool = jest.fn().mockReturnValueOnce(firstPool).mockReturnValueOnce(secondPool); const manager = new MysqlPoolManager(createPool as any); manager.secretCrypto = { decryptText: jest.fn().mockReturnValue('plain-password'), } as any; await manager.getPool(makeDataSource()); await manager.closePool(7); const next = await manager.getPool(makeDataSource()); expect(firstPool.end).toHaveBeenCalledTimes(1); expect(next).toBe(secondPool); expect(createPool).toHaveBeenCalledTimes(2); }); it('creates transient pools without caching and clamps transient connection limits', async () => { const firstPool = { end: jest.fn() }; const secondPool = { end: jest.fn() }; const createPool = jest.fn().mockReturnValueOnce(firstPool).mockReturnValueOnce(secondPool); const manager = new MysqlPoolManager(createPool as any); manager.secretCrypto = { decryptText: jest.fn().mockReturnValue('plain-password'), } as any; const first = await manager.createTransientPool(makeDataSource()); const second = await manager.createTransientPool(makeDataSource()); expect(first).toBe(firstPool); expect(second).toBe(secondPool); expect(createPool).toHaveBeenCalledTimes(2); expect(createPool).toHaveBeenLastCalledWith( expect.objectContaining({ connectionLimit: 3, password: 'plain-password', }) ); }); });