import { beforeEach, describe, expect, it, mock } from 'bun:test'; import { QuestDBHealthMonitor } from '../src/health'; import { QuestDBInfluxWriter } from '../src/influx-writer'; import { QuestDBQueryBuilder } from '../src/query-builder'; import { QuestDBSchemaManager } from '../src/schema'; import type { OHLCVData, QuestDBClientConfig, TradeData } from '../src/types'; // Simple in-memory QuestDB client for testing class SimpleQuestDBClient { private data = new Map(); private schemas = new Map(); private logger: any; private config: QuestDBClientConfig; private connected = false; constructor(config: QuestDBClientConfig, logger?: any) { this.config = config; this.logger = logger || console; } async connect(): Promise { this.connected = true; this.logger.info('Connected to QuestDB'); } async disconnect(): Promise { this.connected = false; this.logger.info('Disconnected from QuestDB'); } async query(sql: string): Promise { if (!this.connected) { throw new Error('Not connected to QuestDB'); } // Parse simple SELECT queries const match = sql.match(/SELECT \* FROM (\w+)/i); if (match) { const table = match[1]; return (this.data.get(table) || []) as T[]; } return []; } async execute(sql: string): Promise { if (!this.connected) { throw new Error('Not connected to QuestDB'); } // Parse simple CREATE TABLE const createMatch = sql.match(/CREATE TABLE IF NOT EXISTS (\w+)/i); if (createMatch) { const table = createMatch[1]; this.schemas.set(table, {}); this.data.set(table, []); } } async insertOHLCV(data: OHLCVData[]): Promise { if (!this.connected) { throw new Error('Not connected to QuestDB'); } const ohlcv = this.data.get('ohlcv') || []; ohlcv.push(...data); this.data.set('ohlcv', ohlcv); } async insertTrades(trades: TradeData[]): Promise { if (!this.connected) { throw new Error('Not connected to QuestDB'); } const tradesData = this.data.get('trades') || []; tradesData.push(...trades); this.data.set('trades', tradesData); } async getLatestOHLCV(symbol: string, limit = 100): Promise { const ohlcv = this.data.get('ohlcv') || []; return ohlcv.filter(item => item.symbol === symbol).slice(-limit); } async getOHLCVRange(symbol: string, startTime: Date, endTime: Date): Promise { const ohlcv = this.data.get('ohlcv') || []; const start = startTime.getTime(); const end = endTime.getTime(); return ohlcv.filter( item => item.symbol === symbol && item.timestamp >= start && item.timestamp <= end ); } async healthCheck(): Promise { return this.connected; } } describe('QuestDB', () => { describe('QuestDBClient', () => { let client: SimpleQuestDBClient; const logger = { info: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), debug: mock(() => {}), }; const config: QuestDBClientConfig = { host: 'localhost', httpPort: 9000, pgPort: 8812, influxPort: 9009, database: 'questdb', }; beforeEach(() => { logger.info = mock(() => {}); logger.error = mock(() => {}); client = new SimpleQuestDBClient(config, logger); }); it('should connect to database', async () => { await client.connect(); expect(logger.info).toHaveBeenCalledWith('Connected to QuestDB'); }); it('should disconnect from database', async () => { await client.connect(); await client.disconnect(); expect(logger.info).toHaveBeenCalledWith('Disconnected from QuestDB'); }); it('should throw error when querying without connection', async () => { await expect(client.query('SELECT * FROM ohlcv')).rejects.toThrow('Not connected to QuestDB'); }); it('should execute CREATE TABLE statements', async () => { await client.connect(); await client.execute('CREATE TABLE IF NOT EXISTS ohlcv'); const result = await client.query('SELECT * FROM ohlcv'); expect(result).toEqual([]); }); it('should insert and retrieve OHLCV data', async () => { await client.connect(); const ohlcvData: OHLCVData[] = [ { symbol: 'AAPL', timestamp: Date.now(), open: 150.0, high: 152.0, low: 149.0, close: 151.0, volume: 1000000, }, ]; await client.insertOHLCV(ohlcvData); const result = await client.getLatestOHLCV('AAPL'); expect(result).toHaveLength(1); expect(result[0].symbol).toBe('AAPL'); expect(result[0].close).toBe(151.0); }); it('should insert and retrieve trade data', async () => { await client.connect(); const trades: TradeData[] = [ { symbol: 'AAPL', timestamp: Date.now(), price: 151.5, quantity: 100, side: 'buy', exchange: 'NASDAQ', }, ]; await client.insertTrades(trades); // Just verify it doesn't throw expect(true).toBe(true); }); it('should get OHLCV data within time range', async () => { await client.connect(); const now = Date.now(); const ohlcvData: OHLCVData[] = [ { symbol: 'AAPL', timestamp: now - 3600000, // 1 hour ago open: 149.0, high: 150.0, low: 148.0, close: 149.5, volume: 500000, }, { symbol: 'AAPL', timestamp: now - 1800000, // 30 minutes ago open: 149.5, high: 151.0, low: 149.0, close: 150.5, volume: 600000, }, { symbol: 'AAPL', timestamp: now, // now open: 150.5, high: 152.0, low: 150.0, close: 151.5, volume: 700000, }, ]; await client.insertOHLCV(ohlcvData); const result = await client.getOHLCVRange( 'AAPL', new Date(now - 2700000), // 45 minutes ago new Date(now) ); expect(result).toHaveLength(2); expect(result[0].timestamp).toBe(now - 1800000); expect(result[1].timestamp).toBe(now); }); it('should perform health check', async () => { expect(await client.healthCheck()).toBe(false); await client.connect(); expect(await client.healthCheck()).toBe(true); await client.disconnect(); expect(await client.healthCheck()).toBe(false); }); }); describe('QuestDBQueryBuilder', () => { it('should build SELECT query', () => { const mockClient = { query: async () => ({ rows: [], count: 0 }), }; const builder = new QuestDBQueryBuilder(mockClient); const query = builder .select('symbol', 'close', 'volume') .from('ohlcv_data') .whereSymbol('AAPL') .orderBy('timestamp', 'DESC') .limit(100) .build(); expect(query).toContain('SELECT symbol, close, volume'); expect(query).toContain('FROM ohlcv_data'); expect(query).toContain("symbol = 'AAPL'"); expect(query).toContain('ORDER BY timestamp DESC'); expect(query).toContain('LIMIT 100'); }); it('should build query with time range', () => { const mockClient = { query: async () => ({ rows: [], count: 0 }), }; const builder = new QuestDBQueryBuilder(mockClient); const startTime = new Date('2023-01-01'); const endTime = new Date('2023-01-31'); const query = builder.from('trades').whereTimeRange(startTime, endTime).build(); expect(query).toContain('timestamp >='); expect(query).toContain('timestamp <='); }); it('should build aggregation query', () => { const mockClient = { query: async () => ({ rows: [], count: 0 }), }; const builder = new QuestDBQueryBuilder(mockClient); const query = builder .selectAgg({ avg_close: 'AVG(close)', total_volume: 'SUM(volume)', }) .from('ohlcv_data') .groupBy('symbol') .build(); expect(query).toContain('AVG(close) as avg_close'); expect(query).toContain('SUM(volume) as total_volume'); expect(query).toContain('GROUP BY symbol'); }); it('should build sample by query', () => { const mockClient = { query: async () => ({ rows: [], count: 0 }), }; const builder = new QuestDBQueryBuilder(mockClient); const query = builder .select('timestamp', 'symbol', 'close') .from('ohlcv_data') .sampleBy('1h') .build(); expect(query).toContain('SAMPLE BY 1h'); }); }); describe('QuestDBInfluxWriter', () => { it('should write OHLCV data', async () => { const mockClient = { getHttpUrl: () => 'http://localhost:9000', }; const writer = new QuestDBInfluxWriter(mockClient); const data = [ { timestamp: new Date('2022-01-01T00:00:00.000Z'), open: 150.0, high: 152.0, low: 149.0, close: 151.0, volume: 1000000, }, ]; // Mock fetch global.fetch = mock(async () => ({ ok: true, status: 200, statusText: 'OK', })); await writer.writeOHLCV('AAPL', 'NASDAQ', data); expect(global.fetch).toHaveBeenCalled(); }); it('should write trade execution data', async () => { const mockClient = { getHttpUrl: () => 'http://localhost:9000', }; const writer = new QuestDBInfluxWriter(mockClient); // Mock fetch global.fetch = mock(async () => ({ ok: true, status: 200, statusText: 'OK', })); await writer.writeTradeExecution({ symbol: 'AAPL', side: 'buy', quantity: 100, price: 151.5, timestamp: new Date(), executionTime: 50, orderId: 'order-123', strategy: 'momentum', }); expect(global.fetch).toHaveBeenCalled(); }); it('should handle batch writes', async () => { const mockClient = { getHttpUrl: () => 'http://localhost:9000', }; const writer = new QuestDBInfluxWriter(mockClient); // Mock fetch global.fetch = mock(async () => ({ ok: true, status: 200, statusText: 'OK', })); const points = [ { measurement: 'test', tags: { symbol: 'AAPL' }, fields: { value: 100 }, timestamp: new Date(), }, { measurement: 'test', tags: { symbol: 'GOOGL' }, fields: { value: 200 }, timestamp: new Date(), }, ]; await writer.writePoints(points); expect(global.fetch).toHaveBeenCalled(); }); }); describe('QuestDBSchemaManager', () => { let mockClient: any; let schemaManager: QuestDBSchemaManager; beforeEach(() => { mockClient = { query: mock(async () => ({ rows: [], count: 0 })), }; schemaManager = new QuestDBSchemaManager(mockClient); }); it('should create table with schema', async () => { const schema = schemaManager.getSchema('ohlcv_data'); expect(schema).toBeDefined(); expect(schema?.tableName).toBe('ohlcv_data'); await schemaManager.createTable(schema!); expect(mockClient.query).toHaveBeenCalled(); const sql = mockClient.query.mock.calls[0][0]; expect(sql).toContain('CREATE TABLE IF NOT EXISTS ohlcv_data'); }); it('should check if table exists', async () => { mockClient.query = mock(async () => ({ rows: [{ count: 1 }], count: 1, })); const exists = await schemaManager.tableExists('ohlcv_data'); expect(exists).toBe(true); }); it('should create all tables', async () => { await schemaManager.createAllTables(); // Should create multiple tables expect(mockClient.query).toHaveBeenCalled(); expect(mockClient.query.mock.calls.length).toBeGreaterThan(3); }); it('should get table stats', async () => { mockClient.query = mock(async () => ({ rows: [ { row_count: 1000, min_timestamp: new Date('2023-01-01'), max_timestamp: new Date('2023-12-31'), }, ], count: 1, })); const stats = await schemaManager.getTableStats('ohlcv_data'); expect(stats.row_count).toBe(1000); expect(stats.min_timestamp).toBeDefined(); expect(stats.max_timestamp).toBeDefined(); }); }); describe('QuestDBHealthMonitor', () => { let mockClient: any; let monitor: QuestDBHealthMonitor; beforeEach(() => { mockClient = { query: mock(async () => ({ rows: [{ health_check: 1 }], count: 1 })), isPgPoolHealthy: mock(() => true), }; monitor = new QuestDBHealthMonitor(mockClient); }); it('should perform health check', async () => { const health = await monitor.performHealthCheck(); expect(health.isHealthy).toBe(true); expect(health.lastCheck).toBeInstanceOf(Date); expect(health.responseTime).toBeGreaterThanOrEqual(0); expect(health.message).toBe('Connection healthy'); }); it('should handle failed health check', async () => { mockClient.query = mock(async () => { throw new Error('Connection failed'); }); const health = await monitor.performHealthCheck(); expect(health.isHealthy).toBe(false); expect(health.error).toBeDefined(); expect(health.message).toContain('Connection failed'); }); it('should record query metrics', () => { monitor.recordQuery(true, 50); monitor.recordQuery(true, 100); monitor.recordQuery(false, 200); const metrics = monitor.getPerformanceMetrics(); expect(metrics.totalQueries).toBe(3); expect(metrics.successfulQueries).toBe(2); expect(metrics.failedQueries).toBe(1); expect(metrics.averageResponseTime).toBeCloseTo(116.67, 1); }); it('should start and stop monitoring', () => { monitor.startMonitoring(1000); // Just verify it doesn't throw expect(true).toBe(true); monitor.stopMonitoring(); }); }); });