Files
opendata/test/test.secedgar.provider.node.ts

262 lines
8.6 KiB
TypeScript
Raw Normal View History

import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as opendata from '../ts/index.js';
// Test configuration
const TEST_USER_AGENT = 'fin.cx test@fin.cx';
const TEST_TICKER = 'AAPL'; // Apple Inc - well-known test case
const RATE_LIMIT_DELAY = 150; // 150ms between requests (< 10 req/sec)
tap.test('SEC EDGAR Provider - Constructor', async () => {
await tap.test('should create provider with valid User-Agent', async () => {
const provider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
expect(provider.name).toEqual('SEC EDGAR');
expect(provider.priority).toEqual(100);
expect(provider.requiresAuth).toBe(false);
expect(provider.rateLimit?.requestsPerMinute).toEqual(600);
});
await tap.test('should throw error if User-Agent is missing', async () => {
expect(() => {
new opendata.SecEdgarProvider({
userAgent: ''
});
}).toThrow('User-Agent is required');
});
await tap.test('should throw error if User-Agent format is invalid', async () => {
expect(() => {
new opendata.SecEdgarProvider({
userAgent: 'InvalidFormat'
});
}).toThrow('Invalid User-Agent format');
});
await tap.test('should accept valid User-Agent with space and email', async () => {
const provider = new opendata.SecEdgarProvider({
userAgent: 'MyCompany contact@example.com'
});
expect(provider).toBeInstanceOf(opendata.SecEdgarProvider);
});
});
tap.test('SEC EDGAR Provider - Availability', async () => {
const provider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
await tap.test('should report as available', async () => {
const isAvailable = await provider.isAvailable();
expect(isAvailable).toBe(true);
});
});
tap.test('SEC EDGAR Provider - Fetch Fundamentals', async () => {
const provider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT,
timeout: 30000
});
await tap.test('should fetch fundamentals for Apple (AAPL)', async () => {
const fundamentals = await provider.fetchData({
type: 'fundamentals-current',
ticker: TEST_TICKER
});
// Verify structure
expect(fundamentals).toBeDefined();
expect(fundamentals).not.toBeInstanceOf(Array);
const data = fundamentals as opendata.IStockFundamentals;
// Basic fields
expect(data.ticker).toEqual('AAPL');
expect(data.cik).toBeDefined();
expect(data.companyName).toEqual('Apple Inc.');
expect(data.provider).toEqual('SEC EDGAR');
expect(data.timestamp).toBeInstanceOf(Date);
expect(data.fetchedAt).toBeInstanceOf(Date);
// Financial metrics (Apple should have all of these)
expect(data.earningsPerShareDiluted).toBeDefined();
expect(data.earningsPerShareDiluted).toBeGreaterThan(0);
expect(data.sharesOutstanding).toBeDefined();
expect(data.sharesOutstanding).toBeGreaterThan(0);
expect(data.revenue).toBeDefined();
expect(data.revenue).toBeGreaterThan(0);
expect(data.netIncome).toBeDefined();
expect(data.assets).toBeDefined();
expect(data.assets).toBeGreaterThan(0);
expect(data.liabilities).toBeDefined();
expect(data.stockholdersEquity).toBeDefined();
// Metadata
expect(data.fiscalYear).toBeDefined();
console.log('\n📊 Sample Apple Fundamentals:');
console.log(` Company: ${data.companyName} (CIK: ${data.cik})`);
console.log(` EPS (Diluted): $${data.earningsPerShareDiluted?.toFixed(2)}`);
console.log(` Shares Outstanding: ${(data.sharesOutstanding! / 1_000_000_000).toFixed(2)}B`);
console.log(` Revenue: $${(data.revenue! / 1_000_000_000).toFixed(2)}B`);
console.log(` Net Income: $${(data.netIncome! / 1_000_000_000).toFixed(2)}B`);
console.log(` Assets: $${(data.assets! / 1_000_000_000).toFixed(2)}B`);
console.log(` Fiscal Year: ${data.fiscalYear}`);
});
await tap.test('should throw error for invalid ticker', async () => {
try {
await provider.fetchData({
type: 'fundamentals-current',
ticker: 'INVALIDTICKER123456'
});
throw new Error('Should have thrown error');
} catch (error) {
expect(error.message).toContain('CIK not found');
}
});
});
tap.test('SEC EDGAR Provider - Batch Fetch', async () => {
const provider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT,
timeout: 30000
});
await tap.test('should fetch fundamentals for multiple tickers', async () => {
const result = await provider.fetchData({
type: 'fundamentals-batch',
tickers: ['AAPL', 'MSFT']
});
expect(result).toBeInstanceOf(Array);
const fundamentalsList = result as opendata.IStockFundamentals[];
expect(fundamentalsList.length).toEqual(2);
// Check Apple
const apple = fundamentalsList.find(f => f.ticker === 'AAPL');
expect(apple).toBeDefined();
expect(apple!.companyName).toEqual('Apple Inc.');
expect(apple!.earningsPerShareDiluted).toBeGreaterThan(0);
// Check Microsoft
const microsoft = fundamentalsList.find(f => f.ticker === 'MSFT');
expect(microsoft).toBeDefined();
expect(microsoft!.companyName).toContain('Microsoft');
expect(microsoft!.earningsPerShareDiluted).toBeGreaterThan(0);
console.log('\n📊 Batch Fetch Results:');
fundamentalsList.forEach(f => {
console.log(` ${f.ticker}: ${f.companyName} - EPS: $${f.earningsPerShareDiluted?.toFixed(2)}`);
});
});
});
tap.test('SEC EDGAR Provider - CIK Caching', async () => {
const provider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
await tap.test('should cache CIK lookups', async () => {
// Clear cache first
provider.clearCache();
let stats = provider.getCacheStats();
expect(stats.cikCacheSize).toEqual(0);
// First fetch (should populate cache)
await provider.fetchData({
type: 'fundamentals-current',
ticker: 'AAPL'
});
stats = provider.getCacheStats();
expect(stats.cikCacheSize).toBeGreaterThan(0);
console.log(`\n💾 CIK Cache: ${stats.cikCacheSize} entries`);
});
await tap.test('should clear cache', async () => {
provider.clearCache();
const stats = provider.getCacheStats();
expect(stats.cikCacheSize).toEqual(0);
expect(stats.hasTickerList).toBe(false);
});
});
tap.test('SEC EDGAR Provider - Rate Limiting', async () => {
const provider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
await tap.test('should handle multiple rapid requests without exceeding rate limit', async () => {
// Make 5 requests in succession
// Rate limiter should ensure we don't exceed 10 req/sec
const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META'];
const startTime = Date.now();
const promises = tickers.map(ticker =>
provider.fetchData({
type: 'fundamentals-current',
ticker
})
);
const results = await Promise.all(promises);
const duration = Date.now() - startTime;
expect(results.length).toEqual(5);
console.log(`\n⏱ 5 requests completed in ${duration}ms (avg: ${Math.round(duration / 5)}ms/request)`);
// Verify all results are valid
results.forEach((result, index) => {
expect(result).toBeDefined();
const data = result as opendata.IStockFundamentals;
expect(data.ticker).toEqual(tickers[index]);
expect(data.earningsPerShareDiluted).toBeGreaterThan(0);
});
});
});
tap.test('SEC EDGAR Provider - Market Cap Calculation', async () => {
const provider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
await tap.test('should provide data needed for market cap calculation', async () => {
const fundamentals = await provider.fetchData({
type: 'fundamentals-current',
ticker: 'AAPL'
}) as opendata.IStockFundamentals;
expect(fundamentals.sharesOutstanding).toBeDefined();
expect(fundamentals.earningsPerShareDiluted).toBeDefined();
// Simulate current price (in real usage, this comes from price provider)
const simulatedPrice = 270.37;
// Calculate market cap
const marketCap = fundamentals.sharesOutstanding! * simulatedPrice;
const pe = simulatedPrice / fundamentals.earningsPerShareDiluted!;
console.log('\n💰 Calculated Metrics (with simulated price $270.37):');
console.log(` Shares Outstanding: ${(fundamentals.sharesOutstanding! / 1_000_000_000).toFixed(2)}B`);
console.log(` Market Cap: $${(marketCap / 1_000_000_000_000).toFixed(2)}T`);
console.log(` EPS: $${fundamentals.earningsPerShareDiluted!.toFixed(2)}`);
console.log(` P/E Ratio: ${pe.toFixed(2)}`);
expect(marketCap).toBeGreaterThan(0);
expect(pe).toBeGreaterThan(0);
});
});
export default tap.start();