""" 验证服务端上报用例的输入投影与失败处理。 """ from types import SimpleNamespace from unittest.mock import AsyncMock, Mock, call import pytest from app.application.server.report import ServerReportService def _service(**overrides) -> ServerReportService: """ 构造不依赖数据库和网络的中心服务上报用例。 """ defaults = { "config_reader": Mock(return_value=None), "config_writer": Mock(), "installed_plugins_provider": Mock(return_value=[]), "subscribes_provider": Mock(return_value=[]), "async_subscribes_provider": AsyncMock(return_value=[]), "plugin_report_sender": Mock( return_value=SimpleNamespace(status_code=200) ), "async_plugin_report_sender": Mock(), "subscribe_report_sender": Mock( return_value=SimpleNamespace(status_code=200) ), "async_subscribe_report_sender": AsyncMock(), "async_config_writer": AsyncMock(), "repo_url_sanitizer": lambda value: value, } defaults.update(overrides) return ServerReportService(**defaults) def test_subscribe_report_uses_fake_local_reader_and_transport(): """ 订阅存量上报只依赖注入的数据读取和发送端口。 """ sender = Mock(return_value=SimpleNamespace(status_code=200)) subscribe = SimpleNamespace(to_dict=lambda: { "name": "Demo", "type": "电影", "media_source": "themoviedb", "media_id": "123", "username": "private", }) service = _service( subscribes_provider=Mock(return_value=[subscribe]), subscribe_report_sender=sender, ) assert service.report_subscribes(enabled=True) is True sender.assert_called_once_with([{ "name": "Demo", "type": "电影", "media_source": "themoviedb", "media_id": "123", }]) def test_initial_report_marker_is_written_only_after_success(): """ 首次上报失败时不得提前写完成标记。 """ writer = Mock() service = _service(config_writer=writer) service.init_report( enabled=True, state_key="report", reporter=Mock(return_value=False), ) writer.assert_not_called() @pytest.mark.asyncio async def test_async_initial_report_marker_uses_async_writer_after_success(): """ 异步首次上报成功后只通过异步配置端口写完成标记。 """ sync_writer = Mock() async_writer = AsyncMock() reporter = AsyncMock(return_value=True) service = _service( config_writer=sync_writer, async_config_writer=async_writer, ) await service.async_init_report( enabled=True, state_key="report", reporter=reporter, ) reporter.assert_awaited_once_with() async_writer.assert_awaited_once_with("report", "1") sync_writer.assert_not_called() @pytest.mark.asyncio async def test_async_subscribe_report_uses_async_reader(): """ 异步订阅上报通过异步读取端口获取数据,不在事件循环内查同步库。 """ sync_reader = Mock(side_effect=AssertionError("不应调用同步订阅读取")) async_reader = AsyncMock(return_value=[]) service = _service( subscribes_provider=sync_reader, async_subscribes_provider=async_reader, ) assert await service.async_report_subscribes(enabled=True) is True sync_reader.assert_not_called() async_reader.assert_awaited_once_with() def test_plugin_report_sanitizes_explicit_sources_before_transport(): """ 插件统计载荷在进入传输适配器前完成来源脱敏。 """ sender = Mock(return_value=SimpleNamespace(status_code=200)) service = _service( plugin_report_sender=sender, repo_url_sanitizer=lambda value: "local://Demo" if value else value, ) assert service.report_plugins( enabled=True, items=[("Demo", "local://Demo?path=/private/repo")], ) is True sender.assert_called_once_with([{ "plugin_id": "Demo", "repo_url": "local://Demo", }]) @pytest.mark.asyncio async def test_plugin_report_entries_share_payload_and_response_decisions(monkeypatch): """ 插件同步、异步入口只替换发送端口,准入和结果判定必须同源。 """ sync_sender = Mock(return_value=SimpleNamespace(status_code=200)) async_sender = AsyncMock(return_value=SimpleNamespace(status_code=200)) service = _service( plugin_report_sender=sync_sender, async_plugin_report_sender=async_sender, ) prepare = Mock(wraps=service._prepare_plugin_report) succeeded = Mock(wraps=service._report_succeeded) monkeypatch.setattr(service, "_prepare_plugin_report", prepare) monkeypatch.setattr(service, "_report_succeeded", succeeded) items = [("Demo", "https://repo.example/Demo")] assert service.report_plugins(enabled=True, items=items) is True assert await service.async_report_plugins(enabled=True, items=items) is True assert prepare.call_args_list == [ call(enabled=True, items=items), call(enabled=True, items=items), ] assert sync_sender.call_args == async_sender.await_args assert succeeded.call_count == 2 @pytest.mark.asyncio async def test_subscribe_report_entries_share_payload_projection(monkeypatch): """ 订阅读取后的过滤与终态决策必须由同步、异步入口共享。 """ subscribe = SimpleNamespace(to_dict=lambda: { "name": "Demo", "media_source": "themoviedb", "media_id": "123", }) service = _service( subscribes_provider=Mock(return_value=[subscribe]), async_subscribes_provider=AsyncMock(return_value=[subscribe]), async_subscribe_report_sender=AsyncMock( return_value=SimpleNamespace(status_code=200) ), ) prepare = Mock(wraps=service._prepare_subscribe_report) monkeypatch.setattr(service, "_prepare_subscribe_report", prepare) assert service.report_subscribes(enabled=True) is True assert await service.async_report_subscribes(enabled=True) is True assert prepare.call_args_list == [(([subscribe],), {}), (([subscribe],), {})] assert service._subscribe_report_sender.call_args == ( service._async_subscribe_report_sender.await_args )