7564b50f7e
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m51s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 15m40s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 1m50s
CI/CD Pipeline / Build Staging API Image (push) Successful in 18m32s
CI/CD Pipeline / Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m25s
CI/CD Pipeline / Validate - Style (push) Successful in 3m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 44s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m53s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m4s
CI/CD Pipeline / Validate - Security (push) Successful in 8m10s
CI/CD Pipeline / Unit Tests (push) Successful in 9m9s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Failing after 9h23m26s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 9h48m20s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 9h22m39s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 9h47m32s
CI/CD Pipeline / PR Build Web Image (push) Failing after 9h47m31s
CI/CD Pipeline / PR Build API Image (push) Failing after 9h47m32s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 9h28m53s
CI/CD Pipeline / CI Gate (push) Failing after 9h22m39s
CI/CD Pipeline / Canary Release to Production (push) Failing after 9h22m37s
CI/CD Pipeline / Build Production Web Image (push) Failing after 9h22m39s
CI/CD Pipeline / Build Production API Image (push) Failing after 9h22m39s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 9h28m53s
CI/CD Pipeline / Frontend Lint (push) Failing after 9h31m49s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 10h4m41s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
335 lines
12 KiB
Python
335 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 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()
|