import { HfmdClient } from '../src/client.js'; export interface MockRoute { /** Substring or RegExp matched against the full URL. */ match: string | RegExp; status?: number; body?: unknown; headers?: Record; } export interface MockFetch { fetch: typeof fetch; calls: string[]; } export function mockFetch(routes: MockRoute[]): MockFetch { const calls: string[] = []; const f = (async (input: RequestInfo | URL, init?: RequestInit) => { const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; calls.push(url); void init; const route = routes.find((r) => (typeof r.match === 'string' ? url.includes(r.match) : r.match.test(url))); if (!route) return new Response(JSON.stringify({ detail: 'Not Found' }), { status: 404, headers: { 'content-type': 'application/json' } }); const status = route.status ?? 200; const headers = { 'content-type': 'application/json', ...(route.headers ?? {}) }; return new Response(typeof route.body === 'string' ? route.body : JSON.stringify(route.body ?? {}), { status, headers }); }) as typeof fetch; return { fetch: f, calls }; } export function clientWith(routes: MockRoute[], opts: { apiKey?: string } = {}): { client: HfmdClient; calls: string[] } { const m = mockFetch(routes); const client = new HfmdClient({ baseUrl: 'https://api.test', apiKey: opts.apiKey ?? '', fetchImpl: m.fetch }); return { client, calls: m.calls }; } export function bars(n: number, start = '2025-01-01', ticker = 'AAPL') { const rows = []; const d = new Date(start + 'T00:00:00Z'); for (let i = 0; i < n; i++) { const date = new Date(d.getTime() + i * 86_400_000).toISOString().slice(0, 10); rows.push({ ticker, datetime: date, open: 100 + i, high: 101 + i, low: 99 + i, close: 100.5 + i, volume: 1000 + i }); } return rows; } export const RATE_HEADERS = { 'x-ratelimit-limit-requests': '30', 'x-ratelimit-remaining-requests': '29', 'x-ratelimit-limit-rows': '100000', 'x-ratelimit-remaining-rows': '99500', 'x-ratelimit-reset': '3599', 'x-row-count': '5', }; /** Minimal fake WebSocket emitting canned messages after open. */ export function fakeWebSocketClass(messages: unknown[], opts: { failOpen?: boolean; delayMs?: number } = {}) { const instances: FakeWS[] = []; class FakeWS { url: string; sent: string[] = []; closed = false; private listeners: Record void)[]> = {}; constructor(url: string) { this.url = url; instances.push(this); setTimeout(() => { if (opts.failOpen) return this.emit('error', {}); this.emit('open', {}); for (const m of messages) setTimeout(() => this.emit('message', { data: typeof m === 'string' ? m : JSON.stringify(m) }), opts.delayMs ?? 1); }, 1); } addEventListener(type: string, cb: (ev: any) => void) { (this.listeners[type] ??= []).push(cb); } emit(type: string, ev: unknown) { if (this.closed) return; for (const cb of this.listeners[type] ?? []) cb(ev); } send(data: string) { this.sent.push(data); } close() { this.closed = true; } } return { FakeWS: FakeWS as unknown as typeof WebSocket, instances }; }