67a1ed6430
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 37s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 38s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m3s
CI/CD Pipeline / Integration Tests (push) Successful in 3m7s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m40s
CI/CD Pipeline / Validate - Style (push) Successful in 3m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m17s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 3m6s
CI/CD Pipeline / Validate - Security (push) Successful in 7m14s
CI/CD Pipeline / Unit Tests (push) Successful in 9m33s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 35m54s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
256 lines
8.3 KiB
Python
256 lines
8.3 KiB
Python
"""Scripts routes 单元测试 — Issue #1795 + #1894 清理.
|
|
|
|
CI 增量映射: scripts.py → test_scripts.py
|
|
本文件同时覆盖 routes/scripts.py 和 schemas/script.py 的增量覆盖率。
|
|
#1894: 删除 title_text/title_category/title_config 三字段,仅保留 title/content/segments/tags。
|
|
"""
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
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="")
|
|
|
|
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
|
|
|
|
def test_partial_update_content_only(self):
|
|
r = UpdateScriptRequest(content="新内容")
|
|
assert r.title is None
|
|
assert r.content == "新内容"
|
|
|
|
|
|
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
|
|
assert r.title == "标题"
|
|
assert r.tags == ["t1"]
|
|
|
|
def test_response_defaults(self):
|
|
now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=UTC)
|
|
r = ScriptResponse(
|
|
id="s1", user_id="u1", title="标题", content="",
|
|
segments=[], tags=[], created_at=now, updated_at=now,
|
|
)
|
|
assert r.segments == []
|
|
assert r.tags == []
|
|
|
|
|
|
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
|
|
|
|
|
|
class TestRouteHandlers:
|
|
def _make_auth_user(self, user_id="u1"):
|
|
user = MagicMock()
|
|
user.id = user_id
|
|
auth = MagicMock()
|
|
auth.user = user
|
|
return auth
|
|
|
|
def _make_mock_script(self, **overrides):
|
|
m = MagicMock()
|
|
m.id = overrides.get("id", "s1")
|
|
m.user_id = overrides.get("user_id", "u1")
|
|
m.title = overrides.get("title", "测试")
|
|
m.content = overrides.get("content", "内容")
|
|
m.segments = overrides.get("segments", [{"text": "段1", "duration": None}])
|
|
m.tags = overrides.get("tags", [])
|
|
m.created_at = overrides.get("created_at", datetime(2026, 9, 8, tzinfo=UTC))
|
|
m.updated_at = overrides.get("updated_at", datetime(2026, 9, 8, tzinfo=UTC))
|
|
return m
|
|
|
|
def test_create_route_calls_service(self):
|
|
from app.api.routes.scripts import create_script
|
|
|
|
svc = MagicMock()
|
|
svc.create_script.return_value = self._make_mock_script(content="内容")
|
|
|
|
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()
|
|
call_kwargs = svc.create_script.call_args.kwargs
|
|
assert "title_text" not in call_kwargs
|
|
assert "title_category" not in call_kwargs
|
|
assert "title_config" not in call_kwargs
|
|
|
|
def test_list_route_returns_paginated(self):
|
|
from app.api.routes.scripts import list_scripts
|
|
|
|
svc = MagicMock()
|
|
svc.list_scripts.return_value = ([self._make_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()
|
|
svc.get_script.return_value = self._make_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()
|
|
svc.update_script.return_value = self._make_mock_script(title="新标题")
|
|
|
|
req = UpdateScriptRequest(title="新标题")
|
|
auth = self._make_auth_user()
|
|
result = update_script("s1", req, authenticated_user=auth, svc=svc)
|
|
assert result.title == "新标题"
|
|
call_kwargs = svc.update_script.call_args.kwargs
|
|
assert "title_text" not in call_kwargs
|
|
assert "title_category" not in call_kwargs
|
|
assert "title_config" not in call_kwargs
|
|
|
|
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)
|
|
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
|