"""FCM push must not break event ingest.""" from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pytest from app.models import Event, Host from app.services.mobile_notify import MobileNotConfiguredError, _event_payload, _send_fcm_data def test_event_payload_lifecycle_uses_telegram_wording(): host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows") event = Event( event_id="00000000-0000-4000-8000-000000000511", host_id=1, host=host, occurred_at=datetime(2026, 5, 29, 9, 0, tzinfo=timezone.utc), category="agent", type="agent.lifecycle", severity="info", title="started", summary="short", details={"lifecycle": "started", "trigger": "settings_reload", "telegram_via": "sac"}, payload={"source": {"product": "ssh-monitor", "product_version": "1.4.0-SAC"}}, ) title, body, data = _event_payload(event) assert title == "✅ Агент запущен" assert "graceful restart" in body assert data["type"] == "agent.lifecycle" def test_send_fcm_data_swallows_token_errors_by_default(): with patch( "app.services.mobile_notify._fcm_access_token", side_effect=MobileNotConfiguredError("FCM: install requests"), ): with patch("app.services.mobile_notify.get_effective_fcm_config") as mock_cfg: mock_cfg.return_value.enabled = True mock_cfg.return_value.project_id = "proj" mock_cfg.return_value.service_account_path = "/etc/fcm.json" mock_cfg.return_value.configured = True mock_cfg.return_value.active = True _send_fcm_data(["token"], {"kind": "test"}, title="t", body="b") def test_send_fcm_data_raises_token_errors_when_required(): with patch( "app.services.mobile_notify._fcm_access_token", side_effect=MobileNotConfiguredError("FCM: install requests"), ): with patch("app.services.mobile_notify.get_effective_fcm_config") as mock_cfg: mock_cfg.return_value.enabled = True mock_cfg.return_value.project_id = "proj" mock_cfg.return_value.service_account_path = "/etc/fcm.json" mock_cfg.return_value.configured = True mock_cfg.return_value.active = True with pytest.raises(MobileNotConfiguredError): _send_fcm_data( ["token"], {"kind": "test"}, title="t", body="b", require_success=True, ) def test_send_fcm_data_payload_is_data_only(): posted: list[dict] = [] def capture_post(_url, *, headers, json): posted.append(json) response = MagicMock() response.raise_for_status.return_value = None return response with patch("app.services.mobile_notify._fcm_access_token", return_value="token"): with patch("app.services.mobile_notify.get_effective_fcm_config") as mock_cfg: mock_cfg.return_value.enabled = True mock_cfg.return_value.project_id = "proj" mock_cfg.return_value.service_account_path = "/etc/fcm.json" mock_cfg.return_value.configured = True mock_cfg.return_value.active = True with patch("httpx.Client") as mock_client: mock_client.return_value.__enter__.return_value.post = capture_post _send_fcm_data( ["device-token"], {"kind": "event", "id": "42", "severity": "high"}, title="Alert", body="Summary text", ) assert len(posted) == 1 message = posted[0]["message"] assert "notification" not in message assert message["data"]["title"] == "Alert" assert message["data"]["body"] == "Summary text" assert message["data"]["kind"] == "event" assert message["data"]["id"] == "42"