feat: expand platform APIs, sources, and test coverage
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Telegram Notify Success (push) Has been cancelled
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m54s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped

This commit is contained in:
2026-03-17 12:56:48 +01:00
parent b505c67968
commit 3d298ce352
101 changed files with 8387 additions and 292 deletions

View File

@@ -0,0 +1,61 @@
from __future__ import annotations
import importlib.util
import os
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from django.test import SimpleTestCase
CELERY_MODULE_PATH = (
Path(__file__).resolve().parents[3] / "src" / "core" / "celery.py"
)
def _load_module(module_name: str):
spec = importlib.util.spec_from_file_location(module_name, CELERY_MODULE_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
class CeleryModuleTest(SimpleTestCase):
def test_import_requires_django_settings_module(self):
with patch.dict(os.environ, {}, clear=True):
with self.assertRaisesMessage(
RuntimeError,
"DJANGO_SETTINGS_MODULE is not set.",
):
_load_module("isolated_core_celery_missing")
def test_import_runs_startup_checks_for_worker_runtime(self):
app_mock = MagicMock()
app_mock.conf = SimpleNamespace()
with patch.dict(os.environ, {"DJANGO_SETTINGS_MODULE": "settings.test"}, clear=True):
with patch.object(sys, "argv", ["celery", "-A", "project", "worker"]):
with patch("apps.core.startup_checks.run_startup_checks") as checks_mock:
with patch("celery.Celery", return_value=app_mock):
module = _load_module("isolated_core_celery_worker")
checks_mock.assert_called_once_with(component="celery")
app_mock.config_from_object.assert_called_once_with(
"django.conf:settings",
namespace="CELERY",
)
app_mock.autodiscover_tasks.assert_called_once_with()
self.assertEqual(module.app, app_mock)
def test_debug_task_prints_request(self):
with patch.dict(os.environ, {"DJANGO_SETTINGS_MODULE": "settings.test"}, clear=True):
with patch.object(sys, "argv", ["python", "manage.py", "shell"]):
module = _load_module("isolated_core_celery_debug")
with patch("builtins.print") as print_mock:
module.debug_task.run()
print_mock.assert_called_once()

View File

@@ -51,6 +51,12 @@ class CustomExceptionHandlerTest(SimpleTestCase):
self.assertEqual(response.status_code, 400)
self.assertEqual(len(response.data["errors"]), 2)
def test_validation_error_scalar_message(self):
exc = ValidationError("plain error")
response = custom_exception_handler(exc, self._context())
self.assertEqual(response.status_code, 400)
self.assertEqual(response.data["errors"][0]["message"], "plain error")
def test_unhandled_exception(self):
response = custom_exception_handler(RuntimeError("boom"), self._context())
self.assertEqual(response.status_code, 500)

View File

@@ -126,3 +126,13 @@ class FilterMixinTest(TestCase):
self.assertIn(filters.DjangoFilterBackend, backends)
self.assertIn(StandardSearchFilter, backends)
self.assertIn(StandardOrderingFilter, backends)
def test_get_queryset_returns_super_queryset(self):
class Parent:
def get_queryset(self):
return ["ok"]
class DummyFilterMixin(FilterMixin, Parent):
pass
self.assertEqual(DummyFilterMixin().get_queryset(), ["ok"])

View File

@@ -156,8 +156,7 @@ class BaseAppCommandTest(TestCase):
cmd.silent = True
def generator():
for idx in range(3):
yield idx
yield from range(3)
result = list(cmd.progress_iter(generator(), "Iter"))
self.assertEqual(result, [0, 1, 2])

View File

@@ -130,6 +130,9 @@ class MixinsBehaviorTest(TransactionTestCase):
):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class Meta:
app_label = "core"

View File

@@ -0,0 +1,22 @@
"""Tests for core pagination classes."""
from apps.core.pagination import StandardCursorPagination, StandardPagination
from django.test import SimpleTestCase
class PaginationSchemaTest(SimpleTestCase):
def test_standard_pagination_schema(self):
schema = StandardPagination().get_paginated_response_schema({"type": "array"})
self.assertEqual(schema["type"], "object")
self.assertEqual(schema["properties"]["data"]["type"], "array")
def test_cursor_pagination_schema(self):
schema = StandardCursorPagination().get_paginated_response_schema(
{"type": "array"}
)
self.assertEqual(schema["type"], "object")
pagination = schema["properties"]["meta"]["properties"]["pagination"]["properties"]
self.assertIn("next_cursor", pagination)
self.assertIn("previous_cursor", pagination)

View File

