Перейти к основному содержимому

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_AUTH1токен-аутентификация vhost (jitsi-jwt.md)
AUTH_TYPEjwtmod_auth_token (HS256)
JWT_APP_IDpin_messengeriss/aud/JWT_APP_ID (совпадает с минтером)
JWT_APP_SECRETтестовый секрет (не прод)общий HS256-секрет prosody ↔ messenger
JWT_ACCEPTED_ISSUERS / JWT_ACCEPTED_AUDIENCESpin_messengerсверка claim-ов
ENABLE_RECORDING1jibri-запись (recording-pipeline.md)
JIBRI_FINALIZE_RECORDING_SCRIPT_PATH/usr/bin/pin_finalize.shзамена jitsi_uploader.sh
JIBRI_RECORDING_DIR/mnt/recordings (том)директория захвата
JIBRI_USAGE_TIMEOUT61 minutesавто-shutdown простоя
XMPP_MODULES+ pin_muc_presenceкастомный presence-модуль (том с .lua)
reservations_api_prefix (vhost)http://host.docker.internal:<port>/api/messenger/internal/jitsiadmission-вебхук к локальному 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/ ConferenceInvited MediaJoinModel.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
  • Фоновые воркеры: call-timeout-reaper (Ringing→Missed), conference-room-reaper (EmptyTimeout), recording-finalize-retry — идемпотентность и guard-условия.

Contract (совместимость границ)

  • Payload вебхуков от Jitsi: JitsiReservationRequest/JitsiMucEvent/JibriFinalizeMeta — схема, обязательные поля, enum'ы (JitsiMucEventType, JibriOutcome); неизвестный EventTypeVALIDATION_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}RingingCallInvite в user:{Callee}answerAnswered → обе стороны входят в комнату call-{id} (медиа клиент ↔ SFU) → media-token refresh до expend.
  • Ожидаемое: 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}/conferencesProvisioning + ConferenceInvite в chat:{ChatId} → reservation-вебхук Provisioning→Activejoin ×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 → Availablecall-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-Bprosody mod_token_verification отклоняет вход (двойной гейт с admission)
Fail-closed: подписьвебхук (reservations/events/ingest) с битым/просроченным X-Pin-Signature401 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-payloadid (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=false503 MSG_CALLS_DISABLED; CONFERENCE_CALLS_ENABLED=false503 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

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы