From 89e9e24a5e355f397daa002f26b0b11165e2ff01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E5=BA=94?= Date: Tue, 15 Sep 2026 10:31:37 +0800 Subject: [PATCH] test(p0): add unit tests for default template fallback (diff cov gate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 覆盖 resolve_draft_plan_id 空tid兜底/400/404/草稿复用/全局模板分支 - 满足 diff coverage ≥40% 门槛 --- .../unit/test_default_template_fallback_p0.py | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 tests/unit/test_default_template_fallback_p0.py diff --git a/tests/unit/test_default_template_fallback_p0.py b/tests/unit/test_default_template_fallback_p0.py new file mode 100644 index 000000000..e63e06d8c --- /dev/null +++ b/tests/unit/test_default_template_fallback_p0.py @@ -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")