Files
xiaoxia-saas/tests/unit/test_1749_variant_plans_route.py
T
xiaoxia a56b3f7b42
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 13s
CI/CD Pipeline / Build Staging API Image (push) Successful in 23s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 54s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 49s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m41s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m48s
CI/CD Pipeline / Integration Tests (push) Successful in 3m54s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m44s
CI/CD Pipeline / Validate - Style (push) Successful in 4m52s
CI/CD Pipeline / Unit Tests (push) Successful in 9m2s
CI/CD Pipeline / Validate - Security (push) Successful in 12m14s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Failing after 37h47m53s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 37h59m30s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 38h0m13s
CI/CD Pipeline / PR Build Web Image (push) Failing after 37h59m40s
CI/CD Pipeline / PR Build API Image (push) Failing after 37h59m40s
CI/CD Pipeline / Deploy Production (push) Failing after 37h47m15s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 37h47m20s
CI/CD Pipeline / Build Production Web Image (push) Failing after 37h47m20s
CI/CD Pipeline / Build Production API Image (push) Failing after 37h47m20s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 37h58m57s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 37h58m57s
CI/CD Pipeline / Frontend Lint (push) Failing after 37h59m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 37h59m41s
fix(backend): P0 #1911后自动创建默认模板避免剪辑页卡死 (#1914)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-15 01:23:30 +08:00

