262 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			262 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 
								 | 
							
								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();
							 |