"""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