modul-x / tests /test_bulk_async.py
sitcod3's picture
Upload folder using huggingface_hub
d92dd26 verified
Raw History Blame Contribute Delete
5.89 kB
import json
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
class BulkAsyncTests(unittest.TestCase):
"""The sync bulk fallback must NOT block the HTTP request: it should
return a task_id immediately, process in the background, and let the
frontend poll /validate/bulk/status/{task_id} for progress + results.
The blocking behavior caused the frontend's 45s fetch timeout, which is
why the Results page showed nothing."""
def setUp(self):
self.captured = {}
async def fake_process(emails, webhook_url=None, deep=True):
self.captured["emails"] = list(emails)
self.captured["deep"] = deep
# Realistic shape: exactly what engine.validate_batch returns.
return [
{
"email": e, "status": "risky", "score": 70,
"syntax": {"passed": True},
"scoring": {"score": 70, "risk_level": "medium",
"breakdown": {}, "warnings": []},
"processing_time_ms": 1.0,
"validated_at": "2026-01-01T00:00:00+00:00",
}
for e in emails
]
self.patchers = [
patch("app.api.routes.process_bulk_sync", new=AsyncMock(side_effect=fake_process)),
patch("app.utils.cache.redis_available", new=AsyncMock(return_value=False)),
patch("app.api.routes.settings.enable_db_persistence", False),
# Skip the Redis connection attempt so the in-memory fallback is
# instant — otherwise the background task blocks ~4s per cache call.
patch("app.utils.cache.get_redis", new=AsyncMock(return_value=None)),
]
for p in self.patchers:
p.start()
def tearDown(self):
for p in self.patchers:
p.stop()
def test_bulk_returns_immediately_with_task_id(self):
import time
t0 = time.time()
r = client.post("/api/v1/validate/bulk", json={"emails": ["a@b.test", "c@d.test"]})
elapsed = time.time() - t0
self.assertEqual(r.status_code, 200)
body = r.json()
self.assertIn("task_id", body)
self.assertNotEqual(body["task_id"], "sync", "must return a REAL task id")
self.assertEqual(body["status"], "processing")
self.assertLess(elapsed, 2.0, f"bulk blocked for {elapsed:.1f}s - must return immediately")
def test_background_results_are_stored_for_polling(self):
import asyncio
from app.utils.cache import _memory_cache
_memory_cache.clear()
r = client.post("/api/v1/validate/bulk", json={"emails": ["a@b.test"]})
task_id = r.json()["task_id"]
# Give the background task a moment to complete.
import time
time.sleep(0.5)
stored = _memory_cache.get(f"bulk_result:{task_id}")
self.assertIsNotNone(stored, "results must be stored for polling")
# _memory_set stores (value, expires) tuples — unwrap to the payload.
if isinstance(stored, tuple):
stored = stored[0]
data = json.loads(stored)
self.assertEqual(data["status"], "completed")
self.assertEqual(len(data["results"]), 1)
def test_status_endpoint_returns_stored_results(self):
from app.utils.cache import _memory_cache
_memory_cache.clear()
r = client.post("/api/v1/validate/bulk", json={"emails": ["a@b.test"]})
task_id = r.json()["task_id"]
import time
time.sleep(0.5)
st = client.get(f"/api/v1/validate/bulk/status/{task_id}")
self.assertEqual(st.status_code, 200)
body = st.json()
self.assertEqual(body["status"], "completed")
self.assertEqual(len(body["results"]), 1)
def test_redis_available_but_flag_off_uses_background_path(self):
"""Redis being reachable must NOT switch bulk to Celery on its own.
The deployed Space runs uvicorn only (Dockerfile CMD) — no Celery
worker. If a live Redis alone selected the Celery branch, every bulk
job would sit in the queue forever and the Results page would show
"processing" indefinitely. The explicit flag (default off) is the
opt-in for hosts that actually run a worker.
"""
with (
patch("app.utils.cache.redis_available",
new=AsyncMock(return_value=True)),
patch("app.api.routes.settings.enable_db_persistence", False),
patch("app.api.routes.settings.celery_bulk_enabled", False),
):
r = client.post("/api/v1/validate/bulk",
json={"emails": ["a@b.test"]})
self.assertEqual(r.status_code, 200)
body = r.json()
self.assertNotEqual(body["task_id"], "sync")
self.assertIn("status", body)
def test_flag_on_and_redis_available_uses_celery(self):
from app.tasks.celery_tasks import bulk_validate_task # noqa: F401
fake_task = MagicMock()
fake_task.id = "celery-123"
with (
patch("app.utils.cache.redis_available",
new=AsyncMock(return_value=True)),
patch("app.api.routes.settings.enable_db_persistence", False),
patch("app.api.routes.settings.celery_bulk_enabled", True),
patch("app.api.routes.bulk_validate_task.delay",
return_value=fake_task) as delay_mock,
):
r = client.post("/api/v1/validate/bulk",
json={"emails": ["a@b.test"]})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()["task_id"], "celery-123")
delay_mock.assert_called_once()
if __name__ == "__main__":
unittest.main()