Download tests/test_retry_worker.py from sitcod3/modul-x: direct link, hf CLI and curl.
- Browser
- Download file 8.06 kB
-
https://huggingface.co/spaces/sitcod3/modul-x/resolve/main/tests/test_retry_worker.py
- Command line
-
hf download hf://spaces/sitcod3/modul-x/tests/test_retry_worker.py
-
curl -L -o test_retry_worker.py https://huggingface.co/spaces/sitcod3/modul-x/resolve/main/tests/test_retry_worker.py
8.06 kB
| import json | |
| import unittest | |
| from app.models import SMTPResult | |
| from app.validator.retry_queue import ( | |
| MemoryRetryStore, | |
| RedisRetryStore, | |
| RetryEntry, | |
| RetryQueue, | |
| drain_retry_queue, | |
| ) | |
| class FakeRedis: | |
| """Minimal async Redis subset (hash + zset) used by RedisRetryStore.""" | |
| def __init__(self): | |
| self.hashes: dict[str, dict[str, str]] = {} | |
| self.zsets: dict[str, dict[str, float]] = {} | |
| async def hset(self, key, field, value): | |
| self.hashes.setdefault(key, {})[field] = value | |
| async def hget(self, key, field): | |
| return self.hashes.get(key, {}).get(field) | |
| async def hdel(self, key, field): | |
| self.hashes.get(key, {}).pop(field, None) | |
| async def zadd(self, key, mapping): | |
| self.zsets.setdefault(key, {}).update(mapping) | |
| async def zrem(self, key, member): | |
| self.zsets.get(key, {}).pop(member, None) | |
| async def zrangebyscore(self, key, min_, max_, start=0, num=None): | |
| items = [ | |
| (m, s) for m, s in self.zsets.get(key, {}).items() if min_ <= s <= max_ | |
| ] | |
| items.sort(key=lambda kv: kv[1]) | |
| members = [m for m, _ in items] | |
| return members[start : (start + num) if num else None] | |
| async def zcard(self, key): | |
| return len(self.zsets.get(key, {})) | |
| async def hlen(self, key): | |
| return len(self.hashes.get(key, {})) | |
| class RedisRetryStoreTests(unittest.IsolatedAsyncioTestCase): | |
| """Production needs the queue to survive a restart and be shared across | |
| workers — an in-process dict does neither.""" | |
| async def test_add_persists_entry(self): | |
| store = RedisRetryStore(client=FakeRedis()) | |
| entry = RetryEntry(email="a@corp.test", mx_host="mx.corp.test", due_at=500.0) | |
| await store.add(entry) | |
| got = await store.get("a@corp.test") | |
| self.assertIsNotNone(got) | |
| self.assertEqual(got.mx_host, "mx.corp.test") | |
| self.assertEqual(got.due_at, 500.0) | |
| async def test_due_returns_only_matured_entries(self): | |
| store = RedisRetryStore(client=FakeRedis()) | |
| await store.add(RetryEntry(email="early@corp.test", mx_host="mx", due_at=100.0)) | |
| await store.add(RetryEntry(email="late@corp.test", mx_host="mx", due_at=9999.0)) | |
| due = await store.due(now=200.0, limit=10) | |
| self.assertEqual([e.email for e in due], ["early@corp.test"]) | |
| async def test_due_is_ordered_by_time(self): | |
| store = RedisRetryStore(client=FakeRedis()) | |
| await store.add(RetryEntry(email="b@corp.test", mx_host="mx", due_at=300.0)) | |
| await store.add(RetryEntry(email="a@corp.test", mx_host="mx", due_at=200.0)) | |
| due = await store.due(now=400.0, limit=10) | |
| self.assertEqual([e.email for e in due], ["a@corp.test", "b@corp.test"]) | |
| async def test_remove_clears_both_indexes(self): | |
| redis = FakeRedis() | |
| store = RedisRetryStore(client=redis) | |
| await store.add(RetryEntry(email="a@corp.test", mx_host="mx", due_at=1.0)) | |
| await store.remove("a@corp.test") | |
| self.assertIsNone(await store.get("a@corp.test")) | |
| self.assertEqual(await store.count(), 0) | |
| async def test_readd_updates_rather_than_duplicates(self): | |
| store = RedisRetryStore(client=FakeRedis()) | |
| await store.add(RetryEntry(email="a@corp.test", mx_host="mx", due_at=1.0)) | |
| await store.add(RetryEntry(email="a@corp.test", mx_host="mx", due_at=99.0, attempts=1)) | |
| self.assertEqual(await store.count(), 1) | |
| self.assertEqual((await store.get("a@corp.test")).attempts, 1) | |
| async def test_corrupt_entry_is_skipped_not_raised(self): | |
| redis = FakeRedis() | |
| store = RedisRetryStore(client=redis) | |
| await store.add(RetryEntry(email="ok@corp.test", mx_host="mx", due_at=1.0)) | |
| # Simulate a half-written/garbage payload for another member. | |
| redis.hashes.setdefault("vx:retry:entries", {})["bad@corp.test"] = "{not json" | |
| redis.zsets.setdefault("vx:retry:due", {})["bad@corp.test"] = 1.0 | |
| due = await store.due(now=5.0, limit=10) | |
| self.assertEqual([e.email for e in due], ["ok@corp.test"]) | |
| class DrainWorkerTests(unittest.IsolatedAsyncioTestCase): | |
| """A queue that only fills is a leak. Due entries must actually be retried, | |
| and a definitive answer must clear the entry.""" | |
| async def test_drain_reprobes_due_entries_and_resolves_them(self): | |
| clock = {"t": 0.0} | |
| queue = RetryQueue( | |
| store=MemoryRetryStore(), backoff_seconds=10, now=lambda: clock["t"] | |
| ) | |
| await queue.enqueue("a@corp.test", "mx.corp.test", code=451) | |
| clock["t"] = 100.0 | |
| probed: list[tuple[str, str]] = [] | |
| async def prober(email, mx_host): | |
| probed.append((email, mx_host)) | |
| return SMTPResult(verified=True, smtp_code=250) | |
| stats = await drain_retry_queue(prober, queue=queue, limit=10) | |
| self.assertEqual(probed, [("a@corp.test", "mx.corp.test")]) | |
| self.assertEqual(stats["resolved"], 1) | |
| self.assertEqual(stats["requeued"], 0) | |
| self.assertEqual(await queue.size(), 0) | |
| async def test_still_greylisted_entry_is_requeued_with_backoff(self): | |
| clock = {"t": 0.0} | |
| queue = RetryQueue( | |
| store=MemoryRetryStore(), backoff_seconds=10, max_attempts=3, | |
| now=lambda: clock["t"], | |
| ) | |
| await queue.enqueue("a@corp.test", "mx.corp.test", code=451) | |
| clock["t"] = 100.0 | |
| async def prober(email, mx_host): | |
| return SMTPResult(verified=None, smtp_code=451, smtp_message="still greylisted") | |
| stats = await drain_retry_queue(prober, queue=queue, limit=10) | |
| self.assertEqual(stats["requeued"], 1) | |
| self.assertEqual(await queue.size(), 1) | |
| # Backed off: not immediately due again. | |
| self.assertEqual(await queue.due(), []) | |
| async def test_rejected_entry_is_resolved_and_removed(self): | |
| clock = {"t": 0.0} | |
| queue = RetryQueue(store=MemoryRetryStore(), backoff_seconds=10, now=lambda: clock["t"]) | |
| await queue.enqueue("a@corp.test", "mx.corp.test", code=451) | |
| clock["t"] = 100.0 | |
| async def prober(email, mx_host): | |
| return SMTPResult(verified=False, smtp_code=550, smtp_message="550 no such user") | |
| stats = await drain_retry_queue(prober, queue=queue, limit=10) | |
| self.assertEqual(stats["resolved"], 1) | |
| self.assertEqual(await queue.size(), 0) | |
| async def test_prober_error_requeues_without_crashing(self): | |
| clock = {"t": 0.0} | |
| queue = RetryQueue(store=MemoryRetryStore(), backoff_seconds=10, now=lambda: clock["t"]) | |
| await queue.enqueue("a@corp.test", "mx.corp.test", code=451) | |
| clock["t"] = 100.0 | |
| async def prober(email, mx_host): | |
| raise RuntimeError("mx exploded") | |
| stats = await drain_retry_queue(prober, queue=queue, limit=10) | |
| self.assertEqual(stats["errors"], 1) | |
| self.assertEqual(await queue.size(), 1, "must stay queued for another try") | |
| async def test_empty_queue_is_a_noop(self): | |
| queue = RetryQueue(store=MemoryRetryStore(), now=lambda: 0.0) | |
| async def prober(email, mx_host): | |
| raise AssertionError("should not be called") | |
| stats = await drain_retry_queue(prober, queue=queue, limit=10) | |
| self.assertEqual( | |
| stats, {"checked": 0, "resolved": 0, "requeued": 0, "errors": 0, "results": []} | |
| ) | |
| async def test_results_are_returned_for_persistence(self): | |
| clock = {"t": 0.0} | |
| queue = RetryQueue(store=MemoryRetryStore(), backoff_seconds=10, now=lambda: clock["t"]) | |
| await queue.enqueue("a@corp.test", "mx.corp.test", code=451) | |
| clock["t"] = 100.0 | |
| async def prober(email, mx_host): | |
| return SMTPResult(verified=True, smtp_code=250) | |
| stats = await drain_retry_queue(prober, queue=queue, limit=10) | |
| self.assertEqual(stats["results"][0][0], "a@corp.test") | |
| self.assertTrue(stats["results"][0][1].verified) | |
| if __name__ == "__main__": | |
| unittest.main() |