fix: generation_tasks results 端点返回 OSS 预签名 URL(24h 有效期) #206
@@ -4,6 +4,7 @@ from typing import Any
|
||||
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.core.celery_app import celery_app
|
||||
from app.core.storage import OSSStorageService, get_storage_service
|
||||
from app.dependencies import (
|
||||
get_asset_library_repository,
|
||||
get_asset_repository,
|
||||
@@ -63,7 +64,7 @@ def _to_generation_task_response(task) -> GenerationTaskResponse:
|
||||
)
|
||||
|
||||
|
||||
def _to_generated_video_response(item) -> GeneratedVideoResponse:
|
||||
def _to_generated_video_response(item, download_url: str | None = None) -> GeneratedVideoResponse:
|
||||
return GeneratedVideoResponse(
|
||||
id=item.id,
|
||||
project_id=item.project_id,
|
||||
@@ -76,6 +77,7 @@ def _to_generated_video_response(item) -> GeneratedVideoResponse:
|
||||
width=item.width,
|
||||
height=item.height,
|
||||
fps=item.fps,
|
||||
download_url=download_url,
|
||||
)
|
||||
|
||||
|
||||
@@ -265,6 +267,7 @@ def list_generation_results(
|
||||
generation_task_repository: Any = Depends(get_generation_task_repository),
|
||||
generated_video_repository: Any = Depends(get_generated_video_repository),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> ListGeneratedVideosResponse:
|
||||
task = generation_task_repository.get(task_id)
|
||||
if task is None:
|
||||
@@ -273,7 +276,11 @@ def list_generation_results(
|
||||
_check_project_access(task.project_id, authenticated_user.user.id, project_repository)
|
||||
use_case = ListGeneratedVideosByTaskUseCase(generated_video_repository)
|
||||
items = use_case.execute(task_id)
|
||||
return ListGeneratedVideosResponse(items=[_to_generated_video_response(item) for item in items])
|
||||
responses = []
|
||||
for item in items:
|
||||
download_url = storage_service.get_download_url(item.file_url, expires_seconds=86400)
|
||||
responses.append(_to_generated_video_response(item, download_url=download_url))
|
||||
return ListGeneratedVideosResponse(items=responses)
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/retry", response_model=GenerationTaskResponse)
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
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, patch
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user