/** * Tiny typed wrapper around the EventSnap REST API for use inside tests. * Used to seed data far faster than driving the UI through every join / * upload, and to set up adversarial states (banned users, locked PINs) that * the UI cannot reach. * * Auth: pass `token` on individual calls; no global state. */ export const ADMIN_PASSWORD = 'admin-test-pw'; export class ApiClient { constructor(private baseUrl: string = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') {} private async request( method: string, path: string, opts: { token?: string; body?: unknown; expectedStatus?: number | number[] } = {} ): Promise<{ status: number; body: T }> { const headers: Record = {}; if (opts.token) headers['Authorization'] = `Bearer ${opts.token}`; if (opts.body !== undefined) headers['Content-Type'] = 'application/json'; const res = await fetch(`${this.baseUrl}/api/v1${path}`, { method, headers, body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, }); const expected = opts.expectedStatus ?? [200, 201, 204]; const allowed = Array.isArray(expected) ? expected : [expected]; let body: unknown = undefined; if (res.status !== 204) { const text = await res.text(); try { body = text.length > 0 ? JSON.parse(text) : undefined; } catch { body = text; } } if (!allowed.includes(res.status)) { throw new Error( `API ${method} ${path} → ${res.status} (expected ${allowed.join('/')}). Body: ${JSON.stringify(body)}` ); } return { status: res.status, body: body as T }; } // ── Auth ─────────────────────────────────────────────────────────────── async join(displayName: string): Promise<{ jwt: string; pin: string; user_id: string; is_new: boolean }> { const { body } = await this.request('POST', '/join', { body: { display_name: displayName }, expectedStatus: [201], }); return body; } async recover(displayName: string, pin: string, opts: { expectedStatus?: number | number[] } = {}) { return this.request('POST', '/recover', { body: { display_name: displayName, pin }, expectedStatus: opts.expectedStatus ?? [200], }); } async adminLogin(password: string = ADMIN_PASSWORD): Promise { const { body } = await this.request<{ jwt: string }>('POST', '/admin/login', { body: { password }, }); return body.jwt; } async logout(token: string) { return this.request('DELETE', '/session', { token, expectedStatus: [204] }); } // ── Test-mode helpers ────────────────────────────────────────────────── async truncate(adminToken: string) { return this.request('POST', '/admin/__truncate', { token: adminToken, expectedStatus: [204], }); } // ── Config ───────────────────────────────────────────────────────────── async patchConfig(adminToken: string, patch: Record) { return this.request('PATCH', '/admin/config', { token: adminToken, body: patch, expectedStatus: [204], }); } async getConfig(adminToken: string): Promise> { const { body } = await this.request>('GET', '/admin/config', { token: adminToken }); return body; } // ── Host moderation ──────────────────────────────────────────────────── async listUsers(token: string) { const { body } = await this.request('GET', '/host/users', { token }); return body; } async setRole(token: string, userId: string, role: 'guest' | 'host') { return this.request('PATCH', `/host/users/${userId}/role`, { token, body: { role }, expectedStatus: [200, 204], }); } async banUser(token: string, userId: string, hideUploads = false) { return this.request('POST', `/host/users/${userId}/ban`, { token, body: { hide_uploads: hideUploads }, expectedStatus: [200, 204], }); } async closeEvent(token: string) { return this.request('POST', '/host/event/close', { token, expectedStatus: [200, 204] }); } async openEvent(token: string) { return this.request('POST', '/host/event/open', { token, expectedStatus: [200, 204] }); } // ── Feed ─────────────────────────────────────────────────────────────── async getFeed(token: string) { const { body } = await this.request('GET', '/feed', { token }); return body; } async getStats(adminToken: string) { const { body } = await this.request('GET', '/admin/stats', { token: adminToken }); return body; } // ── Health ───────────────────────────────────────────────────────────── async waitForHealth(retries = 60): Promise { for (let i = 0; i < retries; i++) { try { const res = await fetch(`${this.baseUrl}/health`); if (res.ok) return; } catch { /* keep retrying */ } await new Promise((r) => setTimeout(r, 1000)); } throw new Error(`Backend never became healthy at ${this.baseUrl}/health`); } }