Files
gh-christianlouis-dmarq/backend/app/tests/test_mta_sts.py
T
2026-05-23 16:30:05 +02:00

304 lines
10 KiB
Python

from datetime import datetime
from unittest.mock import AsyncMock
import httpx
import pytest
from sqlalchemy.exc import IntegrityError
from app.models.dns_cache import DNSCache
from app.services.mta_sts import (
_CACHE_KEY,
MTAStsResult,
check_mta_sts,
check_mta_sts_cached,
parse_mta_sts_policy,
parse_mta_sts_record,
)
def test_parse_mta_sts_record_requires_single_record_with_id():
record, warnings, errors = parse_mta_sts_record(["v=STSv1; id=20260523"])
assert record == "v=STSv1; id=20260523"
assert warnings == []
assert errors == []
def test_parse_mta_sts_record_reports_missing_id_and_multiple_records():
record, warnings, errors = parse_mta_sts_record(["v=STSv1", "v=STSv1; id=two"])
assert record == "v=STSv1"
assert warnings == ["Multiple _mta-sts TXT records were found; publish exactly one."]
assert "id tag" in errors[0]
def test_parse_mta_sts_record_reports_missing_and_malformed_versions():
missing_record, missing_warnings, missing_errors = parse_mta_sts_record([])
malformed_record, malformed_warnings, malformed_errors = parse_mta_sts_record(
["v=STSv1x; id=bad"]
)
assert missing_record is None
assert missing_warnings == []
assert missing_errors == ["No _mta-sts TXT record was found."]
assert malformed_record == "v=STSv1x; id=bad"
assert malformed_warnings == []
assert malformed_errors == ["The _mta-sts TXT record must start with v=STSv1."]
def test_parse_mta_sts_policy_validates_required_fields():
policy, warnings, errors = parse_mta_sts_policy(
"version: STSv1\nmode: testing\nmx: mail.example.com\nmax_age: 86400\n"
)
assert policy["mode"] == "testing"
assert policy["mx"] == ["mail.example.com"]
assert policy["max_age"] == 86400
assert warnings == ["MTA-STS policy is valid but not enforcing mail delivery (testing)."]
assert errors == []
def test_parse_mta_sts_policy_reports_invalid_fields():
policy, warnings, errors = parse_mta_sts_policy(
"# comment\nignored line\nmode: invalid\nmax_age: 0\n"
)
bad_age_policy, bad_age_warnings, bad_age_errors = parse_mta_sts_policy(
"version: STSv1\nmode: enforce\nmax_age: nope\n"
)
assert policy["max_age"] == 0
assert warnings == []
assert "version: STSv1" in errors[0]
assert "mode: enforce, testing, or none" in errors[1]
assert "greater than zero" in errors[2]
assert "at least one mx" in errors[3]
assert bad_age_policy["mode"] == "enforce"
assert bad_age_warnings == []
assert "integer max_age" in bad_age_errors[0]
assert "at least one mx" in bad_age_errors[1]
class _FakeAsyncClient:
def __init__(self, response):
self.response = response
async def __aenter__(self):
return self
async def __aexit__(self, *_args):
return False
async def get(self, _url):
return self.response
@pytest.mark.asyncio
async def test_check_mta_sts_validates_dns_and_policy(monkeypatch):
provider = AsyncMock()
provider.lookup_txt = AsyncMock(return_value=["v=STSv1; id=20260523"])
request = httpx.Request("GET", "https://mta-sts.example.com/.well-known/mta-sts.txt")
response = httpx.Response(
200,
text="version: STSv1\nmode: enforce\nmx: *.example.com\nmax_age: 86400\n",
request=request,
)
monkeypatch.setattr(
"app.services.mta_sts.httpx.AsyncClient",
lambda **_: _FakeAsyncClient(response),
)
result = await check_mta_sts("example.com", provider)
assert result.status == "pass"
assert result.dns_record == "v=STSv1; id=20260523"
assert result.mode == "enforce"
assert result.mx == ["*.example.com"]
assert result.errors == []
@pytest.mark.asyncio
async def test_check_mta_sts_cached_reuses_fresh_result(db_session, monkeypatch):
provider = AsyncMock()
provider.lookup_txt = AsyncMock(return_value=["v=STSv1; id=20260523"])
request = httpx.Request("GET", "https://mta-sts.example.com/.well-known/mta-sts.txt")
response = httpx.Response(
200,
text="version: STSv1\nmode: enforce\nmx: mail.example.com\nmax_age: 86400\n",
request=request,
)
monkeypatch.setattr(
"app.services.mta_sts.httpx.AsyncClient",
lambda **_: _FakeAsyncClient(response),
)
first, first_cached, first_checked = await check_mta_sts_cached(
db_session, provider, "example.com"
)
second, second_cached, second_checked = await check_mta_sts_cached(
db_session, provider, "example.com"
)
assert isinstance(first, MTAStsResult)
assert first.status == "pass"
assert first_cached is False
assert second.status == "pass"
assert second_cached is True
assert isinstance(first_checked, datetime)
assert second_checked == first_checked
provider.lookup_txt.assert_awaited_once()
@pytest.mark.asyncio
async def test_check_mta_sts_returns_missing_record_without_policy_fetch(monkeypatch):
provider = AsyncMock()
provider.lookup_txt = AsyncMock(return_value=[])
fetch_attempted = False
class _UnexpectedAsyncClient:
async def __aenter__(self):
nonlocal fetch_attempted
fetch_attempted = True
return self
async def __aexit__(self, *_args):
return False
monkeypatch.setattr(
"app.services.mta_sts.httpx.AsyncClient",
lambda **_: _UnexpectedAsyncClient(),
)
result = await check_mta_sts("example.com", provider)
assert result.status == "fail"
assert result.errors == ["No _mta-sts TXT record was found."]
assert fetch_attempted is False
@pytest.mark.asyncio
async def test_check_mta_sts_reports_policy_fetch_error(monkeypatch):
provider = AsyncMock()
provider.lookup_txt = AsyncMock(return_value=["v=STSv1; id=20260523"])
request = httpx.Request("GET", "https://mta-sts.example.com/.well-known/mta-sts.txt")
response = httpx.Response(404, text="missing", request=request)
monkeypatch.setattr(
"app.services.mta_sts.httpx.AsyncClient",
lambda **_: _FakeAsyncClient(response),
)
result = await check_mta_sts("example.com", provider)
assert result.status == "fail"
assert result.policy_text is None
assert result.errors[0].startswith("MTA-STS policy fetch failed:")
@pytest.mark.asyncio
async def test_check_mta_sts_cached_refresh_updates_existing_row(db_session, monkeypatch):
provider = AsyncMock()
provider.lookup_txt = AsyncMock(return_value=["v=STSv1; id=20260523"])
request = httpx.Request("GET", "https://mta-sts.example.com/.well-known/mta-sts.txt")
responses = [
httpx.Response(
200,
text="version: STSv1\nmode: testing\nmx: mail.example.com\nmax_age: 86400\n",
request=request,
),
httpx.Response(
200,
text="version: STSv1\nmode: enforce\nmx: mail.example.com\nmax_age: 86400\n",
request=request,
),
]
monkeypatch.setattr(
"app.services.mta_sts.httpx.AsyncClient",
lambda **_: _FakeAsyncClient(responses.pop(0)),
)
first, first_cached, _first_checked = await check_mta_sts_cached(
db_session, provider, "example.com"
)
second, second_cached, _second_checked = await check_mta_sts_cached(
db_session, provider, "example.com", refresh=True
)
assert first.mode == "testing"
assert first_cached is False
assert second.mode == "enforce"
assert second_cached is False
assert db_session.query(DNSCache).count() == 1
@pytest.mark.asyncio
async def test_check_mta_sts_cached_recovers_from_concurrent_insert(db_session, monkeypatch):
"""Concurrent page widgets should not fail on a duplicate cache insert."""
provider = AsyncMock()
provider.lookup_txt = AsyncMock(return_value=["v=STSv1; id=20260523"])
request = httpx.Request("GET", "https://mta-sts.example.com/.well-known/mta-sts.txt")
response = httpx.Response(
200,
text="version: STSv1\nmode: enforce\nmx: mail.example.com\nmax_age: 86400\n",
request=request,
)
monkeypatch.setattr(
"app.services.mta_sts.httpx.AsyncClient",
lambda **_: _FakeAsyncClient(response),
)
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__}:mta-sts",
selectors_key=_CACHE_KEY,
result_json='{"status":"fail","errors":["stale"],"warnings":[],"mx":[]}',
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_mta_sts_cached(db_session, provider, "example.com")
assert result.status == "pass"
assert cached is False
assert db_session.query(DNSCache).count() == 1
@pytest.mark.asyncio
async def test_check_mta_sts_cached_reraises_when_conflict_row_missing(db_session, monkeypatch):
provider = AsyncMock()
provider.lookup_txt = AsyncMock(return_value=["v=STSv1; id=20260523"])
request = httpx.Request("GET", "https://mta-sts.example.com/.well-known/mta-sts.txt")
response = httpx.Response(
200,
text="version: STSv1\nmode: enforce\nmx: mail.example.com\nmax_age: 86400\n",
request=request,
)
monkeypatch.setattr(
"app.services.mta_sts.httpx.AsyncClient",
lambda **_: _FakeAsyncClient(response),
)
def fake_commit():
raise IntegrityError("insert", {}, Exception("duplicate"))
monkeypatch.setattr(db_session, "commit", fake_commit)
with pytest.raises(IntegrityError):
await check_mta_sts_cached(db_session, provider, "example.com")