import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as opendata from '../ts/index.js'; import * as paths from '../ts/paths.js'; import * as plugins from '../ts/plugins.js'; // Test data const testTickers = ['AAPL', 'MSFT', 'GOOGL']; const invalidTicker = 'INVALID_TICKER_XYZ'; let stockService: opendata.StockPriceService; let marketstackProvider: opendata.MarketstackProvider; let testQenv: plugins.qenv.Qenv; tap.test('should create StockPriceService instance', async () => { stockService = new opendata.StockPriceService({ ttl: 30000, // 30 seconds cache maxEntries: 100 }); expect(stockService).toBeInstanceOf(opendata.StockPriceService); }); tap.test('should create MarketstackProvider instance', async () => { try { // Create qenv and get API key testQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir); const apiKey = await testQenv.getEnvVarOnDemand('MARKETSTACK_COM_TOKEN'); marketstackProvider = new opendata.MarketstackProvider(apiKey, { enabled: true, timeout: 10000, retryAttempts: 2, retryDelay: 500 }); expect(marketstackProvider).toBeInstanceOf(opendata.MarketstackProvider); expect(marketstackProvider.name).toEqual('Marketstack'); expect(marketstackProvider.requiresAuth).toEqual(true); expect(marketstackProvider.priority).toEqual(80); } catch (error) { if (error.message.includes('MARKETSTACK_COM_TOKEN')) { console.log('⚠️ MARKETSTACK_COM_TOKEN not set - skipping Marketstack tests'); tap.test('Marketstack token not available', async () => { expect(true).toEqual(true); // Skip gracefully }); return; } throw error; } }); tap.test('should register Marketstack provider with the service', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } stockService.register(marketstackProvider); const providers = stockService.getAllProviders(); expect(providers).toContainEqual(marketstackProvider); expect(stockService.getProvider('Marketstack')).toEqual(marketstackProvider); }); tap.test('should check provider health', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } const health = await stockService.checkProvidersHealth(); expect(health.get('Marketstack')).toEqual(true); }); tap.test('should fetch single stock price', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } const price = await stockService.getPrice({ ticker: 'AAPL' }); expect(price).toHaveProperty('ticker'); expect(price).toHaveProperty('price'); expect(price).toHaveProperty('currency'); expect(price).toHaveProperty('change'); expect(price).toHaveProperty('changePercent'); expect(price).toHaveProperty('previousClose'); expect(price).toHaveProperty('timestamp'); expect(price).toHaveProperty('provider'); expect(price).toHaveProperty('marketState'); expect(price.ticker).toEqual('AAPL'); expect(price.price).toBeGreaterThan(0); expect(price.provider).toEqual('Marketstack'); expect(price.timestamp).toBeInstanceOf(Date); expect(price.marketState).toEqual('CLOSED'); // EOD data console.log(`✓ Fetched AAPL: $${price.price} (${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%)`); }); tap.test('should fetch multiple stock prices', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } const prices = await stockService.getPrices({ tickers: testTickers }); expect(prices).toBeArray(); expect(prices.length).toBeGreaterThan(0); expect(prices.length).toBeLessThanOrEqual(testTickers.length); for (const price of prices) { expect(testTickers).toContain(price.ticker); expect(price.price).toBeGreaterThan(0); expect(price.provider).toEqual('Marketstack'); expect(price.marketState).toEqual('CLOSED'); console.log(` ${price.ticker}: $${price.price}`); } }); tap.test('should serve cached prices on subsequent requests', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } // First request - should hit the API const firstRequest = await stockService.getPrice({ ticker: 'AAPL' }); // Second request - should be served from cache const secondRequest = await stockService.getPrice({ ticker: 'AAPL' }); expect(secondRequest.ticker).toEqual(firstRequest.ticker); expect(secondRequest.price).toEqual(firstRequest.price); expect(secondRequest.timestamp).toEqual(firstRequest.timestamp); console.log('✓ Cache working correctly'); }); tap.test('should handle invalid ticker gracefully', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } try { await stockService.getPrice({ ticker: invalidTicker }); throw new Error('Should have thrown an error for invalid ticker'); } catch (error) { expect(error.message).toInclude('Failed to fetch price'); console.log('✓ Invalid ticker handled correctly'); } }); tap.test('should support market checking', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } expect(marketstackProvider.supportsMarket('US')).toEqual(true); expect(marketstackProvider.supportsMarket('UK')).toEqual(true); expect(marketstackProvider.supportsMarket('DE')).toEqual(true); expect(marketstackProvider.supportsMarket('JP')).toEqual(true); expect(marketstackProvider.supportsMarket('INVALID')).toEqual(false); console.log('✓ Market support check working'); }); tap.test('should validate ticker format', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } expect(marketstackProvider.supportsTicker('AAPL')).toEqual(true); expect(marketstackProvider.supportsTicker('MSFT')).toEqual(true); expect(marketstackProvider.supportsTicker('BRK.B')).toEqual(true); expect(marketstackProvider.supportsTicker('123456789012')).toEqual(false); expect(marketstackProvider.supportsTicker('invalid@ticker')).toEqual(false); console.log('✓ Ticker validation working'); }); tap.test('should get provider statistics', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } const stats = stockService.getProviderStats(); const marketstackStats = stats.get('Marketstack'); expect(marketstackStats).not.toEqual(undefined); expect(marketstackStats.successCount).toBeGreaterThan(0); expect(marketstackStats.errorCount).toBeGreaterThanOrEqual(0); console.log(`✓ Provider stats: ${marketstackStats.successCount} successes, ${marketstackStats.errorCount} errors`); }); tap.test('should test direct provider methods', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } console.log('\n🔍 Testing direct provider methods:'); // Test isAvailable const available = await marketstackProvider.isAvailable(); expect(available).toEqual(true); console.log(' ✓ isAvailable() returned true'); // Test fetchPrice directly const price = await marketstackProvider.fetchPrice({ ticker: 'MSFT' }); expect(price.ticker).toEqual('MSFT'); expect(price.provider).toEqual('Marketstack'); expect(price.price).toBeGreaterThan(0); console.log(` ✓ fetchPrice() for MSFT: $${price.price}`); // Test fetchPrices directly const prices = await marketstackProvider.fetchPrices({ tickers: ['AAPL', 'GOOGL'] }); expect(prices.length).toBeGreaterThan(0); console.log(` ✓ fetchPrices() returned ${prices.length} prices`); for (const p of prices) { console.log(` ${p.ticker}: $${p.price}`); } }); tap.test('should fetch sample EOD data', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } console.log('\n📊 Sample EOD Stock Data from Marketstack:'); console.log('═'.repeat(65)); const sampleTickers = [ { ticker: 'AAPL', name: 'Apple Inc.' }, { ticker: 'MSFT', name: 'Microsoft Corp.' }, { ticker: 'GOOGL', name: 'Alphabet Inc.' }, { ticker: 'AMZN', name: 'Amazon.com Inc.' }, { ticker: 'TSLA', name: 'Tesla Inc.' } ]; try { const prices = await marketstackProvider.fetchPrices({ tickers: sampleTickers.map(t => t.ticker) }); const priceMap = new Map(prices.map(p => [p.ticker, p])); for (const stock of sampleTickers) { const price = priceMap.get(stock.ticker); if (price) { const changeSymbol = price.change >= 0 ? '↑' : '↓'; const changeColor = price.change >= 0 ? '\x1b[32m' : '\x1b[31m'; const resetColor = '\x1b[0m'; console.log( `${stock.name.padEnd(20)} ${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(10)} ${changeColor}${changeSymbol} ${price.change >= 0 ? '+' : ''}${price.change.toFixed(2)} (${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%)${resetColor}` ); } } console.log('═'.repeat(65)); console.log(`Provider: Marketstack (EOD Data)`); console.log(`Last updated: ${new Date().toLocaleString()}\n`); } catch (error) { console.log('Error fetching sample data:', error.message); } expect(true).toEqual(true); }); tap.test('should clear cache', async () => { if (!marketstackProvider) { console.log('⚠️ Skipping - Marketstack provider not initialized'); return; } // Ensure we have something in cache await stockService.getPrice({ ticker: 'AAPL' }); // Clear cache stockService.clearCache(); console.log('✓ Cache cleared'); // Next request should hit the API again const price = await stockService.getPrice({ ticker: 'AAPL' }); expect(price).not.toEqual(undefined); }); export default tap.start();