Files
rudder-js-sdk/test/helpers/fakeGateway.ts
T

186 lines
5.9 KiB
TypeScript

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<string, StorageItem>;
projectStorage: Map<string, ProjectStorageItem>;
currencies: CatalogCurrency[];
resources: Map<string, Map<string, ResourceRow>>;
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<FakeGatewayState>): 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<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);
}
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<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,
};
recorded.push(req);
return handle(req);
}),
);
}
return {
state,
recorded,
install,
};
}