Files
xiaoxia-saas/tests/unit/test_scripts_routes.py
T
xiaoxia 53fb25efcf
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 48s
CI/CD Pipeline / Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m30s
CI/CD Pipeline / Validate - Style (push) Successful in 4m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m33s
CI/CD Pipeline / Validate - Security (push) Successful in 7m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Unit Tests (push) Successful in 10m11s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Failing after 26h14m3s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 26h24m21s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 26h19m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 26h23m44s
CI/CD Pipeline / PR Build API Image (push) Failing after 26h23m44s
CI/CD Pipeline / Deploy Production (push) Failing after 26h13m23s
CI/CD Pipeline / Build Production Web Image (push) Failing after 26h13m26s
CI/CD Pipeline / CI Gate (push) Failing after 26h13m25s
CI/CD Pipeline / Build Production API Image (push) Failing after 26h13m26s
CI/CD Pipeline / Canary Release to Production (push) Failing after 26h13m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 26h19m46s
CI/CD Pipeline / Frontend Lint (push) Failing after 26h23m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 26h23m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 26h19m46s
fix(#1834): 批量修复 UP 系列静态分析警告(UP007/UP006/UP017/UP035) (#1928)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-15 12:59:17 +08:00

263 lines
8.6 KiB
Python

"""Scripts routes 单元测试 — Issue #1795.
CI 增量映射: scripts.py → test_scripts.py
本文件同时覆盖 routes/scripts.py 和 schemas/script.py 的增量覆盖率。
"""
from __future__ import annotations
from datetime import UTC, datetime, timezone
from unittest.mock import MagicMock, patch
import pytest
from app.schemas.script import (
CreateScriptRequest,
ScriptListResponse,
ScriptResponse,
ScriptSegment,
UpdateScriptRequest,
)
# ── Schema 验证测试 ──────────────────────────────────────────────────────────
class TestScriptSegment:
def test_segment_with_duration(self):
s = ScriptSegment(text="测试", duration=5.0)
assert s.text == "测试"
assert s.duration == 5.0
def test_segment_null_duration(self):
s = ScriptSegment(text="测试", duration=None)
assert s.duration is None
def test_segment_default_duration(self):
s = ScriptSegment(text="测试")
assert s.duration is None
class TestCreateScriptRequest:
def test_minimal(self):
r = CreateScriptRequest(title="标题")
assert r.title == "标题"
assert r.content == ""
assert r.segments == []
assert r.tags == []
def test_full(self):
r = CreateScriptRequest(
title="标题",
content="正文",
segments=[ScriptSegment(text="段1", duration=3.0)],
tags=["口播"],
)
assert len(r.segments) == 1
assert r.tags == ["口播"]
def test_title_required(self):
with pytest.raises(ValueError):
CreateScriptRequest(title="") # min_length=1
def test_title_max_length(self):
with pytest.raises(ValueError):
CreateScriptRequest(title="x" * 256)
class TestUpdateScriptRequest:
def test_all_none_default(self):
r = UpdateScriptRequest()
assert r.title is None
assert r.content is None
assert r.segments is None
assert r.tags is None
def test_partial_update(self):
r = UpdateScriptRequest(title="新标题")
assert r.title == "新标题"
assert r.content is None
class TestScriptResponse:
def test_response_construction(self):
now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=UTC)
r = ScriptResponse(
id="s1",
user_id="u1",
title="标题",
content="内容",
segments=[ScriptSegment(text="段1")],
tags=["t1"],
created_at=now,
updated_at=now,
)
assert r.id == "s1"
assert len(r.segments) == 1
class TestScriptListResponse:
def test_empty_list(self):
r = ScriptListResponse(items=[], total=0)
assert r.total == 0
assert r.items == []
def test_with_items(self):
now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=UTC)
item = ScriptResponse(
id="s1",
user_id="u1",
title="标题",
content="内容",
segments=[],
tags=[],
created_at=now,
updated_at=now,
)
r = ScriptListResponse(items=[item], total=1)
assert r.total == 1
assert len(r.items) == 1
# ── Route handler 逻辑测试 (mock service) ────────────────────────────────────
class TestRouteHandlers:
"""测试路由层逻辑(不通过 TestClient,直接调用 handler 函数)."""
def _make_auth_user(self, user_id="u1"):
user = MagicMock()
user.id = user_id
auth = MagicMock()
auth.user = user
return auth
def test_create_route_calls_service(self):
from app.api.routes.scripts import create_script
svc = MagicMock()
mock_script = MagicMock()
mock_script.id = "s1"
mock_script.user_id = "u1"
mock_script.title = "测试"
mock_script.content = "内容"
mock_script.segments = [{"text": "段1", "duration": None}]
mock_script.tags = []
mock_script.created_at = datetime(2026, 9, 8, tzinfo=UTC)
mock_script.updated_at = datetime(2026, 9, 8, tzinfo=UTC)
svc.create_script.return_value = mock_script
req = CreateScriptRequest(title="测试", content="内容")
auth = self._make_auth_user()
result = create_script(req, authenticated_user=auth, svc=svc)
assert result.id == "s1"
svc.create_script.assert_called_once()
def test_list_route_returns_paginated(self):
from app.api.routes.scripts import list_scripts
svc = MagicMock()
mock_script = MagicMock()
mock_script.id = "s1"
mock_script.user_id = "u1"
mock_script.title = "测试"
mock_script.content = ""
mock_script.segments = []
mock_script.tags = []
mock_script.created_at = datetime(2026, 9, 8, tzinfo=UTC)
mock_script.updated_at = datetime(2026, 9, 8, tzinfo=UTC)
svc.list_scripts.return_value = ([mock_script], 1)
auth = self._make_auth_user()
result = list_scripts(skip=0, limit=50, tag=None, authenticated_user=auth, svc=svc)
assert result.total == 1
assert len(result.items) == 1
def test_get_route_found(self):
from app.api.routes.scripts import get_script
svc = MagicMock()
mock_script = MagicMock()
mock_script.id = "s1"
mock_script.user_id = "u1"
mock_script.title = "测试"
mock_script.content = ""
mock_script.segments = []
mock_script.tags = []
mock_script.created_at = datetime(2026, 9, 8, tzinfo=UTC)
mock_script.updated_at = datetime(2026, 9, 8, tzinfo=UTC)
svc.get_script.return_value = mock_script
auth = self._make_auth_user()
result = get_script("s1", authenticated_user=auth, svc=svc)
assert result.id == "s1"
def test_get_route_not_found(self):
from app.api.routes.scripts import get_script
from app.services.script_service import ScriptNotFoundError
from fastapi import HTTPException
svc = MagicMock()
svc.get_script.side_effect = ScriptNotFoundError("not found")
auth = self._make_auth_user()
with pytest.raises(HTTPException) as exc_info:
get_script("nonexistent", authenticated_user=auth, svc=svc)
assert exc_info.value.status_code == 404
def test_update_route_success(self):
from app.api.routes.scripts import update_script
svc = MagicMock()
mock_script = MagicMock()
mock_script.id = "s1"
mock_script.user_id = "u1"
mock_script.title = "新标题"
mock_script.content = "原内容"
mock_script.segments = []
mock_script.tags = []
mock_script.created_at = datetime(2026, 9, 8, tzinfo=UTC)
mock_script.updated_at = datetime(2026, 9, 8, tzinfo=UTC)
svc.update_script.return_value = mock_script
req = UpdateScriptRequest(title="新标题")
auth = self._make_auth_user()
result = update_script("s1", req, authenticated_user=auth, svc=svc)
assert result.title == "新标题"
def test_update_route_not_found(self):
from app.api.routes.scripts import update_script
from app.services.script_service import ScriptNotFoundError
from fastapi import HTTPException
svc = MagicMock()
svc.update_script.side_effect = ScriptNotFoundError("not found")
auth = self._make_auth_user()
req = UpdateScriptRequest(title="x")
with pytest.raises(HTTPException) as exc_info:
update_script("bad", req, authenticated_user=auth, svc=svc)
assert exc_info.value.status_code == 404
def test_delete_route_success(self):
from app.api.routes.scripts import delete_script
svc = MagicMock()
svc.delete_script.return_value = True
auth = self._make_auth_user()
result = delete_script("s1", authenticated_user=auth, svc=svc)
# Should return None (204 No Content)
assert result is None
def test_delete_route_not_found(self):
from app.api.routes.scripts import delete_script
from fastapi import HTTPException
svc = MagicMock()
svc.delete_script.return_value = False
auth = self._make_auth_user()
with pytest.raises(HTTPException) as exc_info:
delete_script("bad", authenticated_user=auth, svc=svc)
assert exc_info.value.status_code == 404