"""Tests for core views (health checks)""" from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase, APIRequestFactory from datetime import timedelta from tests.apps.user.factories import UserFactory from tests.utils.fixtures import fake from django.utils import timezone from apps.core.views import HealthCheckView from apps.core import views as core_views import sys import types class HealthCheckViewTest(APITestCase): """Tests for HealthCheckView""" def test_health_check_url_reverse(self): """Test reverse URL resolution for health check""" url = reverse("core:health") self.assertEqual(url, "/health/") def test_health_check_success(self): """Test health check returns healthy status""" url = reverse("core:health") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertIn("status", response.data) self.assertIn("version", response.data) self.assertIn("checks", response.data) self.assertIn("database", response.data["checks"]) def test_health_check_database_up(self): """Test health check reports database as up""" url = reverse("core:health") response = self.client.get(url) self.assertEqual(response.data["checks"]["database"]["status"], "up") self.assertIn("latency_ms", response.data["checks"]["database"]) def test_health_check_includes_celery_when_requested(self): url = reverse("core:health") response = self.client.get(url, {"include_celery": "true"}) self.assertIn("celery", response.data["checks"]) self.assertIn(response.data["checks"]["celery"]["status"], ["up", "down"]) def test_health_check_redis_present(self): url = reverse("core:health") response = self.client.get(url) self.assertIn("redis", response.data["checks"]) self.assertIn(response.data["checks"]["redis"]["status"], ["up", "down", "skipped"]) class HealthCheckStatusCombinationsTest(APITestCase): def test_unhealthy_when_database_down(self): class DownDbHealthCheck(HealthCheckView): def _check_database(self): # type: ignore[override] return {"status": "down", "error": "db"} def _check_redis(self): # type: ignore[override] return {"status": "up", "latency_ms": 1} factory = APIRequestFactory() request = factory.get("/health/") response = DownDbHealthCheck.as_view()(request) self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE) self.assertEqual(response.data["status"], "unhealthy") def test_degraded_when_redis_down_and_celery_down(self): class DegradedHealthCheck(HealthCheckView): def _check_database(self): # type: ignore[override] return {"status": "up", "latency_ms": 1} def _check_redis(self): # type: ignore[override] return {"status": "down", "error": "redis"} def _check_celery(self): # type: ignore[override] return {"status": "down", "error": "celery"} factory = APIRequestFactory() request = factory.get("/health/", {"include_celery": "true"}) response = DegradedHealthCheck.as_view()(request) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["status"], "degraded") def test_degraded_when_celery_down_only(self): class CeleryDownHealthCheck(HealthCheckView): def _check_database(self): # type: ignore[override] return {"status": "up", "latency_ms": 1} def _check_redis(self): # type: ignore[override] return {"status": "up", "latency_ms": 1} def _check_celery(self): # type: ignore[override] return {"status": "down", "error": "celery"} factory = APIRequestFactory() request = factory.get("/health/", {"include_celery": "true"}) response = CeleryDownHealthCheck.as_view()(request) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["status"], "degraded") class HealthCheckInternalTests(APITestCase): def test_check_database_returns_down_on_error(self): original_connection = core_views.connection class _BrokenCursor: def __enter__(self): raise RuntimeError("db down") def __exit__(self, exc_type, exc, tb): return False class _BrokenConnection: def cursor(self): return _BrokenCursor() try: core_views.connection = _BrokenConnection() result = HealthCheckView()._check_database() finally: core_views.connection = original_connection self.assertEqual(result["status"], "down") def test_check_redis_import_error(self): original_module = sys.modules.get("django_redis") sys.modules["django_redis"] = None try: result = HealthCheckView()._check_redis() finally: if original_module is None: sys.modules.pop("django_redis", None) else: sys.modules["django_redis"] = original_module self.assertEqual(result["status"], "skipped") def test_check_redis_success(self): original_module = sys.modules.get("django_redis") class _FakeRedis: def ping(self): return True fake_module = types.SimpleNamespace() fake_module.get_redis_connection = lambda _alias: _FakeRedis() sys.modules["django_redis"] = fake_module try: result = HealthCheckView()._check_redis() finally: if original_module is None: sys.modules.pop("django_redis", None) else: sys.modules["django_redis"] = original_module self.assertEqual(result["status"], "up") def test_check_celery_up(self): from config import celery as celery_module original_app = celery_module.app class _FakeInspector: def active(self): return {"worker": []} class _FakeControl: def inspect(self, timeout=None): return _FakeInspector() class _FakeApp: control = _FakeControl() try: celery_module.app = _FakeApp() result = HealthCheckView()._check_celery() finally: celery_module.app = original_app self.assertEqual(result["status"], "up") def test_check_celery_error(self): from config import celery as celery_module original_app = celery_module.app class _BrokenControl: def inspect(self, timeout=None): raise RuntimeError("boom") class _BrokenApp: control = _BrokenControl() try: celery_module.app = _BrokenApp() result = HealthCheckView()._check_celery() finally: celery_module.app = original_app self.assertEqual(result["status"], "down") class LivenessViewTest(APITestCase): """Tests for LivenessView""" def test_liveness_url_reverse(self): """Test reverse URL resolution for liveness""" url = reverse("core:liveness") self.assertEqual(url, "/health/live/") def test_liveness_returns_alive(self): """Test liveness probe returns alive status""" url = reverse("core:liveness") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["status"], "alive") class ReadinessViewTest(APITestCase): """Tests for ReadinessView""" def test_readiness_url_reverse(self): """Test reverse URL resolution for readiness""" url = reverse("core:readiness") self.assertEqual(url, "/health/ready/") def test_readiness_returns_ready(self): """Test readiness probe returns ready when DB is available""" url = reverse("core:readiness") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["status"], "ready") def test_readiness_returns_not_ready_on_db_error(self): original_connection = core_views.connection class _BrokenCursor: def __enter__(self): raise RuntimeError("db down") def __exit__(self, exc_type, exc, tb): return False class _BrokenConnection: def cursor(self): return _BrokenCursor() try: core_views.connection = _BrokenConnection() url = reverse("core:readiness") response = self.client.get(url) finally: core_views.connection = original_connection self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE) self.assertEqual(response.data["status"], "not_ready") class APIVersioningURLTest(APITestCase): """Tests for API versioning URL structure""" def test_api_v1_user_register_reverse(self): """Test reverse URL for user registration""" url = reverse("api_v1:user:register") self.assertEqual(url, "/api/v1/users/register/") def test_api_v1_user_login_reverse(self): """Test reverse URL for user login""" url = reverse("api_v1:user:login") self.assertEqual(url, "/api/v1/users/login/") def test_api_v1_user_logout_reverse(self): """Test reverse URL for user logout""" url = reverse("api_v1:user:logout") self.assertEqual(url, "/api/v1/users/logout/") def test_api_v1_user_current_user_reverse(self): """Test reverse URL for current user""" url = reverse("api_v1:user:current_user") self.assertEqual(url, "/api/v1/users/me/") def test_api_v1_user_token_refresh_reverse(self): """Test reverse URL for token refresh""" url = reverse("api_v1:user:token_refresh") self.assertEqual(url, "/api/v1/users/token/refresh/") def test_api_v1_user_password_change_reverse(self): """Test reverse URL for password change""" url = reverse("api_v1:user:password_change") self.assertEqual(url, "/api/v1/users/password/change/") class BackgroundJobsViewTest(APITestCase): def setUp(self): self.user = UserFactory.create_user() self.other = UserFactory.create_user() self.admin = UserFactory.create_superuser() def _create_job(self, *, task_id: str, user_id: int | None, status: str): from apps.core.models import BackgroundJob started_at = timezone.now() completed_at = started_at + timedelta(seconds=5) return BackgroundJob.objects.create( task_id=task_id, task_name="apps.test.task", status=status, user_id=user_id, started_at=started_at, completed_at=completed_at, ) def test_job_status_for_owner(self): job = self._create_job(task_id="job-owner", user_id=self.user.id, status="success") self.client.force_authenticate(self.user) url = reverse("api_v1:jobs:job-status", kwargs={"task_id": job.task_id}) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["task_id"], job.task_id) def test_job_status_forbidden_for_other_user(self): job = self._create_job(task_id="job-forbidden", user_id=self.user.id, status="success") self.client.force_authenticate(self.other) url = reverse("api_v1:jobs:job-status", kwargs={"task_id": job.task_id}) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_job_status_for_admin(self): job = self._create_job(task_id="job-admin", user_id=self.user.id, status="success") self.client.force_authenticate(self.admin) url = reverse("api_v1:jobs:job-status", kwargs={"task_id": job.task_id}) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_job_list_filters_status(self): self._create_job(task_id="job-1", user_id=self.user.id, status="success") self._create_job(task_id="job-2", user_id=self.user.id, status="pending") self.client.force_authenticate(self.user) url = reverse("api_v1:jobs:job-list") response = self.client.get(url, {"status": "success", "limit": 10}) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 1) def test_job_list_limit(self): for idx in range(5): self._create_job( task_id=f"job-{idx}-{fake.random_int(min=1, max=9999)}", user_id=self.user.id, status="success", ) self.client.force_authenticate(self.user) url = reverse("api_v1:jobs:job-list") response = self.client.get(url, {"limit": 2}) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertLessEqual(len(response.data), 2)