added initial integration tests with bun

This commit is contained in:
Bojan Kucera 2025-06-04 12:26:55 -04:00
parent 3e451558ac
commit fb22815450
52 changed files with 7588 additions and 364 deletions

View file

@ -0,0 +1,233 @@
/**
* QuestDB Client Integration Test
*
* This test validates that all components work together correctly
* without requiring an actual QuestDB instance.
*/
import 'jest-extended';
import {
QuestDBClient,
QuestDBHealthMonitor,
QuestDBQueryBuilder,
QuestDBInfluxWriter,
QuestDBSchemaManager,
createQuestDBClient
} from '../src';
import { questdbTestHelpers } from './setup';
describe('QuestDB Client Integration', () => {
let client: QuestDBClient;
beforeEach(() => {
client = new QuestDBClient({
host: 'localhost',
httpPort: 9000,
pgPort: 8812,
influxPort: 9009,
database: 'questdb',
user: 'admin',
password: 'quest'
});
});
afterEach(async () => {
if (client.connected) {
await client.disconnect();
}
});
describe('Client Initialization', () => {
it('should create client with factory function', () => {
const factoryClient = createQuestDBClient();
expect(factoryClient).toBeInstanceOf(QuestDBClient);
});
it('should initialize all supporting classes', () => {
expect(client.getHealthMonitor()).toBeInstanceOf(QuestDBHealthMonitor);
expect(client.queryBuilder()).toBeInstanceOf(QuestDBQueryBuilder);
expect(client.getInfluxWriter()).toBeInstanceOf(QuestDBInfluxWriter);
expect(client.getSchemaManager()).toBeInstanceOf(QuestDBSchemaManager);
});
it('should handle connection configuration', () => {
expect(client.getHttpUrl()).toBe('http://localhost:9000');
expect(client.getInfluxUrl()).toBe('http://localhost:9009');
expect(client.connected).toBe(false);
});
});
describe('Query Builder', () => {
it('should build query using query builder', () => {
const query = client.queryBuilder()
.select('symbol', 'close', 'timestamp')
.from('ohlcv')
.whereSymbol('AAPL')
.whereLastHours(24)
.orderBy('timestamp', 'DESC')
.limit(100)
.build();
expect(query).toContain('SELECT symbol, close, timestamp');
expect(query).toContain('FROM ohlcv');
expect(query).toContain("symbol = 'AAPL'");
expect(query).toContain('ORDER BY timestamp DESC');
expect(query).toContain('LIMIT 100');
expect(questdbTestHelpers.validateQuestDBQuery(query)).toBe(true);
});
it('should build time-series specific queries', () => {
const latestQuery = client.queryBuilder()
.select('*')
.from('ohlcv')
.latestBy('symbol')
.build();
expect(latestQuery).toContain('LATEST BY symbol');
expect(questdbTestHelpers.validateQuestDBQuery(latestQuery)).toBe(true);
const sampleQuery = client.queryBuilder()
.select('symbol', 'avg(close)')
.from('ohlcv')
.sampleBy('1d')
.build();
expect(sampleQuery).toContain('SAMPLE BY 1d');
expect(questdbTestHelpers.validateQuestDBQuery(sampleQuery)).toBe(true);
});
it('should build aggregation queries', () => {
const query = client.aggregate('ohlcv')
.select('symbol', 'avg(close) as avg_price', 'max(high) as max_high')
.whereSymbolIn(['AAPL', 'GOOGL'])
.groupBy('symbol')
.sampleBy('1h')
.build();
expect(query).toContain('SELECT symbol, avg(close) as avg_price, max(high) as max_high');
expect(query).toContain('FROM ohlcv');
expect(query).toContain("symbol IN ('AAPL', 'GOOGL')");
expect(query).toContain('SAMPLE BY 1h');
expect(query).toContain('GROUP BY symbol');
expect(questdbTestHelpers.validateQuestDBQuery(query)).toBe(true);
});
});
describe('InfluxDB Writer', () => {
it('should write OHLCV data using InfluxDB line protocol', async () => {
const ohlcvData = [{
timestamp: new Date('2024-01-01T12:00:00Z'),
open: 150.00,
high: 152.00,
low: 149.50,
close: 151.50,
volume: 1000000
}]; // Mock the actual write operation
jest.spyOn(client.getInfluxWriter(), 'writeOHLCV').mockResolvedValue();
await expect(async () => {
await client.writeOHLCV('AAPL', 'NASDAQ', ohlcvData);
}).not.toThrow();
}); 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', () => {
it('should provide schema access', () => {
const schema = client.getSchemaManager().getSchema('ohlcv_data');
expect(schema).toBeDefined();
expect(schema?.tableName).toBe('ohlcv_data');
const symbolColumn = schema?.columns.find(col => col.name === 'symbol');
expect(symbolColumn).toBeDefined();
expect(symbolColumn?.type).toBe('SYMBOL');
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
const mockHealthStatus = {
isHealthy: false,
lastCheck: new Date(),
responseTime: 100,
message: 'Connection not established',
details: {
pgPool: false,
httpEndpoint: false,
uptime: 0
}
};
jest.spyOn(healthMonitor, 'getHealthStatus').mockResolvedValue(mockHealthStatus);
const health = await healthMonitor.getHealthStatus();
expect(health.isHealthy).toBe(false);
expect(health.lastCheck).toBeInstanceOf(Date);
expect(health.message).toBe('Connection not established');
});
});
describe('Time-Series Operations', () => {
it('should support latest by operations', async () => {
// Mock the query execution
const mockResult = {
rows: [{ symbol: 'AAPL', close: 150.00, timestamp: new Date() }],
rowCount: 1,
executionTime: 10,
metadata: { columns: [] }
}; jest.spyOn(client, 'query').mockResolvedValue(mockResult);
const result = await client.latestBy('ohlcv', ['symbol', 'close'], 'symbol');
expect(result.rows.length).toBe(1);
expect(result.rows[0].symbol).toBe('AAPL');
});
it('should support sample by operations', async () => {
// Mock the query execution
const mockResult = {
rows: [
{ symbol: 'AAPL', avg_close: 150.00, timestamp: new Date() }
],
rowCount: 1,
executionTime: 15,
metadata: { columns: [] }
};
jest.spyOn(client, 'query').mockResolvedValue(mockResult);
const result = await client.sampleBy(
'ohlcv',
['symbol', 'avg(close) as avg_close'],
'1h',
'timestamp', "symbol = 'AAPL'"
);
expect(result.rows.length).toBe(1);
expect(result.executionTime).toBe(15);
});
});
describe('Connection Management', () => {
it('should handle connection configuration', () => {
expect(client.getHttpUrl()).toBe('http://localhost:9000');
expect(client.getInfluxUrl()).toBe('http://localhost:9009');
expect(client.connected).toBe(false);
});
it('should provide configuration access', () => {
const config = client.configuration;
expect(config.host).toBe('localhost');
expect(config.httpPort).toBe(9000);
expect(config.user).toBe('admin');
});
});
});

View file

@ -0,0 +1,215 @@
/**
* QuestDB Client Test Setup
*
* Setup file specific to QuestDB client library tests.
* Provides utilities and mocks for testing database operations.
*/
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
(global as any).fetch = jest.fn();
});
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 jest.Mock).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 jest.Mock).mockResolvedValueOnce({
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 jest.Mock).mockResolvedValueOnce({
ok: false,
status,
json: async () => ({ error: message }),
text: async () => message
});
},
/**
* Mock InfluxDB line protocol response
*/
mockInfluxDBSuccess: () => {
((global as any).fetch as jest.Mock).mockResolvedValueOnce({
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: () => ({
connect: jest.fn().mockResolvedValue({
query: jest.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
release: jest.fn()
}),
end: jest.fn().mockResolvedValue(undefined),
totalCount: 0,
idleCount: 0,
waitingCount: 0
})
};