modul-x / tests /test_ml_mx_gate.py
sitcod3's picture
Upload folder using huggingface_hub
d82b7e3 verified
Raw History Blame Contribute Delete
3.37 kB
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
from app.models import DNSResult, ValidationStatus
from app.validator.engine import EmailValidationEngine
def _dns_with_mx(host):
return DNSResult(
domain_exists=True,
has_mx_records=True,
mx_records=[{"host": host, "priority": 10}],
)
def _clean():
from app.models import DisposableResult
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 FakeProvider:
async def verify_many(self, pairs):
return ({e: SMTPResult(verified=True, smtp_code=250) for e, _ in pairs}, {})
async def verify(self, email, mx_host):
return SMTPResult(verified=True, smtp_code=250)
async def catch_all(self, domain, mx_host):
return (False, 0.9)
def _fake_model(answer):
m = MagicMock()
m.predict = MagicMock(return_value=[("DISPOSABLE", 0.99)])
return m
class TrustedMxGateTests(unittest.IsolatedAsyncioTestCase):
"""A domain running on a trusted enterprise mail platform (Google Workspace,
M365, ...) is almost never disposable — disposable domains live on cheap
hosts. The ML model learned 'unfamiliar domain string = disposable' from an
imbalanced dataset, so for trusted-MX domains the model's opinion must be
ignored: the deterministic MX intelligence is the better signal."""
def setUp(self):
self.patchers = [
patch("app.validator.engine.get_model_async",
new=AsyncMock(return_value=_fake_model("DISPOSABLE"))),
]
for p in self.patchers:
p.start()
def tearDown(self):
for p in self.patchers:
p.stop()
def _engine(self):
return EmailValidationEngine(mailbox=FakeProvider(), cache=NullCache())
async def test_google_workspace_mx_skips_ml_single(self):
engine = self._engine()
with patch("app.validator.engine.check_dns",
new=AsyncMock(return_value=_dns_with_mx("aspmx.l.google.com"))):
result = await engine.validate("info@thefreshfactory.co", deep=True)
self.assertIsNone(result.disposable.ml_label)
self.assertNotIn(
"ML_DISPOSABLE_SUSPECT",
{k for k in result.scoring.breakdown},
)
async def test_unknown_mx_still_gets_ml_signal(self):
engine = self._engine()
with patch("app.validator.engine.check_dns",
new=AsyncMock(return_value=_dns_with_mx("mx.sketchy-host.test"))):
result = await engine.validate("a@sketchy.test", deep=True)
self.assertEqual(result.disposable.ml_label, "DISPOSABLE")
async def test_m365_mx_skips_ml_bulk(self):
engine = self._engine()
with patch(
"app.validator.engine.check_dns",
new=AsyncMock(return_value=_dns_with_mx("x.mail.protection.outlook.com")),
):
results = await engine.validate_batch(["a@contoso.com"], deep=True)
self.assertIsNone(results[0].disposable.ml_label)