/** * QuestDB Client Test Setup * * Setup file specific to QuestDB client library tests. * Provides utilities and mocks for testing database operations. */ import { beforeAll, beforeEach, mock } 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 { // 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, }; }, };