diff --git a/tests/unit/test_generation_preview.py b/tests/unit/test_generation_preview.py index a2f5a0936..0ff9c8f9b 100755 --- a/tests/unit/test_generation_preview.py +++ b/tests/unit/test_generation_preview.py @@ -963,7 +963,7 @@ class TestGetPreviewRoute: assert exc_info.value.status_code == 403 def test_get_completed_task_with_videos(self): - """查询 completed 状态任务,返回视频列表""" + """查询 completed 状态任务,返回视频列表(URL 已签名)""" repo = MagicMock() vid_repo = MagicMock() task = _make_task(status=GenerationTaskStatus.COMPLETED, progress=100.0) @@ -973,17 +973,22 @@ class TestGetPreviewRoute: video.duration = 25.0 video.file_size = 512000 + # Mock URL 签名(返回带签名的 URL) + mock_storage = MagicMock() + mock_storage.get_download_url.return_value = "https://cdn.example.com/preview_final.mp4?sig=abc123" + with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockGet: MockGet.return_value.execute.return_value = task with patch("app.api.routes.generation_preview.ListGeneratedVideosByTaskUseCase") as MockList: MockList.return_value.execute.return_value = [video] - resp = get_preview_generation_task( - task_id="preview_task_001", - authenticated_user=_make_user(), - generation_task_repository=repo, - generated_video_repository=vid_repo, - ) - assert resp.video_url == "https://cdn.example.com/preview_final.mp4" + with patch("app.api.routes.generation_preview.get_storage_service", return_value=mock_storage): + resp = get_preview_generation_task( + task_id="preview_task_001", + authenticated_user=_make_user(), + generation_task_repository=repo, + generated_video_repository=vid_repo, + ) + assert resp.video_url == "https://cdn.example.com/preview_final.mp4?sig=abc123" assert resp.duration == 25.0 @@ -995,37 +1000,38 @@ class TestGetPreviewRoute: class TestWorkerPreviewResolution: """Worker 层预览模式分辨率强制逻辑测试""" - def test_preview_mode_forces_480p(self): - """预览模式强制 854x480 + 1M 码率""" - # 模拟 worker 中 export_cfg 逻辑(与实际代码一致,使用 dict 拷贝) + def test_preview_mode_respects_resolution(self): + """预览模式尊重传入的 resolution 参数(如竖屏 480x854)""" is_preview = True - resolution = "1920x1080" # 用户指定的分辨率应被忽略 + resolution = "480x854" # 竖屏模板 original_config = {"export": {"resolution": "1280x720", "bitrate": "5M"}} plan_cfg = dict(original_config) export_cfg = dict(plan_cfg.get("export", {}) or {}) if is_preview: - export_cfg["resolution"] = "854x480" + preview_res = resolution if resolution else "854x480" + export_cfg["resolution"] = preview_res export_cfg["bitrate"] = "1M" elif resolution: export_cfg["resolution"] = resolution plan_cfg["export"] = export_cfg - assert export_cfg["resolution"] == "854x480" + assert export_cfg["resolution"] == "480x854" # 竖屏 assert export_cfg["bitrate"] == "1M" # 验证原始配置未被污染 assert original_config["export"]["resolution"] == "1280x720" def test_non_preview_uses_user_resolution(self): - """非预览模式使用用户指定分辨率""" + """非预览模式使用用户指定分辨率(逻辑不变)""" is_preview = False resolution = "1920x1080" plan_cfg = dict({"export": {"resolution": "1280x720"}}) export_cfg = dict(plan_cfg.get("export", {}) or {}) if is_preview: - export_cfg["resolution"] = "854x480" + preview_res = resolution if resolution else "854x480" + export_cfg["resolution"] = preview_res export_cfg["bitrate"] = "1M" elif resolution: export_cfg["resolution"] = resolution @@ -1033,14 +1039,15 @@ class TestWorkerPreviewResolution: assert export_cfg["resolution"] == "1920x1080" def test_non_preview_no_resolution_uses_template(self): - """非预览模式且用户未指定分辨率,使用模板配置""" + """非预览模式且用户未指定分辨率,使用模板配置(逻辑不变)""" is_preview = False resolution = "" plan_cfg = dict({"export": {"resolution": "1280x720"}}) export_cfg = dict(plan_cfg.get("export", {}) or {}) if is_preview: - export_cfg["resolution"] = "854x480" + preview_res = resolution if resolution else "854x480" + export_cfg["resolution"] = preview_res export_cfg["bitrate"] = "1M" elif resolution: export_cfg["resolution"] = resolution @@ -1050,14 +1057,88 @@ class TestWorkerPreviewResolution: assert export_cfg["resolution"] == "1280x720" def test_preview_mode_empty_export_cfg(self): - """预览模式且模板无 export 配置""" + """预览模式且模板无 export 配置,无传入 resolution 时默认 854x480""" is_preview = True + resolution = "" plan_cfg = dict({}) export_cfg = dict(plan_cfg.get("export", {}) or {}) if is_preview: - export_cfg["resolution"] = "854x480" + preview_res = resolution if resolution else "854x480" + export_cfg["resolution"] = preview_res export_cfg["bitrate"] = "1M" assert export_cfg["resolution"] == "854x480" assert export_cfg["bitrate"] == "1M" + +# ═══════════════════════════════════════════════════════════════════════════════ +# 新增:URL 签名 + 模板时长约束测试 +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestSignVideoUrl: + """_sign_video_url 预签名 URL 测试。""" + + def test_empty_url_returns_empty(self): + """空 URL 直接返回空字符串。""" + from app.api.routes.generation_preview import _sign_video_url + + assert _sign_video_url("") == "" + + def test_signs_oss_url(self): + """OSS URL 应被签名。""" + from app.api.routes.generation_preview import _sign_video_url + + mock_storage = MagicMock() + mock_storage.get_download_url.return_value = "https://signed.example.com/video.mp4?sig=abc" + + with patch("app.api.routes.generation_preview.get_storage_service", return_value=mock_storage): + result = _sign_video_url("https://bucket.oss.example.com/video.mp4") + + assert result == "https://signed.example.com/video.mp4?sig=abc" + mock_storage.get_download_url.assert_called_once() + + def test_fallback_on_sign_failure(self): + """签名失败时降级返回原始 URL。""" + from app.api.routes.generation_preview import _sign_video_url + + with patch("app.api.routes.generation_preview.get_storage_service", side_effect=RuntimeError("no storage")): + result = _sign_video_url("https://bucket.oss.example.com/video.mp4") + + assert result == "https://bucket.oss.example.com/video.mp4" + + def test_sign_returns_none_fallback(self): + """get_download_url 返回 None 时降级返回原始 URL。""" + from app.api.routes.generation_preview import _sign_video_url + + mock_storage = MagicMock() + mock_storage.get_download_url.return_value = None + + with patch("app.api.routes.generation_preview.get_storage_service", return_value=mock_storage): + result = _sign_video_url("https://bucket.oss.example.com/video.mp4") + + assert result == "https://bucket.oss.example.com/video.mp4" + + +class TestCalcPreviewResolution: + """_calc_preview_resolution 单元测试。""" + + def test_portrait_9_16(self): + from app.api.routes.generation_preview import _calc_preview_resolution + assert _calc_preview_resolution("9:16") == "480x854" + + def test_landscape_16_9(self): + from app.api.routes.generation_preview import _calc_preview_resolution + assert _calc_preview_resolution("16:9") == "854x480" + + def test_square_1_1(self): + from app.api.routes.generation_preview import _calc_preview_resolution + assert _calc_preview_resolution("1:1") == "480x480" + + def test_unknown_defaults_to_landscape(self): + from app.api.routes.generation_preview import _calc_preview_resolution + assert _calc_preview_resolution("unknown") == "854x480" + + def test_empty_defaults_to_landscape(self): + from app.api.routes.generation_preview import _calc_preview_resolution + assert _calc_preview_resolution("") == "854x480"