feat: persist imported DMARC reports
This commit is contained in:
@@ -19,6 +19,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models.report import DMARCReport
|
||||
from app.services.gmail_client import GmailClient
|
||||
from app.services.report_store import ReportStore
|
||||
from app.tests.test_data import SAMPLE_XML
|
||||
@@ -32,6 +33,7 @@ def _make_client(
|
||||
access_token: str = "acc",
|
||||
refresh_token: str = "ref",
|
||||
already_ingested: Optional[list] = None,
|
||||
db=None,
|
||||
) -> GmailClient:
|
||||
"""Instantiate a GmailClient with real Credentials mocked out."""
|
||||
with patch("app.services.gmail_client.Credentials") as mock_creds_class:
|
||||
@@ -46,6 +48,7 @@ def _make_client(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
already_ingested_ids=already_ingested or [],
|
||||
db=db,
|
||||
)
|
||||
# Expose the mock so tests can manipulate it
|
||||
client._mock_creds = mock_creds # type: ignore[attr-defined]
|
||||
@@ -484,6 +487,25 @@ class TestProcessAttachments:
|
||||
assert stats["reports_found"] == 1
|
||||
assert "example.com" in client.report_store.get_domains()
|
||||
|
||||
def test_google_style_zip_attachment_is_persisted(self, db_session):
|
||||
"""Gmail imports write parsed DMARC reports to the database when a DB is provided."""
|
||||
client = _make_client(db=db_session)
|
||||
raw = _make_raw_email(
|
||||
[
|
||||
{
|
||||
"filename": "google.com!example.com!1597449600!1597535999.zip",
|
||||
"content": _zip_xml(),
|
||||
}
|
||||
]
|
||||
)
|
||||
msg = email_mod.message_from_bytes(raw)
|
||||
stats = {"reports_found": 0, "errors": []}
|
||||
|
||||
count = client._process_attachments(msg, stats)
|
||||
|
||||
assert count == 1
|
||||
assert db_session.query(DMARCReport).filter_by(report_id="123456789").count() == 1
|
||||
|
||||
def test_duplicate_report_is_skipped(self):
|
||||
"""Repeated imports of the same domain/report ID should not inflate totals."""
|
||||
client = _make_client()
|
||||
|
||||
@@ -16,6 +16,7 @@ from zipfile import ZipFile
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models.report import DMARCReport
|
||||
from app.services.imap_client import IMAPClient
|
||||
from app.services.report_store import ReportStore
|
||||
|
||||
@@ -411,7 +412,7 @@ class TestHasDmarcAttachments:
|
||||
|
||||
|
||||
class TestProcessAttachments:
|
||||
def _make_client(self):
|
||||
def _make_client(self, db=None):
|
||||
with patch("app.services.imap_client.get_settings") as mock_settings:
|
||||
mock_settings.return_value = MagicMock(
|
||||
IMAP_SERVER="imap.example.com",
|
||||
@@ -419,7 +420,7 @@ class TestProcessAttachments:
|
||||
IMAP_USERNAME="u",
|
||||
IMAP_PASSWORD="p",
|
||||
)
|
||||
return IMAPClient()
|
||||
return IMAPClient(db=db)
|
||||
|
||||
def test_processes_xml_attachment(self):
|
||||
client = self._make_client()
|
||||
@@ -438,6 +439,17 @@ class TestProcessAttachments:
|
||||
count = client._process_attachments(msg)
|
||||
assert count == 1
|
||||
|
||||
def test_processes_xml_attachment_persists_report(self, db_session):
|
||||
client = self._make_client(db=db_session)
|
||||
msg = email.message_from_bytes(
|
||||
_make_email_with_attachment("report.xml", MINIMAL_DMARC_XML, "application/xml")
|
||||
)
|
||||
|
||||
count = client._process_attachments(msg)
|
||||
|
||||
assert count == 1
|
||||
assert db_session.query(DMARCReport).filter_by(report_id="abc-123").count() == 1
|
||||
|
||||
def test_bad_attachment_does_not_raise(self):
|
||||
client = self._make_client()
|
||||
msg = email.message_from_bytes(_make_email_with_attachment("report.xml", b"not xml at all"))
|
||||
|
||||
@@ -3,6 +3,8 @@ import zipfile
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.models.report import DMARCReport, ReportRecord
|
||||
from app.services.report_store import ReportStore
|
||||
from app.tests.test_data import SAMPLE_XML
|
||||
|
||||
|
||||
@@ -27,6 +29,41 @@ def test_upload_report_success(client: TestClient):
|
||||
assert data["domain"] == "example.com"
|
||||
|
||||
|
||||
def test_upload_persists_report_rows(client: TestClient, db_session):
|
||||
"""Uploaded reports are written to the durable report tables."""
|
||||
zip_bytes = _make_zip(SAMPLE_XML)
|
||||
response = client.post(
|
||||
"/api/v1/reports/upload",
|
||||
files={"file": ("report.zip", zip_bytes, "application/zip")},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
report = db_session.query(DMARCReport).filter_by(report_id="123456789").one()
|
||||
assert report.org_name == "google.com"
|
||||
assert report.domain.name == "example.com"
|
||||
assert db_session.query(ReportRecord).filter_by(report_id=report.id).count() == 1
|
||||
|
||||
|
||||
def test_report_reads_hydrate_from_persisted_rows(client: TestClient):
|
||||
"""Report read APIs rebuild the in-memory projection from the database."""
|
||||
zip_bytes = _make_zip(SAMPLE_XML)
|
||||
response = client.post(
|
||||
"/api/v1/reports/upload",
|
||||
files={"file": ("report.zip", zip_bytes, "application/zip")},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
ReportStore.get_instance().clear()
|
||||
|
||||
domains = client.get("/api/v1/reports/domains")
|
||||
assert domains.status_code == 200
|
||||
assert domains.json() == ["example.com"]
|
||||
|
||||
detail = client.get("/api/v1/reports/123456789")
|
||||
assert detail.status_code == 200
|
||||
assert detail.json()["summary"]["total_count"] == 2
|
||||
|
||||
|
||||
def test_upload_populates_domains_list(client: TestClient):
|
||||
"""After uploading a report, the domain appears in the reports/domains endpoint."""
|
||||
zip_bytes = _make_zip(SAMPLE_XML)
|
||||
@@ -89,7 +126,26 @@ def test_duplicate_upload_returns_409(client: TestClient):
|
||||
assert "already been uploaded" in second.json()["detail"].lower()
|
||||
|
||||
|
||||
def test_delete_report_success(client: TestClient):
|
||||
def test_duplicate_upload_checks_persisted_rows(client: TestClient):
|
||||
"""Duplicate detection still works when the in-memory store is empty."""
|
||||
zip_bytes = _make_zip(SAMPLE_XML)
|
||||
|
||||
first = client.post(
|
||||
"/api/v1/reports/upload",
|
||||
files={"file": ("report.zip", zip_bytes, "application/zip")},
|
||||
)
|
||||
assert first.status_code == 200
|
||||
|
||||
ReportStore.get_instance().clear()
|
||||
|
||||
second = client.post(
|
||||
"/api/v1/reports/upload",
|
||||
files={"file": ("report.zip", zip_bytes, "application/zip")},
|
||||
)
|
||||
assert second.status_code == 409
|
||||
|
||||
|
||||
def test_delete_report_success(client: TestClient, db_session):
|
||||
"""Deleting an existing report returns 200 and removes it from the store."""
|
||||
zip_bytes = _make_zip(SAMPLE_XML)
|
||||
client.post(
|
||||
@@ -105,6 +161,7 @@ def test_delete_report_success(client: TestClient):
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert db_session.query(DMARCReport).filter_by(report_id="123456789").count() == 0
|
||||
|
||||
# Domain should be gone now
|
||||
assert client.get("/api/v1/reports/domain/example.com/summary").status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user