import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import { __pricesInternals, getPriceHistory, getQuote } from './prices'; describe('price caching', () => { const originalFetch = globalThis.fetch; beforeEach(() => { __pricesInternals.resetCaches(); }); afterEach(() => { globalThis.fetch = originalFetch; __pricesInternals.resetCaches(); }); it('reuses the cached quote within the ttl window', async () => { const fetchMock = mock(async () => Response.json({ chart: { result: [ { meta: { regularMarketPrice: 123.45 } } ] } })) as unknown as typeof fetch; globalThis.fetch = fetchMock; const first = await getQuote('MSFT'); const second = await getQuote('MSFT'); 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); }); it('reuses cached price history within the ttl window', async () => { const fetchMock = mock(async () => Response.json({ chart: { result: [ { timestamp: [1735689600, 1736294400], indicators: { quote: [ { close: [100, 105] } ] } } ] } })) as unknown as typeof fetch; globalThis.fetch = fetchMock; const first = await getPriceHistory('MSFT'); const second = await getPriceHistory('MSFT'); 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); }); });