test: cover webhook ingestion edge cases
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user