Files
xiaoxia-saas/tests/unit/test_edit_plan_export_api.py
T
xiaoxia 81f9a47210
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 37s
CI/CD Pipeline / Unit Tests (push) Successful in 2m19s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 2m58s
CI Build & Deploy Pipeline / Build Staging API Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Staging Web Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Has been cancelled
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI Build & Deploy Pipeline / Staging E2E Tests (push) Has been cancelled
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production API Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been cancelled
CI Build & Deploy Pipeline / Deploy Production (push) Has been cancelled
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
pr_title
pr_body
2026-07-17 00:56:52 +08:00

351 lines
12 KiB
Python

"""
导出设置 API 单元测试
覆盖:
- GET /export-presets - 导出预设列表
- GET /{plan_id}/export - 获取导出配置
- PUT /{plan_id}/export - 更新导出配置
"""
from __future__ import annotations
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
from unittest.mock import MagicMock
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
from fastapi import FastAPI
from fastapi.testclient import TestClient
from packages.domain.config_schemas import normalize_plan_config
from packages.domain.edit_plan import EditPlan, EditPlanStatus
# ---------------------------------------------------------------------------
# Stub Repository
# ---------------------------------------------------------------------------
class StubEditPlanRepository:
def __init__(self, plans: dict[str, EditPlan] | None = None):
self._plans = plans or {}
def get(self, plan_id: str) -> Optional[EditPlan]:
return self._plans.get(plan_id)
def create(self, plan: EditPlan) -> EditPlan:
self._plans[plan.id] = plan
return plan
def update(self, plan: EditPlan) -> EditPlan:
self._plans[plan.id] = plan
return plan
def list_all(self, *, status=None, skip=0, limit=50):
return list(self._plans.values())[skip : skip + limit]
def list_by_template(self, template_id, *, status=None, skip=0, limit=50):
return [p for p in self._plans.values() if p.template_id == template_id][skip : skip + limit]
def delete(self, plan_id: str) -> bool:
return self._plans.pop(plan_id, None) is not None
def count(self, *, status=None, template_id=None):
return len(self._plans)
class StubEditPlanClipRepository:
def list_by_plan(self, plan_id, *, status=None, skip=0, limit=100):
return []
def count(self, plan_id, *, status=None):
return 0
def get(self, clip_id: str):
return None
def create(self, clip):
return clip
def update(self, clip):
return clip
def delete(self, clip_id: str) -> bool:
return False
def delete_by_plan(self, plan_id: str) -> int:
return 0
# ---------------------------------------------------------------------------
# Test Fixtures
# ---------------------------------------------------------------------------
def _make_sample_plan(plan_id="plan-001"):
return EditPlan(
id=plan_id,
template_id="tpl-001",
name="测试计划",
status=EditPlanStatus.EDITING,
total_duration=30.0,
config=normalize_plan_config({}),
project_id="",
created_by_user_id="user-001",
created_at=datetime(2026, 7, 16, 10, 0, 0),
updated_at=datetime(2026, 7, 16, 10, 0, 0),
)
def _create_test_app():
import app.services.edit_plan_service as service_module
from app.api.routes.edit_plans import router
plan = _make_sample_plan()
stub_plan_repo = StubEditPlanRepository({plan.id: plan})
stub_clip_repo = StubEditPlanClipRepository()
original_plan_repo = service_module.SQLAlchemyEditPlanRepository
original_clip_repo = service_module.SQLAlchemyEditPlanClipRepository
original_gen_repo = service_module.SQLAlchemyGenerationTaskRepository
service_module.SQLAlchemyEditPlanRepository = lambda db: stub_plan_repo
service_module.SQLAlchemyEditPlanClipRepository = lambda db: stub_clip_repo
service_module.SQLAlchemyGenerationTaskRepository = lambda db: MagicMock()
app = FastAPI()
app.include_router(router, prefix="/api/v1/edit-plans")
def _mock_auth():
mock = MagicMock()
mock.user.id = "user-001"
return mock
import app.api.routes._helpers as helpers_module
original_check = helpers_module.check_project_access
helpers_module.check_project_access = lambda *a, **kw: None
from app.api.routes import edit_plans as main_module
app.dependency_overrides[main_module.get_current_user] = _mock_auth
app.dependency_overrides[main_module.get_db_session] = lambda: MagicMock()
app.dependency_overrides[main_module.get_project_repository] = lambda: MagicMock()
import app.api.routes.edit_plans_export as export_module
app.dependency_overrides[export_module.get_current_user] = _mock_auth
app.dependency_overrides[export_module.get_db_session] = lambda: MagicMock()
app.dependency_overrides[export_module.get_project_repository] = lambda: MagicMock()
def cleanup():
service_module.SQLAlchemyEditPlanRepository = original_plan_repo
service_module.SQLAlchemyEditPlanClipRepository = original_clip_repo
service_module.SQLAlchemyGenerationTaskRepository = original_gen_repo
helpers_module.check_project_access = original_check
return app, stub_plan_repo, cleanup
@pytest.fixture
def export_client():
app, plan_repo, cleanup = _create_test_app()
yield TestClient(app), plan_repo
cleanup()
# ---------------------------------------------------------------------------
# Export Presets 测试
# ---------------------------------------------------------------------------
class TestExportPresets:
def test_list_all_presets(self, export_client):
c, _ = export_client
resp = c.get("/api/v1/edit-plans/export-presets")
assert resp.status_code == 200
data = resp.json()
assert data["total"] >= 5
assert len(data["items"]) == data["total"]
first = data["items"][0]
assert "id" in first
assert "name" in first
assert "resolution" in first
assert "fps" in first
assert "video_bitrate" in first
assert "format" in first
assert "description" in first
assert "size_hint" in first
def test_preset_has_valid_resolution(self, export_client):
c, _ = export_client
resp = c.get("/api/v1/edit-plans/export-presets")
data = resp.json()
for item in data["items"]:
assert "x" in item["resolution"]
assert item["fps"] >= 15
assert item["fps"] <= 60
assert item["format"] in ("mp4", "mov")
# ---------------------------------------------------------------------------
# GET /{plan_id}/export 测试
# ---------------------------------------------------------------------------
class TestGetExportConfig:
def test_default_export_config(self, export_client):
c, _ = export_client
resp = c.get("/api/v1/edit-plans/plan-001/export")
assert resp.status_code == 200
data = resp.json()
assert data["resolution"] == "1080x1920"
assert data["fps"] == 30
assert data["video_bitrate"] == 8000
assert data["audio_bitrate"] == 128
assert data["format"] == "mp4"
assert data["quality_preset"] == "balanced"
assert data["watermark_enabled"] is False
assert data["watermark_text"] == ""
def test_export_not_found(self, export_client):
c, _ = export_client
resp = c.get("/api/v1/edit-plans/plan-nonexist/export")
assert resp.status_code == 404
# ---------------------------------------------------------------------------
# PUT /{plan_id}/export 测试
# ---------------------------------------------------------------------------
class TestUpdateExportConfig:
def test_update_resolution_and_fps(self, export_client):
c, plan_repo = export_client
resp = c.put(
"/api/v1/edit-plans/plan-001/export",
json={"resolution": "720x1280", "fps": 60},
)
assert resp.status_code == 200
data = resp.json()
assert data["resolution"] == "720x1280"
assert data["fps"] == 60
plan = plan_repo.get("plan-001")
assert plan.config["export"]["resolution"] == "720x1280"
assert plan.config["export"]["fps"] == 60
def test_update_bitrate(self, export_client):
c, plan_repo = export_client
resp = c.put(
"/api/v1/edit-plans/plan-001/export",
json={"video_bitrate": 12000, "audio_bitrate": 192},
)
assert resp.status_code == 200
data = resp.json()
assert data["video_bitrate"] == 12000
assert data["audio_bitrate"] == 192
def test_update_format(self, export_client):
c, _ = export_client
resp = c.put(
"/api/v1/edit-plans/plan-001/export",
json={"format": "mov"},
)
assert resp.status_code == 200
assert resp.json()["format"] == "mov"
def test_invalid_format(self, export_client):
c, _ = export_client
resp = c.put(
"/api/v1/edit-plans/plan-001/export",
json={"format": "avi"},
)
assert resp.status_code == 422
def test_update_quality_preset(self, export_client):
c, _ = export_client
resp = c.put(
"/api/v1/edit-plans/plan-001/export",
json={"quality_preset": "best"},
)
assert resp.status_code == 200
assert resp.json()["quality_preset"] == "best"
def test_invalid_quality_preset(self, export_client):
c, _ = export_client
resp = c.put(
"/api/v1/edit-plans/plan-001/export",
json={"quality_preset": "ultimate"},
)
assert resp.status_code == 422
def test_update_watermark(self, export_client):
c, plan_repo = export_client
resp = c.put(
"/api/v1/edit-plans/plan-001/export",
json={"watermark_enabled": True, "watermark_text": "我的视频"},
)
assert resp.status_code == 200
data = resp.json()
assert data["watermark_enabled"] is True
assert data["watermark_text"] == "我的视频"
plan = plan_repo.get("plan-001")
assert plan.config["export"]["watermark_enabled"] is True
assert plan.config["export"]["watermark_text"] == "我的视频"
def test_invalid_resolution_format(self, export_client):
c, _ = export_client
resp = c.put(
"/api/v1/edit-plans/plan-001/export",
json={"resolution": "1080*1920"},
)
assert resp.status_code == 422
def test_resolution_too_large(self, export_client):
c, _ = export_client
resp = c.put(
"/api/v1/edit-plans/plan-001/export",
json={"resolution": "8000x8000"},
)
assert resp.status_code == 422
def test_fps_out_of_range(self, export_client):
c, _ = export_client
resp = c.put(
"/api/v1/edit-plans/plan-001/export",
json={"fps": 120},
)
assert resp.status_code == 422
def test_export_not_found(self, export_client):
c, _ = export_client
resp = c.put(
"/api/v1/edit-plans/plan-nonexist/export",
json={"fps": 30},
)
assert resp.status_code == 404
def test_partial_update_preserves_other_fields(self, export_client):
c, _ = export_client
# 先修改一个
c.put("/api/v1/edit-plans/plan-001/export", json={"resolution": "720x1280"})
# 再修改另一个
resp = c.put("/api/v1/edit-plans/plan-001/export", json={"fps": 60})
data = resp.json()
# 分辨率应该保持
assert data["resolution"] == "720x1280"
# fps 更新了
assert data["fps"] == 60
# 其他默认值不变
assert data["format"] == "mp4"
assert data["video_bitrate"] == 8000