"""Scripts routes 单元测试 — Issue #1795. CI 增量映射: scripts.py → test_scripts.py 本文件同时覆盖 routes/scripts.py 和 schemas/script.py 的增量覆盖率。 """ from __future__ import annotations from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pytest from app.schemas.script import ( CreateScriptRequest, ScriptListResponse, ScriptResponse, ScriptSegment, UpdateScriptRequest, ) # ── Schema 验证测试 ────────────────────────────────────────────────────────── class TestScriptSegment: def test_segment_with_duration(self): s = ScriptSegment(text="测试", duration=5.0) assert s.text == "测试" assert s.duration == 5.0 def test_segment_null_duration(self): s = ScriptSegment(text="测试", duration=None) assert s.duration is None def test_segment_default_duration(self): s = ScriptSegment(text="测试") assert s.duration is None class TestCreateScriptRequest: def test_minimal(self): r = CreateScriptRequest(title="标题") assert r.title == "标题" assert r.content == "" assert r.segments == [] assert r.tags == [] def test_full(self): r = CreateScriptRequest( title="标题", content="正文", segments=[ScriptSegment(text="段1", duration=3.0)], tags=["口播"], ) assert len(r.segments) == 1 assert r.tags == ["口播"] def test_title_required(self): with pytest.raises(ValueError): CreateScriptRequest(title="") # min_length=1 def test_title_max_length(self): with pytest.raises(ValueError): CreateScriptRequest(title="x" * 256) class TestUpdateScriptRequest: def test_all_none_default(self): r = UpdateScriptRequest() assert r.title is None assert r.content is None assert r.segments is None assert r.tags is None def test_partial_update(self): r = UpdateScriptRequest(title="新标题") assert r.title == "新标题" assert r.content is None class TestScriptResponse: def test_response_construction(self): now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=timezone.utc) r = ScriptResponse( id="s1", user_id="u1", title="标题", content="内容", segments=[ScriptSegment(text="段1")], tags=["t1"], created_at=now, updated_at=now, ) assert r.id == "s1" assert len(r.segments) == 1 class TestScriptListResponse: def test_empty_list(self): r = ScriptListResponse(items=[], total=0) assert r.total == 0 assert r.items == [] def test_with_items(self): now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=timezone.utc) item = ScriptResponse( id="s1", user_id="u1", title="标题", content="内容", segments=[], tags=[], created_at=now, updated_at=now, ) r = ScriptListResponse(items=[item], total=1) assert r.total == 1 assert len(r.items) == 1 # ── Route handler 逻辑测试 (mock service) ──────────────────────────────────── class TestRouteHandlers: """测试路由层逻辑(不通过 TestClient,直接调用 handler 函数).""" def _make_auth_user(self, user_id="u1"): user = MagicMock() user.id = user_id auth = MagicMock() auth.user = user return auth def test_create_route_calls_service(self): from app.api.routes.scripts import create_script svc = MagicMock() mock_script = MagicMock() mock_script.id = "s1" mock_script.user_id = "u1" mock_script.title = "测试" mock_script.content = "内容" mock_script.segments = [{"text": "段1", "duration": None}] mock_script.tags = [] mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) svc.create_script.return_value = mock_script req = CreateScriptRequest(title="测试", content="内容") auth = self._make_auth_user() result = create_script(req, authenticated_user=auth, svc=svc) assert result.id == "s1" svc.create_script.assert_called_once() def test_list_route_returns_paginated(self): from app.api.routes.scripts import list_scripts svc = MagicMock() mock_script = MagicMock() mock_script.id = "s1" mock_script.user_id = "u1" mock_script.title = "测试" mock_script.content = "" mock_script.segments = [] mock_script.tags = [] mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) svc.list_scripts.return_value = ([mock_script], 1) auth = self._make_auth_user() result = list_scripts(skip=0, limit=50, tag=None, authenticated_user=auth, svc=svc) assert result.total == 1 assert len(result.items) == 1 def test_get_route_found(self): from app.api.routes.scripts import get_script svc = MagicMock() mock_script = MagicMock() mock_script.id = "s1" mock_script.user_id = "u1" mock_script.title = "测试" mock_script.content = "" mock_script.segments = [] mock_script.tags = [] mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) svc.get_script.return_value = mock_script auth = self._make_auth_user() result = get_script("s1", authenticated_user=auth, svc=svc) assert result.id == "s1" def test_get_route_not_found(self): from app.api.routes.scripts import get_script from app.services.script_service import ScriptNotFoundError from fastapi import HTTPException svc = MagicMock() svc.get_script.side_effect = ScriptNotFoundError("not found") auth = self._make_auth_user() with pytest.raises(HTTPException) as exc_info: get_script("nonexistent", authenticated_user=auth, svc=svc) assert exc_info.value.status_code == 404 def test_update_route_success(self): from app.api.routes.scripts import update_script svc = MagicMock() mock_script = MagicMock() mock_script.id = "s1" mock_script.user_id = "u1" mock_script.title = "新标题" mock_script.content = "原内容" mock_script.segments = [] mock_script.tags = [] mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) svc.update_script.return_value = mock_script req = UpdateScriptRequest(title="新标题") auth = self._make_auth_user() result = update_script("s1", req, authenticated_user=auth, svc=svc) assert result.title == "新标题" def test_update_route_not_found(self): from app.api.routes.scripts import update_script from app.services.script_service import ScriptNotFoundError from fastapi import HTTPException svc = MagicMock() svc.update_script.side_effect = ScriptNotFoundError("not found") auth = self._make_auth_user() req = UpdateScriptRequest(title="x") with pytest.raises(HTTPException) as exc_info: update_script("bad", req, authenticated_user=auth, svc=svc) assert exc_info.value.status_code == 404 def test_delete_route_success(self): from app.api.routes.scripts import delete_script svc = MagicMock() svc.delete_script.return_value = True auth = self._make_auth_user() result = delete_script("s1", authenticated_user=auth, svc=svc) # Should return None (204 No Content) assert result is None def test_delete_route_not_found(self): from app.api.routes.scripts import delete_script from fastapi import HTTPException svc = MagicMock() svc.delete_script.return_value = False auth = self._make_auth_user() with pytest.raises(HTTPException) as exc_info: delete_script("bad", authenticated_user=auth, svc=svc) assert exc_info.value.status_code == 404