235 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""#1749 POST /api/v1/generation/variant-plans 路由单元测试。
直接调用路由函数 + mock 依赖(不使用 TestClient,因 httpx2 冲突)。
覆盖:
- 200: 合法请求返回 items
- 400: voice_library_ids 长度不符 (schema validator)
- 400: 缺少 source_edit_plan_id 且无模板兜底 plan
- 400: 选片失败 ValueError -> HTTPException(400)
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from fastapi import HTTPException
def _call_route(request_obj, db=None):
"""直接调用 create_variant_plans 路由函数。"""
from app.api.routes.generation_variant_plans import create_variant_plans
user = MagicMock()
user.user.id = "u1"
if db is None:
db = MagicMock()
return create_variant_plans(request_obj, authenticated_user=user, db=db)
def _base_payload(**kw):
from app.api.routes.generation_variant_plans import VariantPlanRequest
d = {
"template_id": "tpl_1",
"asset_ids": ["a1", "a2"],
"count": 1,
"source_edit_plan_id": "src_plan_1",
}
d.update(kw)
return VariantPlanRequest(**d)
class TestVariantPlansRoute:
def test_200_happy_path(self):
"""合法请求:ensure_variant_plans 返回 plan_id,list_clips 返回 clips。"""
req = _base_payload()
db = MagicMock()
mock_svc_inst = MagicMock()
mock_svc_inst.ensure_variant_plans.return_value = ["plan_v0"]
clip = MagicMock()
clip.id = "c1"
clip.order = 0
clip.asset_id = "a1"
clip.start_time = 0.0
clip.duration = 10.0
clip.clip_type = "main"
clip.transition_effect = "cut"
clip.transition_duration = 0.0
clip.playback_speed = 1.0
clip.text_content = ""
mock_svc_inst.list_clips.return_value = [clip]
mock_svc_cls = MagicMock(return_value=mock_svc_inst)
with (
patch(
"packages.domain.variant_voice_resolver.resolve_variant_voice_ids",
return_value=["voice_1"],
),
patch(
"app.api.routes.generation_tasks._query_voice_durations",
return_value=[30.0],
),
patch(
"app.services.edit_plan_service.EditPlanService",
mock_svc_cls,
),
):
resp = _call_route(req, db)
assert resp.total == 1
assert resp.items[0].plan_id == "plan_v0"
assert len(resp.items[0].clips) == 1
def test_400_voice_library_ids_length_mismatch(self):
"""voice_library_ids 长度 != count -> pydantic model_validator 抛异常。"""
from app.api.routes.generation_variant_plans import VariantPlanRequest
with pytest.raises(Exception): # noqa: B017
VariantPlanRequest(
template_id="tpl_1",
asset_ids=["a1"],
count=3,
source_edit_plan_id="src_plan_1",
voice_library_ids=["v1", "v2"], # len=2 != count=3
)
def test_400_no_source_plan_no_template_fallback(self):
"""无 source_edit_plan_id 且 DB 查不到模板 plan -> HTTPException(400)。"""
from app.api.routes.generation_variant_plans import VariantPlanRequest
req = VariantPlanRequest(
template_id="tpl_nonexist",
asset_ids=[],
count=1,
source_edit_plan_id="",
)
db = MagicMock()
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None
with patch(
"packages.domain.variant_voice_resolver.resolve_variant_voice_ids",
return_value=[""],
):
with pytest.raises(HTTPException) as exc_info:
_call_route(req, db)
assert exc_info.value.status_code == 400
def test_400_selection_failure_value_error(self):
"""ensure_variant_plans 抛 ValueError -> HTTPException(400)。"""
req = _base_payload()
db = MagicMock()
mock_svc_inst = MagicMock()
mock_svc_inst.ensure_variant_plans.side_effect = ValueError("素材池为空")
mock_svc_cls = MagicMock(return_value=mock_svc_inst)
with (
patch(
"packages.domain.variant_voice_resolver.resolve_variant_voice_ids",
return_value=["voice_1"],
),
patch(
"app.api.routes.generation_tasks._query_voice_durations",
return_value=[30.0],
),
patch(
"app.services.edit_plan_service.EditPlanService",
mock_svc_cls,
),
):
with pytest.raises(HTTPException) as exc_info:
_call_route(req, db)
assert exc_info.value.status_code == 400
class TestAutoCreateDefaultTemplate:
"""P0: #1911 前端删除模板选择 UI 后,后端需在无 template_id/source_edit_plan_id 时自动兜底。"""
def test_schema_allows_empty_template_and_source(self):
"""VariantPlanRequest 不再强制要求 template_id/source_edit_plan_id(后端兜底)。"""
from app.api.routes.generation_variant_plans import VariantPlanRequest
req = VariantPlanRequest(asset_ids=["a1"], count=1)
assert req.template_id == ""
assert req.source_edit_plan_id == ""
def test_auto_create_default_when_no_template_no_source(self):
"""无 template_id 且无 source_edit_plan_id 时,后端自动创建默认模板并继续选片。"""
from app.api.routes.generation_variant_plans import VariantPlanRequest
req = VariantPlanRequest(asset_ids=["a1", "a2"], count=1)
db = MagicMock()
# 模拟:用户无现有模板
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None
# 模拟 has_seg subquery 也返回 None
db.query.return_value.filter.return_value.first.return_value = None
mock_svc_inst = MagicMock()
mock_svc_inst.ensure_variant_plans.return_value = ["plan_v0"]
clip = MagicMock()
clip.id = "c1"
clip.order = 0
clip.asset_id = "a1"
clip.start_time = 0.0
clip.duration = 10.0
clip.clip_type = "main"
clip.transition_effect = "cut"
clip.transition_duration = 0.0
clip.playback_speed = 1.0
clip.text_content = ""
mock_svc_inst.list_clips.return_value = [clip]
created_tpl_id = "tpl_default_x"
def fake_create(cmd):
t = MagicMock()
t.id = created_tpl_id
t.user_id = cmd.user_id
t.name = cmd.name
t.mode = cmd.mode
t.segments = []
return t
fake_use_case = MagicMock()
fake_use_case.execute.side_effect = fake_create
# resolve_latest_plan_by_template 第一次调用返回 None(无历史 plan),
# 第二次因 ensure_variant_plans 已处理 source_plan_id,实际在 _get_or_create_default_template_id 后
# 调用 resolve_latest_plan_by_template → 返回 None → 走400。
# 为了让选片走通,直接patch resolve_latest_plan_by_template返回一个假plan
with (
patch(
"packages.domain.variant_voice_resolver.resolve_variant_voice_ids",
return_value=[""],
),
patch(
"app.api.routes.generation_tasks._query_voice_durations",
return_value=[0.0],
),
patch(
"app.services.edit_plan_service.EditPlanService",
MagicMock(return_value=mock_svc_inst),
),
patch(
"app.services.generation_common.resolve_latest_plan_by_template",
return_value="src_plan_new",
),
patch(
"packages.adapters.sqlalchemy_impl.template_repository.SQLAlchemyTemplateRepository",
),
patch(
"packages.application.template.use_cases.CreateTemplateUseCase",
return_value=fake_use_case,
),
):
resp = _call_route(req, db)
assert resp.total == 1
assert resp.items[0].plan_id == "plan_v0"