import os import sys from pathlib import Path from uuid import uuid4 # 设置必要环境变量(必须在导入 app 模块之前) 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")) import pytest from app.api.routes.asset_diagnosis import _build_diagnosis from fastapi import FastAPI from fastapi.testclient import TestClient from packages.domain import ( Asset, AssetLibrary, AssetLibraryKind, AssetStatus, ClassificationStatus, Project, ) def _asset(name: str, mime_type: str, *, status=AssetStatus.READY, duration=None, quality_score=None): return Asset.create( project_id="project-1", library_id="library-1", name=name, storage_key=f"uploads/{name}", mime_type=mime_type, file_size=1024, duration=duration, status=status, classification_status=ClassificationStatus.COMPLETED, quality_score=quality_score, ) def test_asset_diagnosis_reports_missing_video_gap(): diagnosis = _build_diagnosis( "project-1", [_asset("voice.mp3", "audio/mpeg"), _asset("image.jpg", "image/jpeg")], ) assert diagnosis.readiness_score < 80 assert diagnosis.video_assets == 0 assert any(gap.key == "missing_video" and gap.severity == "critical" for gap in diagnosis.gaps) def test_asset_diagnosis_scores_ready_video_assets(): used_asset = _asset("video-1.mp4", "video/mp4", duration=8) used_asset.metadata = {"generation_use_count": 1, "review_status": "pending_review"} diagnosis = _build_diagnosis( "project-1", [ used_asset, _asset("video-2.mp4", "video/mp4", duration=8), _asset("video-3.mov", "video/quicktime", duration=8), _asset("voice.mp3", "audio/mpeg"), ], ) assert diagnosis.readiness_score >= 80 assert diagnosis.video_assets == 3 assert diagnosis.voice_assets == 1 assert diagnosis.estimated_video_count == 3 assert diagnosis.used_assets == 1 assert diagnosis.unused_assets == 3 assert diagnosis.pending_review_assets == 1 smart_view_counts = {item.key: item.count for item in diagnosis.smart_views} assert smart_view_counts["recommended"] == 3 assert smart_view_counts["used"] == 1 assert smart_view_counts["pending_review"] == 1 def test_asset_diagnosis_flags_unready_and_low_quality_assets(): diagnosis = _build_diagnosis( "project-1", [ _asset("video.mp4", "video/mp4", duration=10, quality_score=40), _asset("pending.mp4", "video/mp4", status=AssetStatus.UPLOADING), ], ) gap_keys = {gap.key for gap in diagnosis.gaps} assert "not_ready_assets" in gap_keys assert "low_quality_assets" in gap_keys smart_view_counts = {item.key: item.count for item in diagnosis.smart_views} assert smart_view_counts["needs_attention"] == 2 assert smart_view_counts["high_risk"] == 1 # --------------------------------------------------------------------------- # 路由层测试 — 验证 find_by_project 调用正确性 # --------------------------------------------------------------------------- class _StubProjectRepository: def __init__(self, projects: dict[str, Project] | None = None): self._projects = projects or {} def get(self, project_id: str) -> Project | None: return self._projects.get(project_id) def find_by_id(self, project_id: str) -> Project | None: return self._projects.get(project_id) class _StubAssetLibraryRepository: def __init__(self, libraries: dict[str, AssetLibrary] | None = None): self._libraries = libraries or {} self.find_by_project_called_with: list[str] = [] def find_by_project(self, project_id: str, kind=None) -> list[AssetLibrary]: self.find_by_project_called_with.append(project_id) return [lib for lib in self._libraries.values() if lib.project_id == project_id] def list_by_project(self, project_id: str) -> list[AssetLibrary]: raise AssertionError("路由不应调用 list_by_project,应调用 find_by_project") class _StubAssetRepository: def __init__(self, assets: dict[str, Asset] | None = None): self._assets = assets or {} self.list_by_library_called_with: list[str] = [] def list_by_library(self, library_id: str, skip: int = 0, limit: int = 50) -> list[Asset]: self.list_by_library_called_with.append(library_id) return [a for a in self._assets.values() if a.library_id == library_id] def count_by_library(self, library_id: str) -> int: return len([a for a in self._assets.values() if a.library_id == library_id]) def _dep(name: str): from app import dependencies return getattr(dependencies, name) def _build_route_test_app(project_repo, library_repo, asset_repo): from unittest.mock import MagicMock from app.api.routes.asset_diagnosis import router from app.auth import AuthenticatedUser, get_current_user app = FastAPI() app.include_router(router, prefix="/api/v1") mock_user = MagicMock(spec=AuthenticatedUser) mock_user.id = "user-1" mock_user.email = "test@example.com" mock_user.user.id = "user-1" app.dependency_overrides[get_current_user] = lambda: mock_user app.dependency_overrides[_dep("get_project_repository")] = lambda: project_repo app.dependency_overrides[_dep("get_asset_library_repository")] = lambda: library_repo app.dependency_overrides[_dep("get_asset_repository")] = lambda: asset_repo return app class TestAssetDiagnosisRoute: """路由层测试 — 验证 find_by_project 调用正确性。""" def test_returns_404_when_project_not_found(self): project_repo = _StubProjectRepository() library_repo = _StubAssetLibraryRepository() asset_repo = _StubAssetRepository() app = _build_route_test_app(project_repo, library_repo, asset_repo) client = TestClient(app) resp = client.get("/api/v1/projects/nonexistent/asset-diagnosis") assert resp.status_code == 404 def test_find_by_project_called_with_correct_project_id(self): project = Project(id="proj-123", name="Test", owner_user_id="user-1") library = AssetLibrary(id="lib-1", name="Lib", project_id="proj-123", kind=AssetLibraryKind.VIDEO) project_repo = _StubProjectRepository({"proj-123": project}) library_repo = _StubAssetLibraryRepository({"lib-1": library}) asset_repo = _StubAssetRepository() app = _build_route_test_app(project_repo, library_repo, asset_repo) client = TestClient(app) resp = client.get("/api/v1/projects/proj-123/asset-diagnosis") assert resp.status_code == 200 assert library_repo.find_by_project_called_with == ["proj-123"] def test_list_by_library_called_for_each_library(self): project = Project(id="proj-1", name="Test", owner_user_id="user-1") lib1 = AssetLibrary(id="lib-1", name="Lib1", project_id="proj-1", kind=AssetLibraryKind.VIDEO) lib2 = AssetLibrary(id="lib-2", name="Lib2", project_id="proj-1", kind=AssetLibraryKind.VOICE) project_repo = _StubProjectRepository({"proj-1": project}) library_repo = _StubAssetLibraryRepository({"lib-1": lib1, "lib-2": lib2}) asset_repo = _StubAssetRepository() app = _build_route_test_app(project_repo, library_repo, asset_repo) client = TestClient(app) resp = client.get("/api/v1/projects/proj-1/asset-diagnosis") assert resp.status_code == 200 assert set(asset_repo.list_by_library_called_with) == {"lib-1", "lib-2"} def test_find_by_project_not_list_by_project(self): """路由调用 find_by_project 而非 list_by_project(否则会触发 AssertionError)。""" project = Project(id="proj-1", name="Test", owner_user_id="user-1") library = AssetLibrary(id="lib-1", name="Lib", project_id="proj-1", kind=AssetLibraryKind.VIDEO) project_repo = _StubProjectRepository({"proj-1": project}) library_repo = _StubAssetLibraryRepository({"lib-1": library}) asset_repo = _StubAssetRepository() app = _build_route_test_app(project_repo, library_repo, asset_repo) client = TestClient(app) resp = client.get("/api/v1/projects/proj-1/asset-diagnosis") # 如果调用了 list_by_project,会抛 AssertionError → 500 assert resp.status_code == 200