"""#1894 废弃标题库整合到文案库 — 单元测试. 覆盖: - ScriptModel 新增字段 (title_text, title_category, title_config) - ScriptService 新增字段的 CRUD 操作 - _normalize_title_config 字段名归一化 - get_title_config_for_script 辅助方法 - list_scripts 按 title_category 筛选 """ from __future__ import annotations from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pytest from app.services.script_service import ScriptNotFoundError, ScriptService # ── helpers ────────────────────────────────────────────────────────────────── def _make_mock_script( script_id="s1", user_id="u1", title="测试文案", content="正文内容", segments=None, tags=None, title_text="", title_category="default", title_config=None, ): m = MagicMock() m.id = script_id m.user_id = user_id m.title = title m.content = content m.segments = segments if segments is not None else [{"text": "第一段", "duration": None}] m.tags = tags if tags is not None else ["口播"] m.title_text = title_text m.title_category = title_category m.title_config = title_config if title_config is not None else {} m.created_at = datetime(2026, 9, 8, 12, 0, 0, tzinfo=timezone.utc) m.updated_at = datetime(2026, 9, 8, 12, 0, 0, tzinfo=timezone.utc) return m def _make_service(db=None): if db is None: db = MagicMock() return ScriptService(db), db # ── _normalize_title_config ────────────────────────────────────────────────── class TestNormalizeTitleConfig: def test_empty_config(self): assert ScriptService._normalize_title_config(None) == {} assert ScriptService._normalize_title_config({}) == {} def test_standard_fields_pass_through(self): config = {"font": "思源黑体", "size": 48, "color": "#FFFFFF", "position": "top"} result = ScriptService._normalize_title_config(config) assert result == config def test_legacy_font_size_mapped(self): config = {"font_size": 36} result = ScriptService._normalize_title_config(config) assert result["size"] == 36 assert result["font_size"] == 36 # 旧字段保留 def test_legacy_font_preset_mapped(self): config = {"font_preset": "bold"} result = ScriptService._normalize_title_config(config) assert result["font"] == "bold" assert result["font_preset"] == "bold" def test_legacy_font_color_mapped(self): config = {"font_color": "#FF0000"} result = ScriptService._normalize_title_config(config) assert result["color"] == "#FF0000" assert result["font_color"] == "#FF0000" def test_standard_fields_not_overwritten_by_legacy(self): """如果同时存在标准字段和旧字段,标准字段优先.""" config = {"size": 48, "font_size": 36, "font": "思源黑体", "font_preset": "bold"} result = ScriptService._normalize_title_config(config) assert result["size"] == 48 # 标准字段优先 assert result["font"] == "思源黑体" def test_does_not_mutate_original(self): original = {"font_size": 36} result = ScriptService._normalize_title_config(original) assert "size" not in original # 原字典不被修改 assert "size" in result # ── create with title fields ───────────────────────────────────────────────── class TestCreateScriptWithTitleFields: def test_create_with_title_text(self): svc, db = _make_service() with patch("app.services.script_service.ScriptModel") as MockModel: instance = _make_mock_script(title_text="精选标题") MockModel.return_value = instance svc.create_script(user_id="u1", title="文案标题", title_text="精选标题") call_kwargs = MockModel.call_args[1] assert call_kwargs["title_text"] == "精选标题" def test_create_with_title_category(self): svc, db = _make_service() with patch("app.services.script_service.ScriptModel") as MockModel: instance = _make_mock_script(title_category="科技") MockModel.return_value = instance svc.create_script(user_id="u1", title="文案标题", title_category="科技") call_kwargs = MockModel.call_args[1] assert call_kwargs["title_category"] == "科技" def test_create_with_title_config(self): svc, db = _make_service() config = {"font": "思源黑体", "size": 48, "color": "#FFFFFF"} with patch("app.services.script_service.ScriptModel") as MockModel: instance = _make_mock_script(title_config=config) MockModel.return_value = instance svc.create_script(user_id="u1", title="文案标题", title_config=config) call_kwargs = MockModel.call_args[1] assert call_kwargs["title_config"] == config def test_create_title_config_normalized(self): """创建时 title_config 应自动归一化旧字段名.""" svc, db = _make_service() legacy_config = {"font_size": 36, "font_preset": "bold", "font_color": "#FF0000"} with patch("app.services.script_service.ScriptModel") as MockModel: MockModel.return_value = _make_mock_script() svc.create_script(user_id="u1", title="文案标题", title_config=legacy_config) call_kwargs = MockModel.call_args[1] assert call_kwargs["title_config"]["size"] == 36 assert call_kwargs["title_config"]["font"] == "bold" assert call_kwargs["title_config"]["color"] == "#FF0000" def test_create_defaults_for_title_fields(self): svc, db = _make_service() with patch("app.services.script_service.ScriptModel") as MockModel: MockModel.return_value = _make_mock_script() svc.create_script(user_id="u1", title="文案标题") call_kwargs = MockModel.call_args[1] assert call_kwargs["title_text"] == "" assert call_kwargs["title_category"] == "default" assert call_kwargs["title_config"] == {} # ── update with title fields ───────────────────────────────────────────────── class TestUpdateScriptWithTitleFields: def _setup_mock_script(self, db): mock_script = _make_mock_script() chain = MagicMock() chain.filter.return_value = chain chain.first.return_value = mock_script db.query.return_value = chain return mock_script def test_update_title_text(self): svc, db = _make_service() mock_script = self._setup_mock_script(db) svc.update_script("s1", "u1", title_text="新标题文本") assert mock_script.title_text == "新标题文本" def test_update_title_category(self): svc, db = _make_service() mock_script = self._setup_mock_script(db) svc.update_script("s1", "u1", title_category="娱乐") assert mock_script.title_category == "娱乐" def test_update_title_config_normalized(self): svc, db = _make_service() mock_script = self._setup_mock_script(db) legacy = {"font_size": 42} svc.update_script("s1", "u1", title_config=legacy) assert mock_script.title_config["size"] == 42 def test_update_title_fields_partial(self): """只更新 title_text 不影响其他标题字段.""" svc, db = _make_service() mock_script = self._setup_mock_script(db) mock_script.title_category = "科技" mock_script.title_config = {"font": "思源黑体"} svc.update_script("s1", "u1", title_text="只改标题文本") assert mock_script.title_text == "只改标题文本" assert mock_script.title_category == "科技" assert mock_script.title_config == {"font": "思源黑体"} # ── get_title_config_for_script ────────────────────────────────────────────── class TestGetTitleConfigForScript: def test_returns_merged_config(self): svc, db = _make_service() config = {"font": "思源黑体", "size": 48} mock_script = _make_mock_script(title_text="我的标题", title_config=config) chain = MagicMock() chain.filter.return_value = chain chain.first.return_value = mock_script db.query.return_value = chain result = svc.get_title_config_for_script("s1", "u1") assert result["text"] == "我的标题" assert result["font"] == "思源黑体" assert result["size"] == 48 def test_text_does_not_override_existing(self): """title_config 里已有的 text 不被 title_text 覆盖.""" svc, db = _make_service() config = {"text": "config里的标题", "font": "思源黑体"} mock_script = _make_mock_script(title_text="title_text里的标题", title_config=config) chain = MagicMock() chain.filter.return_value = chain chain.first.return_value = mock_script db.query.return_value = chain result = svc.get_title_config_for_script("s1", "u1") assert result["text"] == "config里的标题" # setdefault 不覆盖已有值 def test_empty_title_text(self): svc, db = _make_service() mock_script = _make_mock_script(title_text="", title_config={"font": "Arial"}) chain = MagicMock() chain.filter.return_value = chain chain.first.return_value = mock_script db.query.return_value = chain result = svc.get_title_config_for_script("s1", "u1") assert "text" not in result # title_text 为空时不添加 text 字段 assert result["font"] == "Arial" def test_script_not_found_raises(self): svc, db = _make_service() chain = MagicMock() chain.filter.return_value = chain chain.first.return_value = None db.query.return_value = chain with pytest.raises(ScriptNotFoundError): svc.get_title_config_for_script("nonexistent", "u1") # ── list with title_category filter ────────────────────────────────────────── class TestListScriptsByTitleCategory: def test_filter_by_title_category(self): svc, db = _make_service() items = [_make_mock_script("s1", title_category="科技")] chain = MagicMock() chain.filter.return_value = chain chain.count.return_value = 1 chain.order_by.return_value = chain chain.offset.return_value = chain chain.limit.return_value = chain chain.all.return_value = items db.query.return_value = chain result, total = svc.list_scripts("u1", title_category="科技") assert total == 1 # filter 至少调用 2 次:user_id + title_category assert chain.filter.call_count >= 2 def test_no_title_category_filter(self): svc, db = _make_service() chain = MagicMock() chain.filter.return_value = chain chain.count.return_value = 0 chain.order_by.return_value = chain chain.offset.return_value = chain chain.limit.return_value = chain chain.all.return_value = [] db.query.return_value = chain result, total = svc.list_scripts("u1") # filter 只调用 1 次:仅 user_id assert chain.filter.call_count == 1