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:
parent
412fa5ad7c
commit
e2806b28c1
@ -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",
|
||||||
|
|||||||
199
backend/verifyMarketDataEndpoints.ts
Normal file
199
backend/verifyMarketDataEndpoints.ts
Normal 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();
|
||||||
Loading…
Reference in New Issue
Block a user