feat: add BIMI posture readiness checks
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.models.dns_cache import DNSCache
|
||||
from app.services.bimi import check_bimi, check_bimi_cached, parse_bimi_record
|
||||
|
||||
|
||||
def test_parse_bimi_record_requires_single_bimi_record():
|
||||
result, warnings, errors = parse_bimi_record(
|
||||
["v=BIMI1; l=https://example.com/logo.svg; a=https://example.com/vmc.pem"]
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.status == "pass"
|
||||
assert result.logo_url == "https://example.com/logo.svg"
|
||||
assert result.certificate_url == "https://example.com/vmc.pem"
|
||||
assert warnings == []
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_parse_bimi_record_reports_missing_and_malformed_records():
|
||||
missing, missing_warnings, missing_errors = parse_bimi_record([])
|
||||
malformed, _, malformed_errors = parse_bimi_record(["v=BIMI1; l=http://example.com/logo.png"])
|
||||
|
||||
assert missing is None
|
||||
assert missing_warnings == []
|
||||
assert missing_errors == ["No BIMI TXT record was found at the selector."]
|
||||
assert malformed is not None
|
||||
assert malformed.status == "fail"
|
||||
assert "HTTPS" in malformed_errors[0]
|
||||
|
||||
|
||||
def test_parse_bimi_record_warns_without_certificate():
|
||||
result, warnings, errors = parse_bimi_record(["v=BIMI1; l=https://example.com/logo.svg"])
|
||||
|
||||
assert result is not None
|
||||
assert result.status == "pass"
|
||||
assert errors == []
|
||||
assert warnings == ["No BIMI certificate URL is published; some mailbox providers require one."]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_bimi_looks_up_default_selector():
|
||||
provider = AsyncMock()
|
||||
provider.lookup_txt = AsyncMock(
|
||||
return_value=["v=BIMI1; l=https://example.com/logo.svg; a=https://example.com/vmc.pem"]
|
||||
)
|
||||
|
||||
result = await check_bimi("example.com", provider)
|
||||
|
||||
assert result.status == "pass"
|
||||
assert result.query_name == "default._bimi.example.com"
|
||||
provider.lookup_txt.assert_awaited_once_with("default._bimi.example.com")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_bimi_cached_reuses_fresh_result(db_session):
|
||||
provider = AsyncMock()
|
||||
provider.lookup_txt = AsyncMock(return_value=["v=BIMI1; l=https://example.com/logo.svg"])
|
||||
|
||||
first, first_cached, first_checked = await check_bimi_cached(
|
||||
db_session, provider, "example.com"
|
||||
)
|
||||
second, second_cached, second_checked = await check_bimi_cached(
|
||||
db_session, provider, "example.com"
|
||||
)
|
||||
|
||||
assert first.status == "pass"
|
||||
assert first_cached is False
|
||||
assert second_cached is True
|
||||
assert second.logo_url == "https://example.com/logo.svg"
|
||||
assert second_checked == first_checked
|
||||
assert provider.lookup_txt.await_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_bimi_cached_recovers_from_concurrent_insert(db_session, monkeypatch):
|
||||
provider = AsyncMock()
|
||||
provider.lookup_txt = AsyncMock(return_value=["v=BIMI1; l=https://example.com/logo.svg"])
|
||||
original_commit = db_session.commit
|
||||
original_rollback = db_session.rollback
|
||||
commit_calls = 0
|
||||
|
||||
def fake_commit():
|
||||
nonlocal commit_calls
|
||||
commit_calls += 1
|
||||
if commit_calls == 1:
|
||||
raise IntegrityError("insert", {}, Exception("duplicate"))
|
||||
original_commit()
|
||||
|
||||
def fake_rollback():
|
||||
original_rollback()
|
||||
db_session.add(
|
||||
DNSCache(
|
||||
domain="example.com",
|
||||
provider=f"{provider.__class__.__name__}:bimi",
|
||||
selectors_key="bimi-v1:default",
|
||||
result_json=(
|
||||
'{"certificate_url":null,"dns_record":null,"errors":["missing"],'
|
||||
'"evidence_url":null,"logo_url":null,"query_name":"default._bimi.example.com",'
|
||||
'"selector":"default","status":"fail","warnings":[]}'
|
||||
),
|
||||
checked_at=datetime(2026, 5, 23, 12, 0, 0),
|
||||
)
|
||||
)
|
||||
original_commit()
|
||||
|
||||
monkeypatch.setattr(db_session, "commit", fake_commit)
|
||||
monkeypatch.setattr(db_session, "rollback", fake_rollback)
|
||||
|
||||
result, cached, _checked = await check_bimi_cached(db_session, provider, "example.com")
|
||||
|
||||
assert result.status == "pass"
|
||||
assert cached is False
|
||||
assert db_session.query(DNSCache).count() == 1
|
||||
@@ -19,6 +19,7 @@ from app.api.api_v1.endpoints import domains as domains_endpoint
|
||||
from app.api.api_v1.endpoints.domains import _spf_fix_hint
|
||||
from app.models.dns_cache import DNSCache
|
||||
from app.models.domain import Domain
|
||||
from app.services.bimi import BIMIResult
|
||||
from app.services.dns_cache import _selectors_key, resolve_domain_dns_cached
|
||||
from app.services.dns_resolver import DomainDNSResult
|
||||
from app.services.mta_sts import MTAStsResult
|
||||
@@ -430,12 +431,22 @@ def test_dns_health_links_checks_to_evidence(client: TestClient):
|
||||
max_age=86400,
|
||||
mx=["*.example.com"],
|
||||
)
|
||||
bimi = BIMIResult(
|
||||
status="pass",
|
||||
dns_record="v=BIMI1; l=https://example.com/logo.svg; a=https://example.com/vmc.pem",
|
||||
logo_url="https://example.com/logo.svg",
|
||||
certificate_url="https://example.com/vmc.pem",
|
||||
)
|
||||
with (
|
||||
_mock_dns(result=missing_dkim),
|
||||
patch(
|
||||
"app.api.api_v1.endpoints.domains.check_mta_sts_cached",
|
||||
new=AsyncMock(return_value=(mta_sts, False, None)),
|
||||
),
|
||||
patch(
|
||||
"app.api.api_v1.endpoints.domains.check_bimi_cached",
|
||||
new=AsyncMock(return_value=(bimi, False, None)),
|
||||
),
|
||||
):
|
||||
response = client.get(f"/api/v1/domains/{DOMAIN}/dns/health")
|
||||
|
||||
@@ -444,10 +455,14 @@ def test_dns_health_links_checks_to_evidence(client: TestClient):
|
||||
assert data["status"] == "degraded"
|
||||
dkim_check = next(check for check in data["checks"] if check["key"] == "dkim")
|
||||
mta_sts_check = next(check for check in data["checks"] if check["key"] == "mta_sts")
|
||||
bimi_check = next(check for check in data["checks"] if check["key"] == "bimi")
|
||||
assert dkim_check["status"] == "fail"
|
||||
assert dkim_check["evidence"][0]["href"] == "#dns-records"
|
||||
assert mta_sts_check["status"] == "pass"
|
||||
assert mta_sts_check["evidence"][1]["href"] == "#mta-sts-posture"
|
||||
assert bimi_check["status"] == "fail"
|
||||
assert bimi_check["evidence"][0]["href"] == "#bimi-posture"
|
||||
assert any(item["type"] == "bimi_dmarc_not_ready" for item in data["recommendations"])
|
||||
assert any(item["type"] == "missing_dkim" for item in data["recommendations"])
|
||||
|
||||
|
||||
@@ -498,6 +513,7 @@ def test_dns_health_marks_all_missing_records_critical(client: TestClient):
|
||||
recommendation_types = [item["type"] for item in data["recommendations"]]
|
||||
assert {"missing_dmarc", "missing_spf", "missing_dkim"}.issubset(set(recommendation_types))
|
||||
assert recommendation_types.count("missing_mta_sts") == 1
|
||||
assert recommendation_types.count("missing_bimi") == 1
|
||||
assert any(item["type"] == "policy_needs_more_data" for item in data["recommendations"])
|
||||
|
||||
|
||||
@@ -531,6 +547,58 @@ def test_mta_sts_endpoint_returns_404_for_unknown_domain(client: TestClient):
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_bimi_endpoint_returns_cached_posture(client: TestClient):
|
||||
"""The domain detail page can fetch BIMI posture with cache metadata."""
|
||||
checked_at = datetime(2026, 5, 23, 12, 0, 0)
|
||||
result = BIMIResult(
|
||||
status="pass",
|
||||
selector="default",
|
||||
query_name=f"default._bimi.{DOMAIN}",
|
||||
dns_record="v=BIMI1; l=https://example.com/logo.svg; a=https://example.com/vmc.pem",
|
||||
logo_url="https://example.com/logo.svg",
|
||||
certificate_url="https://example.com/vmc.pem",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"app.api.api_v1.endpoints.domains.check_bimi_cached",
|
||||
new=AsyncMock(return_value=(result, True, checked_at)),
|
||||
):
|
||||
response = client.get(f"/api/v1/domains/{DOMAIN}/dns/bimi")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "pass"
|
||||
assert data["query_name"] == f"default._bimi.{DOMAIN}"
|
||||
assert data["logo_url"] == "https://example.com/logo.svg"
|
||||
assert data["cached"] is True
|
||||
assert data["checked_at"] == checked_at.isoformat()
|
||||
|
||||
|
||||
def test_bimi_endpoint_returns_404_for_unknown_domain(client: TestClient):
|
||||
response = client.get("/api/v1/domains/unknown.example.com/dns/bimi")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_domain_detail_data_endpoints_support_manually_configured_domain(
|
||||
client: TestClient, db_session
|
||||
):
|
||||
"""Manually monitored domains should render empty detail data instead of 404s."""
|
||||
db_session.add(Domain(name="manual.example", active=True))
|
||||
db_session.commit()
|
||||
|
||||
reports = client.get("/api/v1/domains/manual.example/reports")
|
||||
sources = client.get("/api/v1/domains/manual.example/sources")
|
||||
selectors = client.get("/api/v1/domains/manual.example/selectors")
|
||||
|
||||
assert reports.status_code == 200
|
||||
assert reports.json()["reports"] == []
|
||||
assert sources.status_code == 200
|
||||
assert sources.json()["sources"] == []
|
||||
assert selectors.status_code == 200
|
||||
assert selectors.json() == {"selectors": [], "report_selectors": []}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("policy", "summary", "expected_type", "expected_severity"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user