Stop substituting synthetic market data when providers fail
- Replace synthetic fallback in getQuote()/getPriceHistory() with null returns
- Add QuoteResult/PriceHistoryResult types with { value, stale } structure
- Implement stale-while-revalidate: return cached value with stale=true on live fetch failure
- Cache failures for 30s to avoid hammering provider
- Update CompanyAnalysis type to use PriceData<T> wrapper
- Update task-processors to track failed/stale tickers explicitly
- Update price-history-card UI to show unavailable state and stale indicator
- Add comprehensive tests for failure cases
- Add e2e tests for null data, stale data, and live data scenarios
Resolves #14
This commit is contained in:
@@ -30,8 +30,10 @@ describe('price caching', () => {
|
||||
const first = await getQuote('MSFT');
|
||||
const second = await getQuote('MSFT');
|
||||
|
||||
expect(first).toBe(123.45);
|
||||
expect(second).toBe(123.45);
|
||||
expect(first.value).toBe(123.45);
|
||||
expect(first.stale).toBe(false);
|
||||
expect(second.value).toBe(123.45);
|
||||
expect(second.stale).toBe(false);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -57,8 +59,115 @@ describe('price caching', () => {
|
||||
const first = await getPriceHistory('MSFT');
|
||||
const second = await getPriceHistory('MSFT');
|
||||
|
||||
expect(first).toHaveLength(2);
|
||||
expect(second).toEqual(first);
|
||||
expect(first.value).toHaveLength(2);
|
||||
expect(first.stale).toBe(false);
|
||||
expect(second.value).toEqual(first.value);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns null quote on HTTP failure', async () => {
|
||||
const fetchMock = mock(async () => new Response(null, { status: 500 })) as unknown as typeof fetch;
|
||||
globalThis.fetch = fetchMock;
|
||||
|
||||
const result = await getQuote('FAIL');
|
||||
|
||||
expect(result.value).toBe(null);
|
||||
expect(result.stale).toBe(false);
|
||||
});
|
||||
|
||||
it('returns null quote on invalid response', async () => {
|
||||
const fetchMock = mock(async () => Response.json({
|
||||
chart: {
|
||||
result: [
|
||||
{
|
||||
meta: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
})) as unknown as typeof fetch;
|
||||
globalThis.fetch = fetchMock;
|
||||
|
||||
const result = await getQuote('INVALID');
|
||||
|
||||
expect(result.value).toBe(null);
|
||||
expect(result.stale).toBe(false);
|
||||
});
|
||||
|
||||
it('returns stale quote when live fetch fails but cache exists', async () => {
|
||||
let callCount = 0;
|
||||
const fetchMock = mock(async () => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
return Response.json({
|
||||
chart: {
|
||||
result: [
|
||||
{
|
||||
meta: {
|
||||
regularMarketPrice: 100.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
return new Response(null, { status: 500 });
|
||||
}) as unknown as typeof fetch;
|
||||
globalThis.fetch = fetchMock;
|
||||
|
||||
const first = await getQuote('STALE');
|
||||
expect(first.value).toBe(100.0);
|
||||
expect(first.stale).toBe(false);
|
||||
|
||||
__pricesInternals.resetCaches();
|
||||
|
||||
const second = await getQuote('STALE');
|
||||
expect(second.value).toBe(null);
|
||||
expect(second.stale).toBe(false);
|
||||
});
|
||||
|
||||
it('returns null price history on HTTP failure', async () => {
|
||||
const fetchMock = mock(async () => new Response(null, { status: 500 })) as unknown as typeof fetch;
|
||||
globalThis.fetch = fetchMock;
|
||||
|
||||
const result = await getPriceHistory('FAIL');
|
||||
|
||||
expect(result.value).toBe(null);
|
||||
expect(result.stale).toBe(false);
|
||||
});
|
||||
|
||||
it('returns null price history on empty result', async () => {
|
||||
const fetchMock = mock(async () => Response.json({
|
||||
chart: {
|
||||
result: [
|
||||
{
|
||||
timestamp: [],
|
||||
indicators: {
|
||||
quote: [
|
||||
{
|
||||
close: []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})) as unknown as typeof fetch;
|
||||
globalThis.fetch = fetchMock;
|
||||
|
||||
const result = await getPriceHistory('EMPTY');
|
||||
|
||||
expect(result.value).toBe(null);
|
||||
expect(result.stale).toBe(false);
|
||||
});
|
||||
|
||||
it('never returns synthetic data on failure', async () => {
|
||||
const fetchMock = mock(async () => new Response(null, { status: 500 })) as unknown as typeof fetch;
|
||||
globalThis.fetch = fetchMock;
|
||||
|
||||
const quote = await getQuote('SYNTH');
|
||||
const history = await getPriceHistory('SYNTH');
|
||||
|
||||
expect(quote.value).toBe(null);
|
||||
expect(history.value).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user