306 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			306 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 configuration - explicit paths required
 | 
						|
const testNogitDir = plugins.path.join(paths.packageDir, '.nogit');
 | 
						|
 | 
						|
// 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, testNogitDir);
 | 
						|
    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();
 |