From 674112af05e7f81ef443a7a579690821ffdb06b9 Mon Sep 17 00:00:00 2001 From: Bojan Kucera Date: Wed, 4 Jun 2025 14:15:36 -0400 Subject: [PATCH] added questdb-client integration tests --- libs/logger/test/middleware.test.ts.new | 273 +++++++++++++++++++ libs/questdb-client/test/integration.test.ts | 72 ++--- libs/questdb-client/test/setup.ts | 72 ++++- 3 files changed, 379 insertions(+), 38 deletions(-) create mode 100644 libs/logger/test/middleware.test.ts.new diff --git a/libs/logger/test/middleware.test.ts.new b/libs/logger/test/middleware.test.ts.new new file mode 100644 index 0000000..1a99d28 --- /dev/null +++ b/libs/logger/test/middleware.test.ts.new @@ -0,0 +1,273 @@ +/** + * Logger Middleware Integration Tests + * + * Tests the Hono middleware functionality of the @stock-bot/logger package, + * verifying that all middleware components work correctly together. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { + loggingMiddleware, + errorLoggingMiddleware, + performanceMiddleware, + securityMiddleware, + businessEventMiddleware, + comprehensiveLoggingMiddleware +} from '../src'; +import { loggerTestHelpers } from './setup'; + +describe('Logger Middleware Integration', () => { + beforeEach(() => { + // Clear logs before each test + loggerTestHelpers.clearCapturedLogs(); + }); + + describe('Basic Logging Middleware', () => { + it('should log incoming requests', async () => { + // Create middleware with test logger + const testLogger = loggerTestHelpers.createTestLogger('middleware-test'); + const middleware = loggingMiddleware({ + serviceName: 'test-service', + logger: testLogger + }); + + // Create mock context + const mockContext = loggerTestHelpers.createHonoContextMock({ + path: '/test', + method: 'GET' + }); + const mockNext = loggerTestHelpers.createNextMock(); + + // Execute middleware + await middleware(mockContext, mockNext); + + // Get captured logs + const logs = loggerTestHelpers.getCapturedLogs(); + + // Verify request was logged + expect(logs.length).toBeGreaterThan(0); + expect(logs[0].level).toBe('http'); + expect(logs[0].msg).toContain('HTTP Request'); + }); + + it('should skip logging for defined paths', async () => { + // Create middleware with skip paths + const testLogger = loggerTestHelpers.createTestLogger('middleware-test'); + const middleware = loggingMiddleware({ + serviceName: 'test-service', + logger: testLogger, + skipPaths: ['/test'] + }); + + // Create mock context with path that should be skipped + const mockContext = loggerTestHelpers.createHonoContextMock({ + path: '/test', + method: 'GET' + }); + const mockNext = loggerTestHelpers.createNextMock(); + + // Execute middleware + await middleware(mockContext, mockNext); + + // Get captured logs + const logs = loggerTestHelpers.getCapturedLogs(); + + // Verify no logs were captured + expect(logs.length).toBe(0); + }); + + it('should log request/response details', async () => { + // Create middleware that logs bodies + const testLogger = loggerTestHelpers.createTestLogger('middleware-test'); + const middleware = loggingMiddleware({ + serviceName: 'test-service', + logger: testLogger, + logRequestBody: true, + logResponseBody: true + }); + + // Create mock context with response status + const mockContext = loggerTestHelpers.createHonoContextMock({ + path: '/api/data', + method: 'POST', + res: { + status: 200, + body: { success: true } + } + }); + const mockNext = loggerTestHelpers.createNextMock(); + + // Execute middleware + await middleware(mockContext, mockNext); + + // Get captured logs + const logs = loggerTestHelpers.getCapturedLogs(); + + // Verify request and response were logged + expect(logs.length).toBeGreaterThan(0); + // First log is HTTP request started, and final log is HTTP request completed + expect(logs[0].msg).toContain('HTTP Request started'); + expect(logs[logs.length-1].msg).toContain('HTTP Request completed'); + }); + }); + + describe('Error Logging Middleware', () => { + it('should log errors from next middleware', async () => { + // Create middleware + const testLogger = loggerTestHelpers.createTestLogger('middleware-test'); + const middleware = errorLoggingMiddleware(testLogger); + + // Create mock context + const mockContext = loggerTestHelpers.createHonoContextMock({ + path: '/api/error', + method: 'GET' + }); + + // Create next function that throws error + const mockNext = async () => { + throw new Error('Test error'); + }; + + // Execute middleware + try { + await middleware(mockContext, mockNext); + } catch (error) { + // Error should be re-thrown after logging + } + + // Get captured logs + const logs = loggerTestHelpers.getCapturedLogs(); + + // Verify error was logged + expect(logs.length).toBe(1); + expect(logs[0].level).toBe('error'); + expect(logs[0].msg).toContain('Unhandled HTTP error'); + }); + }); + + describe('Performance Middleware', () => { + it('should log performance metrics for requests', async () => { + // Create middleware + const testLogger = loggerTestHelpers.createTestLogger('middleware-test'); + const middleware = performanceMiddleware('test-operation', testLogger); + + // Create mock context + const mockContext = loggerTestHelpers.createHonoContextMock({ + path: '/api/data', + method: 'GET' + }); + const mockNext = loggerTestHelpers.createNextMock(); + + // Execute middleware + await middleware(mockContext, mockNext); + + // Get captured logs + const logs = loggerTestHelpers.getCapturedLogs(); + + // Verify performance metrics were logged + expect(logs.length).toBe(1); + expect(logs[0].level).toBe('info'); + expect(logs[0].msg).toContain('Operation completed'); + expect(logs[0].performance).toBeDefined(); + }); + }); + + describe('Security Middleware', () => { + it('should log security events', async () => { + // Create middleware + const testLogger = loggerTestHelpers.createTestLogger('middleware-test'); + const middleware = securityMiddleware(testLogger); + + // Create mock context with auth header + const mockContext = loggerTestHelpers.createHonoContextMock({ + path: '/api/secure', + method: 'GET', + req: { + headers: { + 'authorization': 'Bearer test-token' + } + } + }); + const mockNext = loggerTestHelpers.createNextMock(); + + // Execute middleware + await middleware(mockContext, mockNext); + + // Get captured logs + const logs = loggerTestHelpers.getCapturedLogs(); + + // Verify security event was logged + expect(logs.length).toBe(1); + expect(logs[0].level).toBe('info'); + expect(logs[0].msg).toContain('Authentication attempt'); + expect(logs[0].type).toBe('security_event'); + }); + }); + + describe('Business Event Middleware', () => { + it('should log business events with custom metadata', async () => { + // Create middleware with custom business endpoints + const testLogger = loggerTestHelpers.createTestLogger('middleware-test'); + const middleware = businessEventMiddleware(testLogger); + + // Create mock context with business path + const mockContext = loggerTestHelpers.createHonoContextMock({ + path: '/api/orders', + method: 'POST', + res: { status: 201 } + }); + const mockNext = loggerTestHelpers.createNextMock(); + + // Execute middleware + await middleware(mockContext, mockNext); + + // Get captured logs + const logs = loggerTestHelpers.getCapturedLogs(); + + // Verify business event was logged + expect(logs.length).toBe(1); + expect(logs[0].level).toBe('info'); + expect(logs[0].msg).toContain('Business operation completed'); + expect(logs[0].type).toBe('business_event'); + }); + }); + + describe('Comprehensive Logging Middleware', () => { + it('should combine multiple logging features', async () => { + // Create middleware + const testLogger = loggerTestHelpers.createTestLogger('middleware-test'); + const middleware = comprehensiveLoggingMiddleware({ + serviceName: 'test-service', + logger: testLogger + }); + + // Create mock context + const mockContext = loggerTestHelpers.createHonoContextMock({ + path: '/api/data', + method: 'GET', + req: { + headers: { + 'authorization': 'Bearer test-token' + } + } + }); + const mockNext = loggerTestHelpers.createNextMock(); + + // Execute middleware + await middleware(mockContext, mockNext); + + // Get captured logs + const logs = loggerTestHelpers.getCapturedLogs(); + + // Verify multiple log entries from different middleware components + expect(logs.length).toBeGreaterThan(1); + + // Should have logs from security, logging, and performance middleware + const securityLog = logs.find(log => log.type === 'security_event'); + const requestLog = logs.find(log => log.msg?.includes('HTTP Request')); + + expect(securityLog).toBeDefined(); + expect(requestLog).toBeDefined(); + }); + }); +}); diff --git a/libs/questdb-client/test/integration.test.ts b/libs/questdb-client/test/integration.test.ts index 1a5061f..654f3e7 100644 --- a/libs/questdb-client/test/integration.test.ts +++ b/libs/questdb-client/test/integration.test.ts @@ -5,7 +5,7 @@ * without requiring an actual QuestDB instance. */ -import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test'; import { QuestDBClient, QuestDBHealthMonitor, @@ -17,8 +17,7 @@ import { import { questdbTestHelpers } from './setup'; describe('QuestDB Client Integration', () => { - let client: QuestDBClient; - beforeEach(() => { + let client: QuestDBClient; beforeEach(() => { client = new QuestDBClient({ host: 'localhost', httpPort: 9000, @@ -28,14 +27,13 @@ describe('QuestDB Client Integration', () => { user: 'admin', password: 'quest' }); - - // Add connected property for tests - (client as any).connected = false; - }); - - afterEach(async () => { - if (client.connected) { - await client.disconnect(); + }); afterEach(async () => { + if (client && client.connected) { + try { + await client.disconnect(); + } catch (error) { + // Ignore cleanup errors in tests + } } }); @@ -114,7 +112,6 @@ describe('QuestDB Client Integration', () => { expect(questdbTestHelpers.validateQuestDBQuery(query)).toBe(true); }); }); - describe('InfluxDB Writer', () => { it('should write OHLCV data using InfluxDB line protocol', async () => { const ohlcvData = [{ @@ -124,22 +121,26 @@ describe('QuestDB Client Integration', () => { low: 149.50, close: 151.50, volume: 1000000 - }]; // Mock the actual write operation - jest.spyOn(client.getInfluxWriter(), 'writeOHLCV').mockResolvedValue(); + }]; - await expect(async () => { + // Mock the actual write operation + const writeSpy = spyOn(client.getInfluxWriter(), 'writeOHLCV'); + writeSpy.mockReturnValue(Promise.resolve()); await expect(async () => { await client.writeOHLCV('AAPL', 'NASDAQ', ohlcvData); }).not.toThrow(); - }); it('should handle batch operations', () => { + }); + + it('should handle batch operations', () => { const lines = questdbTestHelpers.generateInfluxDBLines(3); expect(lines.length).toBe(3); lines.forEach(line => { expect(line).toContain('ohlcv,symbol=TEST'); expect(line).toMatch(/\d{19}$/); // Nanosecond timestamp - }); - }); - }); describe('Schema Manager', () => { + }); }); + }); + + describe('Schema Manager', () => { it('should provide schema access', () => { const schema = client.getSchemaManager().getSchema('ohlcv_data'); @@ -150,13 +151,15 @@ describe('QuestDB Client Integration', () => { expect(symbolColumn).toBeDefined(); expect(symbolColumn?.type).toBe('SYMBOL'); - expect(schema?.partitionBy).toBe('DAY'); - }); - }); describe('Health Monitor', () => { + expect(schema?.partitionBy).toBe('DAY'); }); + }); + + describe('Health Monitor', () => { it('should provide health monitoring capabilities', async () => { const healthMonitor = client.getHealthMonitor(); expect(healthMonitor).toBeInstanceOf(QuestDBHealthMonitor); - // Mock health status since we're not connected + + // Mock health status since we're not connected const mockHealthStatus = { isHealthy: false, lastCheck: new Date(), @@ -165,11 +168,11 @@ describe('QuestDB Client Integration', () => { details: { pgPool: false, httpEndpoint: false, - uptime: 0 - } + uptime: 0 } }; - jest.spyOn(healthMonitor, 'getHealthStatus').mockResolvedValue(mockHealthStatus); + const healthSpy = spyOn(healthMonitor, 'getHealthStatus'); + healthSpy.mockReturnValue(Promise.resolve(mockHealthStatus)); const health = await healthMonitor.getHealthStatus(); expect(health.isHealthy).toBe(false); @@ -177,7 +180,6 @@ describe('QuestDB Client Integration', () => { expect(health.message).toBe('Connection not established'); }); }); - describe('Time-Series Operations', () => { it('should support latest by operations', async () => { // Mock the query execution @@ -186,10 +188,12 @@ describe('QuestDB Client Integration', () => { rowCount: 1, executionTime: 10, metadata: { columns: [] } - }; jest.spyOn(client, 'query').mockResolvedValue(mockResult); + }; + + const querySpy = spyOn(client, 'query'); + querySpy.mockReturnValue(Promise.resolve(mockResult)); - const result = await client.latestBy('ohlcv', ['symbol', 'close'], 'symbol'); - expect(result.rows.length).toBe(1); + const result = await client.latestBy('ohlcv', ['symbol', 'close'], 'symbol'); expect(result.rows.length).toBe(1); expect(result.rows[0].symbol).toBe('AAPL'); }); @@ -204,13 +208,13 @@ describe('QuestDB Client Integration', () => { metadata: { columns: [] } }; - jest.spyOn(client, 'query').mockResolvedValue(mockResult); - - const result = await client.sampleBy( + const querySpy = spyOn(client, 'query'); + querySpy.mockReturnValue(Promise.resolve(mockResult)); const result = await client.sampleBy( 'ohlcv', ['symbol', 'avg(close) as avg_close'], '1h', - 'timestamp', "symbol = 'AAPL'" + 'timestamp', + "symbol = 'AAPL'" ); expect(result.rows.length).toBe(1); diff --git a/libs/questdb-client/test/setup.ts b/libs/questdb-client/test/setup.ts index 862d91b..f1d2683 100644 --- a/libs/questdb-client/test/setup.ts +++ b/libs/questdb-client/test/setup.ts @@ -6,6 +6,7 @@ */ import { newDb } from 'pg-mem'; +import { mock, spyOn, beforeAll, beforeEach } from 'bun:test'; // Mock PostgreSQL database for unit tests let pgMem: any; @@ -43,10 +44,73 @@ beforeAll(() => { throw new Error(`Unsupported date unit: ${unit}`); } return result; - } - }); // Mock QuestDB HTTP client - (global as any).fetch = () => {}; // Using Bun's built-in spyOn utilities - global.spyOn(global, 'fetch'); + } }); // 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(() => {