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