running prettier for cleanup
This commit is contained in:
parent
fe7733aeb5
commit
d85cd58acd
151 changed files with 29158 additions and 27966 deletions
|
|
@ -1,239 +1,251 @@
|
|||
/**
|
||||
* QuestDB Client Integration Test
|
||||
*
|
||||
* This test validates that all components work together correctly
|
||||
* without requiring an actual QuestDB instance.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
|
||||
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 && client.connected) {
|
||||
try {
|
||||
await client.disconnect();
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors in tests
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
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', () => {
|
||||
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 }
|
||||
};
|
||||
|
||||
const healthSpy = spyOn(healthMonitor, 'getHealthStatus');
|
||||
healthSpy.mockReturnValue(Promise.resolve(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: [] }
|
||||
};
|
||||
|
||||
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);
|
||||
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: [] }
|
||||
};
|
||||
|
||||
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'"
|
||||
);
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
/**
|
||||
* QuestDB Client Integration Test
|
||||
*
|
||||
* This test validates that all components work together correctly
|
||||
* without requiring an actual QuestDB instance.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
||||
import {
|
||||
createQuestDBClient,
|
||||
QuestDBClient,
|
||||
QuestDBHealthMonitor,
|
||||
QuestDBInfluxWriter,
|
||||
QuestDBQueryBuilder,
|
||||
QuestDBSchemaManager,
|
||||
} 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 && client.connected) {
|
||||
try {
|
||||
await client.disconnect();
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors in tests
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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.0,
|
||||
high: 152.0,
|
||||
low: 149.5,
|
||||
close: 151.5,
|
||||
volume: 1000000,
|
||||
},
|
||||
];
|
||||
|
||||
// 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', () => {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
const healthSpy = spyOn(healthMonitor, 'getHealthStatus');
|
||||
healthSpy.mockReturnValue(Promise.resolve(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.0, timestamp: new Date() }],
|
||||
rowCount: 1,
|
||||
executionTime: 10,
|
||||
metadata: { columns: [] },
|
||||
};
|
||||
|
||||
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);
|
||||
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.0, timestamp: new Date() }],
|
||||
rowCount: 1,
|
||||
executionTime: 15,
|
||||
metadata: { columns: [] },
|
||||
};
|
||||
|
||||
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'"
|
||||
);
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,284 +1,280 @@
|
|||
/**
|
||||
* 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';
|
||||
import { mock, spyOn, beforeAll, beforeEach } from 'bun:test';
|
||||
|
||||
// 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
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue