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