modul-x / tests /test_engine_metadata.py
sitcod3's picture
Upload folder using huggingface_hub
f5afa05 verified
Raw History Blame Contribute Delete
10.4 kB
import unittest
from unittest.mock import AsyncMock, patch
from app.models import (
CatchAllResult,
DNSResult,
DisposableResult,
ScoringResult,
SMTPResult,
SyntaxResult,
ValidationStatus,
)
from app.config import settings
from app.validator.engine import EmailValidationEngine
def _syntax(**kw):
base = dict(
passed=True,
normalized_email="person@example.test",
domain="example.test",
local_part="person",
)
base.update(kw)
return SyntaxResult(**base)
def _dns(**kw):
base = dict(
domain_exists=True,
has_mx_records=True,
mx_records=[{"host": "mx.example.test", "priority": 10}],
)
base.update(kw)
return DNSResult(**base)
def _disposable(**kw):
base = dict(is_disposable=False, is_free_provider=False, is_role_based=False)
base.update(kw)
return DisposableResult(**base)
class FakeMailbox:
"""Injectable mailbox provider — controls the Layer 5/6 verdicts exactly.
Injecting the provider (instead of patching module internals) keeps these
tests independent of which concrete provider the engine resolves by default.
"""
def __init__(self, smtp=None, catch_all=(False, 0.9), raises=None):
self._smtp = smtp if smtp is not None else SMTPResult(verified=True, smtp_code=250)
self._catch_all = catch_all
self._raises = raises
async def verify(self, email, mx_host):
if self._raises:
raise self._raises
return self._smtp
async def catch_all(self, domain, mx_host):
if self._raises:
raise self._raises
return self._catch_all
def _engine_with(smtp=None, catch_all=(False, 0.9)):
return EmailValidationEngine(
mailbox=FakeMailbox(smtp=smtp, catch_all=catch_all)
)
class EngineMetadataTests(unittest.IsolatedAsyncioTestCase):
"""The engine must emit the provenance fields the frontend contract expects:
checks_run, reason_code, validator_version, run_id (RC5)."""
async def test_deep_result_emits_provenance_fields(self):
engine = _engine_with()
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_disposable()),
):
result = await engine.validate("person@example.test", deep=True)
self.assertEqual(
result.checks_run,
["syntax", "dns", "mx", "disposable", "catch_all", "smtp", "scoring"],
)
self.assertEqual(result.validator_version, settings.app_version)
self.assertTrue(result.run_id)
self.assertGreater(len(result.run_id), 5)
async def test_fast_mode_checks_run_excludes_mailbox_layers(self):
engine = _engine_with()
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_disposable()),
):
result = await engine.validate("person@example.test", deep=False)
self.assertEqual(
result.checks_run, ["syntax", "dns", "mx", "disposable", "scoring"]
)
self.assertIsNone(result.smtp)
self.assertIsNone(result.catch_all)
async def test_reason_code_dns_timeout(self):
engine = _engine_with()
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch(
"app.validator.engine.check_dns",
new=AsyncMock(
return_value=_dns(
domain_exists=False,
has_mx_records=False,
error="DNS timeout",
error_code="timeout",
)
),
),
):
result = await engine.validate("person@example.test", deep=True)
self.assertEqual(result.reason_code, "dns_timeout")
async def test_reason_code_nxdomain(self):
engine = _engine_with()
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch(
"app.validator.engine.check_dns",
new=AsyncMock(
return_value=_dns(
domain_exists=False,
has_mx_records=False,
error="Domain not found",
error_code="nxdomain",
)
),
),
):
result = await engine.validate("person@example.test", deep=True)
self.assertEqual(result.reason_code, "nxdomain")
async def test_reason_code_catch_all(self):
engine = _engine_with(
smtp=SMTPResult(verified=True, smtp_code=250), catch_all=(True, 0.95)
)
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_disposable()),
):
result = await engine.validate("person@example.test", deep=True)
self.assertEqual(result.reason_code, "catch_all_detected")
self.assertEqual(result.status, ValidationStatus.RISKY)
async def test_explicit_5xx_rejection_outranks_catch_all(self):
# A definitive "mailbox does not exist" (5xx) is stronger and more
# actionable than the domain-level catch-all flag.
engine = _engine_with(
smtp=SMTPResult(
verified=False,
smtp_code=550,
smtp_message="550 mailbox does not exist (provider confirmed)",
),
catch_all=(True, 0.95),
)
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_disposable()),
):
result = await engine.validate("person@example.test", deep=True)
self.assertEqual(result.reason_code, "rcpt_5xx")
self.assertEqual(result.scoring.score, 0)
self.assertEqual(result.status, ValidationStatus.INVALID)
async def test_reason_code_smtp_timeout(self):
engine = _engine_with(
smtp=SMTPResult(verified=None, error="SMTP verification timed out"),
catch_all=(None, 0.0),
)
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_disposable()),
):
result = await engine.validate("person@example.test", deep=True)
self.assertEqual(result.reason_code, "smtp_timeout")
class CatchAllUnknownTests(unittest.IsolatedAsyncioTestCase):
"""An unreachable catch-all probe must read as UNKNOWN (None), never as
False ("domain rejects unknown recipients"). RC6."""
async def test_catch_all_probe_unavailable_is_unknown_not_false(self):
engine = _engine_with(
smtp=SMTPResult(verified=None, error="SMTP verification timed out"),
catch_all=(None, 0.0),
)
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_disposable()),
):
result = await engine.validate("person@example.test", deep=True)
self.assertIsNone(result.catch_all.is_catch_all)
self.assertEqual(result.catch_all.confidence, 0.0)
async def test_catch_all_exception_is_unknown_not_false(self):
engine = EmailValidationEngine(
mailbox=FakeMailbox(raises=RuntimeError("port 25 unreachable"))
)
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_disposable()),
):
result = await engine.validate("person@example.test", deep=True)
self.assertIsNone(result.catch_all.is_catch_all)
class ScoreCoherenceTests(unittest.IsolatedAsyncioTestCase):
"""A perfect score (100/low) must only appear when a mailbox verdict exists."""
async def test_smtp_verified_can_reach_high_score(self):
engine = _engine_with(
smtp=SMTPResult(verified=True, smtp_code=250), catch_all=(False, 0.9)
)
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch(
"app.validator.engine.check_disposable",
return_value=_disposable(is_free_provider=False),
),
):
result = await engine.validate("person@example.test", deep=True)
self.assertGreaterEqual(result.scoring.score, 75)
self.assertEqual(result.scoring.risk_level.value, "low")
self.assertEqual(result.status, ValidationStatus.VALID)
async def test_unverified_mailbox_never_scores_100(self):
engine = _engine_with(
smtp=SMTPResult(verified=None, error="SMTP verification timed out"),
catch_all=(None, 0.0),
)
with (
patch("app.validator.engine.validate_syntax", return_value=_syntax()),
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_disposable()),
):
result = await engine.validate("person@example.test", deep=True)
self.assertLess(result.scoring.score, 75)
self.assertNotEqual(result.scoring.risk_level.value, "low")
if __name__ == "__main__":
unittest.main()