Files
mostovik-backend/tests/apps/core/test_views.py

227 lines
8.0 KiB
Python

"""Tests for core views and API endpoints."""
from unittest.mock import patch
from apps.core.models import JobStatus
from apps.core.services import BackgroundJobService
from apps.user.services import UserService
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from tests.apps.user.factories import UserFactory
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"])
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")
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 BackgroundJobApiTest(APITestCase):
"""Tests for background job API access rules."""
def setUp(self):
self.user = UserFactory.create_user()
tokens = UserService.get_tokens_for_user(self.user)
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {tokens['access']}")
def test_job_status_denies_other_user_job(self):
"""Test users cannot read jobs owned by another user."""
other_user = UserFactory.create_user()
job = BackgroundJobService.create_job(
task_id="task-other-user",
task_name="test.task",
user_id=other_user.id,
)
response = self.client.get(
reverse("api_v1:jobs:job-status", kwargs={"task_id": job.task_id})
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_job_status_denies_system_job_for_regular_user(self):
"""Test user_id=NULL jobs are not visible to regular users."""
job = BackgroundJobService.create_job(
task_id="task-system",
task_name="test.task",
)
response = self.client.get(
reverse("api_v1:jobs:job-status", kwargs={"task_id": job.task_id})
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_job_status_allows_system_job_for_staff(self):
"""Test staff can inspect system jobs."""
staff = UserFactory.create_user(is_staff=True)
tokens = UserService.get_tokens_for_user(staff)
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {tokens['access']}")
job = BackgroundJobService.create_job(
task_id="task-system-staff",
task_name="test.task",
)
response = self.client.get(
reverse("api_v1:jobs:job-status", kwargs={"task_id": job.task_id})
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_job_list_rejects_invalid_limit(self):
"""Test job list validates limit query parameter."""
response = self.client.get(
reverse("api_v1:jobs:job-list"),
{"limit": "bad"},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@patch("apps.core.views.current_app.control.revoke")
def test_job_control_revoke_marks_user_job_revoked(self, mock_revoke):
"""Test owner can revoke a running background job."""
job = BackgroundJobService.create_job(
task_id="task-revoke",
task_name="test.task",
user_id=self.user.id,
)
job.mark_started()
response = self.client.post(
reverse("api_v1:jobs:job-control", kwargs={"task_id": job.task_id}),
{"action": "revoke"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
mock_revoke.assert_called_once_with(job.task_id, terminate=True)
job.refresh_from_db()
self.assertEqual(job.status, JobStatus.REVOKED)
def test_job_control_denies_finished_job(self):
"""Test finished jobs cannot be revoked again."""
job = BackgroundJobService.create_job(
task_id="task-finished",
task_name="test.task",
user_id=self.user.id,
)
job.complete()
response = self.client.post(
reverse("api_v1:jobs:job-control", kwargs={"task_id": job.task_id}),
{"action": "revoke"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_job_stream_returns_completed_sse_event(self):
"""Test dev-compatible SSE job stream endpoint."""
job = BackgroundJobService.create_job(
task_id="task-stream",
task_name="test.task",
user_id=self.user.id,
)
job.complete(result={"ok": True})
response = self.client.get(
reverse("api_v1:jobs:job-stream", kwargs={"task_id": job.task_id})
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = b"".join(response.streaming_content).decode("utf-8")
self.assertIn("event: completed", content)
self.assertIn('"task_id": "task-stream"', content)