feat: обновления парсеров, тестов и миграций
Some checks failed
CI/CD Pipeline / Run Tests (push) Failing after 37s
CI/CD Pipeline / Code Quality Checks (push) Failing after 43s
CI/CD Pipeline / Build & Push Images (push) Has been skipped
CI/CD Pipeline / Deploy (dev) (push) Has been skipped
CI/CD Pipeline / Deploy (prod) (push) Has been skipped
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 0s
CI/CD Pipeline / Run Tests (pull_request) Failing after 0s
CI/CD Pipeline / Build & Push Images (pull_request) Has been skipped
CI/CD Pipeline / Deploy (dev) (pull_request) Has been skipped
CI/CD Pipeline / Deploy (prod) (pull_request) Has been skipped
Some checks failed
CI/CD Pipeline / Run Tests (push) Failing after 37s
CI/CD Pipeline / Code Quality Checks (push) Failing after 43s
CI/CD Pipeline / Build & Push Images (push) Has been skipped
CI/CD Pipeline / Deploy (dev) (push) Has been skipped
CI/CD Pipeline / Deploy (prod) (push) Has been skipped
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 0s
CI/CD Pipeline / Run Tests (pull_request) Failing after 0s
CI/CD Pipeline / Build & Push Images (pull_request) Has been skipped
CI/CD Pipeline / Deploy (dev) (pull_request) Has been skipped
CI/CD Pipeline / Deploy (prod) (pull_request) Has been skipped
- Обновлены клиенты парсеров (checko, fns, minpromtorg, proverki, zakupki) - Добавлены новые миграции для моделей - Расширено покрытие тестами - Обновлены конфигурации и настройки проекта - Добавлены утилиты для тестирования Co-Authored-By: Warp <agent@warp.dev>
This commit is contained in:
@@ -2,7 +2,16 @@
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
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):
|
||||
@@ -32,6 +41,173 @@ class HealthCheckViewTest(APITestCase):
|
||||
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"""
|
||||
@@ -66,6 +242,30 @@ class ReadinessViewTest(APITestCase):
|
||||
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"""
|
||||
@@ -99,3 +299,68 @@ class APIVersioningURLTest(APITestCase):
|
||||
"""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)
|
||||
|
||||
Reference in New Issue
Block a user