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>
This commit is contained in:
Saravana Achu Mac 2026-05-04 16:25:44 -07:00
parent 412fa5ad7c
commit e2806b28c1
2 changed files with 201 additions and 1 deletions

View File

@ -5,7 +5,7 @@
"description": "ByteLyst Trading backend and execution control service", "description": "ByteLyst Trading backend and execution control service",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "npm run check:websocket-contract && npm run check:session-rule-normalization && npm run check:api-contract && npm run check:audit-repository && npm run check:fmp-cache", "test": "npm run check:websocket-contract && npm run check:session-rule-normalization && npm run check:api-contract && npm run check:audit-repository && npm run check:market-data-endpoints && npm run check:fmp-cache",
"dev": "node --import tsx src/bootstrap.ts", "dev": "node --import tsx src/bootstrap.ts",
"build": "tsc", "build": "tsc",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
@ -29,6 +29,7 @@
"check:session-rule-normalization": "node --import tsx testSessionRuleNormalization.ts", "check:session-rule-normalization": "node --import tsx testSessionRuleNormalization.ts",
"check:api-contract": "node --import tsx verifyApiContract.ts", "check:api-contract": "node --import tsx verifyApiContract.ts",
"check:audit-repository": "node --import tsx verifyAuditRepository.ts", "check:audit-repository": "node --import tsx verifyAuditRepository.ts",
"check:market-data-endpoints": "node --import tsx verifyMarketDataEndpoints.ts",
"check:fmp-cache": "node --import tsx testFmpCache.ts", "check:fmp-cache": "node --import tsx testFmpCache.ts",
"check:websocket-contract": "node --import tsx src/scripts/verifyWebsocketContract.ts", "check:websocket-contract": "node --import tsx src/scripts/verifyWebsocketContract.ts",
"coverage:run": "node --loader ts-node/esm runCoverageSuite.ts", "coverage:run": "node --loader ts-node/esm runCoverageSuite.ts",

View File

@ -0,0 +1,199 @@
/**
* 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();