import { vi } from 'vitest'; import type { StorageItem } from '../../src/generated/storage.js'; import type { CallModuleFunctionClientResponse } from '../../src/generated/modules.js'; import type { CatalogCurrency } from '../../src/generated/catalog.js'; export interface RecordedRequest { method: string; path: string; query: URLSearchParams; body: unknown; authToken: string | null; headers: Headers; } export interface FakeGatewayState { storage: Map; currencies: CatalogCurrency[]; /** Response per `module/fn`; falls back to `{ result: {} }`. */ moduleCalls: Map; } export interface FakeGateway { state: FakeGatewayState; recorded: RecordedRequest[]; install(): void; } function json(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }); } const noContent = (): Response => new Response(null, { status: 204 }); export function createFakeGateway(seed?: Partial): FakeGateway { const state: FakeGatewayState = { storage: seed?.storage ?? new Map(), currencies: seed?.currencies ?? [], moduleCalls: seed?.moduleCalls ?? new Map(), }; const recorded: RecordedRequest[] = []; async function handle(req: RecordedRequest): Promise { const { method, path, query, body } = req; if (method === 'POST' && path === '/sdk/v1/authorization/device') { const b = body as { deviceId?: string }; return json({ accessToken: `access-${b.deviceId ?? 'dev'}`, refreshToken: `refresh-${b.deviceId ?? 'dev'}`, }); } if (method === 'GET' && path === '/sdk/v1/player/information') { return json({ player: null, wallets: [] }); } if (method === 'GET' && path === '/sdk/v1/sync') { return json({}); } if (method === 'GET' && path === '/sdk/v1/catalog') { return json({ currencies: state.currencies }); } const moduleMatch = path.match(/^\/sdk\/v1\/modules\/([^/]+)\/([^/]+)$/); if (method === 'POST' && moduleMatch) { return json(state.moduleCalls.get(`${moduleMatch[1]}/${moduleMatch[2]}`) ?? { result: {} }); } if (path === '/sdk/v1/storage') { if (method === 'GET') { const typeFilter = query.get('types'); const items = [...state.storage.values()].filter( (it) => !typeFilter || it.type === typeFilter, ); return json({ items, nextCursor: '' }); } if (method === 'PUT') { const items = (body as { items?: StorageItem[] }).items ?? []; for (const item of items) { if (item.type) state.storage.set(item.type, { id: `id-${item.type}`, ...item }); } return noContent(); } if (method === 'DELETE') { const type = query.get('type'); if (type) state.storage.delete(type); else state.storage.clear(); return noContent(); } } return json({ error: `unhandled route: ${method} ${path}` }, 500); } function install(): void { vi.stubGlobal( 'fetch', vi.fn(async (input: string | URL, init?: RequestInit): Promise => { const url = new URL(typeof input === 'string' ? input : input.toString()); const headers = new Headers(init?.headers); const auth = headers.get('Authorization'); const req: RecordedRequest = { method: (init?.method ?? 'GET').toUpperCase(), path: url.pathname, query: url.searchParams, body: init?.body ? JSON.parse(init.body as string) : undefined, authToken: auth ? auth.replace(/^Bearer\s+/, '') : null, headers, }; recorded.push(req); return handle(req); }), ); } return { state, recorded, install, }; }