"""ScriptService 单元测试 — Issue #1795 口播文案库. CI 增量映射: script_service.py → test_script_service.py """ from __future__ import annotations from datetime import 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=timezone.utc) m.updated_at = datetime(2026, 9, 8, 12, 0, 0, tzinfo=timezone.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()