import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as opendata from '../ts/index.js'; const TEST_USER_AGENT = 'fin.cx test@fin.cx'; tap.test('StockDataService - Basic Setup', async () => { await tap.test('should create StockDataService instance', async () => { const service = new opendata.StockDataService({ cache: { priceTTL: 60000, // 1 minute for testing fundamentalsTTL: 120000, // 2 minutes for testing maxEntries: 100 } }); expect(service).toBeInstanceOf(opendata.StockDataService); const stats = service.getCacheStats(); expect(stats.priceCache.ttl).toEqual(60000); expect(stats.fundamentalsCache.ttl).toEqual(120000); expect(stats.maxEntries).toEqual(100); }); }); tap.test('StockDataService - Provider Registration', async () => { const service = new opendata.StockDataService(); await tap.test('should register price provider', async () => { const yahooProvider = new opendata.YahooFinanceProvider(); service.registerPriceProvider(yahooProvider); const providers = service.getPriceProviders(); expect(providers.length).toEqual(1); expect(providers[0].name).toEqual('Yahoo Finance'); }); await tap.test('should register fundamentals provider', async () => { const secProvider = new opendata.SecEdgarProvider({ userAgent: TEST_USER_AGENT }); service.registerFundamentalsProvider(secProvider); const providers = service.getFundamentalsProviders(); expect(providers.length).toEqual(1); expect(providers[0].name).toEqual('SEC EDGAR'); }); await tap.test('should unregister providers', async () => { service.unregisterPriceProvider('Yahoo Finance'); service.unregisterFundamentalsProvider('SEC EDGAR'); expect(service.getPriceProviders().length).toEqual(0); expect(service.getFundamentalsProviders().length).toEqual(0); }); }); tap.test('StockDataService - Price Fetching', async () => { const service = new opendata.StockDataService(); const yahooProvider = new opendata.YahooFinanceProvider(); service.registerPriceProvider(yahooProvider); await tap.test('should fetch single price', async () => { const price = await service.getPrice('AAPL'); expect(price).toBeDefined(); expect(price.ticker).toEqual('AAPL'); expect(price.price).toBeGreaterThan(0); expect(price.provider).toEqual('Yahoo Finance'); expect(price.timestamp).toBeInstanceOf(Date); console.log(`\n💵 Single Price: ${price.ticker} = $${price.price.toFixed(2)}`); }); await tap.test('should fetch batch prices', async () => { const prices = await service.getPrices(['AAPL', 'MSFT', 'GOOGL']); expect(prices).toBeInstanceOf(Array); expect(prices.length).toBeGreaterThan(0); expect(prices.length).toBeLessThanOrEqual(3); console.log('\n💵 Batch Prices:'); prices.forEach(p => { console.log(` ${p.ticker}: $${p.price.toFixed(2)}`); }); }); await tap.test('should cache prices', async () => { // Clear cache service.clearCache(); const stats1 = service.getCacheStats(); expect(stats1.priceCache.size).toEqual(0); // Fetch price (should hit API) const start1 = Date.now(); await service.getPrice('AAPL'); const duration1 = Date.now() - start1; const stats2 = service.getCacheStats(); expect(stats2.priceCache.size).toEqual(1); // Fetch again (should hit cache - much faster) const start2 = Date.now(); await service.getPrice('AAPL'); const duration2 = Date.now() - start2; expect(duration2).toBeLessThan(duration1); console.log('\n⚡ Cache Performance:'); console.log(` First fetch: ${duration1}ms`); console.log(` Cached fetch: ${duration2}ms`); console.log(` Speedup: ${Math.round(duration1 / duration2)}x`); }); }); tap.test('StockDataService - Fundamentals Fetching', async () => { const service = new opendata.StockDataService(); const secProvider = new opendata.SecEdgarProvider({ userAgent: TEST_USER_AGENT }); service.registerFundamentalsProvider(secProvider); await tap.test('should fetch single fundamentals', async () => { const fundamentals = await service.getFundamentals('AAPL'); expect(fundamentals).toBeDefined(); expect(fundamentals.ticker).toEqual('AAPL'); expect(fundamentals.companyName).toEqual('Apple Inc.'); expect(fundamentals.provider).toEqual('SEC EDGAR'); expect(fundamentals.earningsPerShareDiluted).toBeGreaterThan(0); expect(fundamentals.sharesOutstanding).toBeGreaterThan(0); console.log('\n📊 Single Fundamentals:'); console.log(` ${fundamentals.ticker}: ${fundamentals.companyName}`); console.log(` EPS: $${fundamentals.earningsPerShareDiluted?.toFixed(2)}`); console.log(` Shares: ${(fundamentals.sharesOutstanding! / 1_000_000_000).toFixed(2)}B`); }); await tap.test('should fetch batch fundamentals', async () => { const fundamentals = await service.getBatchFundamentals(['AAPL', 'MSFT']); expect(fundamentals).toBeInstanceOf(Array); expect(fundamentals.length).toEqual(2); console.log('\n📊 Batch Fundamentals:'); fundamentals.forEach(f => { console.log(` ${f.ticker}: ${f.companyName} - EPS: $${f.earningsPerShareDiluted?.toFixed(2)}`); }); }); await tap.test('should cache fundamentals', async () => { // Clear cache service.clearCache(); const stats1 = service.getCacheStats(); expect(stats1.fundamentalsCache.size).toEqual(0); // Fetch fundamentals (should hit API) await service.getFundamentals('AAPL'); const stats2 = service.getCacheStats(); expect(stats2.fundamentalsCache.size).toEqual(1); }); }); tap.test('StockDataService - Complete Stock Data', async () => { const service = new opendata.StockDataService(); // Register both providers const yahooProvider = new opendata.YahooFinanceProvider(); const secProvider = new opendata.SecEdgarProvider({ userAgent: TEST_USER_AGENT }); service.registerPriceProvider(yahooProvider); service.registerFundamentalsProvider(secProvider); await tap.test('should fetch complete stock data with string', async () => { const data = await service.getStockData('AAPL'); expect(data).toBeDefined(); expect(data.ticker).toEqual('AAPL'); expect(data.price).toBeDefined(); expect(data.price.ticker).toEqual('AAPL'); expect(data.fundamentals).toBeDefined(); expect(data.fundamentals?.ticker).toEqual('AAPL'); expect(data.fetchedAt).toBeInstanceOf(Date); // Check automatic enrichment expect(data.fundamentals?.marketCap).toBeDefined(); expect(data.fundamentals?.priceToEarnings).toBeDefined(); expect(data.fundamentals?.marketCap).toBeGreaterThan(0); expect(data.fundamentals?.priceToEarnings).toBeGreaterThan(0); console.log('\n✨ Complete Stock Data (Auto-Enriched):'); console.log(` ${data.ticker}: ${data.fundamentals?.companyName}`); console.log(` Price: $${data.price.price.toFixed(2)}`); console.log(` Market Cap: $${(data.fundamentals!.marketCap! / 1_000_000_000_000).toFixed(2)}T`); console.log(` P/E Ratio: ${data.fundamentals!.priceToEarnings!.toFixed(2)}`); }); await tap.test('should fetch complete stock data with request object', async () => { const data = await service.getStockData({ ticker: 'MSFT', includeFundamentals: true, enrichFundamentals: true }); expect(data).toBeDefined(); expect(data.ticker).toEqual('MSFT'); expect(data.price).toBeDefined(); expect(data.fundamentals).toBeDefined(); expect(data.fundamentals?.marketCap).toBeDefined(); expect(data.fundamentals?.priceToEarnings).toBeDefined(); }); await tap.test('should fetch complete stock data without fundamentals', async () => { const data = await service.getStockData({ ticker: 'GOOGL', includeFundamentals: false }); expect(data).toBeDefined(); expect(data.ticker).toEqual('GOOGL'); expect(data.price).toBeDefined(); expect(data.fundamentals).toBeUndefined(); }); await tap.test('should handle fundamentals fetch failure gracefully', async () => { // Try a ticker that might not have fundamentals const data = await service.getStockData({ ticker: 'BTC-USD', // Crypto - no SEC filings includeFundamentals: true }); expect(data).toBeDefined(); expect(data.price).toBeDefined(); // Fundamentals might be undefined due to error console.log(`\n⚠️ ${data.ticker} - Price available, Fundamentals: ${data.fundamentals ? 'Yes' : 'No'}`); }); }); tap.test('StockDataService - Batch Complete Stock Data', async () => { const service = new opendata.StockDataService(); const yahooProvider = new opendata.YahooFinanceProvider(); const secProvider = new opendata.SecEdgarProvider({ userAgent: TEST_USER_AGENT }); service.registerPriceProvider(yahooProvider); service.registerFundamentalsProvider(secProvider); await tap.test('should fetch batch complete data with array', async () => { const data = await service.getBatchStockData(['AAPL', 'MSFT']); expect(data).toBeInstanceOf(Array); expect(data.length).toEqual(2); data.forEach(stock => { expect(stock.ticker).toBeDefined(); expect(stock.price).toBeDefined(); expect(stock.fundamentals).toBeDefined(); expect(stock.fundamentals?.marketCap).toBeGreaterThan(0); expect(stock.fundamentals?.priceToEarnings).toBeGreaterThan(0); }); console.log('\n✨ Batch Complete Data:'); data.forEach(stock => { console.log(` ${stock.ticker}: Price $${stock.price.price.toFixed(2)}, P/E ${stock.fundamentals!.priceToEarnings!.toFixed(2)}`); }); }); await tap.test('should fetch batch complete data with request object', async () => { const data = await service.getBatchStockData({ tickers: ['AAPL', 'GOOGL'], includeFundamentals: true, enrichFundamentals: true }); expect(data).toBeInstanceOf(Array); expect(data.length).toEqual(2); data.forEach(stock => { expect(stock.fundamentals?.marketCap).toBeGreaterThan(0); }); }); await tap.test('should fetch batch without enrichment', async () => { const data = await service.getBatchStockData({ tickers: ['AAPL', 'MSFT'], includeFundamentals: true, enrichFundamentals: false }); expect(data).toBeInstanceOf(Array); // Check that fundamentals exist but enrichment might not be complete data.forEach(stock => { if (stock.fundamentals) { expect(stock.fundamentals.ticker).toBeDefined(); expect(stock.fundamentals.companyName).toBeDefined(); } }); }); }); tap.test('StockDataService - Health & Statistics', async () => { const service = new opendata.StockDataService(); const yahooProvider = new opendata.YahooFinanceProvider(); const secProvider = new opendata.SecEdgarProvider({ userAgent: TEST_USER_AGENT }); service.registerPriceProvider(yahooProvider); service.registerFundamentalsProvider(secProvider); await tap.test('should check providers health', async () => { const health = await service.checkProvidersHealth(); expect(health.size).toEqual(2); expect(health.get('Yahoo Finance (price)')).toBe(true); expect(health.get('SEC EDGAR (fundamentals)')).toBe(true); console.log('\n💚 Provider Health:'); health.forEach((isHealthy, name) => { console.log(` ${name}: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}`); }); }); await tap.test('should track provider statistics', async () => { // Make some requests to generate stats await service.getPrice('AAPL'); await service.getFundamentals('AAPL'); const stats = service.getProviderStats(); expect(stats.size).toEqual(2); const yahooStats = stats.get('Yahoo Finance'); expect(yahooStats).toBeDefined(); expect(yahooStats!.type).toEqual('price'); expect(yahooStats!.successCount).toBeGreaterThan(0); const secStats = stats.get('SEC EDGAR'); expect(secStats).toBeDefined(); expect(secStats!.type).toEqual('fundamentals'); expect(secStats!.successCount).toBeGreaterThan(0); console.log('\n📈 Provider Statistics:'); stats.forEach((stat, name) => { console.log(` ${name} (${stat.type}): Success=${stat.successCount}, Errors=${stat.errorCount}`); }); }); await tap.test('should clear all caches', async () => { service.clearCache(); const stats = service.getCacheStats(); expect(stats.priceCache.size).toEqual(0); expect(stats.fundamentalsCache.size).toEqual(0); }); }); tap.test('StockDataService - Error Handling', async () => { await tap.test('should throw error when no price provider available', async () => { const service = new opendata.StockDataService(); try { await service.getPrice('AAPL'); throw new Error('Should have thrown error'); } catch (error) { expect(error.message).toContain('No price providers available'); } }); await tap.test('should throw error when no fundamentals provider available', async () => { const service = new opendata.StockDataService(); try { await service.getFundamentals('AAPL'); throw new Error('Should have thrown error'); } catch (error) { expect(error.message).toContain('No fundamentals providers available'); } }); await tap.test('should handle invalid ticker for price', async () => { const service = new opendata.StockDataService(); const yahooProvider = new opendata.YahooFinanceProvider(); service.registerPriceProvider(yahooProvider); try { await service.getPrice('INVALIDTICKER123456'); throw new Error('Should have thrown error'); } catch (error) { expect(error.message).toContain('Failed to fetch price'); } }); await tap.test('should handle invalid ticker for fundamentals', async () => { const service = new opendata.StockDataService(); const secProvider = new opendata.SecEdgarProvider({ userAgent: TEST_USER_AGENT }); service.registerFundamentalsProvider(secProvider); try { await service.getFundamentals('INVALIDTICKER123456'); throw new Error('Should have thrown error'); } catch (error) { expect(error.message).toContain('CIK not found'); } }); }); export default tap.start();