added questdb-client integration tests

This commit is contained in:
Bojan Kucera 2025-06-04 14:15:36 -04:00
parent d0bc9cf32f
commit 674112af05
3 changed files with 379 additions and 38 deletions

View file

@ -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();
});
});
});

View file

@ -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);

View file

@ -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(() => {