287 lines
12 KiB
Python
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
|