@@ -101,6 +101,14 @@ class IsOwnerOrReadOnlyTest(TestCase):
result = self.permission.has_object_permission(request, APIView(), obj)
self.assertTrue(result)
def test_unsafe_methods_use_owner_fallback_field(self):
request = self.factory.patch("/")
request.user = self.user
obj = MockObject(user=None, owner=self.user)
result = self.permission.has_object_permission(request, APIView(), obj)
self.assertTrue(result)
class IsAdminOrReadOnlyTest(TestCase):
"""Tests for IsAdminOrReadOnly permission"""
@@ -250,3 +258,11 @@ class IsOwnerOrAdminTest(TestCase):
result = self.permission.has_object_permission(request, APIView(), obj)
self.assertFalse(result)
def test_owner_fallback_field_is_used_for_non_admin(self):
request = self.factory.get("/")
request.user = self.user
obj = MockObject(user=None, owner=self.user)
result = self.permission.has_object_permission(request, APIView(), obj)
self.assertTrue(result)

View File

@@ -17,6 +17,10 @@ from django.test import TestCase
from tests.utils.fixtures import fake
def _password() -> str:
return fake.password(length=12, special_chars=False)
class SignalDispatcherTest(TestCase):
def test_register_connect_disconnect(self):
dispatcher = SignalDispatcher()
@@ -34,7 +38,7 @@ class SignalDispatcherTest(TestCase):
dispatcher.connect_all()
user = get_user_model().objects.create_user(
email=fake.email(), username=fake.user_name(), password="pass"
email=fake.email(), username=fake.user_name(), password=_password()
)
self.assertIn(user.pk, events)
@@ -81,7 +85,7 @@ class SignalDispatcherTest(TestCase):
signal_dispatcher.connect_all()
user = get_user_model().objects.create_user(
email=fake.email(), username=fake.user_name(), password="pass"
email=fake.email(), username=fake.user_name(), password=_password()
)
self.assertIn(user.pk, events)
self.assertTrue(signal_dispatcher.list_handlers())

View File

