diff --git a/backend/app/services/mobile_notify.py b/backend/app/services/mobile_notify.py index de3b57b..1d85c46 100644 --- a/backend/app/services/mobile_notify.py +++ b/backend/app/services/mobile_notify.py @@ -69,7 +69,12 @@ def _fcm_access_token(service_account_path: str) -> str: from google.oauth2 import service_account from google.auth.transport.requests import Request except ImportError as exc: - raise MobileNotConfiguredError("google-auth package is required for FCM") from exc + msg = str(exc).lower() + if "requests" in msg: + raise MobileNotConfiguredError( + "FCM: установите пакет requests (pip install requests)" + ) from exc + raise MobileNotConfiguredError("FCM: установите google-auth") from exc creds = service_account.Credentials.from_service_account_file( service_account_path, @@ -116,7 +121,19 @@ def _send_fcm_data( raise MobileNotConfiguredError("FCM не настроен на сервере") return - access_token = _fcm_access_token(cfg.service_account_path) + try: + access_token = _fcm_access_token(cfg.service_account_path) + except (MobileNotConfiguredError, MobileSendError) as exc: + logger.warning("FCM skipped: %s", exc) + if require_success: + raise + return + except Exception: + logger.exception("FCM token acquisition failed") + if require_success: + raise MobileSendError("Failed to obtain FCM access token", status_code=502) + return + url = f"https://fcm.googleapis.com/v1/projects/{cfg.project_id}/messages:send" headers = { "Authorization": f"Bearer {access_token}", diff --git a/backend/app/version.py b/backend/app/version.py index 84872d1..1b3feb1 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.9.2" +APP_VERSION = "0.9.3" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/backend/requirements.txt b/backend/requirements.txt index ef30cfc..6a8c8ad 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,6 +11,7 @@ passlib[bcrypt]>=1.7.4,<2 python-jose[cryptography]>=3.3.0,<4 httpx>=0.28.0,<1 google-auth>=2.36.0,<3 +requests>=2.32.0,<3 psutil>=6.1.0,<8 # dev diff --git a/backend/tests/test_mobile_notify.py b/backend/tests/test_mobile_notify.py new file mode 100644 index 0000000..95ddf1c --- /dev/null +++ b/backend/tests/test_mobile_notify.py @@ -0,0 +1,42 @@ +"""FCM push must not break event ingest.""" + +from unittest.mock import patch + +import pytest + +from app.services.mobile_notify import MobileNotConfiguredError, _send_fcm_data + + +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, + )