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_body
351 lines
12 KiB
Python
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
|