import { apiError, json, type FetchRoute } from './helpers'; import type { GuildSound } from '@/api/sounds'; /** * Маршруты ручек звуков для тестов: список, загрузка, переименование, * удаление и просьба сыграть. Состояние живёт в замыкании, поэтому ответы * согласованы между запросами (как у настоящего сервера). */ export interface SoundRoutesOptions { guildId?: string; /** Набор звуков «сервера»: его же проверяют тесты. */ sounds?: GuildSound[]; /** Ошибка на `POST .../play` (лимит 429, `voice.not_connected` и т.п.). */ playError?: { code: string; status: number; retryAfterMs?: number }; /** Ошибка на загрузку звука (`sound.name_taken`, `sounds.limit_reached`). */ uploadError?: { code: string; status: number }; } export interface SoundRoutes { routes: FetchRoute[]; sounds: GuildSound[]; /** Сколько раз просили сыграть звук. */ playCount: () => number; } export function soundRoutes(options: SoundRoutesOptions = {}): SoundRoutes { const guildId = options.guildId ?? 'g-1'; const sounds = [...(options.sounds ?? [])]; let plays = 0; const list = (kind: string | null): GuildSound[] => kind === null ? sounds : sounds.filter((sound) => sound.kind === kind); const routes: FetchRoute[] = [ { match: `/guilds/${guildId}/sounds/`, method: 'POST', response: ({ url }) => { plays += 1; if (options.playError !== undefined) { const { code, status, retryAfterMs } = options.playError; return json( { error: { code, message: 'error', ...(retryAfterMs === undefined ? {} : { retry_after_ms: retryAfterMs }), }, }, status, ); } // `/guilds/{id}/sounds/{sound_id}/play` const soundId = url.split('/sounds/')[1]?.split('/')[0] ?? ''; const known = sounds.find((sound) => sound.id === soundId); return known === undefined ? apiError('sounds.not_found', 404) : json({ ok: true }); }, }, { match: `/guilds/${guildId}/sounds`, method: 'POST', response: ({ url }) => { if (options.uploadError !== undefined) { return apiError(options.uploadError.code, options.uploadError.status); } const kind = url.includes('kind=ui') ? 'ui' : 'soundboard'; const created: GuildSound = { id: `s-${sounds.length + 1}`, name: 'uploaded', file_id: '950', kind, ...(kind === 'ui' ? { event: 'member_join' as const } : {}), }; sounds.push(created); return json({ sound: created }); }, }, { match: `/guilds/${guildId}/sounds/`, method: 'PATCH', response: ({ url, body }) => { const soundId = url.split('/sounds/')[1]?.split(/[/?]/u)[0] ?? ''; const patch = (body ?? {}) as Record; const index = sounds.findIndex((sound) => sound.id === soundId); const current = sounds[index]; if (current === undefined) { return apiError('sounds.not_found', 404); } const next: GuildSound = { ...current, ...(typeof patch['name'] === 'string' ? { name: patch['name'] } : {}), }; sounds[index] = next; return json({ sound: next }); }, }, { match: `/guilds/${guildId}/sounds/`, method: 'DELETE', response: ({ url }) => { const soundId = url.split('/sounds/')[1]?.split(/[/?]/u)[0] ?? ''; const index = sounds.findIndex((sound) => sound.id === soundId); if (index >= 0) { sounds.splice(index, 1); } return json({ ok: true }); }, }, { match: `/guilds/${guildId}/sounds`, response: ({ url }) => { const kind = url.includes('kind=ui') ? 'ui' : url.includes('kind=soundboard') ? 'soundboard' : null; return json({ sounds: list(kind) }); }, }, ]; return { routes, sounds, playCount: () => plays }; }