diff --git a/app/api/max_notifications.py b/app/api/max_notifications.py index c232976..c8e759e 100644 --- a/app/api/max_notifications.py +++ b/app/api/max_notifications.py @@ -10,6 +10,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from pydantic import BaseModel, ConfigDict from sqlalchemy import select +from sqlalchemy.exc import SQLAlchemyError from app.config import get_settings from app.core import clock @@ -106,5 +107,9 @@ async def webhook(request: Request) -> dict[str, bool]: raise HTTPException(422, "event must be an object") from app.channels.max_bot import handle_update - await handle_update(event) + try: + await handle_update(event) + except SQLAlchemyError: + # Let MAX retry; do not log DB exception parameters containing identity data. + raise HTTPException(503, "notification storage unavailable") from None return {"success": True} diff --git a/tests/test_api_max.py b/tests/test_api_max.py index 549ff33..123f13a 100644 --- a/tests/test_api_max.py +++ b/tests/test_api_max.py @@ -1,9 +1,13 @@ """MAX endpoints authenticate before reading bindings or accepting events.""" +import sys +from types import SimpleNamespace +from unittest.mock import AsyncMock from urllib.parse import parse_qs, urlsplit import pytest from httpx import ASGITransport, AsyncClient +from sqlalchemy.exc import OperationalError from app.config import get_settings from app.core import clock @@ -90,3 +94,10 @@ async def test_webhook_secret_checked_before_body_or_adapter(client): assert (await client.post(HOOK, headers=headers, content="not-json")).status_code == 400 assert (await client.post(HOOK, headers=headers, json=[])).status_code == 422 assert (await client.post(HOOK, headers=headers, content=" " * 65537)).status_code == 413 + + +async def test_webhook_storage_failure_is_retryable_without_identity_details(client, monkeypatch): + handler = AsyncMock(side_effect=OperationalError("private SQL", {"token": TOKEN}, Exception("private"))) + monkeypatch.setitem(sys.modules, "app.channels.max_bot", SimpleNamespace(handle_update=handler)) + response = await client.post(HOOK, headers={"X-Max-Bot-Api-Secret": SECRET}, json={}) + assert response.status_code == 503 and TOKEN not in response.text and "private" not in response.text