""" P0-2 修复测试:generation_tasks results 端点返回 OSS 预签名 URL 验证 list_generation_results 端点: - 对每个生成视频的 file_url 调用 storage_service.get_download_url() - 返回的 download_url 是预签名临时 URL(24h 有效期) - 与 generated_videos.py 中的模式一致 """ from __future__ import annotations import os import sys from pathlib import Path from unittest.mock import MagicMock os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing") os.environ.setdefault("DATABASE_URL", "sqlite:///test.db") sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) from app.api.routes.generation_tasks import router from app.auth import AuthenticatedUser, get_current_user from app.core.storage import OSSStorageService, get_storage_service from app.dependencies import ( get_generated_video_repository, get_generation_task_repository, get_project_repository, ) from fastapi import FastAPI from fastapi.testclient import TestClient from packages.domain import ( GeneratedVideo, GenerationTask, GenerationTaskStatus, Project, User, ) # --------------------------------------------------------------------------- # Stub repositories # --------------------------------------------------------------------------- class StubProjectRepository: def __init__(self, projects=None): self._projects = projects or {} def find_by_id(self, project_id): return self._projects.get(project_id) class StubGenerationTaskRepository: def __init__(self, tasks=None): self._tasks = tasks or {} def get(self, task_id): return self._tasks.get(task_id) def count_pending_by_user(self, user_id): return 0 def count_pending_total(self): return 0 def list_by_user_filtered(self, user_id, *, status=None, limit=None, offset=0): items = [t for t in self._tasks.values() if getattr(t, "created_by_user_id", None) == user_id] if status: items = [t for t in items if getattr(t, "status", None) == status] if offset: items = items[offset:] if limit is not None: items = items[:limit] return items def count_by_user_filtered(self, user_id, *, status=None): items = [t for t in self._tasks.values() if getattr(t, "created_by_user_id", None) == user_id] if status: items = [t for t in items if getattr(t, "status", None) == status] return len(items) def list_by_project_filtered(self, project_id, *, status=None, limit=None, offset=0): items = [t for t in self._tasks.values() if getattr(t, "project_id", None) == project_id] if status: items = [t for t in items if getattr(t, "status", None) == status] if offset: items = items[offset:] if limit is not None: items = items[:limit] return items def count_by_project_filtered(self, project_id, *, status=None): items = [t for t in self._tasks.values() if getattr(t, "project_id", None) == project_id] if status: items = [t for t in items if getattr(t, "status", None) == status] return len(items) class StubGeneratedVideoRepository: def __init__(self, videos=None): self._videos = videos or {} def list_by_generation_task(self, task_id): return [v for v in self._videos.values() if v.generation_task_id == task_id] # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- def _make_app(task, videos, storage_mock): app = FastAPI() app.include_router(router, prefix="/api/v1/generation") user = User(id="user-1", email="test@test.com", display_name="Test", username="testuser") auth = AuthenticatedUser(user=user, token_type="access") project = Project(id="project-1", name="Test", description="", owner_user_id="user-1") app.dependency_overrides[get_current_user] = lambda: auth app.dependency_overrides[get_project_repository] = lambda: StubProjectRepository({"project-1": project}) app.dependency_overrides[get_generation_task_repository] = lambda: StubGenerationTaskRepository({"task-1": task}) app.dependency_overrides[get_generated_video_repository] = lambda: StubGeneratedVideoRepository(videos) app.dependency_overrides[get_storage_service] = lambda: storage_mock return app def test_results_endpoint_generates_presigned_urls(): """验证 list_generation_results 为每个视频生成预签名 download_url""" task = GenerationTask( id="task-1", project_id="project-1", asset_library_id="lib-1", strategy_id="s1", status=GenerationTaskStatus.COMPLETED, progress=100, result_count=2, created_by_user_id="user-1", ) videos = { "v1": GeneratedVideo.create( project_id="project-1", generation_task_id="task-1", name="video1.mp4", file_url="https://bucket.oss-cn-hangzhou.aliyuncs.com/generated/v1.mp4", file_size=1024, duration=5.0, width=1280, height=720, fps=25.0, ), "v2": GeneratedVideo.create( project_id="project-1", generation_task_id="task-1", name="video2.mp4", file_url="https://bucket.oss-cn-hangzhou.aliyuncs.com/generated/v2.mp4", file_size=2048, duration=10.0, width=1920, height=1080, fps=30.0, ), } storage_mock = MagicMock(spec=OSSStorageService) storage_mock.get_download_url.side_effect = ( lambda url, expires_seconds=3600: f"{url}?signature=presigned&expires={expires_seconds}" ) app = _make_app(task, videos, storage_mock) client = TestClient(app) response = client.get("/api/v1/generation/tasks/task-1/results") assert response.status_code == 200 data = response.json() assert len(data["items"]) == 2 # Verify presigned URLs were generated with 24h expiry assert storage_mock.get_download_url.call_count == 2 for call in storage_mock.get_download_url.call_args_list: assert call.kwargs["expires_seconds"] == 86400 # Verify download_url is present in response for item in data["items"]: assert item["download_url"] is not None assert "signature=presigned" in item["download_url"] assert "expires=86400" in item["download_url"] # Verify file_url is still the original (raw) URL assert data["items"][0]["file_url"] == videos["v1"].file_url def test_results_endpoint_handles_empty_videos(): """验证无视频时正常返回空列表""" task = GenerationTask( id="task-1", project_id="project-1", asset_library_id="lib-1", strategy_id="s1", status=GenerationTaskStatus.RUNNING, progress=50, created_by_user_id="user-1", ) storage_mock = MagicMock(spec=OSSStorageService) app = _make_app(task, {}, storage_mock) client = TestClient(app) response = client.get("/api/v1/generation/tasks/task-1/results") assert response.status_code == 200 assert response.json()["items"] == [] storage_mock.get_download_url.assert_not_called()