rev 4
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from itertools import count
|
||||
from types import SimpleNamespace
|
||||
import uuid
|
||||
|
||||
from cpv3.modules.media import service as media_service
|
||||
from cpv3.modules.tasks import service as task_service
|
||||
|
||||
|
||||
def test_parse_ffmpeg_progress_time_seconds_from_timecode() -> None:
|
||||
assert media_service._get_ffmpeg_output_time_seconds(
|
||||
{
|
||||
"out_time": "00:00:12.500000",
|
||||
"progress": "continue",
|
||||
}
|
||||
) == 12.5
|
||||
|
||||
|
||||
def test_parse_ffmpeg_progress_time_seconds_returns_none_for_invalid_snapshot() -> None:
|
||||
assert media_service._get_ffmpeg_output_time_seconds(
|
||||
{
|
||||
"out_time": "not-a-timecode",
|
||||
"progress": "continue",
|
||||
}
|
||||
) is None
|
||||
|
||||
|
||||
def test_media_convert_actor_emits_intermediate_progress_events(monkeypatch) -> None:
|
||||
sent_events: list[task_service.TaskWebhookEvent] = []
|
||||
monotonic_values = count(step=2)
|
||||
|
||||
async def fake_convert_to_mp4(
|
||||
_storage: object,
|
||||
*,
|
||||
file_key: str,
|
||||
out_folder: str,
|
||||
on_progress: object | None = None,
|
||||
) -> SimpleNamespace:
|
||||
assert file_key == "uploads/source.mov"
|
||||
assert out_folder == "projects/1"
|
||||
assert callable(on_progress)
|
||||
|
||||
on_progress("converting", 0.0)
|
||||
on_progress("converting", 50.0)
|
||||
on_progress("converting", 100.0)
|
||||
on_progress("uploading", None)
|
||||
|
||||
return SimpleNamespace(
|
||||
file_path="projects/1/converted/video.mp4",
|
||||
file_url="https://example.com/video.mp4",
|
||||
file_size=123,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(task_service, "_run_async", asyncio.run)
|
||||
monkeypatch.setattr(task_service, "_raise_if_job_cancelled", lambda _job_id: None)
|
||||
monkeypatch.setattr(task_service, "_get_storage_service", lambda: object())
|
||||
monkeypatch.setattr(
|
||||
task_service,
|
||||
"_send_webhook_event",
|
||||
lambda _url, event: sent_events.append(event),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
task_service.time,
|
||||
"monotonic",
|
||||
lambda: float(next(monotonic_values)),
|
||||
)
|
||||
monkeypatch.setattr(media_service, "convert_to_mp4", fake_convert_to_mp4)
|
||||
|
||||
task_service.media_convert_actor.fn(
|
||||
job_id=str(uuid.uuid4()),
|
||||
webhook_url="http://backend.test/api/tasks/webhook/job-1/",
|
||||
file_key="uploads/source.mov",
|
||||
out_folder="projects/1",
|
||||
output_format="mp4",
|
||||
)
|
||||
|
||||
progress_events = [event for event in sent_events if event.progress_pct is not None]
|
||||
|
||||
assert [event.progress_pct for event in progress_events] == [
|
||||
5.0,
|
||||
10.0,
|
||||
52.5,
|
||||
95.0,
|
||||
99.0,
|
||||
100.0,
|
||||
]
|
||||
assert [event.current_message for event in progress_events] == [
|
||||
"Подготовка файла",
|
||||
"Конвертация видео",
|
||||
"Конвертация видео",
|
||||
"Загрузка результата",
|
||||
"Сохранение результата",
|
||||
"Завершено",
|
||||
]
|
||||
assert sent_events[-1].status == task_service.JOB_STATUS_DONE
|
||||
assert sent_events[-1].output_data == {
|
||||
"file_path": "projects/1/converted/video.mp4",
|
||||
"file_url": "https://example.com/video.mp4",
|
||||
"file_size": 123,
|
||||
}
|
||||
|
||||
|
||||
def test_silence_apply_actor_emits_intermediate_progress_events(monkeypatch) -> None:
|
||||
sent_events: list[task_service.TaskWebhookEvent] = []
|
||||
monotonic_values = count(step=2)
|
||||
|
||||
async def fake_apply_silence_cuts(
|
||||
_storage: object,
|
||||
*,
|
||||
file_key: str,
|
||||
out_folder: str,
|
||||
cuts: list[dict],
|
||||
output_name: str | None = None,
|
||||
on_progress: object | None = None,
|
||||
) -> SimpleNamespace:
|
||||
assert file_key == "uploads/source.mp4"
|
||||
assert out_folder == "projects/1"
|
||||
assert cuts == [{"start_ms": 100, "end_ms": 200}]
|
||||
assert output_name == "edited.mp4"
|
||||
assert callable(on_progress)
|
||||
|
||||
on_progress("applying_cuts", 0.0)
|
||||
on_progress("applying_cuts", 50.0)
|
||||
on_progress("applying_cuts", 100.0)
|
||||
on_progress("uploading", None)
|
||||
|
||||
return SimpleNamespace(
|
||||
file_path="projects/1/silent/edited.mp4",
|
||||
file_url="https://example.com/edited.mp4",
|
||||
file_size=456,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(task_service, "_run_async", asyncio.run)
|
||||
monkeypatch.setattr(task_service, "_raise_if_job_cancelled", lambda _job_id: None)
|
||||
monkeypatch.setattr(task_service, "_get_storage_service", lambda: object())
|
||||
monkeypatch.setattr(
|
||||
task_service,
|
||||
"_send_webhook_event",
|
||||
lambda _url, event: sent_events.append(event),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
task_service.time,
|
||||
"monotonic",
|
||||
lambda: float(next(monotonic_values)),
|
||||
)
|
||||
monkeypatch.setattr(media_service, "apply_silence_cuts", fake_apply_silence_cuts)
|
||||
|
||||
task_service.silence_apply_actor.fn(
|
||||
job_id=str(uuid.uuid4()),
|
||||
webhook_url="http://backend.test/api/tasks/webhook/job-2/",
|
||||
file_key="uploads/source.mp4",
|
||||
out_folder="projects/1",
|
||||
cuts=[{"start_ms": 100, "end_ms": 200}],
|
||||
output_name="edited.mp4",
|
||||
)
|
||||
|
||||
progress_events = [event for event in sent_events if event.progress_pct is not None]
|
||||
|
||||
assert [event.progress_pct for event in progress_events] == [
|
||||
5.0,
|
||||
10.0,
|
||||
52.5,
|
||||
95.0,
|
||||
99.0,
|
||||
100.0,
|
||||
]
|
||||
assert [event.current_message for event in progress_events] == [
|
||||
"Подготовка файла",
|
||||
"Применение вырезок",
|
||||
"Применение вырезок",
|
||||
"Загрузка результата",
|
||||
"Сохранение результата",
|
||||
"Завершено",
|
||||
]
|
||||
assert sent_events[-1].status == task_service.JOB_STATUS_DONE
|
||||
assert sent_events[-1].output_data == {
|
||||
"file_path": "projects/1/silent/edited.mp4",
|
||||
"file_url": "https://example.com/edited.mp4",
|
||||
"file_size": 456,
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
|
||||
from cpv3.infrastructure.storage.types import FileInfo
|
||||
from cpv3.modules.media import service as media_service
|
||||
from cpv3.modules.tasks import service as task_service
|
||||
|
||||
|
||||
def test_extract_ffmpeg_out_time_ms_prefers_out_time_us() -> None:
|
||||
assert media_service._extract_ffmpeg_out_time_ms(
|
||||
{
|
||||
"out_time_us": "2500000",
|
||||
"out_time_ms": "1000000",
|
||||
}
|
||||
) == 2500.0
|
||||
|
||||
|
||||
def test_extract_ffmpeg_out_time_ms_supports_legacy_out_time_ms_field() -> None:
|
||||
assert media_service._extract_ffmpeg_out_time_ms(
|
||||
{
|
||||
"out_time_ms": "1250000",
|
||||
}
|
||||
) == 1250.0
|
||||
|
||||
|
||||
def test_extract_ffmpeg_out_time_ms_returns_none_for_invalid_values() -> None:
|
||||
assert media_service._extract_ffmpeg_out_time_ms(
|
||||
{
|
||||
"out_time_us": "N/A",
|
||||
}
|
||||
) is None
|
||||
|
||||
|
||||
def test_media_convert_actor_emits_precise_progress_updates(monkeypatch) -> None:
|
||||
sent_events: list[task_service.TaskWebhookEvent] = []
|
||||
|
||||
async def fake_convert_to_mp4(
|
||||
_storage: object,
|
||||
*,
|
||||
file_key: str,
|
||||
out_folder: str,
|
||||
on_progress,
|
||||
) -> FileInfo:
|
||||
assert file_key == "uploads/source.mov"
|
||||
assert out_folder == "projects/1"
|
||||
|
||||
on_progress("converting", 0.0)
|
||||
on_progress("converting", 50.0)
|
||||
on_progress("uploading", None)
|
||||
|
||||
return FileInfo(
|
||||
file_path="projects/1/converted/output.mp4",
|
||||
file_url="https://example.com/output.mp4",
|
||||
file_size=1234,
|
||||
filename="output.mp4",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(media_service, "convert_to_mp4", fake_convert_to_mp4)
|
||||
monkeypatch.setattr(task_service, "_run_async", asyncio.run)
|
||||
monkeypatch.setattr(task_service, "_raise_if_job_cancelled", lambda _job_id: None)
|
||||
monkeypatch.setattr(task_service, "_get_storage_service", lambda: object())
|
||||
monkeypatch.setattr(task_service, "PROGRESS_CONVERT_THROTTLE_SECONDS", 0.0)
|
||||
monkeypatch.setattr(
|
||||
task_service,
|
||||
"_send_webhook_event",
|
||||
lambda _url, event: sent_events.append(event),
|
||||
)
|
||||
|
||||
task_service.media_convert_actor.fn(
|
||||
job_id=str(uuid.uuid4()),
|
||||
webhook_url="http://backend.test/api/tasks/webhook/job-1/",
|
||||
file_key="uploads/source.mov",
|
||||
out_folder="projects/1",
|
||||
output_format="mp4",
|
||||
)
|
||||
|
||||
progress_events = [
|
||||
(event.progress_pct, event.current_message)
|
||||
for event in sent_events
|
||||
if event.progress_pct is not None
|
||||
]
|
||||
|
||||
assert progress_events == [
|
||||
(5.0, "Подготовка файла"),
|
||||
(10.0, "Конвертация видео"),
|
||||
(52.5, "Конвертация видео"),
|
||||
(95.0, "Загрузка результата"),
|
||||
(99.0, "Сохранение результата"),
|
||||
(100.0, "Завершено"),
|
||||
]
|
||||
@@ -0,0 +1,137 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cpv3.modules.media.service import apply_silence_cuts, remove_silence
|
||||
|
||||
|
||||
class _TempDownload:
|
||||
def __init__(self, path: str) -> None:
|
||||
self.path = path
|
||||
|
||||
def cleanup(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class _FakeProcess:
|
||||
returncode = 0
|
||||
|
||||
async def communicate(self) -> tuple[bytes, bytes]:
|
||||
return b"", b""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_silence_uses_single_input_trim_filter(tmp_path) -> None:
|
||||
input_path = tmp_path / "input.mp4"
|
||||
input_path.write_bytes(b"video")
|
||||
|
||||
file_info = SimpleNamespace(
|
||||
file_path="uploads/test-file.txt",
|
||||
file_url="http://example.com/uploads/test-file.txt",
|
||||
file_size=1024,
|
||||
filename="test-file.txt",
|
||||
)
|
||||
storage = SimpleNamespace(
|
||||
download_to_temp=AsyncMock(return_value=_TempDownload(str(input_path))),
|
||||
upload_fileobj=AsyncMock(return_value="uploads/test-file.txt"),
|
||||
get_file_info=AsyncMock(return_value=file_info),
|
||||
)
|
||||
|
||||
captured: dict[str, tuple[str, ...]] = {}
|
||||
|
||||
async def fake_create_subprocess_exec(
|
||||
*cmd: str, stdout=None, stderr=None
|
||||
) -> _FakeProcess:
|
||||
captured["cmd"] = cmd
|
||||
return _FakeProcess()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"cpv3.modules.media.service._compute_non_silent_segments",
|
||||
return_value=[(0, 1000), (2000, 3000)],
|
||||
),
|
||||
patch(
|
||||
"cpv3.modules.media.service.asyncio.create_subprocess_exec",
|
||||
side_effect=fake_create_subprocess_exec,
|
||||
),
|
||||
):
|
||||
await remove_silence(
|
||||
storage,
|
||||
file_key="uploads/input.mp4",
|
||||
out_folder="processed",
|
||||
)
|
||||
|
||||
cmd = list(captured["cmd"])
|
||||
assert cmd.count("-i") == 1
|
||||
|
||||
filter_complex = cmd[cmd.index("-filter_complex") + 1]
|
||||
assert "trim=start=0.000:end=1.000" in filter_complex
|
||||
assert "atrim=start=0.000:end=1.000" in filter_complex
|
||||
assert "trim=start=2.000:end=3.000" in filter_complex
|
||||
assert "atrim=start=2.000:end=3.000" in filter_complex
|
||||
|
||||
|
||||
async def _run_sync_immediately(func):
|
||||
return func()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_silence_cuts_uses_single_input_trim_filter(tmp_path) -> None:
|
||||
input_path = tmp_path / "input.mp4"
|
||||
input_path.write_bytes(b"video")
|
||||
|
||||
file_info = SimpleNamespace(
|
||||
file_path="uploads/test-file.txt",
|
||||
file_url="http://example.com/uploads/test-file.txt",
|
||||
file_size=1024,
|
||||
filename="test-file.txt",
|
||||
)
|
||||
storage = SimpleNamespace(
|
||||
download_to_temp=AsyncMock(return_value=_TempDownload(str(input_path))),
|
||||
upload_fileobj=AsyncMock(return_value="uploads/test-file.txt"),
|
||||
get_file_info=AsyncMock(return_value=file_info),
|
||||
)
|
||||
|
||||
captured: dict[str, tuple[str, ...]] = {}
|
||||
|
||||
async def fake_create_subprocess_exec(
|
||||
*cmd: str, stdout=None, stderr=None
|
||||
) -> _FakeProcess:
|
||||
captured["cmd"] = cmd
|
||||
return _FakeProcess()
|
||||
|
||||
fake_audio = MagicMock()
|
||||
fake_audio.__len__.return_value = 5000
|
||||
|
||||
with (
|
||||
patch(
|
||||
"cpv3.modules.media.service.anyio.to_thread.run_sync",
|
||||
side_effect=_run_sync_immediately,
|
||||
),
|
||||
patch("pydub.AudioSegment.from_file", return_value=fake_audio),
|
||||
patch(
|
||||
"cpv3.modules.media.service.asyncio.create_subprocess_exec",
|
||||
side_effect=fake_create_subprocess_exec,
|
||||
),
|
||||
):
|
||||
await apply_silence_cuts(
|
||||
storage,
|
||||
file_key="uploads/input.mp4",
|
||||
out_folder="processed",
|
||||
cuts=[
|
||||
{"start_ms": 1000, "end_ms": 2000},
|
||||
{"start_ms": 3000, "end_ms": 3500},
|
||||
],
|
||||
)
|
||||
|
||||
cmd = list(captured["cmd"])
|
||||
assert cmd.count("-i") == 1
|
||||
|
||||
filter_complex = cmd[cmd.index("-filter_complex") + 1]
|
||||
assert "trim=start=0.000:end=1.000" in filter_complex
|
||||
assert "atrim=start=0.000:end=1.000" in filter_complex
|
||||
assert "trim=start=2.000:end=3.000" in filter_complex
|
||||
assert "atrim=start=3.500:end=5.000" in filter_complex
|
||||
@@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from cpv3.modules.notifications.service import NotificationService
|
||||
from cpv3.modules.tasks.schemas import TaskWebhookEvent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_notification_persists_operation_title() -> None:
|
||||
service = NotificationService(session=AsyncMock())
|
||||
service._repo = SimpleNamespace(
|
||||
create=AsyncMock(return_value=SimpleNamespace(id=uuid.uuid4()))
|
||||
)
|
||||
|
||||
job = SimpleNamespace(
|
||||
id=uuid.uuid4(),
|
||||
project_id=uuid.uuid4(),
|
||||
job_type="SILENCE_APPLY",
|
||||
status="DONE",
|
||||
project_pct=100,
|
||||
current_message="Завершено",
|
||||
)
|
||||
|
||||
event = TaskWebhookEvent(
|
||||
status="DONE",
|
||||
progress_pct=100,
|
||||
current_message="Завершено",
|
||||
)
|
||||
|
||||
publish_mock = AsyncMock()
|
||||
|
||||
from cpv3.modules import notifications as notifications_module
|
||||
|
||||
original_publish = notifications_module.service.publish_to_user
|
||||
notifications_module.service.publish_to_user = publish_mock
|
||||
try:
|
||||
await service.create_task_notification(
|
||||
user_id=uuid.uuid4(),
|
||||
job=job,
|
||||
event=event,
|
||||
)
|
||||
finally:
|
||||
notifications_module.service.publish_to_user = original_publish
|
||||
|
||||
create_call = service._repo.create.await_args
|
||||
notification = create_call.args[0]
|
||||
|
||||
assert notification.title == "Применение вырезок"
|
||||
assert notification.message == "Завершено"
|
||||
assert notification.payload == {
|
||||
"job_type": "SILENCE_APPLY",
|
||||
"progress_pct": 100,
|
||||
"status": "DONE",
|
||||
}
|
||||
publish_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_notification_preserves_zero_progress_for_websocket() -> None:
|
||||
service = NotificationService(session=AsyncMock())
|
||||
service._repo = SimpleNamespace(
|
||||
create=AsyncMock(return_value=SimpleNamespace(id=uuid.uuid4()))
|
||||
)
|
||||
|
||||
job = SimpleNamespace(
|
||||
id=uuid.uuid4(),
|
||||
project_id=uuid.uuid4(),
|
||||
job_type="MEDIA_CONVERT",
|
||||
status="RUNNING",
|
||||
project_pct=35.0,
|
||||
current_message="Конвертация видео",
|
||||
)
|
||||
|
||||
event = TaskWebhookEvent(
|
||||
progress_pct=0.0,
|
||||
current_message="Подготовка файла",
|
||||
)
|
||||
|
||||
publish_mock = AsyncMock()
|
||||
|
||||
from cpv3.modules import notifications as notifications_module
|
||||
|
||||
original_publish = notifications_module.service.publish_to_user
|
||||
notifications_module.service.publish_to_user = publish_mock
|
||||
try:
|
||||
await service.create_task_notification(
|
||||
user_id=uuid.uuid4(),
|
||||
job=job,
|
||||
event=event,
|
||||
)
|
||||
finally:
|
||||
notifications_module.service.publish_to_user = original_publish
|
||||
|
||||
published_message = publish_mock.await_args.args[1]
|
||||
|
||||
assert published_message.progress_pct == 0.0
|
||||
assert published_message.message == "Подготовка файла"
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from cpv3.modules.transcription import service as transcription_service
|
||||
|
||||
|
||||
class _FakeSSLContext:
|
||||
def __init__(self) -> None:
|
||||
self.loaded_cafile: str | None = None
|
||||
|
||||
def load_verify_locations(self, *, cafile: str) -> None:
|
||||
self.loaded_cafile = cafile
|
||||
|
||||
|
||||
def test_build_salute_ssl_context_uses_default_context(monkeypatch) -> None:
|
||||
fake_context = _FakeSSLContext()
|
||||
|
||||
monkeypatch.setattr(
|
||||
transcription_service,
|
||||
"get_settings",
|
||||
lambda: SimpleNamespace(
|
||||
salute_ssl_verify=True,
|
||||
salute_ca_cert_path=None,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
transcription_service.ssl,
|
||||
"create_default_context",
|
||||
lambda: fake_context,
|
||||
)
|
||||
|
||||
ssl_context = transcription_service._build_salute_ssl_context()
|
||||
|
||||
assert ssl_context is fake_context
|
||||
assert fake_context.loaded_cafile is None
|
||||
|
||||
|
||||
def test_build_salute_ssl_context_loads_custom_ca(monkeypatch) -> None:
|
||||
fake_context = _FakeSSLContext()
|
||||
custom_ca_path = Path("/tmp/salute-ca.pem")
|
||||
|
||||
monkeypatch.setattr(
|
||||
transcription_service,
|
||||
"get_settings",
|
||||
lambda: SimpleNamespace(
|
||||
salute_ssl_verify=True,
|
||||
salute_ca_cert_path=custom_ca_path,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
transcription_service.ssl,
|
||||
"create_default_context",
|
||||
lambda: fake_context,
|
||||
)
|
||||
|
||||
ssl_context = transcription_service._build_salute_ssl_context()
|
||||
|
||||
assert ssl_context is fake_context
|
||||
assert fake_context.loaded_cafile == str(custom_ca_path)
|
||||
|
||||
|
||||
def test_build_salute_ssl_context_disables_verification(monkeypatch) -> None:
|
||||
unverified_context = object()
|
||||
|
||||
monkeypatch.setattr(
|
||||
transcription_service,
|
||||
"get_settings",
|
||||
lambda: SimpleNamespace(
|
||||
salute_ssl_verify=False,
|
||||
salute_ca_cert_path=None,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
transcription_service.ssl,
|
||||
"_create_unverified_context",
|
||||
lambda: unverified_context,
|
||||
)
|
||||
|
||||
ssl_context = transcription_service._build_salute_ssl_context()
|
||||
|
||||
assert ssl_context is unverified_context
|
||||
|
||||
|
||||
def test_get_salute_auth_header_value_returns_basic_header(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
transcription_service,
|
||||
"get_settings",
|
||||
lambda: SimpleNamespace(
|
||||
salute_auth_key=" encoded-credentials ",
|
||||
),
|
||||
)
|
||||
|
||||
header_value = transcription_service._get_salute_auth_header_value()
|
||||
|
||||
assert header_value == "Basic encoded-credentials"
|
||||
|
||||
|
||||
def test_get_salute_auth_header_value_raises_when_missing(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
transcription_service,
|
||||
"get_settings",
|
||||
lambda: SimpleNamespace(
|
||||
salute_auth_key=" ",
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
transcription_service._get_salute_auth_header_value()
|
||||
except RuntimeError as exc:
|
||||
assert str(exc) == transcription_service.ERROR_SALUTE_AUTH_KEY_MISSING
|
||||
else:
|
||||
raise AssertionError("Expected RuntimeError for missing SaluteSpeech auth key")
|
||||
@@ -106,3 +106,40 @@ async def test_cancel_job_marks_job_cancelled_and_keeps_record() -> None:
|
||||
service._create_cancellation_notification.assert_awaited_once_with(
|
||||
cancelled_job
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_webhook_event_updates_progress_for_conversion_job() -> None:
|
||||
job = SimpleNamespace(
|
||||
id=uuid.uuid4(),
|
||||
status="RUNNING",
|
||||
job_type="MEDIA_CONVERT",
|
||||
user_id=None,
|
||||
)
|
||||
updated_job = SimpleNamespace(**job.__dict__, project_pct=52.5)
|
||||
job_repo = SimpleNamespace(
|
||||
get_by_id=AsyncMock(return_value=job),
|
||||
update=AsyncMock(return_value=updated_job),
|
||||
)
|
||||
event_repo = SimpleNamespace(create=AsyncMock())
|
||||
|
||||
service = TaskService(session=AsyncMock())
|
||||
service._job_repo = job_repo
|
||||
service._event_repo = event_repo
|
||||
|
||||
result = await service.record_webhook_event(
|
||||
job_id=job.id,
|
||||
event=TaskWebhookEvent(
|
||||
progress_pct=52.5,
|
||||
current_message="Конвертация видео",
|
||||
),
|
||||
)
|
||||
|
||||
update_call = job_repo.update.await_args.args[1]
|
||||
event_call = event_repo.create.await_args.args[0]
|
||||
|
||||
assert result is updated_job
|
||||
assert update_call.project_pct == 52.5
|
||||
assert update_call.current_message == "Конвертация видео"
|
||||
assert event_call.event_type == "progress"
|
||||
assert event_call.payload["progress_pct"] == 52.5
|
||||
|
||||
Reference in New Issue
Block a user