96 lines
3.3 KiB
TypeScript
96 lines
3.3 KiB
TypeScript
|
|
import { NetaClawDataSourceEntity } from '../src/modules/netaclaw/entity/data_source.js';
|
||
|
|
import { MysqlPoolManager } from '../src/modules/netaclaw/service/mysql_pool.js';
|
||
|
|
|
||
|
|
function makeDataSource(overrides: Partial<NetaClawDataSourceEntity> = {}) {
|
||
|
|
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',
|
||
|
|
})
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|