test: cover webhook ingestion edge cases

This commit is contained in:
Christian Krakau-Louis
2026-05-22 23:53:36 +02:00
parent 302159aeb6
commit 88af60ed2e
2 changed files with 165 additions and 0 deletions
+30
View File
@@ -2132,6 +2132,36 @@ class TestTriggerPollEndpoint:
finally:
main_app.dependency_overrides.clear()
def test_trigger_poll_with_no_enabled_sources(self):
"""With no enabled sources, the endpoint returns an empty-success response."""
from app.core.security import require_admin_auth
from app.main import app as main_app
async def mock_auth():
return {"auth_type": "api_key"}
main_app.dependency_overrides[require_admin_auth] = mock_auth
try:
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.all.return_value = []
with TestClient(main_app) as tc:
with patch("app.main.SessionLocal", return_value=mock_db):
resp = tc.post("/api/v1/admin/trigger-poll?days=14")
assert resp.status_code == 200
data = resp.json()
assert data == {
"success": True,
"message": "No enabled mail sources configured.",
"sources_polled": 0,
"days": 14,
"authenticated_by": "api_key",
}
finally:
main_app.dependency_overrides.clear()
# ---------------------------------------------------------------------------
# Pytest marker to avoid warnings for test methods without assertions
+135
View File
@@ -1,8 +1,12 @@
import base64
from email.header import Header
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from unittest.mock import patch
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app.core.config import get_settings
@@ -55,6 +59,24 @@ def _raw_email_with_report() -> bytes:
return msg.as_bytes()
def _raw_email_with_attachment(
filename: str | None,
content: bytes,
subtype: str = "octet-stream",
subject: str = "DMARC report",
) -> bytes:
msg = MIMEMultipart()
msg["Subject"] = subject
msg.attach(MIMEText("attached"))
part = MIMEApplication(content, _subtype=subtype)
if filename is not None:
part.add_header("Content-Disposition", "attachment", filename=filename)
else:
part.add_header("Content-Disposition", "attachment")
msg.attach(part)
return msg.as_bytes()
def _set_webhook_secret(monkeypatch, value="test-webhook-secret"):
monkeypatch.setenv("WEBHOOK_SECRET", value)
get_settings.cache_clear()
@@ -126,3 +148,116 @@ def test_webhook_raw_email_marks_duplicate(client: TestClient, monkeypatch):
assert first.status_code == 200
assert second.status_code == 200
assert second.json()["duplicates"] == 1
def test_webhook_rejects_invalid_base64(client: TestClient, monkeypatch):
secret = _set_webhook_secret(monkeypatch)
response = client.post(
"/api/v1/webhook/email",
headers={"X-Webhook-Secret": secret},
json={"raw_email": "not-base64"},
)
assert response.status_code == 400
def test_webhook_uses_payload_subject_fallback(client: TestClient, monkeypatch):
secret = _set_webhook_secret(monkeypatch)
raw_email = _raw_email_with_attachment("notes.txt", b"ignored")
response = client.post(
"/api/v1/webhook/email",
headers={"X-Webhook-Secret": secret},
json={
"raw_email": base64.b64encode(raw_email).decode("ascii"),
"subject": "Worker subject",
},
)
assert response.status_code == 200
assert response.json()["subject"] == "Worker subject"
def test_webhook_decodes_encoded_subject(client: TestClient, monkeypatch):
secret = _set_webhook_secret(monkeypatch)
raw_email = _raw_email_with_attachment(
"notes.txt",
b"ignored",
subject=Header("DMARC r\u00e9sum\u00e9", "utf-8").encode(),
)
response = client.post(
"/api/v1/webhook/email/raw",
headers={"X-Webhook-Secret": secret},
content=raw_email,
)
assert response.status_code == 200
assert response.json()["subject"] == "DMARC r\u00e9sum\u00e9"
@pytest.mark.parametrize(
"filename,content", [(None, b"ignored"), ("notes.txt", b"ignored"), ("empty.xml", b"")]
)
def test_webhook_ignores_non_report_attachments(client: TestClient, monkeypatch, filename, content):
secret = _set_webhook_secret(monkeypatch)
response = client.post(
"/api/v1/webhook/email/raw",
headers={"X-Webhook-Secret": secret},
content=_raw_email_with_attachment(filename, content),
)
assert response.status_code == 200
assert response.json()["reports_found"] == 0
def test_webhook_records_attachment_errors(client: TestClient, monkeypatch):
secret = _set_webhook_secret(monkeypatch)
response = client.post(
"/api/v1/webhook/email/raw",
headers={"X-Webhook-Secret": secret},
content=_raw_email_with_attachment("bad.xml", b"not xml", "xml"),
)
assert response.status_code == 200
data = response.json()
assert data["reports_found"] == 0
assert data["errors"] == ["bad.xml"]
def test_webhook_rolls_back_on_unhandled_processing_error(
client: TestClient, db_session, monkeypatch
):
secret = _set_webhook_secret(monkeypatch)
with patch(
"app.api.api_v1.endpoints.webhook.email.message_from_bytes",
side_effect=RuntimeError("parse failed"),
):
response = client.post(
"/api/v1/webhook/email/raw",
headers={"X-Webhook-Secret": secret},
content=b"bad",
)
assert response.status_code == 400
assert db_session.is_active
def test_webhook_preserves_http_exceptions(client: TestClient, monkeypatch):
secret = _set_webhook_secret(monkeypatch)
with patch(
"app.api.api_v1.endpoints.webhook._process_email_attachments",
side_effect=HTTPException(status_code=418, detail="teapot"),
):
response = client.post(
"/api/v1/webhook/email/raw",
headers={"X-Webhook-Secret": secret},
content=b"Subject: DMARC\r\n\r\nbody",
)
assert response.status_code == 418