Jitsi-звонки: план верификации и приёмки
Сквозной план проверки медиа-стека звонков/конференций/записи: как поднять локальный тестовый Jitsi, какая
пирамида тестов покрывает бэкенд messenger, какие e2e-сценарии прогоняются по use-case'ам, что проверяет
безопасность, и по каким критериям фаза считается принятой. Дом контракта — domain//api//events/;
дом реализации — этот гид; здесь — как убедиться, что оно работает. DoD фаз сводится с
implementation-plan.md.
Локальный dev-стек Jitsi
Для интеграционных и e2e-прогонов поднимается community docker-jitsi-meet (compose с
prosody/jicofo/jvb/jibri/coturn) — это самый быстрый путь к работающему стеку с JWT и записью, без
k8s. Продовое развёртывание — Helm/k8s (jitsi-deployment.md); Nomad-IaC из
thirdparty/jitsi/infra-provisioning/nomad/* — референс конфигурации, не запускается локально. Для
локалки ключи переносятся в .env docker-jitsi-meet (маппинг ниже).
Env-матрица .env (docker-jitsi-meet ← ключи гида)
Переменная (.env) | Значение для тестов | Соответствие гиду |
|---|---|---|
ENABLE_AUTH | 1 | токен-аутентификация vhost (jitsi-jwt.md) |
AUTH_TYPE | jwt | mod_auth_token (HS256) |
JWT_APP_ID | pin_messenger | iss/aud/JWT_APP_ID (совпадает с минтером) |
JWT_APP_SECRET | тестовый секрет (не прод) | общий HS256-секрет prosody ↔ messenger |
JWT_ACCEPTED_ISSUERS / JWT_ACCEPTED_AUDIENCES | pin_messenger | сверка claim-ов |
ENABLE_RECORDING | 1 | jibri-запись (recording-pipeline.md) |
JIBRI_FINALIZE_RECORDING_SCRIPT_PATH | /usr/bin/pin_finalize.sh | замена jitsi_uploader.sh |
JIBRI_RECORDING_DIR | /mnt/recordings (том) | директория захвата |
JIBRI_USAGE_TIMEOUT | 61 minutes | авто-shutdown простоя |
XMPP_MODULES | + pin_muc_presence | кастомный presence-модуль (том с .lua) |
reservations_api_prefix (vhost) | http://host.docker.internal:<port>/api/messenger/internal/jitsi | admission-вебхук к локальному messenger |
messenger в dev указывает на стек через MediaDomain/SignalingUrl (prosody /xmpp-websocket) и
JWT_APP_SECRET из того же .env; presence/reservation-вебхуки prosody шлёт на локальный messenger
(host.docker.internal), jibri-finalize стримит туда же. Для приёма вебхуков из контейнеров бэкенд слушает
адрес, доступный из docker-сети (не localhost-only).
Пирамида тестов
Unit (быстрые, без инфраструктуры)
| Под тестом | Что проверяем |
|---|---|
RecordingPolicy.ShouldAutoRecord(chatKind, sessionKind) | Deal-чат → true (Trigger=AutoPolicy, InitiatedByUserId=null, INV-REC-3); Direct вне сделки → false; матрица ChatKind × SessionKind |
JitsiTokenService (минтинг JWT) | claim-набор (jitsi-jwt.md): iss/aud=pin_messenger, room=call-{id}/conf-{id} (не *), nbf=now−30s, exp≈now+4h, context.user.moderator, context.features.{recording,screen-sharing}; HS256-подпись проверяется тем же секретом |
| HMAC-фильтр вебхуков | валидная подпись X-Pin-Signature над сырым телом + X-Pin-Timestamp в окне ≤ 5 мин → pass; подделка/просрочка/replay → reject; секрет не попадает в лог |
| Разбор имени комнаты | call-{Guid}/conf-{Guid} → (SessionKind, sessionId); мусор → отказ |
Сборка MediaJoinModel и минтинг токена тестируются как чистые функции: секрет — тестовый, ассерт на claim'ы,
не на конкретную строку токена.
// Unit-набросок: политика авто-записи
[Theory]
[InlineData(ChatKind.Deal, SessionKind.Conference, true)]
[InlineData(ChatKind.Deal, SessionKind.Direct, true)]
[InlineData(ChatKind.Direct, SessionKind.Direct, false)]
public void ShouldAutoRecord_matrix(ChatKind chat, SessionKind kind, bool expected)
=> _policy.ShouldAutoRecord(chat, kind).Should().Be(expected);
Integration (эндпоинт + БД + outbox, без реального Jitsi)
- REST-операции (
start/answer/decline/end,conferences/*,recordings/*): проверяют переход статуса, записьrealtime_outbox-строки в той же транзакции, отсутствие токенов/SDP/ICE в БД (INV-CALL-4/6, INV-CONF-7), маппинг ошибок (MSG_CALL_*/MSG_CONFERENCE_*). - Hub-push через фейковый dispatcher: подменяется
realtime-dispatch; ассерт, что дляCallInvited/ConferenceInvitedMediaJoinModel.AccessToken/TURN минтятся на выдаче, а вrealtime_outboxлежит толькоRoomName/reference-id (realtime-dispatch.md тест «Media-token at delivery»). - Вебхуки с валидной тестовой подписью:
POST /internal/jitsi/reservations→ allow (Provisioning→Active) / deny (fail-closed) по имени и статусу;DELETE …/{roomName}→ ростерLeft, финал через reaper (jitsi-reservation.md);POST /internal/jitsi/events→ ростерJoined/Left,RecordingStatusChanged(on)→Pending→Recording, идемпотентность по(RoomName,EventType,OccupantJid,OccurredAt)(jitsi-events.md);POST /internal/call-recordings/{sessionId}/ingest(multipart, фейковый file storage) →Uploading→Available- outbox
call-recording-available;Outcome=Failed→Failed(jibri-recording-ingest.md).
- outbox
- Фоновые воркеры:
call-timeout-reaper(Ringing→Missed),conference-room-reaper(EmptyTimeout),recording-finalize-retry— идемпотентность и guard-условия.
Contract (совместимость границ)
- Payload вебхуков от Jitsi:
JitsiReservationRequest/JitsiMucEvent/JibriFinalizeMeta— схема, обязательные поля, enum'ы (JitsiMucEventType,JibriOutcome); неизвестныйEventType→VALIDATION_FAILED(additive-политика). - Kafka-события:
pin.messenger.conference-ended.v1(ключDealId) иpin.messenger.call-recording-available.v1(ключSessionId) — контракт из events/, без URL/секретов в payload; consumer-совместимостьcrm/notifications. .lua-модуль ↔ вебхук: форма запросаmod_pin_muc_presence/mod_reservationsфиксируется контракт-тестом, чтобы rollout модуля не разошёлся с бэкендом.
E2E (реальный стек)
Прогон против поднятого docker-jitsi-meet с headless-клиентом на lib-jitsi-meet (или двумя) — покрывает
медиа-плоскость и presence, которые фейками не воспроизвести.
E2E-сценарии по use-case'ам
Каждый: предусловия → шаги → ожидаемое → проверки наблюдаемости.
UC-MSG-005 — 1:1-звонок (UC-MSG-005)
- Предусловия: два пользователя —
Accepted-контакт вне блокировки;CALLS_MEDIA_ENABLED=1; оба на hub. - Шаги:
POST /calls {calleeUserId, mediaType}→Ringing→CallInviteвuser:{Callee}→answer→Answered→ обе стороны входят в комнатуcall-{id}(медиа клиент ↔ SFU) →media-tokenrefresh доexp→end. - Ожидаемое:
Ringing→Answered→Ended,DurationSecondsпосчитан; медиа течёт клиент ↔ JVB (p2p для 1:1); Kafka-событий нет. - Проверки: строка
call_sessionsбез SDP/ICE/AccessToken;CallInviteсодержитRoom, токен минтился при доставке;media-tokenвернул свежийMediaJoinModelдля той же комнаты; лог без токенов.
UC-MSG-006 — конференция по сделке (UC-MSG-006)
- Предусловия: Deal-чат (
Kind=Deal, неClosed); инициатор — активный участник;CONFERENCE_CALLS_ENABLED=1; нет активной конференции на чат. - Шаги:
POST /chats/{chatId}/conferences→Provisioning+ConferenceInviteвchat:{ChatId}→ reservation-вебхукProvisioning→Active→join ×N→ демонстрация экрана (ScreenShareState=Sharing) →leaveчасти участников → завершение модераторомend(или авто-reaperпо опустошению). - Ожидаемое:
Active→Ended(ModeratorEnded/EmptyTimeout); ровно одноconference-ended.v1(ключDealId) сPeakParticipantCount; ростер отражает фактический состав. - Проверки: ростер сверяется presence-вебхуком — presence «вышел» без REST-
leaveдаёт строкуLeft(INV-CPART-5);conf_sessions/call_participantsбез токенов; outbox содержит ровно одно событие (повтор reaper не дублирует).
UC-MSG-007 — авто-запись звонка по сделке (UC-MSG-007)
- Предусловия: идёт Deal-сессия под
RecordingPolicy;RECORDINGS_ENABLED=1; jibri и file storage доступны; категорияCALL_RECORDINGS+ видео content-types заведены (блокирующие предпосылки, recording-pipeline.md). - Шаги:
ShouldAutoRecord=true→ записьPending+ индикатор (NoticeState=Announced) до захвата → jibri пишет →RecordingStatusChanged(on)Pending→Recording→ завершение сессии →pin_finalize.shстримит multipart в ingest → выгрузка в file storage →Available→call-recording-available.v1→ участник качаетGET /recordings/{id}/download. - Ожидаемое:
AvailableсFileId+SizeBytes+DurationSeconds+Format(иммутабельны); ровно одноcall-recording-available.v1(ключSessionId) без URL; скачивание — короткоживущий пресайн, аудитReadSensitive. - Проверки: индикатор ушёл до входа в
Recording(INV-REC-4 —RecordingприNoticeState=Pending= нарушение);call_recordingsхранитFileId+метаданные, не бинарь (INV-REC-6); событие/лог без URL/секретов;downloadдо готовности →409 MSG_RECORDING_NOT_READY.
Тесты безопасности
| Проверка | Сценарий | Ожидаемое |
|---|---|---|
| Room-scoped JWT | токен, минченный для conf-A, предъявляется в conf-B | prosody mod_token_verification отклоняет вход (двойной гейт с admission) |
| Fail-closed: подпись | вебхук (reservations/events/ingest) с битым/просроченным X-Pin-Signature | 401 MSG_JITSI_SIGNATURE_INVALID, no-retry, сессия не тронута |
| Fail-closed: комната | вебхук с RoomName, не совпадающим со MediaRoom.RoomName живой сессии | 403 MSG_JITSI_ROOM_UNKNOWN (самоподнятая комната отклонена) |
| Replay | повтор валидного вебхука вне окна ≤ 5 мин | reject; в окне и уже-виденный — идемпотентный 200 no-op, состояние не откатывается |
| Нет секретов в проекции | realtime_outbox-строка инвайта | только RoomName/reference-id; AccessToken/TURN отсутствуют |
| Нет токенов/PII в хранении | call_sessions/conf_sessions/call_participants/call_recordings | нет SDP/ICE/AccessToken/URL; запись — только FileId+метаданные |
| Нет секретов в логах/событиях | логи операций и вебхуков, Kafka-payload | id (CallId/RoomName/SessionId) есть, токен/креды/URL/бинарь — нет |
Отдельно — негативные гейты доступа из UC: start не-контакту → 409 NOT_CONTACT; конференция вне Deal-чата →
403 CHAT_ACCESS_DENIED; stop-recording/end не модератором → 403 FORBIDDEN; guest → 401.
Acceptance-чеклист по фазам
Свести с DoD из implementation-plan.md; каждый пункт — с прогнанным тестом.
| Фаза | Готово, когда |
|---|---|
| Ф1. 1:1-сигналинг | AC1–AC6 UC-MSG-005: цикл статусов, Missed reaper-ом, MediaJoinModel раздаётся, SDP/токены не в БД, один активный звонок на пару, Kafka пуст |
| Ф2. Конференции | AC1–AC7 UC-MSG-006: гейт членства, одна активная на чат, presence-авторитет ростера, передача роли модератора, авто-завершение reaper, conference-ended.v1 (ключ DealId) |
| Ф3. Запись | AC1–AC7 UC-MSG-007: авто-политика, индикатор до Recording, ingest fail-closed, Available с иммутабельными метаданными, одно call-recording-available.v1 без URL, пресайн + аудит, Failed+retry не блокирует сессию |
| Ф0. Предпосылки | file storage: категория CALL_RECORDINGS + видео content-types (mp4/webm/mkv); секреты JWT_APP_SECRET/static-auth-secret в vault; mod_pin_muc_presence.lua + pin_finalize.sh в образах |
| Кросс-фазные | security-набор (room-scoped JWT, fail-closed вебхуки, отсутствие секретов) зелёный; наблюдаемость (метрики/трейсы/алерты) на месте |
Feature-флаги проверяются как гейты: CALLS_MEDIA_ENABLED=false → 503 MSG_CALLS_DISABLED;
CONFERENCE_CALLS_ENABLED=false → 503 MSG_CONFERENCES_DISABLED; RECORDINGS_ENABLED=false → сессия идёт без
записи.
Наблюдаемость (проверяется в приёмке)
| Тип | Что подтверждаем |
|---|---|
| Метрики | join-латентность p95 < 3 с; success записи ≥ 99% (messenger_recording_ingest_total{result}); reaper-backlog ≈ 0; presence messenger_jitsi_event_total{type,result}, admission messenger_jitsi_reservation_total{result,reason}; persist→push messenger_realtime_lag_seconds p95 < 1 с, messenger_realtime_pending ≈ 0 |
| Трейсы | сквозной traceparent проходит цепочку REST → outbox → hub и сквозь вебхуки Jitsi → ingest → file storage → Kafka; корреляция звонка/конференции по id |
| Логи | структурные события (reservation allow/deny, presence event, ingest received/uploaded/available) — с id, без токенов/TURN-кред/SDP/URL/бинаря/PII |
| Алерты | всплеск deny=room-unknown (рассинхрон имён/попытка самоподнятия); нет свободного jibri (MSG_RECORDING_PROVISION_FAILED); quarantined_total > 0 в realtime-dispatch; рост pending/lag |
Обратные ссылки
Автоссылки: где используется этот документ.
- calls (2): Медиа-звонки и запись на Jitsi — интеграционный гид (messenger), Медиа-звонки на Jitsi: план поставки (playbook аналитик → разработчик)
Связанные документы
- План/DoD: implementation-plan.md; гид: README.md; развёртывание/локалка: jitsi-deployment.md; изменения стека: jitsi-modifications.md.
- Use-cases (AC-источники): UC-MSG-005, UC-MSG-006, UC-MSG-007.
- API под тестом: calls, conferences, recordings, вебхуки internal; realtime: messenger-hub.md, realtime-dispatch.md.
- JWT/комнаты/запись: jitsi-jwt.md, jitsi-rooms.md, recording-pipeline.md.
- Решение: ADR-0062, ADR-0056 simplicity-first.