import { vi } from 'vitest'; import type { StorageItem } from '../../src/generated/storage.js'; import type { ResourceRow } from '../../src/generated/resources.js'; import type { CallModuleFunctionClientResponse } from '../../src/generated/modules.js'; import type { CatalogCurrency } from '../../src/generated/catalog.js'; import type { ProjectStorageItem } from '../../src/generated/project-storage.js'; export interface RecordedRequest { method: string; path: string; query: URLSearchParams; body: unknown; authToken: string | null; } export interface FakeGatewayState { storage: Map; projectStorage: Map; currencies: CatalogCurrency[]; resources: Map>; moduleCalls: CallModuleFunctionClientResponse; } 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(), projectStorage: seed?.projectStorage ?? new Map(), currencies: seed?.currencies ?? [], resources: seed?.resources ?? new Map(), moduleCalls: seed?.moduleCalls ?? { result: {} }, }; 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); } const resourceItemMatch = path.match(/^\/sdk\/v1\/resources\/([^/]+)\/([^/]+)$/); const resourceListMatch = path.match(/^\/sdk\/v1\/resources\/([^/]+)$/); if (resourceItemMatch) { const slug = decodeURIComponent(resourceItemMatch[1]); const id = decodeURIComponent(resourceItemMatch[2]); const rows = state.resources.get(slug) ?? new Map(); if (method === 'GET') { const row = rows.get(id); return row ? json(row) : json({ error: 'not found' }, 404); } if (method === 'PATCH') { const existing = rows.get(id); if (!existing) return json({ error: 'not found' }, 404); const next = { ...existing, ...(body as ResourceRow), id }; rows.set(id, next); state.resources.set(slug, rows); return json(next); } if (method === 'DELETE') { rows.delete(id); state.resources.set(slug, rows); return noContent(); } } if (resourceListMatch) { const slug = decodeURIComponent(resourceListMatch[1]); const rows = state.resources.get(slug) ?? new Map(); if (method === 'GET') { return json({ items: [...rows.values()], total: rows.size }); } if (method === 'POST') { const created: ResourceRow = { id: (body as ResourceRow).id ?? `row-${rows.size + 1}`, data: (body as ResourceRow).data, version: 1, }; rows.set(created.id!, created); state.resources.set(slug, rows); return json(created); } } 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.id) state.storage.set(item.id, item); } return noContent(); } if (method === 'DELETE') { const type = query.get('type'); for (const [id, item] of state.storage) { if (!type || item.type === type) state.storage.delete(id); } return noContent(); } } if (path === '/sdk/v1/project-storage') { if (method === 'GET') { return json({ items: [...state.projectStorage.values()], nextCursor: '' }); } if (method === 'PUT') { const items = (body as { items?: ProjectStorageItem[] }).items ?? []; for (const item of items) { if (item.id) state.projectStorage.set(item.id, item); } 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, }; recorded.push(req); return handle(req); }), ); } return { state, recorded, install, }; }