"""历史查询应用服务的分页、筛选和 DTO 边界测试。""" from types import SimpleNamespace from unittest.mock import AsyncMock import pytest from app.application.history import HistoryQueryService from app.schemas.history import DownloadHistory, TransferHistory def _make_service( *, downloads: AsyncMock | None = None, transfers: AsyncMock | None = None, ) -> tuple[HistoryQueryService, AsyncMock, AsyncMock]: """构造使用可观察异步仓储的历史查询服务。""" download_repository = downloads or AsyncMock() transfer_repository = transfers or AsyncMock() return ( HistoryQueryService( download_repository=download_repository, transfer_repository=transfer_repository, ), download_repository, transfer_repository, ) @pytest.mark.asyncio async def test_list_download_returns_schema_dtos() -> None: """下载历史查询不得把仓储对象原样泄漏给 API。""" service, download_repository, _ = _make_service() raw_record = SimpleNamespace(id=7, title="Movie") download_repository.async_list_by_page.return_value = [raw_record] records = await service.list_download(page=2, count=10) assert records == [DownloadHistory(id=7, title="Movie")] assert records[0] is not raw_record download_repository.async_list_by_page.assert_awaited_once_with(2, 10) @pytest.mark.asyncio async def test_list_transfer_maps_failed_alias_to_status_filter() -> None: """“失败”快捷条件应继续映射为无标题的失败状态查询。""" service, _, transfer_repository = _make_service() transfer_repository.async_list_by_page.return_value = [ SimpleNamespace(id=3, status=False) ] transfer_repository.async_count.return_value = 1 page = await service.list_transfer(title="失败", page=3, count=5) assert page.list == [TransferHistory(id=3, status=False)] assert page.total == 1 transfer_repository.async_list_by_page.assert_awaited_once_with( page=3, count=5, status=False, ) transfer_repository.async_count.assert_awaited_once_with(status=False) @pytest.mark.asyncio async def test_list_transfer_preserves_glob_escaping() -> None: """glob 查询应转义 SQL 通配符并显式启用 wildcard 模式。""" service, _, transfer_repository = _make_service() transfer_repository.async_list_by_title.return_value = [] transfer_repository.async_count_by_title.return_value = 0 page = await service.list_transfer( title=r"show_100%*.mkv", page=1, count=30, status=True, ) assert page.total == 0 pattern = r"show\_100\%%.mkv" transfer_repository.async_count_by_title.assert_awaited_once_with( pattern, status=True, wildcard=True, ) transfer_repository.async_list_by_title.assert_awaited_once_with( pattern, page=1, count=30, status=True, wildcard=True, ) @pytest.mark.asyncio async def test_get_transfers_preserves_order_and_reports_missing_ids() -> None: """批量 AI 重做准备应保持去重后的输入顺序并报告缺失记录。""" service, _, transfer_repository = _make_service() transfer_repository.async_get.side_effect = [ SimpleNamespace(id=11), None, SimpleNamespace(id=13), ] records, missing_ids = await service.get_transfers([11, 12, 13]) assert [record.id for record in records] == [11, 13] assert missing_ids == [12] assert transfer_repository.async_get.await_args_list == [ ((11,), {}), ((12,), {}), ((13,), {}), ]