import type { Page, Route } from '@playwright/test'; import { createModerationSeed, type ModerationState } from './moderationSeed'; export type MockAdmin = { id: string; email: string; nickname: string; role: string; language: string; avatar_url: string | null; }; export const MOCK_ADMIN: MockAdmin = { id: 'admin-e2e-1', email: 'e2e@eventhub.local', nickname: 'E2E Admin', role: 'superadmin', language: 'ru', avatar_url: null, }; async function json(route: Route, status: number, body: unknown, headers: Record = {}) { await route.fulfill({ status, contentType: 'application/json', headers, body: JSON.stringify(body), }); } function listSlice(items: T[], url: URL): { rows: T[]; total: number } { const limit = Number(url.searchParams.get('limit') || 50); const offset = Number(url.searchParams.get('offset') || 0); return { rows: items.slice(offset, offset + limit), total: items.length }; } type MockOptions = { user?: MockAdmin; moderation?: boolean; }; /** Default admin API mocks for Control Center shell / login / moderation. */ export async function installAdminApiMocks(page: Page, options: MockOptions = {}) { let currentUser: MockAdmin = { ...MOCK_ADMIN, ...options.user }; const moderation: ModerationState | null = options.moderation === false ? null : createModerationSeed(); await page.route('**/v1/admin/**', async (route) => { const req = route.request(); const method = req.method(); const url = new URL(req.url()); const path = url.pathname; if (method === 'POST' && path.endsWith('/v1/admin/login')) { const payload = req.postDataJSON() as { email?: string; password?: string }; if (payload?.email === MOCK_ADMIN.email && payload?.password === 'e2e-pass') { await json(route, 200, { token: 'e2e-access-token', refresh_token: 'e2e-refresh-token', user: currentUser, }); return; } await json(route, 401, { error: 'invalid_credentials' }); return; } if (method === 'GET' && path.endsWith('/v1/admin/me')) { await json(route, 200, currentUser); return; } if (method === 'PUT' && path.endsWith('/v1/admin/me')) { const payload = (req.postDataJSON() || {}) as Partial; currentUser = { ...currentUser, ...payload }; await json(route, 200, currentUser); return; } if (method === 'GET' && path.endsWith('/v1/admin/health')) { await json(route, 200, { status: 'ok', service: 'eventhub', version: '0.1', git_sha: 'e2etestsha000', built_at: '2026-07-14T00:00:00Z', }); return; } if (method === 'GET' && path.endsWith('/v1/admin/stats')) { await json(route, 200, { users_total: 10, events_total: 5, reviews_total: moderation?.reviews.length ?? 0, calendars_total: 3, tickets_total: moderation?.tickets.length ?? 0, tickets_open: moderation?.tickets.filter((t) => t.status === 'open').length ?? 0, reports_total: moderation?.reports.length ?? 0, reports_pending: moderation?.reports.filter((r) => r.status === 'pending').length ?? 0, reports_reviewed: moderation?.reports.filter((r) => r.status === 'reviewed').length ?? 0, avg_report_resolution_h: 1.5, avg_ticket_resolution_h: 2, events_by_day: [], registrations_by_day: [], reviews_by_day: [], reports_by_day: [], tickets_by_day: [], subscriptions_by_day: [], }); return; } if (method === 'GET' && path.includes('/v1/admin/nodes/metrics')) { await json(route, 200, { nodes: [] }); return; } if (moderation) { // ---- reports ---- if (method === 'GET' && path.endsWith('/v1/admin/reports/stats')) { await json(route, 200, { total_reports: moderation.reports.length, reports_by_target_type: {}, reports_by_status: { pending: moderation.reports.filter((r) => r.status === 'pending').length, reviewed: moderation.reports.filter((r) => r.status === 'reviewed').length, dismissed: moderation.reports.filter((r) => r.status === 'dismissed').length, }, top_targets_by_reports: [], }); return; } if (method === 'GET' && path.endsWith('/v1/admin/reports')) { let rows = [...moderation.reports]; const status = url.searchParams.get('status'); if (status) rows = rows.filter((r) => r.status === status); const pageRows = listSlice(rows, url); await json(route, 200, pageRows.rows, { 'X-Total-Count': String(pageRows.total) }); return; } const reportMatch = path.match(/\/v1\/admin\/reports\/([^/]+)$/); if (reportMatch) { const id = decodeURIComponent(reportMatch[1]); const idx = moderation.reports.findIndex((r) => r.id === id); if (idx < 0) { await json(route, 404, { error: 'not_found' }); return; } if (method === 'GET') { await json(route, 200, moderation.reports[idx]); return; } if (method === 'PUT') { const payload = (req.postDataJSON() || {}) as { status?: MockReportStatus }; moderation.reports[idx] = { ...moderation.reports[idx], ...payload, resolved_at: payload.status ? '2026-07-14T13:00:00Z' : moderation.reports[idx].resolved_at, resolved_by: payload.status ? currentUser.id : moderation.reports[idx].resolved_by, }; await json(route, 200, moderation.reports[idx]); return; } } // ---- tickets ---- if (method === 'GET' && path.endsWith('/v1/admin/tickets/stats')) { await json(route, 200, { total_tickets: moderation.tickets.length, open: moderation.tickets.filter((t) => t.status === 'open').length, in_progress: moderation.tickets.filter((t) => t.status === 'in_progress').length, resolved: moderation.tickets.filter((t) => t.status === 'resolved').length, closed: moderation.tickets.filter((t) => t.status === 'closed').length, total_errors: moderation.tickets.reduce((s, t) => s + t.count, 0), }); return; } if (method === 'GET' && path.endsWith('/v1/admin/tickets')) { const pageRows = listSlice(moderation.tickets, url); await json(route, 200, pageRows.rows, { 'X-Total-Count': String(pageRows.total) }); return; } const ticketMatch = path.match(/\/v1\/admin\/tickets\/([^/]+)$/); if (ticketMatch) { const id = decodeURIComponent(ticketMatch[1]); const idx = moderation.tickets.findIndex((t) => t.id === id); if (idx < 0) { await json(route, 404, { error: 'not_found' }); return; } if (method === 'GET') { await json(route, 200, moderation.tickets[idx]); return; } if (method === 'PUT') { const payload = (req.postDataJSON() || {}) as Partial<(typeof moderation.tickets)[0]>; moderation.tickets[idx] = { ...moderation.tickets[idx], ...payload }; await json(route, 200, moderation.tickets[idx]); return; } } // ---- reviews ---- if (method === 'GET' && path.endsWith('/v1/admin/reviews/stats')) { await json(route, 200, { total_reviews: moderation.reviews.length, reviews_by_target_type: {}, reviews_by_status: { visible: moderation.reviews.filter((r) => r.status === 'visible').length, hidden: moderation.reviews.filter((r) => r.status === 'hidden').length, deleted: moderation.reviews.filter((r) => r.status === 'deleted').length, }, top_targets_by_reviews: [], top_targets_by_positive_reviews: [], top_targets_by_negative_reviews: [], }); return; } if (method === 'GET' && path.endsWith('/v1/admin/reviews')) { const pageRows = listSlice(moderation.reviews, url); await json(route, 200, pageRows.rows, { 'X-Total-Count': String(pageRows.total) }); return; } if (method === 'PATCH' && path.endsWith('/v1/admin/reviews')) { const updates = (req.postDataJSON() || []) as Array<{ id: string; status: string; reason?: string }>; for (const u of updates) { const idx = moderation.reviews.findIndex((r) => r.id === u.id); if (idx >= 0) { moderation.reviews[idx] = { ...moderation.reviews[idx], status: u.status as (typeof moderation.reviews)[0]['status'], reason: u.reason ?? moderation.reviews[idx].reason, updated_at: '2026-07-14T13:00:00Z', }; } } await json(route, 200, updates.length); return; } const reviewMatch = path.match(/\/v1\/admin\/reviews\/([^/]+)$/); if (reviewMatch) { const id = decodeURIComponent(reviewMatch[1]); const idx = moderation.reviews.findIndex((r) => r.id === id); if (idx < 0) { await json(route, 404, { error: 'not_found' }); return; } if (method === 'GET') { await json(route, 200, moderation.reviews[idx]); return; } if (method === 'PUT') { const payload = (req.postDataJSON() || {}) as Partial<(typeof moderation.reviews)[0]>; moderation.reviews[idx] = { ...moderation.reviews[idx], ...payload, updated_at: '2026-07-14T13:00:00Z', }; await json(route, 200, moderation.reviews[idx]); return; } } // soft stubs for linked entities in inbox if (method === 'GET' && /\/v1\/admin\/users\/[^/]+$/.test(path)) { await json(route, 200, { id: 'user-1', email: 'user1@eventhub.local', nickname: 'User One', role: 'user', status: 'active', }); return; } if (method === 'GET' && /\/v1\/admin\/events\/[^/]+$/.test(path)) { await json(route, 200, { id: 'evt-1', title: 'E2E Event', status: 'active', }); return; } } if (method === 'GET') { await json(route, 200, [], { 'X-Total-Count': '0' }); return; } await json(route, 200, { ok: true }); }); } type MockReportStatus = 'pending' | 'reviewed' | 'dismissed';