Files
xiaoxia-saas/tests/unit/test_script_title_integration.py
T
xiaoxia ea8e17e80d
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 48s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m20s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m2s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m36s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 25h3m6s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 25h3m19s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 25h2m38s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 25h2m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 25h2m33s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 25h2m37s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 25h2m37s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 25h2m38s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 25h2m40s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 25h2m40s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 25h2m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 25h2m26s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 25h2m28s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 25h2m38s
fix(#1894): 修复缺失的ScriptModel字段、ScriptService方法、schema字段和deprecated header
2026-09-15 14:14:04 +08:00

286 lines
12 KiB
Python

"""#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 uuid
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
import pytest
from fastapi.testclient import TestClient
from app.main import app
# ── 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 ───────────────────────────────────────────
class TestScriptsRoutesNewFields:
"""验证 scripts API 路由正确处理新字段."""
@patch("app.api.routes.scripts._get_service")
@patch("app.auth.get_current_user")
def test_create_script_passes_title_fields(self, mock_auth, mock_svc_dep):
mock_auth.return_value = MagicMock(user=MagicMock(id="user-001"))
mock_script = _make_script(
title_text="测试标题",
title_category="片头",
title_config={"text": "测试标题", "font_size": 48},
)
mock_svc = MagicMock()
mock_svc.create_script.return_value = mock_script
mock_svc_dep.return_value = mock_svc
client = TestClient(app)
resp = client.post(
"/scripts",
json={
"title": "新文案",
"content": "内容",
"title_text": "测试标题",
"title_category": "片头",
"title_config": {"text": "测试标题", "font_size": 48},
},
headers={"X-User-Id": "user-001"},
)
assert resp.status_code == 201
# 验证 service 被调用时传入了新字段
call_kwargs = 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}
@patch("app.api.routes.scripts._get_service")
@patch("app.auth.get_current_user")
def test_get_script_response_includes_title_fields(self, mock_auth, mock_svc_dep):
mock_auth.return_value = MagicMock(user=MagicMock(id="user-001"))
mock_script = _make_script(
title_text="响应标题",
title_category="片尾",
title_config={"text": "响应标题", "position": "bottom"},
)
mock_svc = MagicMock()
mock_svc.get_script.return_value = mock_script
mock_svc_dep.return_value = mock_svc
client = TestClient(app)
resp = client.get("/scripts/script-123", headers={"X-User-Id": "user-001"})
assert resp.status_code == 200
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."""
@patch("app.api.routes.titles._get_title_repository")
@patch("app.auth.get_current_user")
def test_list_titles_has_warning_header(self, mock_auth, mock_repo_dep):
mock_auth.return_value = MagicMock(user=MagicMock(id="user-001"))
mock_repo = MagicMock()
mock_repo.count_by_user.return_value = 0
mock_repo_dep.return_value = mock_repo
client = TestClient(app)
resp = client.get("/titles", headers={"X-User-Id": "user-001"})
assert resp.status_code == 200
assert "Warning" in resp.headers or "warning" in {k.lower() for k in resp.headers}
@patch("app.api.routes.titles._get_title_repository")
@patch("app.auth.get_current_user")
def test_get_title_has_warning_header(self, mock_auth, mock_repo_dep):
mock_auth.return_value = MagicMock(user=MagicMock(id="user-001"))
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),
)
mock_repo = MagicMock()
mock_repo_dep.return_value = mock_repo
with patch("packages.application.title_library.use_cases.GetTitleLibraryUseCase") as mock_uc:
mock_uc.return_value.execute.return_value = mock_item
client = TestClient(app)
resp = client.get("/titles/t1", headers={"X-User-Id": "user-001"})
assert resp.status_code == 200
warning_header = resp.headers.get("Warning", "")
assert "1894" in warning_header or "deprecated" in warning_header.lower() or "废弃" in warning_header