127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
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<string, StorageItem>;
|
|
currencies: CatalogCurrency[];
|
|
/** Response per `module/fn`; falls back to `{ result: {} }`. */
|
|
moduleCalls: Map<string, 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<FakeGatewayState>): 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<Response> {
|
|
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<Response> => {
|
|
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,
|
|
};
|
|
}
|