"""#1894 废弃标题库整合到文案库 — 集成测试. 覆盖: - ScriptModel 新字段 (title_text / title_category / title_config) - ScriptService CRUD 新字段支持 - ScriptService.get_title_config_for_script 方法 - Scripts API 路由的新字段传递 - title_libraries API deprecated Warning header """ from __future__ import annotations import os import sys import uuid from datetime import UTC, datetime, timezone from pathlib import Path from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient # 确保 apps/api 在 sys.path 中(conftest 已加 root,但 apps/api 也需要) _APPS_API = str(Path(__file__).resolve().parents[2] / "apps" / "api") if _APPS_API not in sys.path: sys.path.insert(0, _APPS_API) os.environ.setdefault("JWT_SECRET_KEY", "dev-secret-key-for-testing") from main import app # noqa: E402 # ── helpers ────────────────────────────────────────────────────────────── def _make_script(**overrides): """构造一个模拟 ScriptModel 对象.""" defaults = dict( id=str(uuid.uuid4()), user_id="user-001", title="测试文案", content="这是内容", segments=[], tags=["测试"], title_text="开场大标题", title_category="片头", title_config={ "text": "开场大标题", "font": "思源黑体", "font_size": 48, "font_color": "#FFFFFF", "position": "top", }, created_at=datetime(2026, 9, 1, tzinfo=timezone.utc), updated_at=datetime(2026, 9, 1, tzinfo=timezone.utc), ) defaults.update(overrides) return MagicMock(**defaults) # ── TestScriptModelNewFields ───────────────────────────────────────────── class TestScriptModelNewFields: """验证 ScriptModel 新增字段的定义.""" def test_model_has_title_text_column(self): from packages.adapters.sqlalchemy_impl.models import ScriptModel assert hasattr(ScriptModel, "title_text") col = ScriptModel.__table__.columns["title_text"] assert col is not None assert str(col.type) == "VARCHAR(500)" def test_model_has_title_category_column(self): from packages.adapters.sqlalchemy_impl.models import ScriptModel assert hasattr(ScriptModel, "title_category") col = ScriptModel.__table__.columns["title_category"] assert col is not None assert str(col.type) == "VARCHAR(50)" def test_model_has_title_config_column(self): from packages.adapters.sqlalchemy_impl.models import ScriptModel assert hasattr(ScriptModel, "title_config") col = ScriptModel.__table__.columns["title_config"] assert col is not None def test_model_defaults(self): """新字段默认值为空字符串/空 dict.""" from packages.adapters.sqlalchemy_impl.models import ScriptModel s = ScriptModel(id="x", user_id="u", title="t") # 检查 default 值 assert ScriptModel.__table__.columns["title_text"].default.arg == "" assert ScriptModel.__table__.columns["title_category"].default.arg == "" # ── TestScriptServiceTitleConfig ───────────────────────────────────────── class TestScriptServiceTitleConfig: """验证 ScriptService 新方法 get_title_config_for_script.""" def test_get_title_config_returns_script_config(self): from app.services.script_service import ScriptService db = MagicMock() mock_script = _make_script( title_text="从文案读取", title_config={"text": "从文案读取", "font": "Arial", "font_size": 36}, ) db.query.return_value.filter.return_value.first.return_value = mock_script svc = ScriptService(db) result = svc.get_title_config_for_script("script-1", "user-001") assert result["text"] == "从文案读取" assert result["font"] == "Arial" assert result["font_size"] == 36 def test_get_title_config_fills_text_from_title_text(self): """title_config 为空时,用 title_text 填充 text 字段.""" from app.services.script_service import ScriptService db = MagicMock() mock_script = _make_script( title_text="纯文本标题", title_config={}, ) db.query.return_value.filter.return_value.first.return_value = mock_script svc = ScriptService(db) result = svc.get_title_config_for_script("script-2", "user-001") assert result["text"] == "纯文本标题" def test_get_title_config_raises_on_not_found(self): from app.services.script_service import ScriptNotFoundError, ScriptService db = MagicMock() db.query.return_value.filter.return_value.first.return_value = None svc = ScriptService(db) with pytest.raises(ScriptNotFoundError): svc.get_title_config_for_script("nonexistent", "user-001") def test_get_title_config_validates_user_ownership(self): """script 不属于当前用户时应抛异常.""" from app.services.script_service import ScriptNotFoundError, ScriptService db = MagicMock() db.query.return_value.filter.return_value.first.return_value = None # 不同用户查不到 svc = ScriptService(db) with pytest.raises(ScriptNotFoundError): svc.get_title_config_for_script("script-other-user", "user-001") # ── TestScriptServiceCreateWithNewFields ───────────────────────────────── class TestScriptServiceCreateWithNewFields: """验证 create_script 和 update_script 支持新字段.""" def test_create_script_with_title_fields(self): from app.services.script_service import ScriptService db = MagicMock() svc = ScriptService(db) script = svc.create_script( user_id="user-001", title="新文案", content="内容", title_text="标题文字", title_category="片尾", title_config={"text": "标题文字", "font_size": 24}, ) db.add.assert_called_once() db.commit.assert_called_once() assert script.title_text == "标题文字" assert script.title_category == "片尾" assert script.title_config == {"text": "标题文字", "font_size": 24} def test_update_script_title_fields(self): from app.services.script_service import ScriptService db = MagicMock() existing = _make_script(title_text="旧标题", title_category="旧分类", title_config={"old": True}) db.query.return_value.filter.return_value.first.return_value = existing svc = ScriptService(db) updated = svc.update_script( script_id=existing.id, user_id="user-001", title_text="新标题", title_category="新分类", title_config={"new": True}, ) assert updated.title_text == "新标题" assert updated.title_category == "新分类" assert updated.title_config == {"new": True} # ── TestScriptsRoutesNewFields ─────────────────────────────────────────── def _make_mock_auth_user(user_id="user-001"): """创建 mock 认证用户.""" return MagicMock(user=MagicMock(id=user_id)) class TestScriptsRoutesNewFields: """验证 scripts API 路由正确处理新字段 — 使用 dependency_overrides 绕过真实 DB/Auth.""" def setup_method(self): from app.api.routes.scripts import _get_service, get_current_user self._mock_svc = MagicMock() self._mock_user = _make_mock_auth_user() def _override_svc(): return self._mock_svc def _override_user(): return self._mock_user app.dependency_overrides[_get_service] = _override_svc app.dependency_overrides[get_current_user] = _override_user self.client = TestClient(app) def teardown_method(self): app.dependency_overrides.clear() def test_create_script_passes_title_fields(self): mock_script = _make_script( title_text="测试标题", title_category="片头", title_config={"text": "测试标题", "font_size": 48}, ) self._mock_svc.create_script.return_value = mock_script resp = self.client.post( "/api/v1/scripts", json={ "title": "新文案", "content": "内容", "title_text": "测试标题", "title_category": "片头", "title_config": {"text": "测试标题", "font_size": 48}, }, ) assert resp.status_code == 201, resp.text call_kwargs = self._mock_svc.create_script.call_args[1] assert call_kwargs["title_text"] == "测试标题" assert call_kwargs["title_category"] == "片头" assert call_kwargs["title_config"] == {"text": "测试标题", "font_size": 48} def test_get_script_response_includes_title_fields(self): mock_script = _make_script( title_text="响应标题", title_category="片尾", title_config={"text": "响应标题", "position": "bottom"}, ) self._mock_svc.get_script.return_value = mock_script resp = self.client.get("/api/v1/scripts/script-123") assert resp.status_code == 200, resp.text data = resp.json() assert data["title_text"] == "响应标题" assert data["title_category"] == "片尾" assert data["title_config"]["position"] == "bottom" # ── TestTitleLibraryDeprecated ─────────────────────────────────────────── class TestTitleLibraryDeprecated: """验证 title_libraries API 返回 deprecated Warning header — 使用 dependency_overrides 绕过真实 DB/Auth.""" def setup_method(self): from app.api.routes.titles import _get_title_repository, get_current_user from app.dependencies import get_user_repository self._mock_repo = MagicMock() self._mock_user_repo = MagicMock() self._mock_user = _make_mock_auth_user() app.dependency_overrides[_get_title_repository] = lambda: self._mock_repo app.dependency_overrides[get_user_repository] = lambda: self._mock_user_repo app.dependency_overrides[get_current_user] = lambda: self._mock_user self.client = TestClient(app) def teardown_method(self): app.dependency_overrides.clear() def test_list_titles_has_warning_header(self): # list_titles 调 use_case + repo, 注入真实用例但 mock 掉 repo 的 list/count self._mock_repo.list_by_user.return_value = [] self._mock_repo.count_by_user.return_value = 0 resp = self.client.get("/api/v1/titles") assert resp.status_code == 200, resp.text headers_lower = {k.lower(): v for k, v in resp.headers.items()} assert "warning" in headers_lower or "deprecation" in headers_lower assert "1894" in resp.headers.get("Warning", "") or "1894" in resp.headers.get("warning", "") def test_get_title_has_warning_header(self): from packages.domain.title_library import TitleLibraryItem mock_item = TitleLibraryItem( id="t1", user_id="user-001", name="测试", text="标题文字", category="通用", description="", tags=[], usage_count=0, is_active=True, created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), updated_at=datetime(2026, 1, 1, tzinfo=timezone.utc), ) self._mock_repo.get.return_value = mock_item resp = self.client.get("/api/v1/titles/t1") assert resp.status_code == 200, resp.text warning_header = resp.headers.get("Warning", "") or resp.headers.get("warning", "") assert "1894" in warning_header or "deprecated" in warning_header.lower()