mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-03 06:08:29 +00:00
✨(visio) use compatible with summary v2
This commit introduces the compatibility with summary v2. It tecnically doesn't break the compatibility with v1 as v1 params are still sent. But we advise people using the transcribe feature in their own deployments to adapt to the new v2 API, as this compatibility will be removed in a future major version. * RecordingStatusChoices now has EXTERNAL_PROCESS_SUCCESSFUL & EXTERNAL_PROCESS_FAILED Values, which are changed by a new webhook that can be called by the transcribe service. This webhook is protected by its own bearer token. * Title for the document is computed in visio, * Tests are added / updated accordingly
This commit is contained in:
committed by
aleb_the_flash
parent
ecf8f0fe3f
commit
7fa172d100
@@ -243,3 +243,168 @@ def test_notify_user_by_email_smtp_exception(mocked_current_site, caplog):
|
||||
assert result is False
|
||||
assert mock_send_mail.call_count == 2
|
||||
assert "notification could not be sent:" in caplog.text
|
||||
|
||||
|
||||
@mock.patch("core.recording.event.notification.requests.post")
|
||||
@mock.patch("core.recording.event.notification.generate_download_s3_url")
|
||||
@mock.patch.object(
|
||||
NotificationService, "_get_recording_timestamps", new_callable=mock.AsyncMock
|
||||
)
|
||||
def test_notify_summary_service_post_args_with_metadata(
|
||||
mock_get_recording_timestamps,
|
||||
mock_generate_download_s3_url,
|
||||
mock_post,
|
||||
settings,
|
||||
):
|
||||
"""Test summary notification computed request args when metadata is enabled."""
|
||||
settings.SUMMARY_SERVICE_VERSION = 2
|
||||
settings.SUMMARY_SERVICE_ENDPOINT = "https://summary.test/api/v2/tasks"
|
||||
settings.SUMMARY_SERVICE_API_TOKEN = "summary-token"
|
||||
settings.RECORDING_DOWNLOAD_BASE_URL = "https://app.test/recordings"
|
||||
settings.SCREEN_RECORDING_BASE_URL = None
|
||||
settings.METADATA_COLLECTOR_ENABLED = True
|
||||
settings.METADATA_COLLECTOR_OUTPUT_FOLDER = "recordings-metadata"
|
||||
|
||||
recording = factories.RecordingFactory(
|
||||
room__name="Engineering Sync",
|
||||
worker_id="egress-1",
|
||||
options={"collect_metadata": True, "language": "en-us"},
|
||||
)
|
||||
owner = factories.UserFactory(
|
||||
email="owner@test.com",
|
||||
sub="owner-sub",
|
||||
language="fr-fr",
|
||||
timezone="Europe/Paris",
|
||||
)
|
||||
factories.UserRecordingAccessFactory(
|
||||
recording=recording, role=models.RoleChoices.OWNER, user=owner
|
||||
)
|
||||
|
||||
started_at = datetime.datetime(2026, 1, 2, 10, 30, tzinfo=datetime.timezone.utc)
|
||||
ended_at = datetime.datetime(2026, 1, 2, 11, 45, tzinfo=datetime.timezone.utc)
|
||||
mock_get_recording_timestamps.return_value = (started_at, ended_at)
|
||||
mock_generate_download_s3_url.side_effect = [
|
||||
"https://storage.test/metadata.json",
|
||||
"https://storage.test/recording.ogg",
|
||||
]
|
||||
|
||||
mock_response = mock.Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = {"job_id": "job-42"}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
result = NotificationService._notify_summary_service(recording)
|
||||
recording.refresh_from_db()
|
||||
|
||||
assert result is True
|
||||
assert recording.external_process_id == "job-42"
|
||||
|
||||
metadata_filename = (
|
||||
f"{settings.METADATA_COLLECTOR_OUTPUT_FOLDER}/{recording.id}-metadata.json"
|
||||
)
|
||||
expected_payload = {
|
||||
"user_sub": owner.sub,
|
||||
"user_email": owner.email,
|
||||
"cloud_storage_url": "https://storage.test/recording.ogg",
|
||||
"language": "en-us",
|
||||
"context_language": owner.language,
|
||||
"push_to_docs_config": {
|
||||
"user_email": owner.email,
|
||||
"title": 'Réunion "Engineering Sync" du 2026-01-02 à 11:30',
|
||||
"download_link": f"{settings.RECORDING_DOWNLOAD_BASE_URL}/{recording.id}",
|
||||
"auto_create_summary": True,
|
||||
"form_link": None,
|
||||
},
|
||||
"metadata": {
|
||||
"cloud_storage_url": "https://storage.test/metadata.json",
|
||||
"started_at": started_at.isoformat(),
|
||||
"ended_at": ended_at.isoformat(),
|
||||
},
|
||||
}
|
||||
expected_headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer summary-token",
|
||||
}
|
||||
mock_post.assert_called_once_with(
|
||||
"https://summary.test/api/v2/tasks",
|
||||
json=expected_payload,
|
||||
headers=expected_headers,
|
||||
timeout=30,
|
||||
)
|
||||
assert mock_generate_download_s3_url.call_args_list == [
|
||||
mock.call(metadata_filename, expires_in=60 * 60 * 24, override_domain=False),
|
||||
mock.call(recording.key, expires_in=60 * 60 * 24, override_domain=False),
|
||||
]
|
||||
mock_get_recording_timestamps.assert_awaited_once_with("egress-1")
|
||||
|
||||
|
||||
@mock.patch("core.recording.event.notification.requests.post")
|
||||
@mock.patch("core.recording.event.notification.generate_download_s3_url")
|
||||
@mock.patch.object(
|
||||
NotificationService, "_get_recording_timestamps", new_callable=mock.AsyncMock
|
||||
)
|
||||
def test_notify_summary_service_post_args_without_metadata(
|
||||
mock_get_recording_timestamps,
|
||||
mock_generate_download_s3_url,
|
||||
mock_post,
|
||||
settings,
|
||||
):
|
||||
"""Test summary notification computed request args when metadata is not available."""
|
||||
settings.SUMMARY_SERVICE_VERSION = 2
|
||||
settings.SUMMARY_SERVICE_ENDPOINT = "https://summary.test/api/v2/tasks"
|
||||
settings.SUMMARY_SERVICE_API_TOKEN = "summary-token"
|
||||
settings.RECORDING_DOWNLOAD_BASE_URL = "https://app.test/recordings"
|
||||
settings.SCREEN_RECORDING_BASE_URL = None
|
||||
settings.METADATA_COLLECTOR_ENABLED = False
|
||||
|
||||
recording = factories.RecordingFactory(room__name="Daily")
|
||||
owner = factories.UserFactory(
|
||||
email="owner@test.com",
|
||||
sub="owner-sub",
|
||||
language="en-us",
|
||||
timezone="UTC",
|
||||
)
|
||||
factories.UserRecordingAccessFactory(
|
||||
recording=recording, role=models.RoleChoices.OWNER, user=owner
|
||||
)
|
||||
|
||||
mock_get_recording_timestamps.return_value = (None, None)
|
||||
mock_generate_download_s3_url.return_value = "https://storage.test/recording.mp4"
|
||||
|
||||
mock_response = mock.Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = {"job_id": "job-51"}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
result = NotificationService._notify_summary_service(recording)
|
||||
|
||||
assert result is True
|
||||
expected_payload = {
|
||||
"user_sub": owner.sub,
|
||||
"user_email": owner.email,
|
||||
"cloud_storage_url": "https://storage.test/recording.mp4",
|
||||
"language": "en",
|
||||
"context_language": owner.language,
|
||||
"push_to_docs_config": {
|
||||
"user_email": owner.email,
|
||||
"title": "Transcription",
|
||||
"download_link": f"{settings.RECORDING_DOWNLOAD_BASE_URL}/{recording.id}",
|
||||
"auto_create_summary": True,
|
||||
"form_link": None,
|
||||
},
|
||||
"metadata": None,
|
||||
}
|
||||
expected_headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer summary-token",
|
||||
}
|
||||
mock_post.assert_called_once_with(
|
||||
"https://summary.test/api/v2/tasks",
|
||||
json=expected_payload,
|
||||
headers=expected_headers,
|
||||
timeout=30,
|
||||
)
|
||||
mock_generate_download_s3_url.assert_called_once_with(
|
||||
recording.key, expires_in=60 * 60 * 24, override_domain=False
|
||||
)
|
||||
mock_get_recording_timestamps.assert_awaited_once_with(recording.worker_id)
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Test recordings API endpoints: external process hook.
|
||||
"""
|
||||
|
||||
# pylint: disable=redefined-outer-name,unused-argument
|
||||
|
||||
import pytest
|
||||
|
||||
from ...factories import RecordingFactory
|
||||
from ...models import RecordingStatusChoices
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def external_process_settings(settings):
|
||||
"""Configure authentication token for the external process webhook."""
|
||||
settings.SUMMARY_SERVICE_WEBHOOK_API_TOKEN = "testWebhookToken"
|
||||
return settings
|
||||
|
||||
|
||||
def test_external_process_event_missing_authorization_header(
|
||||
external_process_settings, client
|
||||
):
|
||||
"""Requests without authorization must be rejected."""
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "job-1", "type": "transcript", "status": "success"},
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_external_process_event_wrong_bearer_token(external_process_settings, client):
|
||||
"""Requests with invalid bearer token must be rejected."""
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "job-1", "type": "transcript", "status": "success"},
|
||||
HTTP_AUTHORIZATION="Bearer wrongToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_external_process_event_missing_job_id(external_process_settings, client):
|
||||
"""Payload without job_id must fail validation."""
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"type": "transcript", "status": "success"},
|
||||
HTTP_AUTHORIZATION="Bearer testWebhookToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"job_id": ["This field is required."]}
|
||||
|
||||
|
||||
def test_external_process_event_success_updates_recording_status(
|
||||
external_process_settings, client
|
||||
):
|
||||
"""A successful transcript process should update recording status."""
|
||||
recording = RecordingFactory(
|
||||
status=RecordingStatusChoices.SAVED,
|
||||
external_process_id="job-123",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "job-123", "type": "transcript", "status": "success"},
|
||||
HTTP_AUTHORIZATION="Bearer testWebhookToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Event processed."}
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == RecordingStatusChoices.EXTERNAL_PROCESS_SUCCESSFUL
|
||||
|
||||
|
||||
def test_external_process_event_failure_updates_recording_status(
|
||||
external_process_settings, client
|
||||
):
|
||||
"""A failing transcript process should update recording status."""
|
||||
recording = RecordingFactory(
|
||||
status=RecordingStatusChoices.SAVED,
|
||||
external_process_id="job-456",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "job-456", "type": "transcript", "status": "failure"},
|
||||
HTTP_AUTHORIZATION="Bearer testWebhookToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Event processed."}
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == RecordingStatusChoices.EXTERNAL_PROCESS_FAILED
|
||||
|
||||
|
||||
def test_external_process_event_unknown_recording_is_ignored(
|
||||
external_process_settings, client
|
||||
):
|
||||
"""Unknown job_id should not fail the webhook processing."""
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "missing-job", "type": "transcript", "status": "success"},
|
||||
HTTP_AUTHORIZATION="Bearer testWebhookToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Event processed."}
|
||||
|
||||
|
||||
def test_external_process_event_non_transcript_event_does_not_change_status(
|
||||
external_process_settings, client
|
||||
):
|
||||
"""Only transcript events should update recording status."""
|
||||
recording = RecordingFactory(
|
||||
status=RecordingStatusChoices.SAVED,
|
||||
external_process_id="job-789",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "job-789", "type": "thumbnail", "status": "success"},
|
||||
HTTP_AUTHORIZATION="Bearer testWebhookToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Event processed."}
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == RecordingStatusChoices.SAVED
|
||||
Reference in New Issue
Block a user