feat: import selected fork operational fixes

This commit is contained in:
Christian Krakau-Louis
2026-05-22 23:48:00 +02:00
parent 9f5cbdb851
commit 302159aeb6
16 changed files with 507 additions and 60 deletions
+14
View File
@@ -188,6 +188,20 @@ class TestLogtoSettings:
assert settings.LOGTO_SKIP_SSL_VERIFY is True
class TestImapSettings:
def test_delete_imported_emails_defaults_false(self, monkeypatch):
monkeypatch.delenv("DELETE_IMPORTED_EMAILS", raising=False)
settings = Settings()
assert settings.DELETE_IMPORTED_EMAILS is False
def test_delete_imported_emails_reads_env(self, monkeypatch):
monkeypatch.setenv("DELETE_IMPORTED_EMAILS", "true")
settings = Settings()
assert settings.DELETE_IMPORTED_EMAILS is True
class TestProductionStartupSettings:
"""Tests for production-critical settings and startup validation."""
+69 -2
View File
@@ -139,6 +139,19 @@ class TestIMAPClientInit:
assert client.password == "secret"
assert client.delete_emails is True
def test_delete_emails_defaults_to_settings(self):
settings = SimpleNamespace(
IMAP_SERVER="imap.example.com",
IMAP_PORT=993,
IMAP_USERNAME="u",
IMAP_PASSWORD="p",
DELETE_IMPORTED_EMAILS=True,
)
with patch("app.services.imap_client.get_settings", return_value=settings):
client = IMAPClient()
assert client.delete_emails is True
def test_folder_uses_explicit_value_or_settings_default(self):
"""Folder defaults to settings and can be overridden explicitly."""
settings = SimpleNamespace(
@@ -223,6 +236,7 @@ class TestTestConnection:
IMAP_PORT=993,
IMAP_USERNAME=username,
IMAP_PASSWORD=password,
DELETE_IMPORTED_EMAILS=False,
)
return IMAPClient()
@@ -233,6 +247,7 @@ class TestTestConnection:
IMAP_PORT=993,
IMAP_USERNAME=None,
IMAP_PASSWORD=None,
DELETE_IMPORTED_EMAILS=False,
)
client = IMAPClient()
success, message, stats = client.test_connection()
@@ -634,11 +649,31 @@ class TestProcessSingleEmail:
mock_mail.fetch.return_value = ("OK", [(b"1", raw)])
mock_mail.store.return_value = ("OK", None)
stats = {"processed": 0, "reports_found": 0, "errors": []}
stats = {"processed": 0, "reports_found": 0, "deleted": 0, "errors": []}
client._process_single_email(mock_mail, b"1", stats)
# store should have been called twice: once for \\Seen, once for \\Deleted
assert mock_mail.store.call_count >= 2
assert stats["deleted"] == 1
def test_does_not_delete_when_no_report_imported(self):
client = self._make_client()
client.delete_emails = True
raw = _make_email_with_attachment(
"not-a-report.txt",
b"not a report",
"text/plain",
subject="DMARC Report",
)
mock_mail = MagicMock()
mock_mail.fetch.return_value = ("OK", [(b"1", raw)])
mock_mail.store.return_value = ("OK", None)
stats = {"processed": 0, "reports_found": 0, "deleted": 0, "errors": []}
client._process_single_email(mock_mail, b"1", stats)
mock_mail.store.assert_called_once_with(b"1", "+FLAGS", "\\Seen")
assert stats["deleted"] == 0
# ---------------------------------------------------------------------------
@@ -736,7 +771,22 @@ class TestFetchReports:
mock_mail = MagicMock()
mock_mail.login.return_value = None
mock_mail.select.return_value = ("OK", [b"0"])
mock_mail.search.return_value = ("OK", [b""])
mock_mail.search.return_value = ("OK", [b"1"])
mock_mail.fetch.return_value = (
"OK",
[
(
b"1",
_make_email_with_attachment(
"report.xml",
MINIMAL_DMARC_XML,
"application/xml",
subject="DMARC Report",
),
)
],
)
mock_mail.store.return_value = ("OK", None)
mock_mail.logout.return_value = None
with patch("imaplib.IMAP4_SSL", return_value=mock_mail):
@@ -744,3 +794,20 @@ class TestFetchReports:
mock_mail.expunge.assert_called_once()
assert result["success"] is True
assert result["deleted"] == 1
def test_delete_emails_skips_expunge_when_nothing_deleted(self):
client = self._make_client()
client.delete_emails = True
mock_mail = MagicMock()
mock_mail.login.return_value = None
mock_mail.select.return_value = ("OK", [b"0"])
mock_mail.search.return_value = ("OK", [b""])
mock_mail.logout.return_value = None
with patch("imaplib.IMAP4_SSL", return_value=mock_mail):
result = client.fetch_reports(days=3)
mock_mail.expunge.assert_not_called()
assert result["success"] is True
assert result["deleted"] == 0
+16 -4
View File
@@ -83,7 +83,10 @@ class TestImapFetchReports:
"errors": [],
}
with patch("app.api.api_v1.endpoints.imap.IMAPClient", return_value=mock_client):
with (
patch("app.api.api_v1.endpoints.imap.IMAPClient", return_value=mock_client),
patch("app.api.api_v1.endpoints.imap.SessionLocal", return_value=MagicMock()),
):
response = authed_client.post("/api/v1/imap/fetch-reports?days=7")
assert response.status_code == 200
@@ -103,7 +106,10 @@ class TestImapFetchReports:
def test_fetch_background_for_long_range(self, authed_client: TestClient):
"""Days > 14 should queue a background task."""
mock_client = MagicMock()
with patch("app.api.api_v1.endpoints.imap.IMAPClient", return_value=mock_client):
with (
patch("app.api.api_v1.endpoints.imap.IMAPClient", return_value=mock_client),
patch("app.api.api_v1.endpoints.imap.SessionLocal", return_value=MagicMock()),
):
response = authed_client.post("/api/v1/imap/fetch-reports?days=30")
assert response.status_code == 200
@@ -121,7 +127,10 @@ class TestImapFetchReports:
"errors": ["Could not parse email 1"],
}
with patch("app.api.api_v1.endpoints.imap.IMAPClient", return_value=mock_client):
with (
patch("app.api.api_v1.endpoints.imap.IMAPClient", return_value=mock_client),
patch("app.api.api_v1.endpoints.imap.SessionLocal", return_value=MagicMock()),
):
response = authed_client.post("/api/v1/imap/fetch-reports?days=3")
assert response.status_code == 200
@@ -132,7 +141,10 @@ class TestImapFetchReports:
mock_client = MagicMock()
mock_client.fetch_reports.side_effect = RuntimeError("unexpected")
with patch("app.api.api_v1.endpoints.imap.IMAPClient", return_value=mock_client):
with (
patch("app.api.api_v1.endpoints.imap.IMAPClient", return_value=mock_client),
patch("app.api.api_v1.endpoints.imap.SessionLocal", return_value=MagicMock()),
):
response = authed_client.post("/api/v1/imap/fetch-reports?days=5")
assert response.status_code == 500
+34 -6
View File
@@ -875,9 +875,7 @@ class TestManualSourceFetchEndpoint:
assert data["error_count"] == 1
assert "bad attachment with newline" in caplog.text
history_resp = authed_client.get(f"/api/v1/mail-sources/{source_id}/imports")
assert history_resp.json()[0]["details"] == [
{"status": "error", "filename": "bad.xml"}
]
assert history_resp.json()[0]["details"] == [{"status": "error", "filename": "bad.xml"}]
mock_imap.fetch_reports.assert_called_once_with(days=30)
def test_fetch_gmail_source(self, authed_client: TestClient, db_session: Session):
@@ -1806,8 +1804,33 @@ class TestTriggerPollImapSource:
assert result["processed"] == 3
assert result["reports_found"] == 2
assert result["new_domains"] == ["dom.example"]
mock_imap.fetch_reports.assert_called_once_with(days=7)
mock_db.commit.assert_called_once()
def test_uses_requested_days(self):
from app.main import _trigger_poll_imap_source
src = MagicMock()
src.id = 5
src.name = "My IMAP"
src.server = "imap.example.com"
src.port = 993
src.username = "u"
src.password = "p"
mock_imap = MagicMock()
mock_imap.fetch_reports.return_value = {
"success": True,
"processed": 0,
"reports_found": 0,
"new_domains": [],
}
with patch("app.main.IMAPClient", return_value=mock_imap):
_trigger_poll_imap_source(src, MagicMock(), days=30)
mock_imap.fetch_reports.assert_called_once_with(days=30)
class TestTriggerPollGmailSource:
"""Unit tests for app.main._trigger_poll_gmail_source."""
@@ -1938,7 +1961,7 @@ class TestPollSourceForTrigger:
result = _poll_source_for_trigger(src, MagicMock())
assert result is expected
mock_fn.assert_called_once()
assert mock_fn.call_args.kwargs["days"] == 7
def test_imap_exception_returns_failure_dict(self):
from app.main import _poll_source_for_trigger
@@ -2092,15 +2115,20 @@ class TestTriggerPollEndpoint:
with TestClient(main_app) as tc:
with (
patch("app.main.SessionLocal", return_value=mock_db),
patch("app.main._poll_source_for_trigger", return_value=mock_result),
patch(
"app.main._poll_source_for_trigger", return_value=mock_result
) as mock_poll,
):
resp = tc.post("/api/v1/admin/trigger-poll")
resp = tc.post("/api/v1/admin/trigger-poll?days=30")
assert resp.status_code == 200
data = resp.json()
assert data["days"] == 30
assert "sources" in data
assert len(data["sources"]) == 1
assert data["sources"][0]["success"] is True
assert mock_result == data["sources"][0]
assert mock_poll.call_args.kwargs["days"] == 30
finally:
main_app.dependency_overrides.clear()
-1
View File
@@ -79,7 +79,6 @@ def test_poll_single_imap_source_passes_configured_folder():
port=993,
username="u",
password="p",
delete_emails=False,
folder="Junk Mail",
db=db,
)
+128
View File
@@ -0,0 +1,128 @@
import base64
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
import pytest
from fastapi.testclient import TestClient
from app.core.config import get_settings
from app.models.report import DMARCReport
MINIMAL_DMARC_XML = b"""\
<?xml version="1.0"?>
<feedback>
<report_metadata>
<org_name>Webhook Test</org_name>
<email>dmarc@example.com</email>
<report_id>webhook-001</report_id>
<date_range>
<begin>1609459200</begin>
<end>1609545600</end>
</date_range>
</report_metadata>
<policy_published>
<domain>webhook.example</domain>
<adkim>r</adkim>
<aspf>r</aspf>
<p>none</p>
<sp>none</sp>
<pct>100</pct>
</policy_published>
<record>
<row>
<source_ip>1.2.3.4</source_ip>
<count>1</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>webhook.example</header_from>
</identifiers>
</record>
</feedback>
"""
def _raw_email_with_report() -> bytes:
msg = MIMEMultipart()
msg["Subject"] = "DMARC report"
part = MIMEApplication(MINIMAL_DMARC_XML, _subtype="xml")
part.add_header("Content-Disposition", "attachment", filename="report.xml")
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()
return value
@pytest.fixture(autouse=True)
def _clear_settings_cache():
yield
get_settings.cache_clear()
def test_webhook_requires_configured_secret(client: TestClient, monkeypatch):
monkeypatch.delenv("WEBHOOK_SECRET", raising=False)
get_settings.cache_clear()
response = client.post(
"/api/v1/webhook/email",
json={"raw_email": base64.b64encode(_raw_email_with_report()).decode("ascii")},
)
assert response.status_code == 503
def test_webhook_rejects_invalid_secret(client: TestClient, monkeypatch):
_set_webhook_secret(monkeypatch)
response = client.post(
"/api/v1/webhook/email",
headers={"X-Webhook-Secret": "wrong"},
json={"raw_email": base64.b64encode(_raw_email_with_report()).decode("ascii")},
)
assert response.status_code == 401
def test_webhook_imports_base64_email(client: TestClient, db_session, monkeypatch):
secret = _set_webhook_secret(monkeypatch)
response = client.post(
"/api/v1/webhook/email",
headers={"X-Webhook-Secret": secret},
json={"raw_email": base64.b64encode(_raw_email_with_report()).decode("ascii")},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["reports_found"] == 1
assert data["imported"] == 1
assert db_session.query(DMARCReport).count() == 1
def test_webhook_raw_email_marks_duplicate(client: TestClient, monkeypatch):
secret = _set_webhook_secret(monkeypatch)
raw_email = _raw_email_with_report()
first = client.post(
"/api/v1/webhook/email/raw",
headers={"X-Webhook-Secret": secret},
content=raw_email,
)
second = client.post(
"/api/v1/webhook/email/raw",
headers={"X-Webhook-Secret": secret},
content=raw_email,
)
assert first.status_code == 200
assert second.status_code == 200
assert second.json()["duplicates"] == 1