/** * Sprint 16: тонкий HTTP-клиент для regression-factories. * * Не используем @playwright/test request — он завязан на test-context; * a factory должна работать как из теста, так и из standalone-скриптов * (nightly verify, seed-rare-data). Делаем поверх `fetch` (Node 20+ ships * нативно). */ const BASE = process.env.E2E_ADMIN_URL ?? 'https://test.admin.food-market.kz' interface RequestOpts { method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' body?: unknown /** Bearer-токен, если запрос требует авторизации. */ token?: string /** Доп. headers. */ headers?: Record /** Не падать при non-2xx. По умолчанию падаем. */ allowError?: boolean /** Кастомный timeout (ms). По умолчанию 30 секунд. */ timeoutMs?: number } export class ApiError extends Error { constructor( public status: number, public bodyText: string, public url: string, public method: string, ) { super(`${method} ${url} → ${status}: ${bodyText.substring(0, 400)}`) } } /** Универсальный helper. Возвращает распарсенный JSON (или текст для не-JSON). */ export async function request(path: string, opts: RequestOpts = {}): Promise { const url = path.startsWith('http') ? path : `${BASE}${path}` const method = opts.method ?? (opts.body !== undefined ? 'POST' : 'GET') const headers: Record = { 'Accept': 'application/json', ...(opts.headers ?? {}), } let body: BodyInit | undefined if (opts.body !== undefined) { if (typeof opts.body === 'string') { body = opts.body // если уже задан Content-Type — оставляем как есть headers['Content-Type'] ??= 'application/x-www-form-urlencoded' } else { body = JSON.stringify(opts.body) headers['Content-Type'] = 'application/json' } } if (opts.token) headers['Authorization'] = `Bearer ${opts.token}` const controller = new AbortController() const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 30_000) let resp: Response try { resp = await fetch(url, { method, headers, body, signal: controller.signal }) } finally { clearTimeout(timer) } const text = await resp.text() if (!resp.ok && !opts.allowError) { throw new ApiError(resp.status, text, url, method) } // 204 / пустой body → undefined if (text.length === 0) return undefined as T try { return JSON.parse(text) as T } catch { return text as unknown as T } } /** Sleep helper для retry'ев в фабрике (rate-limit signup). */ export function sleep(ms: number): Promise { return new Promise(res => setTimeout(res, ms)) } export const baseUrl = BASE