Files
gh-christianlouis-docuelevate/tests/test_queue_monitoring.py
T

247 lines
9.0 KiB
Python

"""Tests for app/api/queue.py and app/views/queue.py modules."""
from unittest.mock import MagicMock, Mock, patch
import pytest
@pytest.mark.unit
class TestGetRedisQueueLength:
"""Tests for the _get_redis_queue_length helper."""
def test_returns_queue_length(self):
"""Test returns queue length from Redis."""
from app.api.queue import _get_redis_queue_length
mock_redis = MagicMock()
mock_redis.llen.return_value = 42
assert _get_redis_queue_length(mock_redis, "document_processor") == 42
mock_redis.llen.assert_called_once_with("document_processor")
def test_returns_zero_on_error(self):
"""Test returns 0 when Redis call fails."""
from app.api.queue import _get_redis_queue_length
mock_redis = MagicMock()
mock_redis.llen.side_effect = Exception("Connection refused")
assert _get_redis_queue_length(mock_redis, "default") == 0
@pytest.mark.unit
class TestGetCeleryInspectStats:
"""Tests for the _get_celery_inspect_stats helper."""
@patch("app.celery_app.celery")
def test_returns_worker_stats(self, mock_celery_mod):
"""Test returns active, reserved, scheduled tasks."""
from app.api.queue import _get_celery_inspect_stats
mock_inspector = MagicMock()
mock_inspector.active.return_value = {
"worker1": [
{"id": "task-1", "name": "app.tasks.process_document.process_document", "args": [1], "time_start": 123}
]
}
mock_inspector.reserved.return_value = {
"worker1": [{"id": "task-2", "name": "app.tasks.upload_to_s3.upload_to_s3", "args": [2]}]
}
mock_inspector.scheduled.return_value = {
"worker1": [
{
"request": {"id": "task-3", "name": "app.tasks.check_credentials.check_credentials"},
"eta": "2026-01-01",
}
]
}
mock_celery_mod.control.inspect.return_value = mock_inspector
result = _get_celery_inspect_stats()
assert result["workers_online"] == 1
assert len(result["active"]) == 1
assert result["active"][0]["id"] == "task-1"
assert len(result["reserved"]) == 1
assert len(result["scheduled"]) == 1
@patch("app.celery_app.celery")
def test_handles_no_workers(self, mock_celery_mod):
"""Test handles case where no workers are online."""
from app.api.queue import _get_celery_inspect_stats
mock_inspector = MagicMock()
mock_inspector.active.return_value = None
mock_inspector.reserved.return_value = None
mock_inspector.scheduled.return_value = None
mock_celery_mod.control.inspect.return_value = mock_inspector
result = _get_celery_inspect_stats()
assert result["workers_online"] == 0
assert result["active"] == []
assert result["reserved"] == []
assert result["scheduled"] == []
@patch("app.celery_app.celery")
def test_handles_inspect_exception(self, mock_celery_mod):
"""Test handles exception during inspect."""
from app.api.queue import _get_celery_inspect_stats
mock_celery_mod.control.inspect.side_effect = Exception("Broker unreachable")
result = _get_celery_inspect_stats()
assert result["workers_online"] == 0
assert result["active"] == []
@pytest.mark.unit
class TestGetDbProcessingSummary:
"""Tests for the _get_db_processing_summary helper."""
def test_returns_summary(self, db_session):
"""Test returns processing summary from DB."""
from app.api.queue import _get_db_processing_summary
from app.models import FileProcessingStep, FileRecord
# Create some test files
file1 = FileRecord(filehash="abc1", local_filename="f1.pdf", file_size=100, is_duplicate=False)
file2 = FileRecord(filehash="abc2", local_filename="f2.pdf", file_size=200, is_duplicate=False)
db_session.add_all([file1, file2])
db_session.commit()
# Add steps: file1 completed, file2 in_progress
step1 = FileProcessingStep(file_id=file1.id, step_name="extract_text", status="success")
step2 = FileProcessingStep(file_id=file2.id, step_name="extract_text", status="in_progress")
db_session.add_all([step1, step2])
db_session.commit()
result = _get_db_processing_summary(db_session)
assert result["total_files"] == 2
assert result["processing"] == 1
assert isinstance(result["recent_processing"], list)
def test_returns_empty_on_error(self):
"""Test returns empty summary on DB error."""
from app.api.queue import _get_db_processing_summary
mock_db = MagicMock()
mock_db.query.side_effect = Exception("DB error")
result = _get_db_processing_summary(mock_db)
assert result["total_files"] == 0
assert result["processing"] == 0
assert result["recent_processing"] == []
@pytest.mark.integration
class TestQueueStatsEndpoint:
"""Tests for the GET /api/queue/stats endpoint."""
@patch("app.api.queue.redis.Redis")
@patch("app.celery_app.celery")
def test_queue_stats_returns_200(self, mock_celery_mod, mock_redis_cls, client):
"""Test queue stats endpoint returns 200 with data."""
# Mock Redis
mock_redis_instance = MagicMock()
mock_redis_instance.llen.return_value = 5
mock_redis_cls.from_url.return_value = mock_redis_instance
# Mock Celery inspector
mock_inspector = MagicMock()
mock_inspector.active.return_value = {}
mock_inspector.reserved.return_value = {}
mock_inspector.scheduled.return_value = {}
mock_celery_mod.control.inspect.return_value = mock_inspector
response = client.get("/api/queue/stats")
assert response.status_code == 200
data = response.json()
assert "queues" in data
assert "total_queued" in data
assert "celery" in data
assert "db_summary" in data
@patch("app.api.queue.redis.Redis")
def test_queue_stats_handles_redis_error(self, mock_redis_cls, client):
"""Test queue stats handles Redis connection failure."""
mock_redis_cls.from_url.side_effect = Exception("Redis unavailable")
response = client.get("/api/queue/stats")
assert response.status_code == 200
data = response.json()
assert data["total_queued"] == 0
@pytest.mark.integration
class TestPendingCountEndpoint:
"""Tests for the GET /api/queue/pending-count endpoint."""
@patch("app.api.queue.redis.Redis")
def test_pending_count_returns_200(self, mock_redis_cls, client):
"""Test pending count endpoint returns 200."""
mock_redis_instance = MagicMock()
mock_redis_instance.llen.return_value = 3
mock_redis_cls.from_url.return_value = mock_redis_instance
response = client.get("/api/queue/pending-count")
assert response.status_code == 200
data = response.json()
assert "total_pending" in data
assert data["total_pending"] >= 0
@patch("app.api.queue.redis.Redis")
def test_pending_count_redis_failure_still_works(self, mock_redis_cls, client):
"""Test pending count still works when Redis is down."""
mock_redis_cls.from_url.side_effect = Exception("Connection refused")
response = client.get("/api/queue/pending-count")
assert response.status_code == 200
data = response.json()
assert "total_pending" in data
@pytest.mark.integration
class TestQueueDashboardView:
"""Tests for the GET /admin/queue view."""
def test_queue_dashboard_requires_login(self, client):
"""Test queue dashboard requires authentication."""
response = client.get("/admin/queue", follow_redirects=False)
assert response.status_code in [200, 302, 401]
def test_queue_dashboard_non_admin_redirect(self, client):
"""Test queue dashboard redirects non-admin users."""
# Set non-admin session
with client:
client.cookies.set("session", "test")
response = client.get("/admin/queue", follow_redirects=False)
# Should redirect or deny since no admin session
assert response.status_code in [200, 302, 401]
@pytest.mark.unit
class TestQueueDashboardViewFunction:
"""Tests for the queue_dashboard view function."""
@patch("app.views.queue.templates")
@pytest.mark.asyncio
async def test_queue_dashboard_returns_template(self, mock_templates):
"""Test queue dashboard returns template response."""
from app.views.queue import queue_dashboard
mock_request = Mock()
mock_request.session = {"user": {"is_admin": True}}
mock_db = MagicMock()
result = await queue_dashboard(mock_request, db=mock_db)
mock_templates.TemplateResponse.assert_called_once()
call_args = mock_templates.TemplateResponse.call_args
assert call_args[0][0] == "queue_dashboard.html"
context = call_args[0][1]
assert "request" in context