Подготовка новых моделей тестов
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов для служебных классов и утилит
|
||||
"""
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов классов авторизации
|
||||
"""
|
||||
|
||||
from .expiring_token_authentication_tests import ExpiringTokenAuthenticationTests
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ExpiringTokenAuthenticationTests
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils import translation
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.management.userstory import AccountTestData
|
||||
from net.xeaf.rack.management.userstory import SessionTestData
|
||||
from net.xeaf.rack.models import AccountModel
|
||||
from net.xeaf.rack.models import SessionModel
|
||||
from net.xeaf.rack.utils.auth import ExpiringTokenAuthentication
|
||||
|
||||
|
||||
class ExpiringTokenAuthenticationTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для ExpiringTokenAuthentication
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Подготовка к тестированию
|
||||
"""
|
||||
|
||||
super().setUp()
|
||||
self.auth = ExpiringTokenAuthentication()
|
||||
self.factory = APIRequestFactory()
|
||||
self.old_language = translation.get_language()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
"""
|
||||
Восстанавливаем язык глобального контекста после тестов
|
||||
"""
|
||||
|
||||
super().tearDown()
|
||||
translation.activate(self.old_language)
|
||||
|
||||
@classmethod
|
||||
def _wrap_to_drf_request(cls, wsgi_request) -> Request:
|
||||
"""
|
||||
Вспомогательный метод для честного оборачивания Django WSGIRequest в DRF Request
|
||||
"""
|
||||
|
||||
return Request(wsgi_request, parsers=[JSONParser()])
|
||||
|
||||
def test_authenticate_adjusts_language_via_query_param(self) -> None:
|
||||
"""
|
||||
Должен переключить глобальную локаль Django, используя механизмы LocaleManager
|
||||
"""
|
||||
|
||||
# 1. Создаем нативный запрос Django
|
||||
wsgi_req = self.factory.get("/", {"x-ui-language": "en"})
|
||||
# 2. Оборачиваем его в честный DRF Request, у которого ЕСТЬ query_params
|
||||
drf_request = self._wrap_to_drf_request(wsgi_req)
|
||||
|
||||
self.auth.authenticate(drf_request)
|
||||
|
||||
# Проверяем, что LocaleManager отработал "вживую" и активировал перевод
|
||||
self.assertEqual(translation.get_language(), "en")
|
||||
self.assertEqual(getattr(drf_request, "LANGUAGE_CODE", None), "EN")
|
||||
|
||||
def test_authenticate_extracts_token_from_query_params_and_logs_in(self) -> None:
|
||||
"""
|
||||
Должен успешно авторизовать Ивана Иванова, если его токен передан в GET параметре
|
||||
"""
|
||||
|
||||
wsgi_req = self.factory.get("/", {ExpiringTokenAuthentication.AUTH_TOKEN_PARAM: SessionTestData.I_IVANOV_SESSION_TOKEN})
|
||||
drf_request = self._wrap_to_drf_request(wsgi_req)
|
||||
|
||||
result = self.auth.authenticate(drf_request)
|
||||
|
||||
# Линтер-безопасная проверка контейнера
|
||||
self.assertIsInstance(result, tuple)
|
||||
assert result is not None
|
||||
|
||||
account, session = result
|
||||
self.assertEqual(account.id, AccountTestData.I_IVANOV_ID)
|
||||
self.assertEqual(session.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
def test_authenticate_fallback_to_super_headers_if_no_query_param(self) -> None:
|
||||
"""
|
||||
Должен отработать через стандартный заголовок 'Authorization: Token ...' родительского класса
|
||||
"""
|
||||
|
||||
wsgi_req = self.factory.get("/", HTTP_AUTHORIZATION=f"Token {SessionTestData.P_PETROV_SESSION_TOKEN}")
|
||||
drf_request = self._wrap_to_drf_request(wsgi_req)
|
||||
|
||||
result = self.auth.authenticate(drf_request)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
assert result is not None
|
||||
|
||||
account, session = result
|
||||
self.assertEqual(account.id, AccountTestData.P_PETROV_ID)
|
||||
|
||||
def test_authenticate_credentials_returns_none_tuple_if_token_not_found(self) -> None:
|
||||
"""
|
||||
Должен мягко вернуть (None, None), если токена вообще нет в базе сессий
|
||||
"""
|
||||
|
||||
result = self.auth.authenticate_credentials("completely_fake_token")
|
||||
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
def test_authenticate_credentials_returns_none_tuple_if_session_is_expired(self) -> None:
|
||||
"""
|
||||
Должен вернуть (None, None), если сессия Петра Петрова просрочена
|
||||
"""
|
||||
|
||||
session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||
session.expires_at = timezone.now() - datetime.timedelta(seconds=1)
|
||||
session.save()
|
||||
|
||||
result = self.auth.authenticate_credentials(SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
def test_authenticate_credentials_returns_none_tuple_if_account_is_not_active(self) -> None:
|
||||
"""
|
||||
Должен вернуть (None, None), если AccountManager заблокировал вход (is_active = False)
|
||||
"""
|
||||
|
||||
account = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
|
||||
account.is_active = False
|
||||
account.save()
|
||||
|
||||
result = self.auth.authenticate_credentials(SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
def test_authenticate_credentials_returns_none_tuple_if_account_is_deleted(self) -> None:
|
||||
"""
|
||||
Должен вернуть (None, None), если пользователь помечен как удаленный
|
||||
"""
|
||||
|
||||
account = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
|
||||
account.soft_delete(save=True)
|
||||
|
||||
result = self.auth.authenticate_credentials(SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
def test_authenticate_credentials_renews_session_timestamp_on_success(self) -> None:
|
||||
"""
|
||||
Должен обновить время жизни сессии в БД и вернуть валидные объекты при успешном входе
|
||||
"""
|
||||
|
||||
session_before = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
result = self.auth.authenticate_credentials(SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
assert result is not None
|
||||
account, session = result
|
||||
|
||||
self.assertIsInstance(account, AccountModel)
|
||||
self.assertEqual(account.id, AccountTestData.I_IVANOV_ID)
|
||||
|
||||
self.assertIsInstance(session, SessionModel)
|
||||
session.refresh_from_db()
|
||||
self.assertGreater(session.expires_at, session_before.expires_at)
|
||||
@@ -1,99 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса DateUtilsTest
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.utils import DateUtils
|
||||
|
||||
|
||||
class DateUtilsTestCase(SimpleTestCase):
|
||||
"""
|
||||
Классы для DateUtils.
|
||||
"""
|
||||
|
||||
def test_now_returns_frozen_current_time(self) -> None:
|
||||
"""
|
||||
Должен вернуть точно зафиксированное текущее время через mock.patch
|
||||
"""
|
||||
|
||||
fixed_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=timezone.get_current_timezone())
|
||||
|
||||
with patch("django.utils.timezone.now", return_value=fixed_time):
|
||||
result = DateUtils.now()
|
||||
self.assertEqual(result, fixed_time)
|
||||
|
||||
def test_now_returns_timezone_aware_datetime(self) -> None:
|
||||
"""
|
||||
Должен возвращать объект datetime с информацией о часовом поясе (is_aware)
|
||||
"""
|
||||
|
||||
result = DateUtils.now()
|
||||
self.assertTrue(timezone.is_aware(result))
|
||||
|
||||
def test_shift_time_uses_now_if_date_time_is_none(self) -> None:
|
||||
"""
|
||||
Должен автоматически взять текущее время (now), если date_time передан как None
|
||||
"""
|
||||
|
||||
fixed_now = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2026, 7, 17, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
result = DateUtils.shift_time(date_time=None, days=3)
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_uses_now_by_omission(self) -> None:
|
||||
"""
|
||||
Должен автоматически взять текущее время (now), если параметр date_time вообще опущен
|
||||
"""
|
||||
|
||||
fixed_now = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2026, 7, 14, 14, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
result = DateUtils.shift_time(hours=2)
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_adds_days_correctly_with_explicit_date(self) -> None:
|
||||
"""
|
||||
Должен корректно сдвигать время вперед, если передан явный объект datetime
|
||||
"""
|
||||
|
||||
base_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2026, 7, 19, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
result = DateUtils.shift_time(base_time, days=5)
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_subtracts_hours_correctly(self) -> None:
|
||||
"""
|
||||
Должен корректно сдвигать время назад при передаче отрицательных значений
|
||||
"""
|
||||
|
||||
base_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2026, 7, 14, 9, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
result = DateUtils.shift_time(base_time, hours=-3)
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_complex_modification(self) -> None:
|
||||
"""
|
||||
Должен атомарно применять комбинированный сдвиг по всем осям времени
|
||||
"""
|
||||
|
||||
base_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2026, 7, 15, 14, 30, 15, tzinfo=datetime.timezone.utc)
|
||||
|
||||
result = DateUtils.shift_time(base_time, days=1, hours=2, minutes=30, seconds=15)
|
||||
self.assertEqual(result, expected_time)
|
||||
@@ -1,16 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов для классов исключений
|
||||
"""
|
||||
|
||||
from .bad_request_exception_tests import BadRequestExceptionTests
|
||||
from .critical_exception_tests import CriticalExceptionTests
|
||||
from .file_not_found_exception_tests import FileNotFoundExceptionTests
|
||||
from .forbidden_exception_tests import ForbiddenExceptionTests
|
||||
from .not_found_exception_tests import NotFoundExceptionTests
|
||||
from .unauthorized_exception_tests import UnauthorizedExceptionTests
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса BadRequestExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import BadRequestException
|
||||
|
||||
|
||||
class BadRequestExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для BadRequestException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет инициализацию по умолчанию (без параметров)
|
||||
"""
|
||||
|
||||
exc = BadRequestException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.bad_request")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_false(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает False для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = BadRequestException()
|
||||
|
||||
self.assertFalse(exc.is_pure_status())
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CriticalExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import CriticalException
|
||||
|
||||
|
||||
class CriticalExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для CriticalException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет дефолтные параметры: статус и сообщение об ошибке
|
||||
"""
|
||||
|
||||
exc = CriticalException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.internal_server_error")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_true(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает True для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = CriticalException()
|
||||
|
||||
self.assertTrue(exc.is_pure_status())
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса FileNotFoundExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import FileNotFoundException
|
||||
|
||||
|
||||
class FileNotFoundExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для FileNotFoundException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет дефолтные параметры: статус и сообщение об ошибке
|
||||
"""
|
||||
|
||||
exc = FileNotFoundException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.not_found")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_true(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает True для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = FileNotFoundException()
|
||||
|
||||
self.assertTrue(exc.is_pure_status())
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ForbiddenExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import ForbiddenException
|
||||
|
||||
|
||||
class ForbiddenExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для BadRequestException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет инициализацию по умолчанию (без параметров)
|
||||
"""
|
||||
|
||||
exc = ForbiddenException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.forbidden")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_false(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает False для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = ForbiddenException()
|
||||
|
||||
self.assertFalse(exc.is_pure_status())
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса NotFoundExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import NotFoundException
|
||||
|
||||
|
||||
class NotFoundExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для NotFoundException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет дефолтные параметры: статус и сообщение об ошибке
|
||||
"""
|
||||
|
||||
exc = NotFoundException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.not_found")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_false(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает False для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = NotFoundException()
|
||||
|
||||
self.assertFalse(exc.is_pure_status())
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса UnauthorizedExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||
|
||||
|
||||
class UnauthorizedExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для UnauthorizedException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет дефолтные параметры: статус и сообщение об ошибке
|
||||
"""
|
||||
|
||||
exc = UnauthorizedException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.unauthorized")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_false(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает False для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = UnauthorizedException()
|
||||
|
||||
self.assertFalse(exc.is_pure_status())
|
||||
Reference in New Issue
Block a user