Files
xiaoxia-saas/tests/unit/test_script_service.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

261 lines
9.6 KiB
Python

"""ScriptService 单元测试 — Issue #1795 口播文案库.
CI 增量映射: script_service.py → test_script_service.py
"""
from __future__ import annotations
from datetime import UTC, datetime, timezone
from unittest.mock import MagicMock, patch
import pytest
from app.services.script_service import ScriptNotFoundError, ScriptService
# ── helpers ──────────────────────────────────────────────────────────────────
def _make_mock_script(
script_id="s1",
user_id="u1",
title="测试文案",
content="正文内容",
segments=None,
tags=None,
):
m = MagicMock()
m.id = script_id
m.user_id = user_id
m.title = title
m.content = content
m.segments = segments if segments is not None else [{"text": "第一段", "duration": None}]
m.tags = tags if tags is not None else ["口播"]
m.created_at = datetime(2026, 9, 8, 12, 0, 0, tzinfo=UTC)
m.updated_at = datetime(2026, 9, 8, 12, 0, 0, tzinfo=UTC)
return m
def _make_service(db=None):
if db is None:
db = MagicMock()
return ScriptService(db), db
# ── create ───────────────────────────────────────────────────────────────────
class TestCreateScript:
def test_create_minimal(self):
svc, db = _make_service()
# query chain for get_script (not called here but add mock anyway)
with patch("app.services.script_service.ScriptModel") as MockModel:
instance = _make_mock_script()
MockModel.return_value = instance
result = svc.create_script(user_id="u1", title="测试文案")
# ScriptModel was called to create a new instance
MockModel.assert_called_once()
db.add.assert_called_once()
db.commit.assert_called_once()
db.refresh.assert_called_once()
def test_create_with_segments_and_tags(self):
svc, db = _make_service()
segments = [{"text": "第一段", "duration": 5.0}, {"text": "第二段", "duration": None}]
tags = ["口播", "教程"]
with patch("app.services.script_service.ScriptModel") as MockModel:
instance = _make_mock_script(segments=segments, tags=tags)
MockModel.return_value = instance
result = svc.create_script(
user_id="u1",
title="分段文案",
content="完整内容",
segments=segments,
tags=tags,
)
db.add.assert_called_once()
call_kwargs = MockModel.call_args
assert call_kwargs[1]["segments"] == segments
assert call_kwargs[1]["tags"] == tags
def test_create_defaults_empty_segments_tags(self):
svc, db = _make_service()
with patch("app.services.script_service.ScriptModel") as MockModel:
MockModel.return_value = _make_mock_script()
svc.create_script(user_id="u1", title="空文案")
call_kwargs = MockModel.call_args[1]
assert call_kwargs["segments"] == []
assert call_kwargs["tags"] == []
# ── get ──────────────────────────────────────────────────────────────────────
class TestGetScript:
def test_get_existing(self):
svc, db = _make_service()
mock_script = _make_mock_script()
chain = MagicMock()
chain.filter.return_value = chain
chain.first.return_value = mock_script
db.query.return_value = chain
result = svc.get_script("s1", "u1")
assert result == mock_script
# Verify filter was called with correct conditions
assert chain.filter.called
def test_get_not_found_raises(self):
svc, db = _make_service()
chain = MagicMock()
chain.filter.return_value = chain
chain.first.return_value = None
db.query.return_value = chain
with pytest.raises(ScriptNotFoundError):
svc.get_script("nonexistent", "u1")
def test_get_wrong_user_raises(self):
"""不同用户不能访问其他人的文案."""
svc, db = _make_service()
chain = MagicMock()
chain.filter.return_value = chain
chain.first.return_value = None # filter by user_id returns None
db.query.return_value = chain
with pytest.raises(ScriptNotFoundError):
svc.get_script("s1", "other_user")
# ── list ─────────────────────────────────────────────────────────────────────
class TestListScripts:
def test_list_default(self):
svc, db = _make_service()
items = [_make_mock_script("s1"), _make_mock_script("s2")]
chain = MagicMock()
chain.filter.return_value = chain
chain.count.return_value = 2
chain.order_by.return_value = chain
chain.offset.return_value = chain
chain.limit.return_value = chain
chain.all.return_value = items
db.query.return_value = chain
result, total = svc.list_scripts("u1")
assert total == 2
assert len(result) == 2
chain.offset.assert_called_with(0)
chain.limit.assert_called_with(50)
def test_list_with_pagination(self):
svc, db = _make_service()
chain = MagicMock()
chain.filter.return_value = chain
chain.count.return_value = 100
chain.order_by.return_value = chain
chain.offset.return_value = chain
chain.limit.return_value = chain
chain.all.return_value = []
db.query.return_value = chain
result, total = svc.list_scripts("u1", skip=20, limit=10)
chain.offset.assert_called_with(20)
chain.limit.assert_called_with(10)
def test_list_filter_by_tag(self):
svc, db = _make_service()
chain = MagicMock()
chain.filter.return_value = chain
chain.count.return_value = 1
chain.order_by.return_value = chain
chain.offset.return_value = chain
chain.limit.return_value = chain
chain.all.return_value = [_make_mock_script()]
db.query.return_value = chain
result, total = svc.list_scripts("u1", tag="口播")
# filter should be called twice: once for user_id, once for tag
assert chain.filter.call_count == 2
# ── update ───────────────────────────────────────────────────────────────────
class TestUpdateScript:
def test_update_title(self):
svc, db = _make_service()
mock_script = _make_mock_script()
chain = MagicMock()
chain.filter.return_value = chain
chain.first.return_value = mock_script
db.query.return_value = chain
result = svc.update_script("s1", "u1", title="新标题")
assert mock_script.title == "新标题"
db.commit.assert_called_once()
def test_update_segments(self):
svc, db = _make_service()
mock_script = _make_mock_script()
chain = MagicMock()
chain.filter.return_value = chain
chain.first.return_value = mock_script
db.query.return_value = chain
new_segments = [{"text": "更新后段落", "duration": 10.0}]
result = svc.update_script("s1", "u1", segments=new_segments)
assert mock_script.segments == new_segments
def test_update_not_found_raises(self):
svc, db = _make_service()
chain = MagicMock()
chain.filter.return_value = chain
chain.first.return_value = None
db.query.return_value = chain
with pytest.raises(ScriptNotFoundError):
svc.update_script("nonexistent", "u1", title="x")
def test_update_partial_only_changes_specified(self):
svc, db = _make_service()
mock_script = _make_mock_script(title="原标题", content="原内容")
chain = MagicMock()
chain.filter.return_value = chain
chain.first.return_value = mock_script
db.query.return_value = chain
# Only update tags, title and content should stay the same
svc.update_script("s1", "u1", tags=["新标签"])
assert mock_script.title == "原标题"
assert mock_script.content == "原内容"
assert mock_script.tags == ["新标签"]
# ── delete ───────────────────────────────────────────────────────────────────
class TestDeleteScript:
def test_delete_existing(self):
svc, db = _make_service()
mock_script = _make_mock_script()
chain = MagicMock()
chain.filter.return_value = chain
chain.first.return_value = mock_script
db.query.return_value = chain
result = svc.delete_script("s1", "u1")
assert result is True
db.delete.assert_called_once_with(mock_script)
db.commit.assert_called_once()
def test_delete_not_found(self):
svc, db = _make_service()
chain = MagicMock()
chain.filter.return_value = chain
chain.first.return_value = None
db.query.return_value = chain
result = svc.delete_script("nonexistent", "u1")
assert result is False
db.delete.assert_not_called()