modul-x / tests /test_engine_retry.py
sitcod3's picture
Upload folder using huggingface_hub
58d0e85 verified
Raw History Blame Contribute Delete
6.17 kB
import unittest
from unittest.mock import AsyncMock, patch
from app.models import DNSResult, DisposableResult, SMTPResult, ValidationStatus
from app.validator.engine import EmailValidationEngine
from app.validator.retry_queue import MemoryRetryStore, RetryQueue
def _dns(host="mx.corp.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 NullCache:
async def get_dns(self, domain):
return None
async def put_dns(self, domain, result):
return None
async def get_verdict(self, email, deep):
return None
async def put_verdict(self, email, deep, smtp, catch_all):
return None
class ScriptedProvider:
def __init__(self, mapping):
self.mapping = mapping
async def verify_many(self, pairs):
return (
{e: self.mapping.get(e, SMTPResult(verified=True, smtp_code=250)) for e, _ in pairs},
{},
)
async def verify(self, email, mx_host):
return self.mapping.get(email, SMTPResult(verified=True, smtp_code=250))
async def catch_all(self, domain, mx_host):
return (False, 0.9)
class EngineRetryIntegrationTests(unittest.IsolatedAsyncioTestCase):
"""4xx rows must be re-queued (bounded), definitive rows must not."""
async def test_greylisted_row_is_enqueued(self):
clock = {"t": 0.0}
queue = RetryQueue(store=MemoryRetryStore(), backoff_seconds=600, now=lambda: clock["t"])
engine = EmailValidationEngine(
mailbox=ScriptedProvider(
{"a@corp.test": SMTPResult(verified=None, smtp_code=451,
smtp_message="451 greylisted, try later")}
),
cache=NullCache(),
retry_queue=queue,
)
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(await queue.size(), 1)
entry = (await queue.due(limit=1)) if clock["t"] > 600 else None
# Still pending (not yet due), but queued.
self.assertEqual(results[0].status, ValidationStatus.RISKY)
async def test_verified_row_is_not_enqueued(self):
queue = RetryQueue(store=MemoryRetryStore(), now=lambda: 0.0)
engine = EmailValidationEngine(
mailbox=ScriptedProvider(
{"a@corp.test": SMTPResult(verified=True, smtp_code=250)}
),
cache=NullCache(),
retry_queue=queue,
)
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.assertEqual(await queue.size(), 0)
async def test_rejected_row_is_not_enqueued(self):
queue = RetryQueue(store=MemoryRetryStore(), now=lambda: 0.0)
engine = EmailValidationEngine(
mailbox=ScriptedProvider(
{"a@corp.test": SMTPResult(verified=False, smtp_code=550,
smtp_message="550 no such user")}
),
cache=NullCache(),
retry_queue=queue,
)
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.assertEqual(await queue.size(), 0)
async def test_transport_failure_is_not_enqueued(self):
# Systemic to the MX — the circuit breaker owns this, not the queue.
queue = RetryQueue(store=MemoryRetryStore(), now=lambda: 0.0)
engine = EmailValidationEngine(
mailbox=ScriptedProvider(
{"a@corp.test": SMTPResult(verified=None, error="transport_error: timed out")}
),
cache=NullCache(),
retry_queue=queue,
)
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.assertEqual(await queue.size(), 0)
async def test_queue_failure_does_not_break_validation(self):
class BrokenQueue:
async def enqueue(self, *a, **k):
raise RuntimeError("redis down")
engine = EmailValidationEngine(
mailbox=ScriptedProvider(
{"a@corp.test": SMTPResult(verified=None, smtp_code=451)}
),
cache=NullCache(),
retry_queue=BrokenQueue(),
)
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)
# The row still has a verdict; a broken queue must never lose it.
self.assertEqual(len(results), 1)
async def test_engine_without_queue_still_works(self):
engine = EmailValidationEngine(
mailbox=ScriptedProvider({"a@corp.test": SMTPResult(verified=True, smtp_code=250)}),
cache=NullCache(),
)
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].status, ValidationStatus.VALID)
if __name__ == "__main__":
unittest.main()