test(p0): add unit tests for default template fallback (diff cov gate)
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 2s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m25s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 20s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 4m39s
AI Code Review / AI Code Review (pull_request) Successful in 6m31s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 3m45s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m12s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m57s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 11m11s
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 8m56s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 39s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 57s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 28h38m5s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 28h46m40s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 28h46m58s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 28h47m24s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 28h50m35s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 28h50m35s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 28h50m35s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 28h37m29s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 28h37m29s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 28h37m29s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 28h46m30s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 28h46m3s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 28h46m3s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 28h37m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 28h46m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 28h46m21s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 28h46m21s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 28h46m47s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 28h50m36s

- 覆盖 resolve_draft_plan_id 空tid兜底/400/404/草稿复用/全局模板分支
- 满足 diff coverage ≥40% 门槛
This commit is contained in:
灵应
2026-09-15 10:31:37 +08:00
parent c243c4dd58
commit 89e9e24a5e
@@ -0,0 +1,146 @@
"""P0 #1922 默认模板自动兜底单元测试。
覆盖 resolve_draft_plan_id 的核心路径(使用可mock的外部依赖):
- 空 tid 时调用 get_or_create_default_template_id 兜底
- auto_create_default=False 时空tid抛400
- 模板不存在/无权访问抛404
- 已有草稿直接返回 plan_id
- 旧模板(templates表)走 generate_from_template 创建草稿
_get_or_create_default_template_id 是薄SQL封装,其逻辑分支通过依赖注入路径间接覆盖;
直接SQL分支通过integration test/容器验证,不做单元级mock(避免内部import脆弱mock)。
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from app.api.routes.templates_editor.dependencies import resolve_draft_plan_id
class TestResolveDraftPlanId:
"""resolve_draft_plan_id 行为。"""
def _make_services(self, global_tpl=None, draft=None):
tpl_svc = MagicMock()
plan_svc = MagicMock()
tpl_svc.get_template.return_value = global_tpl
tpl_svc.get_template_draft.return_value = draft
return (tpl_svc, plan_svc)
def _make_user(self, uid: str = "user-1"):
u = MagicMock()
u.user.id = uid
return u
def test_empty_tid_auto_fallback_creates_default_then_draft(self):
"""template_id 为空 + auto_create_default=True:先兜底拿到tid,再走旧模板草稿创建。"""
db = MagicMock()
services = self._make_services()
user = self._make_user()
old_tpl = MagicMock()
old_tpl.id = "tpl-auto"
old_tpl.name = "默认配音模板"
old_tpl.mode = "voice_over"
old_tpl.segments = [MagicMock(id="seg-1", segment_order=0, duration_min=1.0, duration_max=30.0)]
mock_repo = MagicMock()
mock_repo.get_active.return_value = old_tpl
mock_plan = MagicMock()
mock_plan.id = "plan-new"
mock_generator = MagicMock()
mock_generator.generate_from_template.return_value = {"plan": mock_plan}
with (
patch("app.api.routes.templates_editor.dependencies.SQLAlchemyTemplateRepository", return_value=mock_repo),
patch(
"app.api.routes.templates_editor.dependencies.get_or_create_default_template_id",
return_value="tpl-auto",
) as mock_fb,
patch("app.services.plan_generator_service.PlanGeneratorService", return_value=mock_generator),
):
plan_id = resolve_draft_plan_id(
template_id="",
services=services,
current_user=user,
db=db,
auto_create_default=True,
)
assert plan_id == "plan-new"
mock_fb.assert_called_once_with(db, "user-1")
def test_empty_tid_without_auto_raises_400(self):
"""auto_create_default=False 且 tid 为空 → 400。"""
from fastapi import HTTPException
db = MagicMock()
services = self._make_services()
with pytest.raises(HTTPException) as exc:
resolve_draft_plan_id(
template_id="",
services=services,
current_user=self._make_user(),
db=db,
auto_create_default=False,
)
assert exc.value.status_code == 400
def test_missing_template_raises_404(self):
"""tid 存在但模板找不到/无权访问 → 404。"""
from fastapi import HTTPException
db = MagicMock()
services = self._make_services() # get_template returns None
mock_repo = MagicMock()
mock_repo.get_active.return_value = None
with patch("app.api.routes.templates_editor.dependencies.SQLAlchemyTemplateRepository", return_value=mock_repo):
with pytest.raises(HTTPException) as exc:
resolve_draft_plan_id(
template_id="tpl-ghost",
services=services,
current_user=self._make_user(),
db=db,
auto_create_default=False,
)
assert exc.value.status_code == 404
def test_existing_draft_returned_directly(self):
"""已有模板草稿直接返回 plan_id,不重复创建。"""
db = MagicMock()
draft = MagicMock()
draft.id = "plan-existing"
services = self._make_services(draft=draft)
old_tpl = MagicMock()
old_tpl.id = "tpl-1"
old_tpl.segments = []
mock_repo = MagicMock()
mock_repo.get_active.return_value = old_tpl
with patch("app.api.routes.templates_editor.dependencies.SQLAlchemyTemplateRepository", return_value=mock_repo):
plan_id = resolve_draft_plan_id(
template_id="tpl-1",
services=services,
current_user=self._make_user(),
db=db,
auto_create_default=False,
)
assert plan_id == "plan-existing"
# 没触发generate
services[1].update_plan_config.assert_not_called()
def test_global_template_creates_draft_via_new_service(self):
"""全局模板(新系统edit_templates表存在)走tpl_svc.create_template_draft。"""
db = MagicMock()
services = self._make_services(global_tpl=MagicMock(), draft=None)
new_draft = MagicMock()
new_draft.id = "plan-global"
services[0].create_template_draft.return_value = new_draft
plan_id = resolve_draft_plan_id(
template_id="tpl-global",
services=services,
current_user=self._make_user(),
db=db,
auto_create_default=False,
)
assert plan_id == "plan-global"
services[0].create_template_draft.assert_called_once_with("tpl-global", user_id="user-1")