import { describe, expect, it, vi } from 'vitest'; import { RateService } from '../src/rates'; function fakeFetch(price: number) { return vi.fn(async (url: string | URL | Request) => { const u = String(url); const id = u.includes('tether') ? 'tether' : 'usd-coin'; return new Response(JSON.stringify({ [id]: { usd: price } }), { status: 200, headers: { 'content-type': 'application/json' }, }); }) as unknown as typeof fetch & ReturnType; } describe('RateService', () => { it('fetches and converts a price to integer rate units', async () => { const svc = new RateService({ fetchFn: fakeFetch(0.9998) }); const quote = await svc.getRate('USDT', 'USD'); expect(quote.rateUnits).toBe(999800n); expect(quote.source).toBe('coingecko'); }); it('caches for 60 seconds and refetches after TTL', async () => { let now = 0; const f = fakeFetch(1); const svc = new RateService({ fetchFn: f, now: () => now }); await svc.getRate('USDC', 'USD'); now = 59_000; await svc.getRate('USDC', 'USD'); expect(f).toHaveBeenCalledTimes(1); now = 61_000; await svc.getRate('USDC', 'USD'); expect(f).toHaveBeenCalledTimes(2); }); it('caches per asset+fiat pair', async () => { const f = fakeFetch(1); const svc = new RateService({ fetchFn: f, now: () => 0 }); await svc.getRate('USDT', 'USD'); await svc.getRate('USDC', 'USD'); expect(f).toHaveBeenCalledTimes(2); }); it('throws on provider errors and missing prices', async () => { const err = vi.fn(async () => new Response('nope', { status: 500 })) as unknown as typeof fetch; await expect(new RateService({ fetchFn: err }).getRate('USDT', 'USD')).rejects.toThrow(/HTTP 500/); const empty = vi.fn(async () => new Response('{}', { status: 200 })) as unknown as typeof fetch; await expect(new RateService({ fetchFn: empty }).getRate('USDT', 'USD')).rejects.toThrow(/no USD price/); }); });