learning_ai_invt_trdg/backend/testFmpCache.ts
Saravana Achu Mac 082800745c fix(C2): cache FMP upstream responses
Add a 30-minute in-memory cache keyed by the complete Financial Modeling Prep upstream URL so repeated profile, metrics, earnings, and screener requests do not burn the free-tier quota. The cache keeps non-OK responses out of storage so transient rate-limit or provider errors can recover on the next request.

Refs: docs/AUDIT_REDESIGN.md item C2.

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

82 lines
2.5 KiB
TypeScript

import assert from 'node:assert/strict';
import {
clearFmpCacheForTests,
fetchFmpJson,
FMP_CACHE_TTL_MS,
FmpFetchError,
setFmpCacheNowForTests,
} from './src/services/fmpCache.js';
type MockResponse = {
ok: boolean;
status: number;
json: () => Promise<unknown>;
};
function createFetch(payloads: unknown[]) {
let calls = 0;
const fetchJson = async (): Promise<MockResponse> => {
const payload = payloads[calls] ?? payloads[payloads.length - 1];
calls += 1;
return {
ok: true,
status: 200,
json: async () => payload,
};
};
return {
fetchJson,
get calls() {
return calls;
},
};
}
async function main() {
clearFmpCacheForTests();
let now = 1_000;
setFmpCacheNowForTests(() => now);
const url = 'https://financialmodelingprep.com/api/v3/profile/AAPL?apikey=test';
const otherUrl = 'https://financialmodelingprep.com/api/v3/profile/MSFT?apikey=test';
const fetcher = createFetch([{ symbol: 'AAPL' }, { symbol: 'AAPL-refresh' }]);
assert.deepEqual(await fetchFmpJson(url, fetcher.fetchJson), { symbol: 'AAPL' });
assert.deepEqual(await fetchFmpJson(url, fetcher.fetchJson), { symbol: 'AAPL' });
assert.equal(fetcher.calls, 1, 'same full URL should be served from cache before TTL expiry');
assert.deepEqual(await fetchFmpJson(otherUrl, fetcher.fetchJson), { symbol: 'AAPL-refresh' });
assert.equal(fetcher.calls, 2, 'different full URL should get its own cache entry');
now += FMP_CACHE_TTL_MS + 1;
assert.deepEqual(await fetchFmpJson(url, fetcher.fetchJson), { symbol: 'AAPL-refresh' });
assert.equal(fetcher.calls, 3, 'expired cache entry should be refetched');
clearFmpCacheForTests();
let failingCalls = 0;
const failingFetch = async (): Promise<MockResponse> => {
failingCalls += 1;
return {
ok: false,
status: 429,
json: async () => ({ error: 'rate limit' }),
};
};
await assert.rejects(
() => fetchFmpJson(url, failingFetch),
(error) => error instanceof FmpFetchError && error.status === 429,
);
await assert.rejects(() => fetchFmpJson(url, failingFetch));
assert.equal(failingCalls, 2, 'failed upstream responses should not be cached');
clearFmpCacheForTests();
console.log('FMP cache tests passed');
}
main().catch((error) => {
console.error(error);
process.exit(1);
});