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