from types import SimpleNamespace from uuid import UUID import pytest from src.api.v1 import telegram as telegram_api class _FakeGateway: def __init__(self, result: dict) -> None: self.result = result self.mirror_calls: list[dict] = [] async def handle_callback(self, **_kwargs): return self.result async def mirror_callback_query_received(self, **kwargs): self.mirror_calls.append(kwargs) return "event-1" class _FakeApprovalService: def __init__( self, approval, execution_triggered: bool, sign_message: str = "Approval complete", ) -> None: self.approval = approval self.execution_triggered = execution_triggered self.sign_message = sign_message async def sign_approval(self, **_kwargs): return self.approval, self.sign_message, self.execution_triggered async def reject_approval(self, **_kwargs): return self.approval, "Approval rejected" class _FakeAlertOperationLogRepository: def __init__(self) -> None: self.rows: list[dict] = [] async def append(self, *args, **kwargs): self.rows.append({"args": args, "kwargs": kwargs}) def _callback_update(callback_data: str) -> telegram_api.TelegramUpdate: return telegram_api.TelegramUpdate( update_id=123, callback_query={ "id": "callback-1", "data": callback_data, "from": {"id": 42, "username": "ops"}, "message": {"message_id": 99, "text": "ACTION REQUIRED"}, }, ) @pytest.mark.asyncio async def test_telegram_approval_schedules_executor_after_required_signature(monkeypatch): approval_id = "11111111-1111-1111-1111-111111111111" approval = SimpleNamespace( id=UUID(approval_id), status=SimpleNamespace(value="approved"), incident_id="INC-20260513-TGEXEC", ) finalizer_calls: list[dict] = [] op_log_repo = _FakeAlertOperationLogRepository() async def fake_finalize(*, approval, execution_triggered: bool) -> bool: finalizer_calls.append({ "approval_id": str(approval.id), "incident_id": approval.incident_id, "execution_triggered": execution_triggered, }) return True fake_gateway = _FakeGateway({ "success": True, "action": "approve", "approval_id": approval_id, "user": {"id": 42, "username": "ops"}, }) monkeypatch.setattr(telegram_api, "get_telegram_gateway", lambda: fake_gateway) monkeypatch.setattr( telegram_api, "get_approval_service", lambda: _FakeApprovalService(approval, execution_triggered=True), ) monkeypatch.setattr(telegram_api, "_finalize_telegram_approval", fake_finalize) monkeypatch.setattr( "src.repositories.alert_operation_log_repository.get_alert_operation_log_repository", lambda: op_log_repo, ) result = await telegram_api.telegram_webhook(_callback_update(f"approve:{approval_id}:ts:nonce")) assert result["ok"] is True assert result["message"] == "Approved" assert result["execution_triggered"] is True assert result["execution_scheduled"] is True assert fake_gateway.mirror_calls == [{ "update_id": 123, "callback_query_id": "callback-1", "callback_data": f"approve:{approval_id}:ts:nonce", "user_id": 42, "username": "ops", "message_id": 99, "chat_id": None, }] assert finalizer_calls == [{ "approval_id": approval_id, "incident_id": "INC-20260513-TGEXEC", "execution_triggered": True, }] assert op_log_repo.rows[0]["kwargs"]["action_detail"] == "approve" @pytest.mark.asyncio async def test_telegram_approval_suppresses_executor_for_no_action(monkeypatch): approval_id = "55555555-5555-5555-5555-555555555555" approval = SimpleNamespace( id=UUID(approval_id), status=SimpleNamespace(value="approved"), incident_id="INC-20260611-NOEXEC", action="NO_ACTION - REPAIR_CANDIDATE_MISSING: LLM 分析失敗", ) finalizer_calls: list[dict] = [] op_log_repo = _FakeAlertOperationLogRepository() async def fake_finalize(*, approval, execution_triggered: bool) -> bool: finalizer_calls.append({ "approval_id": str(approval.id), "execution_triggered": execution_triggered, }) return False fake_gateway = _FakeGateway({ "success": True, "action": "approve", "approval_id": approval_id, "user": {"id": 42, "username": "ops"}, }) monkeypatch.setattr(telegram_api, "get_telegram_gateway", lambda: fake_gateway) monkeypatch.setattr( telegram_api, "get_approval_service", lambda: _FakeApprovalService(approval, execution_triggered=True), ) monkeypatch.setattr(telegram_api, "_finalize_telegram_approval", fake_finalize) monkeypatch.setattr( "src.repositories.alert_operation_log_repository.get_alert_operation_log_repository", lambda: op_log_repo, ) result = await telegram_api.telegram_webhook(_callback_update(f"approve:{approval_id}:ts:nonce")) assert result["ok"] is True assert result["message"] == "ApprovedWithoutExecution" assert result["execution_triggered"] is True assert result["execution_scheduled"] is False assert result["execution_suppressed"] is True assert finalizer_calls == [{ "approval_id": approval_id, "execution_triggered": True, }] @pytest.mark.asyncio async def test_telegram_approval_duplicate_does_not_schedule_executor(monkeypatch): approval_id = "33333333-3333-3333-3333-333333333333" approval = SimpleNamespace( id=UUID(approval_id), status=SimpleNamespace(value="execution_success"), incident_id="INC-20260531-DUPE", ) finalizer_calls: list[dict] = [] op_log_repo = _FakeAlertOperationLogRepository() async def fake_finalize(*, approval, execution_triggered: bool) -> bool: finalizer_calls.append({ "approval_id": str(approval.id), "execution_triggered": execution_triggered, }) return True monkeypatch.setattr( telegram_api, "get_telegram_gateway", lambda: _FakeGateway({ "success": True, "action": "approve", "approval_id": approval_id, "user": {"id": 42, "username": "ops"}, }), ) monkeypatch.setattr( telegram_api, "get_approval_service", lambda: _FakeApprovalService( approval, execution_triggered=False, sign_message="Cannot sign: status is execution_success", ), ) monkeypatch.setattr(telegram_api, "_finalize_telegram_approval", fake_finalize) monkeypatch.setattr( "src.repositories.alert_operation_log_repository.get_alert_operation_log_repository", lambda: op_log_repo, ) result = await telegram_api.telegram_webhook(_callback_update(f"approve:{approval_id}:ts:nonce")) assert result["ok"] is True assert result["message"] == "Already processed" assert result["execution_triggered"] is False assert result["execution_scheduled"] is False assert finalizer_calls == [] assert op_log_repo.rows[0]["kwargs"]["action_detail"] == "approve_duplicate" @pytest.mark.asyncio async def test_telegram_rejection_syncs_incident_state(monkeypatch): approval_id = "22222222-2222-2222-2222-222222222222" approval = SimpleNamespace( id=UUID(approval_id), status=SimpleNamespace(value="rejected"), incident_id="INC-20260513-TGREJ", ) sync_calls: list[str] = [] op_log_repo = _FakeAlertOperationLogRepository() async def fake_sync(rejected_approval_id: str) -> bool: sync_calls.append(rejected_approval_id) return True monkeypatch.setattr( telegram_api, "get_telegram_gateway", lambda: _FakeGateway({ "success": True, "action": "reject", "approval_id": approval_id, "user": {"id": 42, "username": "ops"}, }), ) monkeypatch.setattr( telegram_api, "get_approval_service", lambda: _FakeApprovalService(approval, execution_triggered=False), ) monkeypatch.setattr(telegram_api, "_sync_telegram_rejection", fake_sync) monkeypatch.setattr( "src.repositories.alert_operation_log_repository.get_alert_operation_log_repository", lambda: op_log_repo, ) result = await telegram_api.telegram_webhook(_callback_update(f"reject:{approval_id}:ts:nonce")) assert result["ok"] is True assert result["message"] == "Rejected" assert result["incident_synced"] is True assert sync_calls == [approval_id] assert op_log_repo.rows[0]["kwargs"]["action_detail"] == "reject" @pytest.mark.asyncio async def test_finalize_telegram_approval_runs_executor_task(monkeypatch): executed: list[str] = [] approval = SimpleNamespace( id=UUID("33333333-3333-3333-3333-333333333333"), incident_id="INC-20260513-TGRUN", ) class _FakeExecutionService: async def execute_approved_action(self, received_approval): executed.append(str(received_approval.id)) return True monkeypatch.setattr( telegram_api, "get_execution_service", lambda: _FakeExecutionService(), ) scheduled = await telegram_api._finalize_telegram_approval( approval=approval, execution_triggered=True, ) assert scheduled is True await telegram_api.asyncio.sleep(0) assert executed == ["33333333-3333-3333-3333-333333333333"] @pytest.mark.asyncio async def test_finalize_telegram_approval_does_not_schedule_no_action(monkeypatch): executed: list[str] = [] approval = SimpleNamespace( id=UUID("66666666-6666-6666-6666-666666666666"), incident_id="INC-20260611-NOOP", action="OBSERVE", ) class _FakeExecutionService: async def execute_approved_action(self, received_approval): executed.append(str(received_approval.id)) return True monkeypatch.setattr( telegram_api, "get_execution_service", lambda: _FakeExecutionService(), ) scheduled = await telegram_api._finalize_telegram_approval( approval=approval, execution_triggered=True, ) assert scheduled is False await telegram_api.asyncio.sleep(0) assert executed == []