learning_ai_invt_trdg/backend/testFmpCache.ts

93 lines
3.0 KiB
TypeScript

import assert from 'node:assert/strict';
import {
buildFmpCacheKey,
clearFmpCacheForTests,
fetchFmpJson,
FMP_CACHE_TTL_MS,
FmpFetchError,
redactFmpUrlForLogs,
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' }]);
const cacheKey = buildFmpCacheKey(url);
assert.notEqual(cacheKey, url, 'cache keys should not retain raw FMP URLs with query-string API keys');
assert(!cacheKey.includes('apikey'), 'cache keys should not expose the FMP apikey parameter');
assert.equal(
redactFmpUrlForLogs(url),
'https://financialmodelingprep.com/api/v3/profile/AAPL?apikey=%5BREDACTED%5D',
'log-safe FMP URLs should redact apikey values',
);
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);
});