"""Scripts routes 单元测试 — Issue #1795 + #1894 清理. CI 增量映射: scripts.py → test_scripts.py 本文件同时覆盖 routes/scripts.py 和 schemas/script.py 的增量覆盖率。 #1894: 删除 title_text/title_category/title_config 三字段,仅保留 title/content/segments/tags。 """ from __future__ import annotations from datetime import UTC, datetime, timezone from unittest.mock import MagicMock, patch import pytest from app.schemas.script import ( CreateScriptRequest, ScriptListResponse, ScriptResponse, ScriptSegment, UpdateScriptRequest, ) 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="") 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 def test_partial_update_content_only(self): r = UpdateScriptRequest(content="新内容") assert r.title is None assert r.content == "新内容" class TestScriptResponse: def test_response_construction(self): now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=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 assert r.title == "标题" assert r.tags == ["t1"] def test_response_defaults(self): now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=UTC) r = ScriptResponse( id="s1", user_id="u1", title="标题", content="", segments=[], tags=[], created_at=now, updated_at=now, ) assert r.segments == [] assert r.tags == [] 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=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 class TestRouteHandlers: def _make_auth_user(self, user_id="u1"): user = MagicMock() user.id = user_id auth = MagicMock() auth.user = user return auth def _make_mock_script(self, **overrides): m = MagicMock() m.id = overrides.get("id", "s1") m.user_id = overrides.get("user_id", "u1") m.title = overrides.get("title", "测试") m.content = overrides.get("content", "内容") m.segments = overrides.get("segments", [{"text": "段1", "duration": None}]) m.tags = overrides.get("tags", []) m.created_at = overrides.get("created_at", datetime(2026, 9, 8, tzinfo=UTC)) m.updated_at = overrides.get("updated_at", datetime(2026, 9, 8, tzinfo=UTC)) return m def test_create_route_calls_service(self): from app.api.routes.scripts import create_script svc = MagicMock() svc.create_script.return_value = self._make_mock_script(content="内容") 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() call_kwargs = svc.create_script.call_args.kwargs assert "title_text" not in call_kwargs assert "title_category" not in call_kwargs assert "title_config" not in call_kwargs def test_list_route_returns_paginated(self): from app.api.routes.scripts import list_scripts svc = MagicMock() svc.list_scripts.return_value = ([self._make_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() svc.get_script.return_value = self._make_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() svc.update_script.return_value = self._make_mock_script(title="新标题") req = UpdateScriptRequest(title="新标题") auth = self._make_auth_user() result = update_script("s1", req, authenticated_user=auth, svc=svc) assert result.title == "新标题" call_kwargs = svc.update_script.call_args.kwargs assert "title_text" not in call_kwargs assert "title_category" not in call_kwargs assert "title_config" not in call_kwargs 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) 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