@@ -0,0 +1,206 @@
from __future__ import annotations
import importlib
import os
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from apps.core import startup_checks
from django.test import SimpleTestCase, override_settings
TEST_DATABASES = {
"default": {
"NAME": "mostovik",
"USER": "postgres",
"PASSWORD": "secret",
"HOST": "db.example.test",
"PORT": 5432,
"OPTIONS": {"sslmode": "require"},
}
}
TEST_CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://redis.example.test:6379/1",
}
}
class StartupChecksTest(SimpleTestCase):
@override_settings(DATABASES=TEST_DATABASES)
@patch("apps.core.startup_checks.psycopg2.connect")
def test_check_db_success(self, connect_mock):
cursor = MagicMock()
connection = MagicMock()
connection.cursor.return_value.__enter__.return_value = cursor
connect_mock.return_value = connection
success, message = startup_checks._check_db(7)
self.assertTrue(success)
self.assertEqual(message, "OK")
connect_mock.assert_called_once_with(
dbname="mostovik",
user="postgres",
password="secret",
host="db.example.test",
port=5432,
connect_timeout=7,
sslmode="require",
)
cursor.execute.assert_called_once_with("SELECT 1")
cursor.fetchone.assert_called_once_with()
connection.close.assert_called_once_with()
@override_settings(DATABASES=TEST_DATABASES)
@patch(
"apps.core.startup_checks.psycopg2.connect",
side_effect=RuntimeError("database down"),
)
def test_check_db_failure(self, connect_mock):
success, message = startup_checks._check_db(5)
self.assertFalse(success)
self.assertIn("db.example.test:5432/mostovik", message)
self.assertIn("database down", message)
connect_mock.assert_called_once()
@override_settings(CACHES=TEST_CACHES)
@patch("apps.core.startup_checks.redis.Redis.from_url")
def test_check_redis_success(self, from_url_mock):
client = MagicMock()
from_url_mock.return_value = client
success, message = startup_checks._check_redis(4)
self.assertTrue(success)
self.assertEqual(message, "OK")
from_url_mock.assert_called_once_with(
"redis://redis.example.test:6379/1",
socket_connect_timeout=4,
socket_timeout=4,
)
client.ping.assert_called_once_with()
@override_settings(CACHES=TEST_CACHES)
@patch(
"apps.core.startup_checks.redis.Redis.from_url",
side_effect=RuntimeError("redis down"),
)
def test_check_redis_failure(self, from_url_mock):
success, message = startup_checks._check_redis(6)
self.assertFalse(success)
self.assertIn("redis.example.test:6379/1", message)
self.assertIn("redis down", message)
from_url_mock.assert_called_once()
@override_settings(STARTUP_CHECKS_ENABLED=False)
@patch("apps.core.startup_checks._check_db")
@patch("apps.core.startup_checks._check_redis")
def test_run_startup_checks_skips_when_disabled(
self,
redis_mock,
db_mock,
):
startup_checks.run_startup_checks(component="web")
db_mock.assert_not_called()
redis_mock.assert_not_called()
@override_settings(
STARTUP_CHECKS_ENABLED=True,
STARTUP_DB_TIMEOUT_SECONDS=9,
STARTUP_REDIS_TIMEOUT_SECONDS=11,
)
@patch("apps.core.startup_checks._check_db", return_value=(True, "OK"))
@patch("apps.core.startup_checks._check_redis", return_value=(True, "OK"))
def test_run_startup_checks_success(self, redis_mock, db_mock):
startup_checks.run_startup_checks(component="worker")
db_mock.assert_called_once_with(9)
redis_mock.assert_called_once_with(11)
@override_settings(STARTUP_CHECKS_ENABLED=True, STARTUP_DB_TIMEOUT_SECONDS=8)
@patch("apps.core.startup_checks._check_db", return_value=(False, "db failed"))
@patch("apps.core.startup_checks._log")
def test_run_startup_checks_exits_on_db_failure(self, log_mock, db_mock):
with self.assertRaises(SystemExit) as error:
startup_checks.run_startup_checks(component="wsgi")
self.assertEqual(error.exception.code, 1)
db_mock.assert_called_once_with(8)
log_mock.assert_called_once()
self.assertIn("[startup:wsgi] DB check failed", log_mock.call_args.args[0])
self.assertIn("db failed", log_mock.call_args.args[0])
@override_settings(
STARTUP_CHECKS_ENABLED=True,
STARTUP_DB_TIMEOUT_SECONDS=3,
STARTUP_REDIS_TIMEOUT_SECONDS=12,
)
@patch("apps.core.startup_checks._check_db", return_value=(True, "OK"))
@patch("apps.core.startup_checks._check_redis", return_value=(False, "redis failed"))
@patch("apps.core.startup_checks._log")
def test_run_startup_checks_exits_on_redis_failure(
self,
log_mock,
redis_mock,
db_mock,
):
with self.assertRaises(SystemExit) as error:
startup_checks.run_startup_checks(component="asgi")
self.assertEqual(error.exception.code, 1)
db_mock.assert_called_once_with(3)
redis_mock.assert_called_once_with(12)
self.assertIn("[startup:asgi] Redis check failed", log_mock.call_args.args[0])
self.assertIn("redis failed", log_mock.call_args.args[0])
class EntryPointImportTest(SimpleTestCase):
def _import_fresh(self, module_name: str):
sys.modules.pop("core", None)
sys.modules.pop(module_name, None)
return importlib.import_module(module_name)
def test_import_core_asgi_runs_startup_checks_and_sets_default_settings(self):
sentinel_application = object()
celery_stub = SimpleNamespace(app=object())
with patch.dict(os.environ, {}, clear=False):
os.environ.pop("DJANGO_SETTINGS_MODULE", None)
with patch.dict(sys.modules, {"core.celery": celery_stub}):
with patch("apps.core.startup_checks.run_startup_checks") as checks_mock:
with patch(
"django.core.asgi.get_asgi_application",
return_value=sentinel_application,
):
module = self._import_fresh("core.asgi")
self.assertEqual(os.environ["DJANGO_SETTINGS_MODULE"], "settings.test")
checks_mock.assert_called_once_with(component="asgi")
self.assertIs(module.application, sentinel_application)
sys.modules.pop("core.asgi", None)
def test_import_core_wsgi_runs_startup_checks_and_sets_default_settings(self):
sentinel_application = object()
celery_stub = SimpleNamespace(app=object())
with patch.dict(os.environ, {}, clear=False):
os.environ.pop("DJANGO_SETTINGS_MODULE", None)
with patch.dict(sys.modules, {"core.celery": celery_stub}):
with patch("apps.core.startup_checks.run_startup_checks") as checks_mock:
with patch(
"django.core.wsgi.get_wsgi_application",
return_value=sentinel_application,
):
module = self._import_fresh("core.wsgi")
self.assertEqual(os.environ["DJANGO_SETTINGS_MODULE"], "settings.test")
checks_mock.assert_called_once_with(component="wsgi")
self.assertIs(module.application, sentinel_application)
sys.modules.pop("core.wsgi", None)

View File

@@ -352,6 +352,13 @@ class BackgroundJobsViewTest(APITestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_job_status_forbidden_for_unowned_job(self):
job = self._create_job(task_id="job-unowned", user_id=None, 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_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")
@@ -375,7 +382,9 @@ class BackgroundJobsViewTest(APITestCase):
self.assertLessEqual(len(response.data), 2)
def test_job_list_invalid_limit_returns_400(self):
self._create_job(task_id="job-invalid-limit", user_id=self.user.id, status="success")
self._create_job(
task_id="job-invalid-limit", 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": "abc"})