stock-bot/libs/questdb-client/test/setup.ts

280 lines
7 KiB
TypeScript

/**
* QuestDB Client Test Setup
*
* Setup file specific to QuestDB client library tests.
* Provides utilities and mocks for testing database operations.
*/
import { beforeAll, beforeEach, mock, spyOn } from 'bun:test';
import { newDb } from 'pg-mem';
// Mock PostgreSQL database for unit tests
let pgMem: any;
beforeAll(() => {
// Create in-memory PostgreSQL database
pgMem = newDb();
// Register QuestDB-specific functions
pgMem.public.registerFunction({
name: 'now',
implementation: () => new Date().toISOString(),
});
pgMem.public.registerFunction({
name: 'dateadd',
args: [{ type: 'text' }, { type: 'int' }, { type: 'timestamp' }],
returns: 'timestamp',
implementation: (unit: string, amount: number, date: Date) => {
const result = new Date(date);
switch (unit) {
case 'd':
case 'day':
result.setDate(result.getDate() + amount);
break;
case 'h':
case 'hour':
result.setHours(result.getHours() + amount);
break;
case 'm':
case 'minute':
result.setMinutes(result.getMinutes() + amount);
break;
default:
throw new Error(`Unsupported date unit: ${unit}`);
}
return result;
},
}); // Mock QuestDB HTTP client
// Mock fetch using Bun's built-in mock
(global as any).fetch = mock(() => {});
// Mock the logger module to avoid Pino configuration conflicts
mock.module('@stock-bot/logger', () => ({
Logger: mock(() => ({
info: mock(() => {}),
warn: mock(() => {}),
error: mock(() => {}),
debug: mock(() => {}),
fatal: mock(() => {}),
trace: mock(() => {}),
child: mock(() => ({
info: mock(() => {}),
warn: mock(() => {}),
error: mock(() => {}),
debug: mock(() => {}),
fatal: mock(() => {}),
trace: mock(() => {}),
})),
})),
getLogger: mock(() => ({
info: mock(() => {}),
warn: mock(() => {}),
error: mock(() => {}),
debug: mock(() => {}),
fatal: mock(() => {}),
trace: mock(() => {}),
child: mock(() => ({
info: mock(() => {}),
warn: mock(() => {}),
error: mock(() => {}),
debug: mock(() => {}),
fatal: mock(() => {}),
trace: mock(() => {}),
})),
})),
}));
// Mock Pino and its transports to avoid configuration conflicts
mock.module('pino', () => ({
default: mock(() => ({
info: mock(() => {}),
warn: mock(() => {}),
error: mock(() => {}),
debug: mock(() => {}),
fatal: mock(() => {}),
trace: mock(() => {}),
child: mock(() => ({
info: mock(() => {}),
warn: mock(() => {}),
error: mock(() => {}),
debug: mock(() => {}),
fatal: mock(() => {}),
trace: mock(() => {}),
})),
})),
}));
mock.module('pino-pretty', () => ({
default: mock(() => ({})),
}));
mock.module('pino-loki', () => ({
default: mock(() => ({})),
}));
});
beforeEach(() => {
// Reset database state
if (pgMem) {
try {
pgMem.public.none('DROP TABLE IF EXISTS ohlcv CASCADE');
pgMem.public.none('DROP TABLE IF EXISTS trades CASCADE');
pgMem.public.none('DROP TABLE IF EXISTS quotes CASCADE');
pgMem.public.none('DROP TABLE IF EXISTS indicators CASCADE');
pgMem.public.none('DROP TABLE IF EXISTS performance CASCADE');
pgMem.public.none('DROP TABLE IF EXISTS risk_metrics CASCADE');
} catch (error) {
// Tables might not exist, ignore errors
}
} // Reset fetch mock
if ((global as any).fetch) {
((global as any).fetch as any).mockClear?.();
}
});
/**
* QuestDB-specific test utilities
*/
export const questdbTestHelpers = {
/**
* Get mock PostgreSQL adapter
*/
getMockPgAdapter: () => pgMem?.adapters?.createPg?.(),
/**
* Execute SQL in mock database
*/
executeMockSQL: (sql: string, params?: any[]) => {
return pgMem?.public?.query(sql, params);
},
/**
* Mock successful QuestDB HTTP response
*/ mockQuestDBHttpSuccess: (data: any) => {
((global as any).fetch as any).mockResolvedValue?.({
ok: true,
status: 200,
json: async () => data,
text: async () => JSON.stringify(data),
});
},
/**
* Mock QuestDB HTTP error
*/ mockQuestDBHttpError: (status: number, message: string) => {
((global as any).fetch as any).mockResolvedValue?.({
ok: false,
status,
json: async () => ({ error: message }),
text: async () => message,
});
},
/**
* Mock InfluxDB line protocol response
*/ mockInfluxDBSuccess: () => {
((global as any).fetch as any).mockResolvedValue?.({
ok: true,
status: 204,
text: async () => '',
});
},
/**
* Create test OHLCV table
*/
createTestOHLCVTable: () => {
const sql = `
CREATE TABLE ohlcv (
symbol VARCHAR(10),
timestamp TIMESTAMP,
open DECIMAL(10,2),
high DECIMAL(10,2),
low DECIMAL(10,2),
close DECIMAL(10,2),
volume BIGINT,
source VARCHAR(50)
)
`;
return pgMem?.public?.none(sql);
},
/**
* Insert test OHLCV data
*/
insertTestOHLCVData: (data: any[]) => {
const sql = `
INSERT INTO ohlcv (symbol, timestamp, open, high, low, close, volume, source)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`;
return Promise.all(
data.map(row =>
pgMem?.public?.none(sql, [
row.symbol,
row.timestamp,
row.open,
row.high,
row.low,
row.close,
row.volume,
row.source || 'test',
])
)
);
},
/**
* Generate InfluxDB line protocol test data
*/
generateInfluxDBLines: (count: number = 5) => {
const lines: string[] = [];
const baseTime = Date.now() * 1000000; // Convert to nanoseconds
for (let i = 0; i < count; i++) {
const time = baseTime + i * 60000000000; // 1 minute intervals
const price = 150 + Math.random() * 10;
lines.push(
`ohlcv,symbol=TEST open=${price},high=${price + 1},low=${price - 1},close=${price + 0.5},volume=1000i ${time}`
);
}
return lines;
},
/**
* Validate QuestDB query syntax
*/
validateQuestDBQuery: (query: string): boolean => {
// Basic validation for QuestDB-specific syntax
const questdbKeywords = ['SAMPLE BY', 'LATEST BY', 'ASOF JOIN', 'SPLICE JOIN', 'LT JOIN'];
// Check for valid SQL structure
const hasSelect = /SELECT\s+/i.test(query);
const hasFrom = /FROM\s+/i.test(query);
return hasSelect && hasFrom;
},
/**
* Mock connection pool
*/ createMockPool: () => {
const mockQuery = () => Promise.resolve({ rows: [], rowCount: 0 });
const mockRelease = () => {};
const mockConnect = () =>
Promise.resolve({
query: mockQuery,
release: mockRelease,
});
const mockEnd = () => Promise.resolve(undefined);
return {
connect: mockConnect,
end: mockEnd,
totalCount: 0,
idleCount: 0,
waitingCount: 0,
};
},
};