learning_ai_invt_trdg/backend/verifyMarketDataEndpoints.ts
Saravana Achu Mac e2806b28c1 test(F6): cover market data proxy endpoints
Add static contract coverage for the dashboard market data and research proxy routes so auth, upstream URL construction, response normalization, and FMP cache usage stay guarded by the backend test gate.

Refs: docs/AUDIT_REDESIGN.md item F6.

Co-Authored-By: GPT-5 Codex <noreply@openai.com>
2026-05-04 16:25:44 -07:00

200 lines
7.4 KiB
TypeScript

/**
* verifyMarketDataEndpoints.ts
*
* Static contract checks for the dashboard market-data proxy endpoints.
* These routes are thin upstream proxies, so the high-value regression
* surface is auth, provider URL construction, normalization, and cache usage.
*/
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const apiServerSource = readFileSync(join(__dirname, 'src/services/apiServer.ts'), 'utf8');
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function assertRouteRequiresAuth(route: string) {
const routePattern = new RegExp(
`this\\.app\\.get\\('${escapeRegExp(route)}',\\s*this\\.requireAuth,`,
);
assert.match(
apiServerSource,
routePattern,
`${route} must be registered with requireAuth middleware`,
);
}
function assertSourceIncludes(fragment: string, message: string) {
assert.ok(apiServerSource.includes(fragment), message);
}
function assertSourceMatches(pattern: RegExp, message: string) {
assert.match(apiServerSource, pattern, message);
}
function testRoutesRequireAuth() {
for (const route of [
'/api/chart/bars',
'/api/news',
'/api/market/indices',
'/api/research/profile',
'/api/research/metrics',
'/api/research/earnings',
'/api/screener',
]) {
assertRouteRequiresAuth(route);
}
console.log('[PASS] Market-data endpoints require authenticated requests.');
}
function testChartBarsContract() {
assertSourceMatches(
/this\.app\.get\('\/api\/chart\/bars'[\s\S]*if \(!symbol\)[\s\S]*status\(400\)\.json\(\{ error: 'symbol required' \}\)/,
'/api/chart/bars must reject missing symbols',
);
assertSourceMatches(
/this\.app\.get\('\/api\/chart\/bars'[\s\S]*Alpaca credentials not configured/,
'/api/chart/bars must fail closed when Alpaca credentials are absent',
);
for (const [period, timeframe, limit] of [
['1D', '5Min', '100'],
['5D', '15Min', '200'],
['1M', '1Day', '31'],
['3M', '1Day', '92'],
['6M', '1Day', '183'],
['YTD', '1Day', '365'],
['1Y', '1Day', '365'],
['5Y', '1Week', '260'],
['MAX', '1Month', '240'],
]) {
assertSourceMatches(
new RegExp(`case '${period}':[\\s\\S]*timeframe = '${timeframe}';\\s*limit = ${limit};`),
`/api/chart/bars must preserve ${period} -> ${timeframe}/${limit} mapping`,
);
}
assertSourceIncludes(
"symbol.includes('/')",
'/api/chart/bars must detect crypto symbols separately from equities',
);
assertSourceIncludes(
'https://data.alpaca.markets/v1beta3/crypto/us/bars?symbols=${encodedSymbol}&${qs.toString()}',
'/api/chart/bars must call Alpaca crypto bars with symbols query',
);
assertSourceIncludes(
'https://data.alpaca.markets/v2/stocks/${encodedSymbol}/bars?${qs.toString()}',
'/api/chart/bars must call Alpaca stock bars endpoint',
);
assertSourceIncludes(
"qs.set('feed', 'iex')",
'/api/chart/bars stock requests must use IEX feed',
);
assertSourceIncludes(
'rawBars = cryptoBars[symbol] ?? Object.values(cryptoBars)[0] ?? []',
'/api/chart/bars must unwrap crypto keyed bar responses',
);
assertSourceMatches(
/const bars = rawBars\.map[\s\S]*ts:\s+new Date\(b\.t\)\.getTime\(\)[\s\S]*open:\s+b\.o[\s\S]*high:\s+b\.h[\s\S]*low:\s+b\.l[\s\S]*close:\s+b\.c[\s\S]*volume:\s*b\.v/,
'/api/chart/bars must normalize Alpaca bar fields for the web chart',
);
console.log('[PASS] /api/chart/bars contract is covered.');
}
function testAlpacaProxyContracts() {
assertSourceMatches(
/this\.app\.get\('\/api\/news'[\s\S]*Math\.max\(1, Math\.min\(50, Number\(req\.query\.limit\) \|\| 10\)\)/,
'/api/news must clamp limit to 1..50',
);
assertSourceIncludes(
'https://data.alpaca.markets/v1beta1/news?${qs.toString()}',
'/api/news must proxy Alpaca news',
);
assertSourceIncludes("sort: 'desc'", '/api/news must request newest stories first');
assertSourceIncludes(
'res.json({ news: data.news ?? data })',
'/api/news must normalize responses to a news array wrapper',
);
assertSourceIncludes(
'https://data.alpaca.markets/v2/stocks/snapshots?symbols=SPY,DIA,QQQ&feed=iex',
'/api/market/indices must request SPY/DIA/QQQ IEX snapshots',
);
assertSourceIncludes(
'Alpaca snapshots fetch failed',
'/api/market/indices must propagate Alpaca snapshot failures',
);
console.log('[PASS] Alpaca news and index proxy contracts are covered.');
}
function testFmpProxyContracts() {
assertSourceIncludes(
"import { fetchFmpJson, FmpFetchError } from './fmpCache.js';",
'FMP routes must use the shared cache helper and typed upstream error',
);
const fetchFmpCalls = apiServerSource.match(/fetchFmpJson\(url\)/g) ?? [];
assert.equal(fetchFmpCalls.length, 4, 'each FMP route must fetch through fetchFmpJson(url)');
for (const [route, endpoint, errorMessage] of [
['/api/research/profile', '/api/v3/profile/${symbol}?apikey=${apiKey}', 'FMP profile fetch failed'],
['/api/research/metrics', '/api/v3/key-metrics/${symbol}?limit=4&apikey=${apiKey}', 'FMP metrics fetch failed'],
['/api/research/earnings', '/api/v3/historical/earning_calendar/${symbol}?limit=8&apikey=${apiKey}', 'FMP earnings fetch failed'],
]) {
assertSourceMatches(
new RegExp(`this\\.app\\.get\\('${escapeRegExp(route)}'[\\s\\S]*if \\(!symbol\\)[\\s\\S]*status\\(400\\)\\.json\\(\\{ error: 'symbol required' \\}\\)`),
`${route} must reject missing symbols`,
);
assertSourceIncludes(endpoint, `${route} must call the expected FMP endpoint`);
assertSourceIncludes(errorMessage, `${route} must preserve its FMP upstream error message`);
}
assertSourceIncludes(
'res.json(Array.isArray(data) ? data[0] ?? {} : data)',
'profile and metrics endpoints must unwrap first FMP array result',
);
assertSourceIncludes(
'res.json({ earnings: Array.isArray(data) ? data : [] })',
'/api/research/earnings must normalize to an earnings array wrapper',
);
assertSourceIncludes(
'https://financialmodelingprep.com/api/v3/stock-screener?${qs.toString()}',
'/api/screener must call FMP stock-screener',
);
assertSourceIncludes(
"qs.set('isEtf', 'false')",
'/api/screener must exclude ETFs by default',
);
assertSourceIncludes(
'String(Math.min(100, Number(req.query.limit) || 50))',
'/api/screener must cap result limit at 100',
);
assertSourceIncludes(
'res.json({ results: Array.isArray(data) ? data : [] })',
'/api/screener must normalize to a results array wrapper',
);
assertSourceIncludes(
'FMP screener fetch failed',
'/api/screener must preserve its FMP upstream error message',
);
console.log('[PASS] FMP research and screener proxy contracts are covered.');
}
function main() {
testRoutesRequireAuth();
testChartBarsContract();
testAlpacaProxyContracts();
testFmpProxyContracts();
console.log('Market-data endpoint contract checks passed');
}
main();