fix(#1894): add alembic 077 migration; fix test imports (from main import app) + URL prefix + dependency_overrides; ASCII Warning header
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m56s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m9s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m45s
AI Code Review / AI Code Review (pull_request) Successful in 6m37s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 7m17s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 18h1m41s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 18h1m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 18h1m5s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 18h1m10s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 18h1m10s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 18h1m12s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 18h1m15s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 18h1m15s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 18h1m16s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 18h0m59s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 18h0m59s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 18h1m13s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 18h1m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 18h36m50s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m56s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m9s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m45s
AI Code Review / AI Code Review (pull_request) Successful in 6m37s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 7m17s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 18h1m41s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 18h1m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 18h1m5s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 18h1m10s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 18h1m10s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 18h1m12s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 18h1m15s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 18h1m15s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 18h1m16s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 18h0m59s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 18h0m59s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 18h1m13s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 18h1m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 18h36m50s
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""#1894: merge title_library into scripts — add title_text/title_category/title_config to scripts
|
||||
|
||||
Revision ID: 077_merge_title_library_into_scripts
|
||||
Revises: 076_membership_points
|
||||
Create Date: 2026-09-15
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "077_merge_title_library_into_scripts"
|
||||
down_revision = "076_membership_points"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# scripts 表新增标题配置三字段
|
||||
with op.batch_alter_table("scripts") as batch:
|
||||
batch.add_column(
|
||||
sa.Column("title_text", sa.String(500), nullable=False, server_default=""),
|
||||
)
|
||||
batch.add_column(
|
||||
sa.Column("title_category", sa.String(50), nullable=False, server_default=""),
|
||||
)
|
||||
batch.add_column(
|
||||
sa.Column("title_config", sa.JSON, nullable=False, server_default="{}"),
|
||||
)
|
||||
|
||||
# 从 title_libraries 迁移活跃数据到 scripts(若 title_libraries 表存在)
|
||||
conn = op.get_bind()
|
||||
exists = conn.execute(sa.text("SELECT to_regclass('public.title_libraries') IS NOT NULL")).scalar()
|
||||
if exists:
|
||||
conn.execute(sa.text("""
|
||||
INSERT INTO scripts (id, user_id, title, content, segments, tags, title_text, title_category, title_config, created_at, updated_at)
|
||||
SELECT
|
||||
gen_random_uuid()::TEXT,
|
||||
tl.user_id,
|
||||
COALESCE(tl.name, '迁移标题'),
|
||||
COALESCE(tl.text, ''),
|
||||
'[]'::JSONB,
|
||||
COALESCE(tl.tags, '[]'::JSONB),
|
||||
COALESCE(tl.text, ''),
|
||||
COALESCE(tl.category, ''),
|
||||
COALESCE(tl."metadata", '{}'::JSONB),
|
||||
tl.created_at,
|
||||
tl.updated_at
|
||||
FROM title_libraries tl
|
||||
WHERE tl.is_active = true
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM scripts s
|
||||
WHERE s.user_id = tl.user_id
|
||||
AND s.title_text = COALESCE(tl.text, '')
|
||||
AND s.title_category = COALESCE(tl.category, '')
|
||||
AND s.created_at = tl.created_at
|
||||
)
|
||||
"""))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("scripts") as batch:
|
||||
batch.drop_column("title_config")
|
||||
batch.drop_column("title_category")
|
||||
batch.drop_column("title_text")
|
||||
@@ -43,12 +43,15 @@ from packages.ports.user_repository import UserRepository
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEPRECATION_WARNING = "标题库 API 已废弃,请迁移到 scripts 的 title_text/title_category/title_config 字段(#1894)"
|
||||
_DEPRECATION_WARNING = (
|
||||
'299 - "Title library API is deprecated; migrate to scripts.title_text/'
|
||||
'title_category/title_config (issue #1894)"'
|
||||
)
|
||||
|
||||
|
||||
def _deprecation_headers() -> dict:
|
||||
"""返回 deprecation Warning header."""
|
||||
return {"Warning": f'299 - {_DEPRECATION_WARNING}'}
|
||||
"""返回 deprecation Warning header (ASCII-only, RFC 7234 §5.5)."""
|
||||
return {"Warning": _DEPRECATION_WARNING, "Deprecation": "true"}
|
||||
|
||||
|
||||
def _log_deprecation(endpoint: str) -> None:
|
||||
|
||||
@@ -10,14 +10,24 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from app.main import app
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -197,25 +207,43 @@ class TestScriptServiceCreateWithNewFields:
|
||||
# ── TestScriptsRoutesNewFields ───────────────────────────────────────────
|
||||
|
||||
|
||||
class TestScriptsRoutesNewFields:
|
||||
"""验证 scripts API 路由正确处理新字段."""
|
||||
def _make_mock_auth_user(user_id="user-001"):
|
||||
"""创建 mock 认证用户."""
|
||||
return MagicMock(user=MagicMock(id=user_id))
|
||||
|
||||
@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"))
|
||||
|
||||
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},
|
||||
)
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.create_script.return_value = mock_script
|
||||
mock_svc_dep.return_value = mock_svc
|
||||
self._mock_svc.create_script.return_value = mock_script
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.post(
|
||||
"/scripts",
|
||||
resp = self.client.post(
|
||||
"/api/v1/scripts",
|
||||
json={
|
||||
"title": "新文案",
|
||||
"content": "内容",
|
||||
@@ -223,33 +251,25 @@ class TestScriptsRoutesNewFields:
|
||||
"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 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}
|
||||
|
||||
@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"))
|
||||
def test_get_script_response_includes_title_fields(self):
|
||||
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
|
||||
self._mock_svc.get_script.return_value = mock_script
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.get("/scripts/script-123", headers={"X-User-Id": "user-001"})
|
||||
resp = self.client.get("/api/v1/scripts/script-123")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.status_code == 200, resp.text
|
||||
data = resp.json()
|
||||
assert data["title_text"] == "响应标题"
|
||||
assert data["title_category"] == "片尾"
|
||||
@@ -260,26 +280,37 @@ class TestScriptsRoutesNewFields:
|
||||
|
||||
|
||||
class TestTitleLibraryDeprecated:
|
||||
"""验证 title_libraries API 返回 deprecated Warning header."""
|
||||
"""验证 title_libraries API 返回 deprecated Warning header — 使用 dependency_overrides 绕过真实 DB/Auth."""
|
||||
|
||||
@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
|
||||
def setup_method(self):
|
||||
from app.api.routes.titles import _get_title_repository, get_current_user
|
||||
from app.dependencies import get_user_repository
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.get("/titles", headers={"X-User-Id": "user-001"})
|
||||
self._mock_repo = MagicMock()
|
||||
self._mock_user_repo = MagicMock()
|
||||
self._mock_user = _make_mock_auth_user()
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "Warning" in resp.headers or "warning" in {k.lower() for k in resp.headers}
|
||||
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)
|
||||
|
||||
@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"))
|
||||
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(
|
||||
@@ -295,13 +326,9 @@ class TestTitleLibraryDeprecated:
|
||||
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
|
||||
self._mock_repo.get.return_value = mock_item
|
||||
|
||||
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
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user