import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as opendata from '../ts/index.js'; // Test data const testCryptos = ['BTC', 'ETH', 'USDT']; const testCryptoIds = ['bitcoin', 'ethereum', 'tether']; const invalidCrypto = 'INVALID_CRYPTO_XYZ_12345'; let stockService: opendata.StockPriceService; let coingeckoProvider: opendata.CoinGeckoProvider; 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 CoinGeckoProvider instance without API key', async () => { coingeckoProvider = new opendata.CoinGeckoProvider(); expect(coingeckoProvider).toBeInstanceOf(opendata.CoinGeckoProvider); expect(coingeckoProvider.name).toEqual('CoinGecko'); expect(coingeckoProvider.requiresAuth).toEqual(false); expect(coingeckoProvider.priority).toEqual(90); }); tap.test('should register CoinGecko provider with the service', async () => { stockService.register(coingeckoProvider); const providers = stockService.getAllProviders(); expect(providers).toContainEqual(coingeckoProvider); expect(stockService.getProvider('CoinGecko')).toEqual(coingeckoProvider); }); tap.test('should check CoinGecko provider health', async () => { const health = await stockService.checkProvidersHealth(); expect(health.get('CoinGecko')).toEqual(true); }); tap.test('should fetch single crypto price using ticker symbol (BTC)', async () => { const price = await stockService.getPrice({ ticker: 'BTC' }); 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('BTC'); expect(price.price).toBeGreaterThan(0); expect(price.currency).toEqual('USD'); expect(price.provider).toEqual('CoinGecko'); expect(price.marketState).toEqual('REGULAR'); // Crypto is 24/7 expect(price.timestamp).toBeInstanceOf(Date); expect(price.dataType).toEqual('live'); console.log(`\nšŸ“Š BTC Price: $${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`); console.log(` Change: ${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%`); }); tap.test('should fetch single crypto price using CoinGecko ID (bitcoin)', async () => { // Clear cache to ensure fresh fetch stockService.clearCache(); const price = await stockService.getPrice({ ticker: 'bitcoin' }); expect(price.ticker).toEqual('BITCOIN'); expect(price.price).toBeGreaterThan(0); expect(price.provider).toEqual('CoinGecko'); expect(price.companyName).toInclude('Bitcoin'); }); tap.test('should fetch multiple crypto prices (batch)', async () => { stockService.clearCache(); const prices = await stockService.getPrices({ tickers: testCryptos }); expect(prices).toBeArray(); expect(prices.length).toEqual(testCryptos.length); for (const price of prices) { expect(testCryptos).toContain(price.ticker); expect(price.price).toBeGreaterThan(0); expect(price.provider).toEqual('CoinGecko'); expect(price.marketState).toEqual('REGULAR'); console.log(`\nšŸ’° ${price.ticker}: $${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 8 })}`); console.log(` Change 24h: ${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%`); if (price.volume) { console.log(` Volume 24h: $${price.volume.toLocaleString('en-US')}`); } } }); tap.test('should fetch historical crypto prices', async () => { // Add delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 3000)); const to = new Date(); const from = new Date(to.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago const prices = await stockService.getData({ type: 'historical', ticker: 'BTC', from: from, to: to }); expect(prices).toBeArray(); expect((prices as opendata.IStockPrice[]).length).toBeGreaterThan(0); const pricesArray = prices as opendata.IStockPrice[]; console.log(`\nšŸ“ˆ Historical BTC Prices (${pricesArray.length} days):`); // Show first few and last few const toShow = Math.min(3, pricesArray.length); for (let i = 0; i < toShow; i++) { const price = pricesArray[i]; const date = price.timestamp.toISOString().split('T')[0]; console.log(` ${date}: $${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`); } if (pricesArray.length > toShow * 2) { console.log(' ...'); } for (let i = Math.max(toShow, pricesArray.length - toShow); i < pricesArray.length; i++) { const price = pricesArray[i]; const date = price.timestamp.toISOString().split('T')[0]; console.log(` ${date}: $${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`); } // Validate first entry const firstPrice = pricesArray[0]; expect(firstPrice.ticker).toEqual('BTC'); expect(firstPrice.dataType).toEqual('eod'); expect(firstPrice.provider).toEqual('CoinGecko'); }); tap.test('should fetch intraday crypto prices (hourly)', async () => { // Add delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 3000)); const prices = await stockService.getData({ type: 'intraday', ticker: 'ETH', interval: '1hour', limit: 12 // Last 12 hours }); expect(prices).toBeArray(); expect((prices as opendata.IStockPrice[]).length).toBeGreaterThan(0); const pricesArray = prices as opendata.IStockPrice[]; console.log(`\nā° Intraday ETH Prices (hourly, last ${pricesArray.length} hours):`); // Show first few entries const toShow = Math.min(5, pricesArray.length); for (let i = 0; i < toShow; i++) { const price = pricesArray[i]; const time = price.timestamp.toISOString().replace('T', ' ').substring(0, 16); console.log(` ${time}: $${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`); } // Validate first entry const firstPrice = pricesArray[0]; expect(firstPrice.ticker).toEqual('ETH'); expect(firstPrice.dataType).toEqual('intraday'); expect(firstPrice.provider).toEqual('CoinGecko'); }); tap.test('should serve cached prices on subsequent requests', async () => { // First request - should hit the API const firstRequest = await stockService.getPrice({ ticker: 'BTC' }); // Second request - should be served from cache const secondRequest = await stockService.getPrice({ ticker: 'BTC' }); expect(secondRequest.ticker).toEqual(firstRequest.ticker); expect(secondRequest.price).toEqual(firstRequest.price); expect(secondRequest.timestamp).toEqual(firstRequest.timestamp); expect(secondRequest.fetchedAt).toEqual(firstRequest.fetchedAt); }); tap.test('should handle invalid crypto ticker gracefully', async () => { try { await stockService.getPrice({ ticker: invalidCrypto }); throw new Error('Should have thrown an error for invalid ticker'); } catch (error) { expect(error.message).toInclude('Failed to fetch'); } }); tap.test('should support market checking', async () => { expect(coingeckoProvider.supportsMarket('CRYPTO')).toEqual(true); expect(coingeckoProvider.supportsMarket('BTC')).toEqual(true); expect(coingeckoProvider.supportsMarket('ETH')).toEqual(true); expect(coingeckoProvider.supportsMarket('NASDAQ')).toEqual(false); }); tap.test('should support ticker validation', async () => { expect(coingeckoProvider.supportsTicker('BTC')).toEqual(true); expect(coingeckoProvider.supportsTicker('bitcoin')).toEqual(true); expect(coingeckoProvider.supportsTicker('wrapped-bitcoin')).toEqual(true); expect(coingeckoProvider.supportsTicker('BTC!')).toEqual(false); expect(coingeckoProvider.supportsTicker('BTC@USD')).toEqual(false); }); tap.test('should display provider statistics', async () => { const stats = stockService.getProviderStats(); const coingeckoStats = stats.get('CoinGecko'); expect(coingeckoStats).toBeTruthy(); expect(coingeckoStats.successCount).toBeGreaterThan(0); console.log('\nšŸ“Š CoinGecko Provider Statistics:'); console.log(` Success Count: ${coingeckoStats.successCount}`); console.log(` Error Count: ${coingeckoStats.errorCount}`); if (coingeckoStats.lastError) { console.log(` Last Error: ${coingeckoStats.lastError}`); } }); tap.test('should display crypto price dashboard', async () => { // Add delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 3000)); stockService.clearCache(); const cryptos = ['BTC', 'ETH', 'BNB', 'SOL', 'ADA']; const prices = await stockService.getPrices({ tickers: cryptos }); console.log('\n╔═══════════════════════════════════════════════════════════╗'); console.log('ā•‘ 🌐 CRYPTOCURRENCY PRICE DASHBOARD ā•‘'); console.log('╠═══════════════════════════════════════════════════════════╣'); for (const price of prices) { const priceStr = `$${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 8 })}`; const changeStr = `${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%`; const changeIcon = price.changePercent >= 0 ? 'šŸ“ˆ' : 'šŸ“‰'; console.log(`ā•‘ ${price.ticker.padEnd(6)} ${changeIcon} ${priceStr.padStart(20)} │ ${changeStr.padStart(10)} ā•‘`); } console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•'); console.log(`Provider: ${prices[0].provider} | Market State: ${prices[0].marketState} (24/7)`); console.log(`Fetched at: ${prices[0].fetchedAt.toISOString()}`); }); tap.test('should clear cache', async () => { stockService.clearCache(); // Cache is cleared, no assertions needed }); export default tap.start();