396 lines
12 KiB
TypeScript
396 lines
12 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as opendata from '../ts/index.js';
|
|
|
|
/**
|
|
* Test to inspect actual cache contents and verify data integrity
|
|
*/
|
|
|
|
class MockProvider implements opendata.IStockProvider {
|
|
name = 'MockProvider';
|
|
priority = 100;
|
|
requiresAuth = false;
|
|
|
|
public callLog: Array<{ type: string; ticker: string; timestamp: Date }> = [];
|
|
|
|
async fetchData(request: opendata.IStockDataRequest): Promise<opendata.IStockPrice | opendata.IStockPrice[]> {
|
|
this.callLog.push({
|
|
type: request.type,
|
|
ticker: request.type === 'batch' ? request.tickers.join(',') : (request as any).ticker,
|
|
timestamp: new Date()
|
|
});
|
|
|
|
if (request.type === 'intraday') {
|
|
const count = request.limit || 10;
|
|
const prices: opendata.IStockPrice[] = [];
|
|
const baseTime = request.date || new Date('2025-01-07T09:30:00.000Z');
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
prices.push({
|
|
ticker: request.ticker,
|
|
price: 100 + i,
|
|
currency: 'USD',
|
|
timestamp: new Date(baseTime.getTime() + i * 60 * 1000),
|
|
fetchedAt: new Date(),
|
|
provider: this.name,
|
|
dataType: 'intraday',
|
|
marketState: 'REGULAR',
|
|
open: 100,
|
|
high: 101,
|
|
low: 99,
|
|
volume: 1000000,
|
|
change: 0,
|
|
changePercent: 0,
|
|
previousClose: 100
|
|
});
|
|
}
|
|
|
|
return prices;
|
|
}
|
|
|
|
// Default single price
|
|
return {
|
|
ticker: (request as any).ticker,
|
|
price: 150,
|
|
currency: 'USD',
|
|
timestamp: new Date(),
|
|
fetchedAt: new Date(),
|
|
provider: this.name,
|
|
dataType: 'eod',
|
|
marketState: 'CLOSED',
|
|
open: 149,
|
|
high: 151,
|
|
low: 148,
|
|
volume: 5000000,
|
|
change: 1,
|
|
changePercent: 0.67,
|
|
previousClose: 149
|
|
};
|
|
}
|
|
|
|
async isAvailable(): Promise<boolean> {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
let stockService: opendata.StockPriceService;
|
|
let mockProvider: MockProvider;
|
|
|
|
tap.test('Cache Inspection - Setup', async () => {
|
|
stockService = new opendata.StockPriceService({
|
|
ttl: 60000,
|
|
maxEntries: 100
|
|
});
|
|
|
|
mockProvider = new MockProvider();
|
|
stockService.register(mockProvider);
|
|
|
|
console.log('✓ Service and provider initialized');
|
|
});
|
|
|
|
tap.test('Cache Inspection - Verify Cache Key Generation', async () => {
|
|
await tap.test('should generate unique cache keys for different requests', async () => {
|
|
stockService.clearCache();
|
|
mockProvider.callLog = [];
|
|
|
|
// Fetch with different parameters
|
|
await stockService.getData({ type: 'intraday', ticker: 'AAPL', interval: '1min', limit: 10 });
|
|
await stockService.getData({ type: 'intraday', ticker: 'AAPL', interval: '1min', limit: 20 });
|
|
await stockService.getData({ type: 'intraday', ticker: 'AAPL', interval: '5min', limit: 10 });
|
|
await stockService.getData({ type: 'intraday', ticker: 'MSFT', interval: '1min', limit: 10 });
|
|
|
|
// Should have made 4 provider calls (all different cache keys)
|
|
expect(mockProvider.callLog.length).toEqual(4);
|
|
|
|
console.log('✓ Cache keys are unique for different parameters');
|
|
console.log(` Total provider calls: ${mockProvider.callLog.length}`);
|
|
});
|
|
|
|
await tap.test('should reuse cache for identical requests', async () => {
|
|
stockService.clearCache();
|
|
mockProvider.callLog = [];
|
|
|
|
// Same request 3 times
|
|
const result1 = await stockService.getData({ type: 'intraday', ticker: 'AAPL', interval: '1min', limit: 10 });
|
|
const result2 = await stockService.getData({ type: 'intraday', ticker: 'AAPL', interval: '1min', limit: 10 });
|
|
const result3 = await stockService.getData({ type: 'intraday', ticker: 'AAPL', interval: '1min', limit: 10 });
|
|
|
|
// Should have made only 1 provider call
|
|
expect(mockProvider.callLog.length).toEqual(1);
|
|
|
|
// All results should be identical (same reference from cache)
|
|
expect((result1 as opendata.IStockPrice[]).length).toEqual((result2 as opendata.IStockPrice[]).length);
|
|
expect((result1 as opendata.IStockPrice[]).length).toEqual((result3 as opendata.IStockPrice[]).length);
|
|
|
|
// Verify timestamps match (exact same cached data)
|
|
const ts1 = (result1 as opendata.IStockPrice[])[0].timestamp.getTime();
|
|
const ts2 = (result2 as opendata.IStockPrice[])[0].timestamp.getTime();
|
|
const ts3 = (result3 as opendata.IStockPrice[])[0].timestamp.getTime();
|
|
|
|
expect(ts1).toEqual(ts2);
|
|
expect(ts2).toEqual(ts3);
|
|
|
|
console.log('✓ Cache reused for identical requests');
|
|
console.log(` 3 requests → 1 provider call`);
|
|
});
|
|
});
|
|
|
|
tap.test('Cache Inspection - Verify Data Structure', async () => {
|
|
await tap.test('should cache complete IStockPrice objects', async () => {
|
|
stockService.clearCache();
|
|
|
|
const result = await stockService.getData({
|
|
type: 'intraday',
|
|
ticker: 'TSLA',
|
|
interval: '1min',
|
|
limit: 5
|
|
});
|
|
|
|
expect(result).toBeArray();
|
|
const prices = result as opendata.IStockPrice[];
|
|
|
|
// Verify structure of cached data
|
|
for (const price of prices) {
|
|
expect(price).toHaveProperty('ticker');
|
|
expect(price).toHaveProperty('price');
|
|
expect(price).toHaveProperty('currency');
|
|
expect(price).toHaveProperty('timestamp');
|
|
expect(price).toHaveProperty('fetchedAt');
|
|
expect(price).toHaveProperty('provider');
|
|
expect(price).toHaveProperty('dataType');
|
|
expect(price).toHaveProperty('marketState');
|
|
expect(price).toHaveProperty('open');
|
|
expect(price).toHaveProperty('high');
|
|
expect(price).toHaveProperty('low');
|
|
expect(price).toHaveProperty('volume');
|
|
|
|
// Verify types
|
|
expect(typeof price.ticker).toEqual('string');
|
|
expect(typeof price.price).toEqual('number');
|
|
expect(price.timestamp).toBeInstanceOf(Date);
|
|
expect(price.fetchedAt).toBeInstanceOf(Date);
|
|
}
|
|
|
|
console.log('✓ Cached data has complete IStockPrice structure');
|
|
console.log(` Sample: ${prices[0].ticker} @ $${prices[0].price} (${prices[0].timestamp.toISOString()})`);
|
|
});
|
|
|
|
await tap.test('should preserve array order in cache', async () => {
|
|
stockService.clearCache();
|
|
|
|
const result1 = await stockService.getData({
|
|
type: 'intraday',
|
|
ticker: 'AAPL',
|
|
interval: '1min',
|
|
limit: 10
|
|
});
|
|
|
|
const result2 = await stockService.getData({
|
|
type: 'intraday',
|
|
ticker: 'AAPL',
|
|
interval: '1min',
|
|
limit: 10
|
|
});
|
|
|
|
const prices1 = result1 as opendata.IStockPrice[];
|
|
const prices2 = result2 as opendata.IStockPrice[];
|
|
|
|
// Verify order is preserved
|
|
for (let i = 0; i < prices1.length; i++) {
|
|
expect(prices1[i].timestamp.getTime()).toEqual(prices2[i].timestamp.getTime());
|
|
expect(prices1[i].price).toEqual(prices2[i].price);
|
|
}
|
|
|
|
console.log('✓ Array order preserved in cache');
|
|
});
|
|
});
|
|
|
|
tap.test('Cache Inspection - Verify TTL Behavior', async () => {
|
|
await tap.test('should respect cache TTL for intraday data', async (testArg) => {
|
|
// Create service with very short TTL for testing
|
|
const shortTTLService = new opendata.StockPriceService({
|
|
ttl: 100, // 100ms
|
|
maxEntries: 100
|
|
});
|
|
|
|
const testProvider = new MockProvider();
|
|
shortTTLService.register(testProvider);
|
|
|
|
// First fetch
|
|
await shortTTLService.getData({
|
|
type: 'intraday',
|
|
ticker: 'TEST',
|
|
interval: '1min',
|
|
limit: 5
|
|
});
|
|
|
|
const callCount1 = testProvider.callLog.length;
|
|
|
|
// Immediate second fetch - should hit cache
|
|
await shortTTLService.getData({
|
|
type: 'intraday',
|
|
ticker: 'TEST',
|
|
interval: '1min',
|
|
limit: 5
|
|
});
|
|
|
|
const callCount2 = testProvider.callLog.length;
|
|
expect(callCount2).toEqual(callCount1); // No new call
|
|
|
|
// Wait for TTL to expire
|
|
await new Promise(resolve => setTimeout(resolve, 150));
|
|
|
|
// Third fetch - should hit provider (cache expired)
|
|
await shortTTLService.getData({
|
|
type: 'intraday',
|
|
ticker: 'TEST',
|
|
interval: '1min',
|
|
limit: 5
|
|
});
|
|
|
|
const callCount3 = testProvider.callLog.length;
|
|
expect(callCount3).toBeGreaterThan(callCount2); // New call made
|
|
|
|
console.log('✓ Cache TTL working correctly');
|
|
console.log(` Before expiry: ${callCount2 - callCount1} new calls`);
|
|
console.log(` After expiry: ${callCount3 - callCount2} new calls`);
|
|
});
|
|
});
|
|
|
|
tap.test('Cache Inspection - Memory Efficiency', async () => {
|
|
await tap.test('should store deduplicated data in cache', async () => {
|
|
stockService.clearCache();
|
|
mockProvider.callLog = [];
|
|
|
|
// Fetch data
|
|
const result1 = await stockService.getData({
|
|
type: 'intraday',
|
|
ticker: 'AAPL',
|
|
interval: '1min',
|
|
limit: 100
|
|
});
|
|
|
|
const prices = result1 as opendata.IStockPrice[];
|
|
|
|
// Verify no duplicate timestamps in cached data
|
|
const timestamps = prices.map(p => p.timestamp.getTime());
|
|
const uniqueTimestamps = new Set(timestamps);
|
|
|
|
expect(uniqueTimestamps.size).toEqual(timestamps.length);
|
|
|
|
console.log('✓ No duplicate timestamps in cached data');
|
|
console.log(` Records: ${prices.length}`);
|
|
console.log(` Unique timestamps: ${uniqueTimestamps.size}`);
|
|
});
|
|
|
|
await tap.test('should estimate memory usage', async () => {
|
|
stockService.clearCache();
|
|
|
|
// Fetch various sizes
|
|
await stockService.getData({ type: 'intraday', ticker: 'AAPL', interval: '1min', limit: 100 });
|
|
await stockService.getData({ type: 'intraday', ticker: 'MSFT', interval: '1min', limit: 100 });
|
|
await stockService.getData({ type: 'intraday', ticker: 'GOOGL', interval: '5min', limit: 50 });
|
|
|
|
// Estimate memory (rough calculation)
|
|
// Each IStockPrice is approximately 300-400 bytes
|
|
const totalRecords = 100 + 100 + 50;
|
|
const estimatedBytes = totalRecords * 350; // Average 350 bytes per record
|
|
const estimatedKB = (estimatedBytes / 1024).toFixed(2);
|
|
|
|
console.log('✓ Cache memory estimation:');
|
|
console.log(` Total records cached: ${totalRecords}`);
|
|
console.log(` Estimated memory: ~${estimatedKB} KB`);
|
|
console.log(` Average per record: ~350 bytes`);
|
|
});
|
|
});
|
|
|
|
tap.test('Cache Inspection - Edge Cases', async () => {
|
|
await tap.test('should handle empty results', async () => {
|
|
const emptyProvider = new MockProvider();
|
|
emptyProvider.fetchData = async () => [];
|
|
|
|
const emptyService = new opendata.StockPriceService();
|
|
emptyService.register(emptyProvider);
|
|
|
|
const result = await emptyService.getData({
|
|
type: 'intraday',
|
|
ticker: 'EMPTY',
|
|
interval: '1min'
|
|
});
|
|
|
|
expect(result).toBeArray();
|
|
expect((result as opendata.IStockPrice[]).length).toEqual(0);
|
|
|
|
// Second fetch should still hit cache (even though empty)
|
|
const result2 = await emptyService.getData({
|
|
type: 'intraday',
|
|
ticker: 'EMPTY',
|
|
interval: '1min'
|
|
});
|
|
|
|
expect(result2).toBeArray();
|
|
expect((result2 as opendata.IStockPrice[]).length).toEqual(0);
|
|
|
|
console.log('✓ Empty results cached correctly');
|
|
});
|
|
|
|
await tap.test('should handle single record', async () => {
|
|
stockService.clearCache();
|
|
|
|
const result = await stockService.getData({
|
|
type: 'intraday',
|
|
ticker: 'SINGLE',
|
|
interval: '1min',
|
|
limit: 1
|
|
});
|
|
|
|
expect(result).toBeArray();
|
|
expect((result as opendata.IStockPrice[]).length).toEqual(1);
|
|
|
|
console.log('✓ Single record cached correctly');
|
|
});
|
|
});
|
|
|
|
tap.test('Cache Inspection - Verify fetchedAt Timestamps', async () => {
|
|
await tap.test('should preserve fetchedAt in cached data', async () => {
|
|
stockService.clearCache();
|
|
|
|
const beforeFetch = Date.now();
|
|
const result = await stockService.getData({
|
|
type: 'intraday',
|
|
ticker: 'AAPL',
|
|
interval: '1min',
|
|
limit: 5
|
|
});
|
|
const afterFetch = Date.now();
|
|
|
|
const prices = result as opendata.IStockPrice[];
|
|
|
|
for (const price of prices) {
|
|
const fetchedTime = price.fetchedAt.getTime();
|
|
expect(fetchedTime).toBeGreaterThanOrEqual(beforeFetch);
|
|
expect(fetchedTime).toBeLessThanOrEqual(afterFetch);
|
|
}
|
|
|
|
// Fetch again - fetchedAt should be the same (from cache)
|
|
await new Promise(resolve => setTimeout(resolve, 50)); // Small delay
|
|
|
|
const result2 = await stockService.getData({
|
|
type: 'intraday',
|
|
ticker: 'AAPL',
|
|
interval: '1min',
|
|
limit: 5
|
|
});
|
|
|
|
const prices2 = result2 as opendata.IStockPrice[];
|
|
|
|
// Verify fetchedAt matches (same cached data)
|
|
for (let i = 0; i < prices.length; i++) {
|
|
expect(prices2[i].fetchedAt.getTime()).toEqual(prices[i].fetchedAt.getTime());
|
|
}
|
|
|
|
console.log('✓ fetchedAt timestamps preserved in cache');
|
|
});
|
|
});
|
|
|
|
export default tap.start();
|