Files
xiaoxia-saas/tests/unit/test_script_title_integration.py
T

287 lines
12 KiB
Python

"""#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