modul-x / tests /test_engine_cache.py
sitcod3's picture
Upload folder using huggingface_hub
c94194a verified
Raw History Blame Contribute Delete
8.56 kB
import unittest
from unittest.mock import AsyncMock, patch
from app.models import DNSResult, DisposableResult, SMTPResult, ValidationStatus
from app.validator.engine import EmailValidationEngine
def _dns(host="mx.example.test"):
return DNSResult(
domain_exists=True,
has_mx_records=True,
mx_records=[{"host": host, "priority": 10}],
)
def _clean():
return DisposableResult(
is_disposable=False, is_free_provider=False, is_role_based=False
)
class RecordingCache:
"""In-memory stand-in for ValidationCache that records interactions."""
def __init__(self):
self.dns: dict[str, DNSResult] = {}
self.verdicts: dict[tuple[str, bool], tuple[SMTPResult, object]] = {}
self.dns_puts: list[str] = []
self.verdict_puts: list[str] = []
async def get_dns(self, domain):
return self.dns.get(domain.lower())
async def put_dns(self, domain, result):
if result.error_code in {"timeout", "resolver_error"}:
return
self.dns_puts.append(domain)
self.dns[domain.lower()] = result
async def get_verdict(self, email, deep):
return self.verdicts.get((email.lower(), deep))
async def put_verdict(self, email, deep, smtp, catch_all):
if smtp.verified is None and catch_all is not True:
return
self.verdict_puts.append(email)
self.verdicts[(email.lower(), deep)] = (smtp, catch_all)
class CountingProvider:
def __init__(self, smtp_map=None):
self._smtp = smtp_map or {}
self.batch_calls = 0
self.pairs_seen: list[tuple[str, str]] = []
async def verify_many(self, pairs):
self.batch_calls += 1
self.pairs_seen.extend(pairs)
return (
{e: self._smtp.get(e, SMTPResult(verified=True, smtp_code=250)) for e, _ in pairs},
{},
)
async def verify(self, email, mx_host):
return self._smtp.get(email, SMTPResult(verified=True, smtp_code=250))
async def catch_all(self, domain, mx_host):
return (False, 0.9)
class EngineCacheTests(unittest.IsolatedAsyncioTestCase):
"""The batch pipeline must consult and populate the cache."""
async def test_dns_cache_hit_skips_resolution(self):
cache = RecordingCache()
cache.dns["corp.test"] = _dns()
engine = EmailValidationEngine(mailbox=CountingProvider(), cache=cache)
with (
patch(
"app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())
) as dns_mock,
patch("app.validator.engine.check_disposable", return_value=_clean()),
):
results = await engine.validate_batch(["a@corp.test"], deep=True)
self.assertEqual(dns_mock.await_count, 0, "cache hit should skip DNS")
self.assertEqual(results[0].status, ValidationStatus.VALID)
async def test_dns_miss_is_populated_for_next_run(self):
cache = RecordingCache()
engine = EmailValidationEngine(mailbox=CountingProvider(), cache=cache)
with (
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_clean()),
):
await engine.validate_batch(["a@corp.test"], deep=True)
self.assertIn("corp.test", cache.dns_puts)
async def test_dns_result_is_reused_across_addresses_in_one_run(self):
cache = RecordingCache()
engine = EmailValidationEngine(mailbox=CountingProvider(), cache=cache)
with (
patch(
"app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())
) as dns_mock,
patch("app.validator.engine.check_disposable", return_value=_clean()),
):
await engine.validate_batch(
["a@corp.test", "b@corp.test", "c@corp.test"], deep=True
)
self.assertEqual(dns_mock.await_count, 1)
async def test_cached_verdict_skips_the_mailbox_probe(self):
cache = RecordingCache()
cache.dns["corp.test"] = _dns()
cache.verdicts[("a@corp.test", True)] = (
SMTPResult(verified=True, smtp_code=250),
False,
)
provider = CountingProvider()
engine = EmailValidationEngine(mailbox=provider, cache=cache)
with (
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_clean()),
):
results = await engine.validate_batch(["a@corp.test"], deep=True)
self.assertEqual(provider.batch_calls, 0, "cached verdict should skip the probe")
self.assertEqual(results[0].status, ValidationStatus.VALID)
self.assertTrue(results[0].smtp.verified)
async def test_only_uncached_addresses_are_probed(self):
cache = RecordingCache()
cache.dns["corp.test"] = _dns()
cache.verdicts[("a@corp.test", True)] = (
SMTPResult(verified=True, smtp_code=250),
False,
)
provider = CountingProvider()
engine = EmailValidationEngine(mailbox=provider, cache=cache)
with (
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_clean()),
):
await engine.validate_batch(["a@corp.test", "b@corp.test"], deep=True)
self.assertEqual(provider.pairs_seen, [("b@corp.test", "mx.example.test")])
async def test_definitive_verdict_is_cached(self):
cache = RecordingCache()
engine = EmailValidationEngine(
mailbox=CountingProvider(
{"a@corp.test": SMTPResult(verified=False, smtp_code=550,
smtp_message="550 no such user")}
),
cache=cache,
)
with (
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_clean()),
):
await engine.validate_batch(["a@corp.test"], deep=True)
self.assertIn("a@corp.test", cache.verdict_puts)
async def test_inconclusive_verdict_is_not_cached(self):
cache = RecordingCache()
engine = EmailValidationEngine(
mailbox=CountingProvider(
{"a@corp.test": SMTPResult(verified=None, error="transport_error")}
),
cache=cache,
)
with (
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_clean()),
):
await engine.validate_batch(["a@corp.test"], deep=True)
self.assertNotIn("a@corp.test", cache.verdict_puts)
async def test_cached_row_still_reports_all_seven_layers(self):
cache = RecordingCache()
cache.dns["corp.test"] = _dns()
cache.verdicts[("a@corp.test", True)] = (
SMTPResult(verified=True, smtp_code=250),
False,
)
engine = EmailValidationEngine(mailbox=CountingProvider(), cache=cache)
with (
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_clean()),
):
results = await engine.validate_batch(["a@corp.test"], deep=True)
self.assertEqual(
results[0].checks_run,
["syntax", "dns", "mx", "disposable", "catch_all", "smtp", "scoring"],
)
async def test_fast_mode_does_not_read_deep_verdicts(self):
cache = RecordingCache()
cache.dns["corp.test"] = _dns()
cache.verdicts[("a@corp.test", True)] = (
SMTPResult(verified=True, smtp_code=250),
False,
)
engine = EmailValidationEngine(mailbox=CountingProvider(), cache=cache)
with (
patch("app.validator.engine.check_dns", new=AsyncMock(return_value=_dns())),
patch("app.validator.engine.check_disposable", return_value=_clean()),
):
results = await engine.validate_batch(["a@corp.test"], deep=False)
# Fast mode skips the mailbox layers entirely, so it must not surface a
# deep verdict as if it had probed.
self.assertIsNone(results[0].smtp)
if __name__ == "__main__":
unittest.main()