from types import SimpleNamespace from unittest.mock import Mock import pytest from app.api.dependencies import subscription as subscription_dependencies from app.application.subscription.delete import SubscribeDeletionCandidate from app.application.subscription.search import ( SearchSubscriptionsCommand, SubscribeSearchActor, ) from app.runtime.tasks import TaskRegistry class _Repository: """提供手工订阅搜索测试需要的归属和列表数据。""" def __init__(self, candidate=None, subscribe_ids=None): """保存预设单条候选和批量编号。""" self.candidate = candidate self.subscribe_ids = subscribe_ids or [] async def get_candidate(self, _subscribe_id): """返回预设订阅候选。""" return self.candidate async def list_search_ids(self, username, state): """校验普通用户搜索状态并返回预设编号。""" assert username == "alice" assert state == "R" return self.subscribe_ids def _candidate(username): """构造只包含归属信息的订阅候选。""" return SubscribeDeletionCandidate( subscribe_id=7, username=username, event_payload={}, ) @pytest.mark.asyncio async def test_superuser_search_all_uses_single_global_scheduler_request(): """管理员搜索全部订阅时保持一次 state=R 的全局调度语义。""" scheduled = [] command = SearchSubscriptionsCommand( repository=_Repository(), schedule_search=lambda sid, state: scheduled.append((sid, state)), ) assert await command.execute( SubscribeSearchActor(username="admin", is_superuser=True) ) is True assert scheduled == [(None, "R")] @pytest.mark.asyncio async def test_regular_user_search_all_schedules_only_owned_subscriptions(): """普通用户搜索全部时把归属订阅合并为一次后台批次。""" scheduled = [] command = SearchSubscriptionsCommand( repository=_Repository(subscribe_ids=[2, 5]), schedule_search=lambda sid, state: scheduled.append((sid, state)), ) assert await command.execute( SubscribeSearchActor(username="alice", is_superuser=False) ) is True assert scheduled == [((2, 5), None)] @pytest.mark.asyncio async def test_regular_user_search_all_with_no_targets_does_not_schedule(): """普通用户没有可搜索订阅时不创建空后台任务。""" scheduled = [] command = SearchSubscriptionsCommand( repository=_Repository(), schedule_search=lambda ids, state: scheduled.append((ids, state)), ) assert await command.execute( SubscribeSearchActor(username="alice", is_superuser=False) ) is True assert scheduled == [] @pytest.mark.asyncio async def test_targeted_search_rejects_missing_or_other_users_subscription(): """单条搜索不得泄漏订阅是否属于其他普通用户。""" scheduled = [] command = SearchSubscriptionsCommand( repository=_Repository(candidate=_candidate("bob")), schedule_search=lambda sid, state: scheduled.append((sid, state)), ) assert await command.execute( SubscribeSearchActor(username="alice", is_superuser=False), subscribe_id=7, ) is False assert scheduled == [] @pytest.mark.asyncio async def test_targeted_search_schedules_accessible_subscription(): """归属用户搜索单条订阅时提交历史兼容参数。""" scheduled = [] command = SearchSubscriptionsCommand( repository=_Repository(candidate=_candidate("alice")), schedule_search=lambda sid, state: scheduled.append((sid, state)), ) assert await command.execute( SubscribeSearchActor(username="alice", is_superuser=False), subscribe_id=7, ) is True assert scheduled == [((7,), None)] def test_subscription_search_batch_uses_one_scheduler_generation(monkeypatch): """一个后台批次只占用一次调度任务运行权。""" calls = [] monkeypatch.setattr( subscription_dependencies, "start_scheduler_job", lambda job_id, **kwargs: calls.append((job_id, kwargs)), ) subscription_dependencies._start_subscription_search_batch((2, 5), None) assert calls == [ ( "subscribe_search", {"sids": (2, 5), "state": None, "manual": True}, ), ] @pytest.mark.asyncio async def test_search_dependency_registers_one_owned_background_batch(): """请求适配器只登记一个具名后台批次。""" registry = TaskRegistry() registry.create_sync = Mock() repository = _Repository(subscribe_ids=[2, 5]) runtime = SimpleNamespace( subscription=SimpleNamespace(repository=lambda _db: repository), ) command = subscription_dependencies.get_search_subscriptions_command( task_registry=registry, db=object(), runtime=runtime, ) assert await command.execute( SubscribeSearchActor(username="alice", is_superuser=False) ) is True registry.create_sync.assert_called_once_with( subscription_dependencies._start_subscription_search_batch, (2, 5), None, owner="api.subscribe.search", )