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 { 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 { 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();