Рефакторинг инфраструктуры и конфигурации проекта
- Перенесена структура Django-конфига в src/core и src/settings - Унифицирована Docker-сборка и docker-compose для dev/prod - Добавлены startup-checks (DB/Redis) и обновлены env-шаблоны - Расширена OpenAPI-документация и ответы API - Удалены устаревшие deploy/requirements/служебные скрипты - Обновлены CI/CD, README и тесты
This commit is contained in:
@@ -33,9 +33,9 @@ make test TARGET=user # Все тесты user app
|
||||
make test TARGET=models # Только тесты моделей
|
||||
make test TARGET=views # Только тесты представлений
|
||||
|
||||
# Или напрямую через скрипт
|
||||
python run_tests_simple.py
|
||||
python run_tests_simple.py user
|
||||
# Или напрямую через pytest
|
||||
uv run pytest tests
|
||||
uv run pytest tests/apps/user
|
||||
```
|
||||
|
||||
### Различные способы запуска
|
||||
@@ -58,48 +58,25 @@ make test TARGET=test_models # То же что и models
|
||||
make test TARGET=test_views # То же что и views
|
||||
```
|
||||
|
||||
#### 2. Через улучшенный Django runner
|
||||
#### 2. Через pytest
|
||||
|
||||
```bash
|
||||
```bash
|
||||
# Все тесты
|
||||
python run_tests_simple.py
|
||||
uv run pytest tests
|
||||
|
||||
# Конкретное приложение
|
||||
python run_tests_simple.py user
|
||||
uv run pytest tests/apps/user
|
||||
|
||||
# Конкретные группы тестов
|
||||
python run_tests_simple.py models
|
||||
python run_tests_simple.py views
|
||||
python run_tests_simple.py serializers
|
||||
python run_tests_simple.py services
|
||||
|
||||
# Полные имена файлов
|
||||
python run_tests_simple.py test_models
|
||||
python run_tests_simple.py test_views
|
||||
```
|
||||
|
||||
#### 3. Через стандартный Django test runner
|
||||
|
||||
```bash
|
||||
# Все тесты
|
||||
python run_tests.py
|
||||
|
||||
# Конкретное приложение
|
||||
python run_tests.py test tests.apps.user
|
||||
|
||||
# Конкретный класс тестов
|
||||
python run_tests.py test tests.apps.user.test_models.UserModelTest
|
||||
```
|
||||
|
||||
#### 4. Через pytest (возможны проблемы с pdbpp)
|
||||
|
||||
```bash
|
||||
# Через скрипт-обертку
|
||||
python run_pytest.py
|
||||
uv run pytest tests -k test_models
|
||||
uv run pytest tests -k test_views
|
||||
uv run pytest tests -k test_serializers
|
||||
uv run pytest tests -k test_services
|
||||
|
||||
# Или напрямую, если настроен PYTHONPATH
|
||||
export PYTHONPATH=src:$PYTHONPATH
|
||||
export DJANGO_SETTINGS_MODULE=config.settings.test
|
||||
export DJANGO_SETTINGS_MODULE=settings.test
|
||||
pytest tests/
|
||||
```
|
||||
|
||||
@@ -107,7 +84,7 @@ pytest tests/
|
||||
|
||||
### Настройки тестов
|
||||
|
||||
Тесты используют специальные настройки Django из `src/config/settings/test.py`:
|
||||
Тесты используют специальные настройки Django из `src/settings/test.py`:
|
||||
|
||||
- **База данных**: SQLite в памяти для быстрого выполнения
|
||||
- **Кэш**: Local memory cache вместо Redis
|
||||
@@ -132,10 +109,10 @@ pytest tests/
|
||||
make test TARGET=models
|
||||
|
||||
# Запуск конкретного файла напрямую
|
||||
python run_tests_simple.py test_models
|
||||
uv run pytest tests/apps/user/test_models.py
|
||||
|
||||
# Все тесты с подробным выводом
|
||||
python run_tests_simple.py
|
||||
uv run pytest tests -v
|
||||
```
|
||||
|
||||
## 🏭 Фабрики тестовых данных
|
||||
@@ -225,13 +202,13 @@ def test_heavy_operation():
|
||||
|
||||
```bash
|
||||
# Только юнит тесты
|
||||
python run_pytest.py -m "unit"
|
||||
uv run pytest -m "unit"
|
||||
|
||||
# Исключить медленные тесты
|
||||
python run_pytest.py -m "not slow"
|
||||
uv run pytest -m "not slow"
|
||||
|
||||
# Тесты моделей
|
||||
python run_pytest.py -m "models"
|
||||
uv run pytest -m "models"
|
||||
```
|
||||
|
||||
## 🔍 Отладка тестов
|
||||
@@ -240,13 +217,13 @@ python run_pytest.py -m "models"
|
||||
|
||||
```bash
|
||||
# Показать print statements
|
||||
python run_pytest.py -s
|
||||
uv run pytest -s
|
||||
|
||||
# Подробные ошибки
|
||||
python run_pytest.py --tb=long
|
||||
uv run pytest --tb=long
|
||||
|
||||
# Показать локальные переменные при ошибке
|
||||
python run_pytest.py --tb=long --showlocals
|
||||
uv run pytest --tb=long --showlocals
|
||||
```
|
||||
|
||||
### Использование pdb
|
||||
@@ -259,7 +236,7 @@ def test_something():
|
||||
|
||||
```bash
|
||||
# Запуск с автоматическим pdb при ошибках
|
||||
python run_pytest.py --pdb
|
||||
uv run pytest --pdb
|
||||
```
|
||||
|
||||
## 📈 Покрытие кода
|
||||
@@ -268,10 +245,10 @@ python run_pytest.py --pdb
|
||||
|
||||
```bash
|
||||
# HTML отчет
|
||||
make test-coverage
|
||||
make test-cov
|
||||
|
||||
# Или напрямую
|
||||
python run_pytest.py --cov=src --cov-report=html:htmlcov
|
||||
uv run pytest --cov=src --cov-report=html:htmlcov
|
||||
|
||||
# Открыть отчет в браузере
|
||||
open htmlcov/index.html
|
||||
@@ -280,7 +257,7 @@ open htmlcov/index.html
|
||||
### Просмотр в терминале
|
||||
|
||||
```bash
|
||||
python run_pytest.py --cov=src --cov-report=term-missing
|
||||
uv run pytest --cov=src --cov-report=term-missing
|
||||
```
|
||||
|
||||
## 🔧 Добавление новых тестов
|
||||
@@ -311,10 +288,10 @@ class NewModuleTest(TestCase):
|
||||
"""Test description"""
|
||||
# Arrange
|
||||
expected_value = "test"
|
||||
|
||||
|
||||
# Act
|
||||
result = some_function()
|
||||
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_value)
|
||||
```
|
||||
@@ -323,7 +300,7 @@ class NewModuleTest(TestCase):
|
||||
|
||||
### Частые ошибки
|
||||
|
||||
1. **Ошибка импорта**: Проверьте, что `PYTHONPATH` включает папку `src`
|
||||
1. **Ошибка импорта**: Запускайте через `uv run pytest ...`, чтобы окружение и пути подхватывались корректно
|
||||
2. **База данных**: Убедитесь, что используются тестовые настройки
|
||||
3. **Миграции**: В тестах миграции отключены, но модели должны быть синхронизированы
|
||||
|
||||
@@ -335,7 +312,7 @@ make clean
|
||||
|
||||
# Пересоздание тестовой базы данных
|
||||
rm -f test_db.sqlite3
|
||||
python run_pytest.py --create-db
|
||||
uv run pytest --create-db
|
||||
```
|
||||
|
||||
## 📚 Полезные ссылки
|
||||
@@ -365,4 +342,4 @@ make test TARGET=services # Сервисы (18 тестов)
|
||||
3. **Изоляция**: Каждый тест должен быть независимым
|
||||
4. **Покрытие**: Стремитесь к покрытию не менее 80%
|
||||
5. **Быстрота**: Избегайте медленных операций в юнит тестах
|
||||
6. **Читаемость**: Тесты должны быть понятными и хорошо документированными
|
||||
6. **Читаемость**: Тесты должны быть понятными и хорошо документированными
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for core admin configurations."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||
@@ -88,6 +89,8 @@ class CoreAdminTest(TestCase):
|
||||
)
|
||||
request = self._request()
|
||||
qs = BackgroundJob.objects.all()
|
||||
self.admin.revoke_jobs(request, qs)
|
||||
with patch("celery.current_app.control.revoke") as revoke_mock:
|
||||
self.admin.revoke_jobs(request, qs)
|
||||
revoke_mock.assert_called_once_with(job.task_id, terminate=True)
|
||||
job.refresh_from_db()
|
||||
self.assertEqual(job.status, "revoked")
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from django.test import SimpleTestCase, override_settings
|
||||
from drf_yasg import openapi
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.core.openapi import _get_status_description, api_docs
|
||||
from apps.core.openapi import _get_status_description, api_docs, swagger_tag
|
||||
|
||||
|
||||
class DummySerializer(serializers.Serializer):
|
||||
@@ -34,3 +34,10 @@ class OpenAPIDocsTest(SimpleTestCase):
|
||||
|
||||
decorated = decorator(view)
|
||||
self.assertTrue(callable(decorated))
|
||||
|
||||
def test_swagger_tag_default_russian(self):
|
||||
self.assertEqual(swagger_tag("Пользователь", "User"), "Пользователь")
|
||||
|
||||
@override_settings(OPENAPI_USE_ENGLISH_TAGS=True)
|
||||
def test_swagger_tag_english_in_dev_mode(self):
|
||||
self.assertEqual(swagger_tag("Пользователь", "User"), "User")
|
||||
|
||||
@@ -11,7 +11,7 @@ from apps.core.tasks import (
|
||||
TransactionalTask,
|
||||
)
|
||||
from celery import Task
|
||||
from config.celery import app as celery_app
|
||||
from core.celery import app as celery_app
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
from faker import Faker
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework.test import APIClient, APITestCase
|
||||
|
||||
from .factories import ProfileFactory, UserFactory
|
||||
|
||||
@@ -310,3 +310,33 @@ class TokenRefreshViewTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("error", response.data)
|
||||
|
||||
|
||||
class ApiJwtOnlyAuthenticationTest(APITestCase):
|
||||
"""Tests that API auth flow is JWT-only and not session-cookie based."""
|
||||
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create_user()
|
||||
self.tokens = UserService.get_tokens_for_user(self.user)
|
||||
self.update_url = reverse("api_v1:user:user_update")
|
||||
self.patch_data = {"username": fake.unique.user_name()}
|
||||
|
||||
# Explicitly enable CSRF checks to catch accidental SessionAuthentication usage.
|
||||
self.client = APIClient(enforce_csrf_checks=True)
|
||||
self.client.cookies["sessionid"] = "fake-admin-session"
|
||||
self.client.cookies["csrftoken"] = "fake-csrf-token"
|
||||
|
||||
def test_patch_with_bearer_and_session_cookies_returns_200(self):
|
||||
"""Bearer JWT should authenticate even if session cookies are present."""
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.tokens['access']}")
|
||||
|
||||
response = self.client.patch(self.update_url, self.patch_data, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["id"], self.user.id)
|
||||
|
||||
def test_patch_with_only_session_cookies_returns_401_not_403(self):
|
||||
"""Session cookies without JWT should not trigger CSRF 403 for API auth."""
|
||||
response = self.client.patch(self.update_url, self.patch_data, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
Reference in New Issue
Block a user