import { apiError, json, type FetchRoute } from './helpers'; import { fakeVoiceToken } from './voiceRoutes'; import type { DmCall, DmCallMedia, DmCallParticipantState } from '@/api/calls'; /** * Маршруты звонков в беседах для тестов: состояние живёт в замыкании, поэтому * ответы согласованы между запросами — как у настоящего сервера (AGENT.md 7.8). */ export function makeDmCall(overrides: Partial & Pick): DmCall { return { initiator_id: 'user-1', status: 'ringing', media: 'audio', created_at: '2026-09-26T10:00:00Z', duration_seconds: 0, participants: [], ...overrides, }; } /** Участник звонка в моке. */ export function makeCallParticipant( user_id: string, state: DmCallParticipantState, overrides: Partial = {}, ): DmCall['participants'][number] { return { user_id, state, self_mute: false, self_deaf: false, camera: false, screen: false, ...overrides, }; } export interface DmCallRoutes { routes: FetchRoute[]; /** Текущий звонок «сервера»: тесты читают его после запросов. */ call: () => DmCall | null; /** Что вернул сервер на последний запрос; для проверки тела PATCH. */ setCall: (call: DmCall | null) => void; } export interface DmCallRoutesOptions { channelId?: string; /** Стартовый звонок: null — никто не звонит. */ call?: DmCall | null; /** История звонков беседы. */ history?: DmCall[]; /** Код ошибки на создание звонка (`voice.disabled`, `dm.call_limit`, ...). */ startError?: { code: string; status: number }; token?: string; url?: string; } export function dmCallRoutes(options: DmCallRoutesOptions = {}): DmCallRoutes { const channelId = options.channelId ?? 'c-dm'; let call: DmCall | null = options.call ?? null; const history = options.history ?? []; const join = (): Response => { if (call === null) { return apiError('dm.call_not_found', 404); } return json({ call, token: options.token ?? fakeVoiceToken(), url: options.url ?? 'wss://gl.localhost/rtc', room: `dm_call_${call.id}`, }); }; const routes: FetchRoute[] = [ { match: `/channels/${channelId}/call/accept`, method: 'POST', response: () => { if (call !== null) { call = { ...call, status: 'active', started_at: '2026-09-26T10:00:05Z', participants: call.participants.map((participant) => participant.state === 'invited' ? { ...participant, state: 'joined' } : participant, ), }; } return join(); }, }, { match: `/channels/${channelId}/call/decline`, method: 'POST', response: () => { if (call !== null) { call = { ...call, status: 'ended', end_reason: 'declined', ended_at: '2026-09-26T10:00:06Z', participants: call.participants.map((participant) => participant.state === 'invited' ? { ...participant, state: 'declined' } : participant, ), }; } return json({ call }); }, }, { match: `/channels/${channelId}/call/end`, method: 'POST', response: () => { if (call !== null) { call = { ...call, status: 'ended', end_reason: 'completed', ended_at: '2026-09-26T10:01:00Z', participants: call.participants.map((participant) => participant.state === 'joined' ? { ...participant, state: 'left' } : participant, ), }; } return json({ call }); }, }, { match: `/channels/${channelId}/call/@me`, method: 'PATCH', response: ({ body }) => { const patch = (body ?? {}) as Record; if (call !== null) { call = { ...call, participants: call.participants.map((participant) => participant.user_id === 'user-1' ? { ...participant, self_mute: patch['self_mute'] ?? participant.self_mute, self_deaf: patch['self_deaf'] ?? participant.self_deaf, camera: patch['camera'] ?? participant.camera, screen: patch['screen'] ?? participant.screen, } : participant, ), }; } return json({ call }); }, }, { match: `/channels/${channelId}/calls`, method: 'GET', response: () => json({ calls: history }), }, { match: `/channels/${channelId}/call`, method: 'GET', response: () => json({ call }), }, { match: `/channels/${channelId}/call`, method: 'POST', response: ({ body }) => { if (options.startError !== undefined) { return apiError(options.startError.code, options.startError.status); } const media = ((body ?? {}) as { media?: DmCallMedia }).media ?? 'audio'; call = makeDmCall({ id: 'call-1', channel_id: channelId, media, participants: [ makeCallParticipant('user-1', 'joined'), makeCallParticipant('user-2', 'invited'), ], }); return join(); }, }, ]; return { routes, call: () => call, setCall: (next) => { call = next; }, }; }