Подготовка новых моделей тестов

This commit is contained in:
2026-07-17 13:23:17 +03:00
parent b68f17f069
commit 0c1c2bd61c
65 changed files with 337 additions and 5147 deletions

View File

@@ -29,7 +29,7 @@ class CoreCommand(BaseCommand):
call_command("flush", "--no-input") call_command("flush", "--no-input")
@classmethod @classmethod
def is_debug_or_test(cls): def _is_debug_or_test(cls):
""" """
Возвращает признак режима отладки или тестов Возвращает признак режима отладки или тестов
""" """

View File

@@ -8,4 +8,5 @@
Пакет базовых классов для реализации тестов Пакет базовых классов для реализации тестов
""" """
from .core_test_case import CoreTestCase from .core_simple_test_case import CoreSimpleTestCase
from .core_user_story_test_case import CoreUserStoryTestCase

View File

@@ -0,0 +1,27 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreUserStoryTestCase
"""
from django.test import SimpleTestCase
from rest_framework.test import APIRequestFactory
from .request_test_case_mixin import RequestTestCaseMixin
class CoreSimpleTestCase(SimpleTestCase, RequestTestCaseMixin):
"""
Реализует методы для тестирования без использования БД
"""
def setUp(self):
"""
Настройка перед тестированием
"""
self.factory = APIRequestFactory()

View File

@@ -0,0 +1,78 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreUserStoryTestCase
"""
from typing import Any
from django.core.management import call_command
from django.test import TestCase
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.management.userstory import SessionTestData
from .request_test_case_mixin import RequestTestCaseMixin
class CoreUserStoryTestCase(TestCase, RequestTestCaseMixin):
"""
Реализует методы для тестирования пользовательских историй
"""
def setUp(self):
"""
Настройка перед тестированием
"""
call_command("rack_test_database")
self.factory = APIRequestFactory()
def create_i_ivanov_request(self, method: HttpMethod, path: str, headers: dict[str, str] | None = None, data: dict[str, Any] | None = None, query_params: dict[str, Any] | None = None) -> Request:
"""
Создает запрос от имени Ивана И. Иванова
:param method: HTTP метод (GET, POST, PUT, PATCH, DELETE)
:param path: URL путь
:param headers: Заголовки запроса
:param data: Данные для тела запроса
:param query_params: Параметры запроса
:return: Объект запроса с аутентификацией Ивана Иванова
"""
return self.create_authenticated_request(
method=method,
path=path,
headers=headers,
data=data,
query_params=query_params,
token=SessionTestData.IVANOV_SESSION_TOKEN,
)
def create_p_petrov_request(self, method: HttpMethod, path: str, headers: dict[str, str] | None = None, data: dict[str, Any] | None = None, query_params: dict[str, Any] | None = None) -> Request:
"""
Создает запрос от имени Петра П. Петрова
:param method: HTTP метод (GET, POST, PUT, PATCH, DELETE)
:param path: URL путь
:param headers: Заголовки запроса
:param data: Данные для тела запроса
:param query_params: Параметры запроса
:return: Объект запроса с аутентификацией Петра Петрова
"""
return self.create_authenticated_request(
method=method,
path=path,
headers=headers,
data=data,
query_params=query_params,
token=SessionTestData.PETROV_SESSION_TOKEN
)

View File

@@ -0,0 +1,164 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса RequestTestCaseMixin
"""
import json
from typing import Any
from rest_framework.parsers import JSONParser
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.managers import SessionManager
from net.xeaf.rack.models import SessionModel
class RequestTestCaseMixin:
"""
Реализует методы для тестирования запросов
"""
factory: APIRequestFactory
""" Фабрика запросов """
def create_request(self,
method: HttpMethod,
path: str,
headers: dict[str, str] | None = None,
data: dict[str, Any] | None = None,
query_params: dict[str, str] | None = None,
auth: SessionModel | None = None) -> Request:
"""
Создает объект запроса для тестирования
:param method: HTTP метод (GET, POST, PUT, PATCH, DELETE)
:param path: URL путь
:param headers: Заголовки запроса
:param data: Данные для тела запроса
:param query_params: Параметры запроса
:param auth: Объект аутентификации (сессия)
:return: DRF Request объект
"""
# Уточняем значения параметров
headers = self.__prepare_headers(headers or {})
query_params = query_params or {}
data = data or {}
# Создаем внутренний объект запроса
match method:
case HttpMethod.GET:
wsgi_request = self.factory.get(path, data=query_params, **headers)
case HttpMethod.POST:
wsgi_request = self.factory.post(path, data=data, format='json', **headers)
case HttpMethod.PUT:
wsgi_request = self.factory.put(path, data=data, format='json', **headers)
case HttpMethod.PATCH:
wsgi_request = self.factory.patch(path, data=data, format='json', **headers)
case HttpMethod.DELETE:
wsgi_request = self.factory.delete(path, **headers)
case _:
wsgi_request = self.factory.generic(
method,
path,
content_type='application/json',
data=json.dumps(data) if data else ''
)
# Преобразуем к объекту запроса DRF
drf_request = Request(wsgi_request, parsers=[JSONParser()])
if method != HttpMethod.GET:
self.__assign_query_params(drf_request, query_params)
self.__assign_auth(drf_request, auth)
return drf_request
def create_authenticated_request(self,
method: HttpMethod,
path: str,
headers: dict[str, str] | None = None,
data: dict[str, Any] | None = None,
query_params: dict[str, str] | None = None,
token: str | None = None) -> Request:
"""
Создает объект авторизованного запроса для тестирования
:param method: HTTP метод (GET, POST, PUT, PATCH, DELETE)
:param path: URL путь
:param headers: Заголовки запроса
:param data: Данные для тела запроса
:param query_params: Параметры запроса
:param token: Токен сессии авторизации
:return: DRF Request объект
"""
# Получаем сессию
if token is None:
session = None
else:
session = SessionManager.find_by_token(token)
# Возвращаем объект запроса
return self.create_request(method=method, path=path, headers=headers, data=data, query_params=query_params, auth=session)
@classmethod
def __prepare_headers(cls, headers: dict[str, str]) -> dict[str, str]:
"""
Подготавливаем объект заголовков для передачи в фабрику
:param headers: Заголовки запроса
:return: Объект заголовков для фабрики
"""
wsgi_headers = {}
for key, value in headers.items():
if key.startswith('HTTP_'):
wsgi_headers[key] = value
else:
norm_key = key.upper().replace('-', '_')
if norm_key in ('CONTENT_TYPE', 'CONTENT_LENGTH'):
wsgi_headers[norm_key.lower()] = value
else:
wsgi_headers[f'HTTP_{norm_key}'] = value
return wsgi_headers
@classmethod
def __assign_query_params(cls, request: Request, query_params: dict[str, Any]) -> None:
"""
Присваивает параметры запроса в DRF Request объект
:param request: DRF Request объект
:param query_params: Параметры запроса
"""
# noinspection PyTypeChecker
request.query_params._mutable = True
for key, value in query_params.items():
request.query_params[key] = value
request.query_params._mutable = False
@classmethod
def __assign_auth(cls, request: Request, auth: SessionModel | None) -> None:
"""
Присваивает аутентификацию в DRF Request объект
:param request: Объект запроса
:param auth: Объект сессии пользователя
"""
if auth:
request.auth = auth
if hasattr(auth, 'account'):
request.user = auth.account

View File

@@ -7,6 +7,7 @@
""" """
Описание класса CoreViewSet Описание класса CoreViewSet
""" """
from typing import Any from typing import Any
from typing import Type from typing import Type
from uuid import UUID from uuid import UUID

View File

@@ -30,7 +30,7 @@ class Command(CoreCommand):
:params options: Дополнительные параметры :params options: Дополнительные параметры
""" """
if self.is_debug_or_test(): if self._is_debug_or_test():
# Готовимся # Готовимся
self._clear_database() self._clear_database()

View File

@@ -27,7 +27,7 @@ class Command(CoreCommand):
:params options: Дополнительные параметры :params options: Дополнительные параметры
""" """
if self.is_debug_or_test(): if self._is_debug_or_test():
self._clear_database() self._clear_database()
else: else:
self.stdout.write(self.style.ERROR("The rack_wipe_database command can only be executed in debug or test mode.")) self.stdout.write(self.style.ERROR("The rack_wipe_database command can only be executed in debug or test mode."))

View File

@@ -19,17 +19,17 @@ class AccountTestData:
Инициализирует модели данных тестовых Аккаунтов Инициализирует модели данных тестовых Аккаунтов
""" """
I_IVANOV_ID = uuid.UUID("bd428358-6944-4657-8044-a3dc1c4dff8a") IVANOV_ID = uuid.UUID("bd428358-6944-4657-8044-a3dc1c4dff8a")
""" Идентификатор пользователя Иван И. Иванов """ """ Идентификатор пользователя Иван Иванов """
I_IVANOV_USERNAME = 'i.ivanov@xeaf.net' IVANOV_USERNAME = 'i.ivanov@xeaf.net'
""" Имя пользователя Иван И. Иванов """ """ Имя пользователя Иван Иванов """
P_PETROV_ID = uuid.UUID("9c5010cc-f3be-42ae-bc05-d142d68a610a") PETROV_ID = uuid.UUID("9c5010cc-f3be-42ae-bc05-d142d68a610a")
""" Идентификатор пользователя Петр П. Петров """ """ Идентификатор пользователя Петр Петров """
P_PETROV_USERNAME = 'p.petrov@xeaf.net' PETROV_USERNAME = 'p.petrov@xeaf.net'
""" Имя пользователя Петр П. Петров """ """ Имя пользователя Петр Петров """
DEFAULT_PASSWORD = "manager123!" DEFAULT_PASSWORD = "manager123!"
""" Пароль по умолчанию для тестов """ """ Пароль по умолчанию для тестов """
@@ -40,17 +40,17 @@ class AccountTestData:
Инициализирует Аккаунты пользователя Инициализирует Аккаунты пользователя
""" """
cls.init_i_ivanov_account() cls.init_ivanov_account()
cls.init_p_petrov_account() cls.init_petrov_account()
@classmethod @classmethod
def init_i_ivanov_account(cls): def init_ivanov_account(cls):
""" """
Инициализирует Аккаунт пользователя Иван И. Иванов Инициализирует Аккаунт пользователя Иван Иванов
""" """
account = AccountModel() account = AccountModel()
account.id = cls.I_IVANOV_ID account.id = cls.IVANOV_ID
account.username = "i.ivanov@xeaf.net" account.username = "i.ivanov@xeaf.net"
account.nick_name = "Иван" account.nick_name = "Иван"
account.full_name = "Иван И. Иванов" account.full_name = "Иван И. Иванов"
@@ -60,13 +60,13 @@ class AccountTestData:
account.save() account.save()
@classmethod @classmethod
def init_p_petrov_account(cls): def init_petrov_account(cls):
""" """
Инициализирует Аккаунт пользователя Петр П. Петров Инициализирует Аккаунт пользователя Петр Петров
""" """
account = AccountModel() account = AccountModel()
account.id = cls.P_PETROV_ID account.id = cls.PETROV_ID
account.username = "p.petrov@xeaf.net" account.username = "p.petrov@xeaf.net"
account.nick_name = "Петр" account.nick_name = "Петр"
account.full_name = "Петр П. Петров" account.full_name = "Петр П. Петров"
@@ -76,17 +76,17 @@ class AccountTestData:
account.save() account.save()
@classmethod @classmethod
def get_i_ivanov_account(cls): def get_ivanov_account(cls):
""" """
Возвращает аккаунт пользователя Иван И. Иванов Возвращает аккаунт пользователя Иван Иванов
""" """
return AccountModel.objects.get(id=cls.I_IVANOV_ID) return AccountModel.objects.get(id=cls.IVANOV_ID)
@classmethod @classmethod
def get_p_petrov_account(cls): def get_petrov_account(cls):
""" """
Возвращает аккаунт пользователя Петр П. Петров Возвращает аккаунт пользователя Петр Петров
""" """
return AccountModel.objects.get(id=cls.P_PETROV_ID) return AccountModel.objects.get(id=cls.PETROV_ID)

View File

@@ -17,11 +17,11 @@ class SessionTestData:
Инициализирует модели данных тестовых Сессий пользователей Инициализирует модели данных тестовых Сессий пользователей
""" """
I_IVANOV_SESSION_TOKEN = "ced4217bbd12b10a59a3f9342164793819a29238" IVANOV_SESSION_TOKEN = "ced4217bbd12b10a59a3f9342164793819a29238"
""" Токен сессии пользователя Иван И. Иванов """ """ Токен сессии пользователя Иван Иванов """
P_PETROV_SESSION_TOKEN = "1e20d0f236efdda2f31688b6c176bf817cb3357e" PETROV_SESSION_TOKEN = "1e20d0f236efdda2f31688b6c176bf817cb3357e"
""" Токен сессии пользователя Петр П. Петров """ """ Токен сессии пользователя Петр Петров """
TEST_SESSION_TTL = 3600 TEST_SESSION_TTL = 3600
""" Время жизни тестовой сессии """ """ Время жизни тестовой сессии """
@@ -32,51 +32,51 @@ class SessionTestData:
Инициализирует сессии для тестовых пользователей Инициализирует сессии для тестовых пользователей
""" """
cls.init_i_ivanov_session() cls.init_ivanov_session()
cls.init_p_petrov_session() cls.init_petrov_session()
@classmethod @classmethod
def init_i_ivanov_session(cls): def init_ivanov_session(cls):
""" """
Создает Сессию для пользователя Иван И. Иванов Создает Сессию для пользователя Иван Иванов
""" """
session = SessionModel() session = SessionModel()
session.token = cls.I_IVANOV_SESSION_TOKEN session.token = cls.IVANOV_SESSION_TOKEN
session.account_id = AccountTestData.I_IVANOV_ID session.account_id = AccountTestData.IVANOV_ID
session.actual_account_id = AccountTestData.I_IVANOV_ID session.actual_account_id = AccountTestData.IVANOV_ID
session.renew(cls.TEST_SESSION_TTL) session.renew(cls.TEST_SESSION_TTL)
session.save() session.save()
@classmethod @classmethod
def init_p_petrov_session(cls): def init_petrov_session(cls):
""" """
Создает Сессию для пользователя Петр П. Петров Создает Сессию для пользователя Петр Петров
""" """
session = SessionModel() session = SessionModel()
session.token = cls.P_PETROV_SESSION_TOKEN session.token = cls.PETROV_SESSION_TOKEN
session.account_id = AccountTestData.P_PETROV_ID session.account_id = AccountTestData.PETROV_ID
session.actual_account_id = AccountTestData.P_PETROV_ID session.actual_account_id = AccountTestData.PETROV_ID
session.renew(cls.TEST_SESSION_TTL) session.renew(cls.TEST_SESSION_TTL)
session.save() session.save()
@classmethod @classmethod
def get_i_ivanov_session(cls) -> SessionModel: def get_ivanov_session(cls) -> SessionModel:
""" """
Возвращает сессию пользователя Иван И. Иванов Возвращает сессию пользователя Иван Иванов
:return: Сессия пользователя Иван И. Иванов :return: Сессия пользователя Иван Иванов
""" """
return SessionModel.objects.get(token=cls.I_IVANOV_SESSION_TOKEN) return SessionModel.objects.get(token=cls.IVANOV_SESSION_TOKEN)
@classmethod @classmethod
def get_p_petrov_session(cls) -> SessionModel: def get_petrov_session(cls) -> SessionModel:
""" """
Возвращает сессию пользователя Петр П. Петров Возвращает сессию пользователя Петр Петров
:return: Сессия пользователя Петр П. Петров :return: Сессия пользователя Петр Петров
""" """
return SessionModel.objects.get(token=cls.P_PETROV_SESSION_TOKEN) return SessionModel.objects.get(token=cls.PETROV_SESSION_TOKEN)

View File

@@ -7,7 +7,7 @@
""" """
Описание класса ExpiredAtMixin Описание класса ExpiredAtMixin
""" """
from django.conf import settings
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -19,6 +19,9 @@ class ExpiresAtMixin(models.Model):
Добавляет к модели свойство даты и времени истечения Добавляет к модели свойство даты и времени истечения
""" """
_RENEW_THROTTLING_SECONDS = getattr(settings, "AUTH_SESSION_THROTTLING", 0)
""" Не сохранять в БД, если сдвиг времени меньше этого значения """
expires_at = models.DateTimeField( expires_at = models.DateTimeField(
null=True, null=True,
blank=True, blank=True,
@@ -27,11 +30,12 @@ class ExpiresAtMixin(models.Model):
) )
""" Дата и время истечения """ """ Дата и время истечения """
def renew(self, seconds: int, save: bool = False) -> None: def renew(self, seconds: int, force: bool = False, save: bool = False) -> None:
""" """
Устанавливает время истечения на заданное количество секунд Устанавливает время истечения на заданное количество секунд
:param seconds: Количество секунд до истечения :param seconds: Количество секунд до истечения
:param force: Признак принудительного обновления
:param save: Признак необходимости сохранения :param save: Признак необходимости сохранения
""" """
@@ -39,7 +43,16 @@ class ExpiresAtMixin(models.Model):
if seconds <= 0: if seconds <= 0:
raise ValueError(f"The seconds parameter must be greater than 0") raise ValueError(f"The seconds parameter must be greater than 0")
self.expires_at = DateUtils.shift_time(seconds=seconds) # Если None - то значение не может быть просрочено
if self.expires_at is not None:
value = DateUtils.shift_time(seconds=seconds)
delta = (value - self.expires_at).total_seconds()
if delta >= self._RENEW_THROTTLING_SECONDS or force:
self.expires_at = value
# Проверяем сохранение
if save: if save:
self.save() self.save()

View File

@@ -1,15 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов для базовых классов
"""
from .core_enum_tests import CoreEnumTests
from .core_exception_tests import CoreExceptionTests
from .core_response_tests import CoreResponseTests
from .views import *

View File

@@ -1,234 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreEnumTests
"""
from django.test import SimpleTestCase
from net.xeaf.rack.core import CoreEnum
class TestColorEnum(CoreEnum):
"""
Тестовое перечисление для проверки
"""
RED = "red"
GREEN = "green"
BLUE = "blue"
class TestStatusEnum(CoreEnum):
"""
Еще одно тестовое перечисление
"""
ACTIVE = "active"
INACTIVE = "inactive"
PENDING = "pending"
class TestEmptyEnum(CoreEnum):
"""
Пустое перечисление для граничных случаев
"""
pass
class CoreEnumTests(SimpleTestCase):
"""
Тесты для класса CoreEnum
"""
def test_choices_returns_list_of_tuples(self):
"""
Проверяет, что choices() возвращает список кортежей (value, name)
"""
expected = [
("red", "RED"),
("green", "GREEN"),
("blue", "BLUE"),
]
self.assertEqual(TestColorEnum.choices(), expected)
def test_choices_contains_all_members(self):
"""
Проверяет, что choices() содержит все элементы перечисления
"""
choices = TestColorEnum.choices()
self.assertEqual(len(choices), 3)
self.assertIn(("red", "RED"), choices)
self.assertIn(("green", "GREEN"), choices)
self.assertIn(("blue", "BLUE"), choices)
def test_choices_order_matches_definition(self):
"""
Проверяет, что порядок элементов сохраняется
"""
choices = TestColorEnum.choices()
self.assertEqual(choices[0][0], "red")
self.assertEqual(choices[1][0], "green")
self.assertEqual(choices[2][0], "blue")
def test_keys_returns_list_of_names(self):
"""
Проверяет, что keys() возвращает список имен
"""
expected = ["RED", "GREEN", "BLUE"]
self.assertEqual(TestColorEnum.keys(), expected)
def test_keys_contains_all_names(self):
"""
Проверяет, что keys() содержит все имена элементов
"""
keys = TestColorEnum.keys()
self.assertEqual(len(keys), 3)
self.assertIn("RED", keys)
self.assertIn("GREEN", keys)
self.assertIn("BLUE", keys)
def test_values_returns_list_of_values(self):
"""
Проверяет, что values() возвращает список значений
"""
expected = ["red", "green", "blue"]
self.assertEqual(TestColorEnum.values(), expected)
def test_values_contains_all_values(self):
"""
Проверяет, что values() содержит все значения элементов
"""
values = TestColorEnum.values()
self.assertEqual(len(values), 3)
self.assertIn("red", values)
self.assertIn("green", values)
self.assertIn("blue", values)
def test_choices_with_different_enum(self):
"""
Проверяет работу choices() с другим перечислением
"""
expected = [
("active", "ACTIVE"),
("inactive", "INACTIVE"),
("pending", "PENDING"),
]
self.assertEqual(TestStatusEnum.choices(), expected)
def test_keys_with_different_enum(self):
"""
Проверяет работу keys() с другим перечислением
"""
expected = ["ACTIVE", "INACTIVE", "PENDING"]
self.assertEqual(TestStatusEnum.keys(), expected)
def test_values_with_different_enum(self):
"""
Проверяет работу values() с другим перечислением
"""
expected = ["active", "inactive", "pending"]
self.assertEqual(TestStatusEnum.values(), expected)
def test_empty_enum_choices(self):
"""
Проверяет поведение choices() для пустого перечисления
"""
self.assertEqual(TestEmptyEnum.choices(), [])
def test_empty_enum_keys(self):
"""
Проверяет поведение keys() для пустого перечисления
"""
self.assertEqual(TestEmptyEnum.keys(), [])
def test_empty_enum_values(self):
"""
Проверяет поведение values() для пустого перечисления
"""
self.assertEqual(TestEmptyEnum.values(), [])
def test_choices_returns_different_type(self):
"""
Проверяет, что choices() возвращает list, а не tuple
"""
self.assertIsInstance(TestColorEnum.choices(), list)
self.assertIsInstance(TestColorEnum.choices()[0], tuple)
def test_keys_returns_list(self):
"""
Проверяет, что keys() возвращает list
"""
self.assertIsInstance(TestColorEnum.keys(), list)
def test_values_returns_list(self):
"""
Проверяет, что values() возвращает list
"""
self.assertIsInstance(TestColorEnum.values(), list)
def test_choices_values_match(self):
"""
Проверяет согласованность choices() и values()
"""
choices_values = [choice[0] for choice in TestColorEnum.choices()]
self.assertEqual(choices_values, TestColorEnum.values())
def test_choices_keys_match(self):
"""
Проверяет согласованность choices() и keys()
"""
choices_keys = [choice[1] for choice in TestColorEnum.choices()]
self.assertEqual(choices_keys, TestColorEnum.keys())
def test_keys_and_values_length_match(self):
"""
Проверяет, что keys() и values() имеют одинаковую длину
"""
self.assertEqual(len(TestColorEnum.keys()), len(TestColorEnum.values()))
def test_choices_length_matches_members(self):
"""
Проверяет, что количество choices совпадает с количеством элементов
"""
self.assertEqual(len(TestColorEnum.choices()), len(TestColorEnum.__members__))
def test_all_elements_are_strings(self):
"""
Проверяет, что все значения являются строками (StrEnum)
"""
for value in TestColorEnum.values():
self.assertIsInstance(value, str)
for key in TestColorEnum.keys():
self.assertIsInstance(key, str)
for choice in TestColorEnum.choices():
self.assertIsInstance(choice[0], str)
self.assertIsInstance(choice[1], str)

View File

@@ -1,141 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreExceptionTests
"""
from django.test import SimpleTestCase
from rest_framework import status
from net.xeaf.rack.core import CoreException
class CoreExceptionTests(SimpleTestCase):
"""
Тесты для класса CoreException
"""
def setUp(self):
"""
Заполняем параметры для теста
"""
self.default_status = status.HTTP_400_BAD_REQUEST
self.default_detail = "Тестовая ошибка"
self.exc_instance = CoreException(status_code=self.default_status)
def test_init_with_basic_params(self):
"""
Проверяет базовую инициализацию без метаданных
"""
exc = CoreException(status_code=self.default_status, detail=self.default_detail)
self.assertEqual(exc.status_code, self.default_status)
self.assertEqual(exc.detail, self.default_detail)
self.assertIsNone(exc.meta)
def test_init_with_meta_only(self):
"""
Проверяет инициализацию, когда передан только словарь meta
"""
meta_data = {"action": "REDIRECT", "url": "/login/"}
exc = CoreException(status_code=self.default_status, meta=meta_data)
self.assertEqual(exc.status_code, self.default_status)
self.assertEqual(exc.meta, meta_data)
self.assertIsInstance(exc.meta, dict)
self.assertEqual(exc.meta["action"], "REDIRECT")
def test_init_with_kwargs_only(self):
"""
Проверяет инициализацию, когда meta=None, но переданы именованные аргументы (kwargs)
"""
exc = CoreException(status_code=self.default_status, foo="bar", code=123)
self.assertEqual(exc.status_code, self.default_status)
self.assertIsNotNone(exc.meta)
self.assertIsInstance(exc.meta, dict)
self.assertEqual(exc.meta["foo"], "bar")
self.assertEqual(exc.meta["code"], 123)
def test_process_meta_with_none(self):
"""
Если meta=None и kwargs пусты, self.meta должен остаться None
"""
self.exc_instance.meta = None # Сбрасываем состояние перед проверкой
self.exc_instance._process_meta(meta=None)
self.assertIsNone(self.exc_instance.meta)
def test_process_meta_with_dict(self):
"""
Если передан только словарь meta, он должен записаться в self.meta
"""
self.exc_instance.meta = None
test_meta = {"source": "auth_service"}
self.exc_instance._process_meta(meta=test_meta)
self.assertEqual(self.exc_instance.meta, test_meta)
def test_process_meta_with_kwargs_only(self):
"""
Если meta=None, но есть kwargs, должен создаться словарь с этими kwargs
"""
self.exc_instance.meta = None
self.exc_instance._process_meta(meta=None, request_id="req_123", user_id=99)
self.assertIsNotNone(self.exc_instance.meta)
self.assertIsInstance(self.exc_instance.meta, dict)
self.assertEqual(self.exc_instance.meta["request_id"], "req_123")
self.assertEqual(self.exc_instance.meta["user_id"], 99)
def test_process_meta_combined(self):
"""
Если переданы и meta, и kwargs, они должны успешно объединиться (update)
"""
self.exc_instance.meta = None
initial_meta = {"version": "v1"}
self.exc_instance._process_meta(meta=initial_meta, debug=True, env="prod")
self.assertIsNotNone(self.exc_instance.meta)
self.assertIsInstance(self.exc_instance.meta, dict)
self.assertEqual(self.exc_instance.meta["version"], "v1")
self.assertEqual(self.exc_instance.meta["debug"], True)
self.assertEqual(self.exc_instance.meta["env"], "prod")
def test_process_meta_with_empty_dict_and_kwargs(self):
"""
Критический случай: meta пришел пустым словарем, но kwargs заполнены
"""
self.exc_instance.meta = None
self.exc_instance._process_meta(meta={}, trace_id="xyz")
self.assertIsNotNone(self.exc_instance.meta)
self.assertIsInstance(self.exc_instance.meta, dict)
self.assertEqual(self.exc_instance.meta["trace_id"], "xyz")
def test_is_pure_status_defaults_to_false(self):
"""
Проверяет, что по умолчанию исключение возвращает False для чистого статуса
"""
exc = CoreException(status_code=400)
self.assertFalse(exc.is_pure_status())

View File

@@ -1,98 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreResponseTests
"""
from django.test import SimpleTestCase
from rest_framework import status
from net.xeaf.rack.core import CoreResponse
class CoreResponseTests(SimpleTestCase):
"""
Тесты для класса CoreResponse
"""
def test_init_success_format(self):
"""
Проверяет формат успешного ответа (код < 400): данные улетают в 'data'
"""
test_data = {"user_id": 42}
internal_code = status.HTTP_200_OK
external_code = status.HTTP_200_OK
response = CoreResponse(
internal_status=internal_code,
external_status=external_code,
data=test_data
)
# Проверяем внешний HTTP-статус в заголовках
self.assertEqual(response.status_code, external_code)
# Проверяем структуру тела JSON-ответа (внутреннее свойство DRF .data)
self.assertEqual(response.data["status"], internal_code)
self.assertIn("timestamp", response.data)
# Так как это успех, данные обязаны быть в ключе "data"
self.assertEqual(response.data["data"], test_data)
self.assertNotIn("detail", response.data)
self.assertNotIn("meta", response.data)
def test_init_error_format(self):
"""
Проверяет формат ответа с ошибкой (код >= 400): данные улетают в 'detail'
"""
error_msg = "Некорректные параметры запроса"
internal_code = status.HTTP_400_BAD_REQUEST
external_code = status.HTTP_200_OK # Маскируем под 200 OK для фронтенда
response = CoreResponse(
internal_status=internal_code,
external_status=external_code,
data=error_msg
)
self.assertEqual(response.status_code, external_code)
self.assertEqual(response.data["status"], internal_code)
# Так как это ошибка, данные обязаны быть в ключе "detail"
self.assertEqual(response.data["detail"], error_msg)
self.assertNotIn("data", response.data)
def test_init_with_meta(self):
"""
Проверяет корректную интеграцию словаря meta в тело ответа
"""
meta_data = {"page": 1, "has_next": False}
response = CoreResponse(
internal_status=status.HTTP_200_OK,
external_status=status.HTTP_200_OK,
data={"items": []},
meta=meta_data
)
self.assertIn("meta", response.data)
self.assertEqual(response.data["meta"], meta_data)
def test_init_with_empty_data(self):
"""
Проверяет, что если data=None или пустой, ключи 'data' и 'detail' не создаются
"""
response = CoreResponse(
internal_status=status.HTTP_204_NO_CONTENT,
external_status=status.HTTP_204_NO_CONTENT,
data=None
)
self.assertNotIn("data", response.data)
self.assertNotIn("detail", response.data)

View File

@@ -1,17 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов для базовых классов представлений
"""
from .core_view_set_get_serializer_tests import CoreViewSetGetSerializerTests
from .core_view_set_is_partial_update import CoreViewSetIsPartialUpdateTests
from .core_view_set_process_data_params import CoreViewSetProcessDataParamsTests
from .core_view_set_process_pk_param_tests import CoreViewSetProcessPkParamTests
from .core_view_set_process_query_params_tests import CoreViewSetProcessQueryParamsTests
from .core_view_set_path_generation_tests import CoreViewSetPathGenerationTests
from .core_view_set_request_context_tests import CoreViewSetRequestContextTests

View File

@@ -1,71 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreViewSetGetSerializerTest
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import SimpleTestCase
from rest_framework import serializers
from net.xeaf.rack.core.views import CoreViewSet
class MockSerializer(serializers.Serializer):
"""
Тестовый класс сериализатора
"""
field = serializers.CharField()
""" Поле сериализатора """
class MockViewSet(CoreViewSet):
"""
Тестовый класс представления
"""
pass
class CoreViewSetGetSerializerTests(SimpleTestCase):
"""
Тесты метода CoreViewSet._get_serializer
"""
def test_get_serializer_with_serializer_class(self):
"""
Передан явный класс сериализатора
"""
view_set = MockViewSet()
custom_serializer = MockSerializer
result = view_set._get_serializer(custom_serializer, None)
self.assertEqual(result, custom_serializer)
def test_get_serializer_with_default(self):
"""
Передан класс по умолчанию
"""
view_set = MockViewSet()
default_serializer = MockSerializer
result = view_set._get_serializer(None, default_serializer)
self.assertEqual(result, default_serializer)
def test_get_serializer_no_serializer_raises_error(self):
"""
Не передан сериализатор - ошибка
"""
view_set = MockViewSet()
with self.assertRaises(ImproperlyConfigured) as context:
view_set._get_serializer(None, None)
self.assertIn("must define 'serializer' attribute", str(context.exception))

View File

@@ -1,107 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreViewSetIsPartialUpdateTest
"""
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.core.views import CoreViewSet
class MockViewSetForIsPartialUpdate(CoreViewSet):
"""
ViewSet для тестов _is_partial_update
"""
permission_classes = ()
class CoreViewSetIsPartialUpdateTests(CoreTestCase):
"""
Тесты для CoreViewSet._is_partial_update
"""
def setUp(self):
"""
Подготовка к тестированию
"""
super().setUp()
self.view_set = MockViewSetForIsPartialUpdate()
def test_is_partial_update_for_patch(self):
"""
PATCH запрос - возвращает True
"""
request = self.create_i_ivanov_request(
'PATCH',
'/test',
data={'name': 'Updated'}
)
self.view_set.request = request
result = self.view_set._is_partial_update()
self.assertTrue(result)
def test_is_partial_update_for_put(self):
"""
PUT запрос - возвращает False
"""
request = self.create_i_ivanov_request(
'PUT',
'/test',
data={'name': 'Updated', 'age': 30}
)
self.view_set.request = request
result = self.view_set._is_partial_update()
self.assertFalse(result)
def test_is_partial_update_for_post(self):
"""
POST запрос - возвращает False
"""
request = self.create_i_ivanov_request(
'POST',
'/test',
data={'name': 'New', 'age': 25}
)
self.view_set.request = request
result = self.view_set._is_partial_update()
self.assertFalse(result)
def test_is_partial_update_for_delete(self):
"""
DELETE запрос - возвращает False
"""
request = self.create_i_ivanov_request(
'DELETE',
'/test/123/'
)
self.view_set.request = request
result = self.view_set._is_partial_update()
self.assertFalse(result)
def test_is_partial_update_for_get(self):
"""
GET запрос - возвращает False
"""
request = self.create_i_ivanov_request(
'GET',
'/test'
)
self.view_set.request = request
result = self.view_set._is_partial_update()
self.assertFalse(result)

View File

@@ -1,163 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreViewSetPathGenerationTests
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import SimpleTestCase
from net.xeaf.rack.core.views import CoreViewSet
from net.xeaf.rack.enums import ViewSetAction
class CoreViewSetPathGenerationTests(SimpleTestCase):
"""
Тесты генерации URL паттернов
"""
class FullCRUDViewSet(CoreViewSet):
"""
ViewSet с полным CRUD
"""
item_list_pattern = "test/"
item_detail_pattern = "test/<uuid:pk>/"
@classmethod
def item_list(cls, request):
pass
@classmethod
def item_create(cls, request):
pass
@classmethod
def item_retrieve(cls, request, pk):
pass
@classmethod
def item_update(cls, request, pk):
pass
@classmethod
def item_partial_update(cls, request, pk):
pass
@classmethod
def item_destroy(cls, request, pk):
pass
class ListOnlyViewSet(CoreViewSet):
"""
ViewSet только для списка
"""
item_list_pattern = "test/"
@classmethod
def item_list(cls, request):
pass
class DetailOnlyViewSet(CoreViewSet):
"""
ViewSet только для деталей
"""
item_detail_pattern = "test/<uuid:pk>/"
@classmethod
def item_retrieve(cls, request, pk):
pass
def test_get_paths_full_crud(self):
"""
Полный CRUD - все пути сгенерированы
"""
paths = self.FullCRUDViewSet.get_paths()
self.assertEqual(len(paths), 2)
list_path = paths[0]
detail_path = paths[1]
self.assertEqual(list_path.pattern._route, "test/")
self.assertEqual(detail_path.pattern._route, "test/<uuid:pk>/")
def test_get_paths_list_only(self):
"""
Только список - только list path
"""
paths = self.ListOnlyViewSet.get_paths()
self.assertEqual(len(paths), 1)
list_path = paths[0]
self.assertEqual(list_path.pattern._route, "test/")
def test_get_paths_detail_only(self):
"""
Только детали - только detail path
"""
paths = self.DetailOnlyViewSet.get_paths()
self.assertEqual(len(paths), 1)
detail_path = paths[0]
self.assertEqual(detail_path.pattern._route, "test/<uuid:pk>/")
def test_get_paths_no_patterns(self):
"""
Нет паттернов - пустой список
"""
class EmptyViewSet(CoreViewSet):
pass
paths = EmptyViewSet.get_paths()
self.assertEqual(paths, [])
def test_get_paths_list_without_methods(self):
"""
Есть паттерн, но нет методов - пустой список
"""
class NoMethodsViewSet(CoreViewSet):
item_list_pattern = "test/"
with self.assertRaises(ImproperlyConfigured):
NoMethodsViewSet.get_paths()
def test_path_method_valid_action(self):
"""
Получение метода для валидного действия
"""
result = CoreViewSet._path_method(ViewSetAction.LIST)
self.assertEqual(result, "item_list")
def test_path_method_exists_true(self):
"""
Проверка существования метода - True
"""
result = self.FullCRUDViewSet._path_method_exists(ViewSetAction.LIST)
self.assertTrue(result)
def test_path_method_exists_false(self):
"""
Проверка существования метода - False
"""
class EmptyViewSet(CoreViewSet):
pass
result = EmptyViewSet._path_method_exists(ViewSetAction.LIST)
self.assertFalse(result)

View File

@@ -1,320 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreViewSetProcessDataParamsTestsTest
"""
from rest_framework import serializers
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.core.views import CoreViewSet
class DataParamsSerializer(serializers.Serializer):
"""
Сериализатор для тестов data params
"""
name = serializers.CharField(max_length=100)
age = serializers.IntegerField(min_value=0, max_value=150)
email = serializers.EmailField(required=False)
class MockViewSetForDataParams(CoreViewSet):
"""
ViewSet для тестов data params
"""
permission_classes = ()
def _get_request_method(self):
"""
Метод по умолчанию
"""
return 'POST' # По умолчанию POST
def _get_request_session(self):
"""
Нет сессии
"""
return None
class CoreViewSetProcessDataParamsTests(CoreTestCase):
"""
Тесты для CoreViewSet._process_data_params
"""
def setUp(self):
"""
Подготовка к тестам
"""
super().setUp()
self.view_set = MockViewSetForDataParams()
def test_process_data_params_no_serializer(self):
"""
Без сериализатора - возвращаются исходные данные
"""
data = {'name': 'Test', 'age': 25}
request = self.create_i_ivanov_request(
'POST',
'/test',
data=data
)
self.view_set.request = request
result = self.view_set._process_data_params(None)
self.assertEqual(result, data)
def test_process_data_params_with_serializer(self):
"""
С сериализатором - валидирует и возвращает данные
Проверяет, что данные проходят валидацию через сериализатор
"""
request = self.create_i_ivanov_request(
'POST',
'/test',
data={'name': 'Ivan Ivanov', 'age': 30}
)
self.view_set.request = request
result = self.view_set._process_data_params(DataParamsSerializer)
self.assertEqual(result['name'], 'Ivan Ivanov')
self.assertEqual(result['age'], 30)
self.assertIsInstance(result['age'], int)
def test_process_data_params_with_serializer_optional_fields(self):
"""
С сериализатором - опциональные поля
Проверяет, что опциональные поля могут отсутствовать
"""
request = self.create_i_ivanov_request(
'POST',
'/test',
data={'name': 'Ivan Ivanov', 'age': 30}
)
self.view_set.request = request
result = self.view_set._process_data_params(DataParamsSerializer)
self.assertEqual(result['name'], 'Ivan Ivanov')
self.assertEqual(result['age'], 30)
self.assertNotIn('email', result)
def test_process_data_params_with_serializer_optional_field_present(self):
"""
Опциональное поле передано
Проверяет, что опциональное поле обрабатывается корректно
"""
request = self.create_i_ivanov_request(
'POST',
'/test',
data={
'name': 'Ivan Ivanov',
'age': 30,
'email': 'ivan@example.com'
}
)
self.view_set.request = request
result = self.view_set._process_data_params(DataParamsSerializer)
self.assertEqual(result['name'], 'Ivan Ivanov')
self.assertEqual(result['age'], 30)
self.assertEqual(result['email'], 'ivan@example.com')
def test_process_data_params_with_serializer_invalid_raises_error(self):
"""
Невалидные данные - ошибка валидации
Проверяет, что сериализатор выбрасывает ValidationError
"""
request = self.create_i_ivanov_request(
'POST',
'/test',
data={'name': '', 'age': 200} # name пустой, age > 150
)
self.view_set.request = request
with self.assertRaises(serializers.ValidationError):
self.view_set._process_data_params(DataParamsSerializer)
def test_process_data_params_with_serializer_missing_required(self):
"""
Отсутствие обязательного поля
Проверяет, что при отсутствии required поля выбрасывается ошибка
"""
# Arrange
request = self.create_i_ivanov_request(
'POST',
'/test',
data={'age': 30} # name отсутствует
)
self.view_set.request = request
with self.assertRaises(serializers.ValidationError):
self.view_set._process_data_params(DataParamsSerializer)
def test_process_data_params_with_serializer_invalid_email(self):
"""
Невалидный email
Проверяет, что невалидный email вызывает ошибку
"""
request = self.create_i_ivanov_request(
'POST',
'/test',
data={
'name': 'Ivan Ivanov',
'age': 30,
'email': 'invalid-email' # невалидный email
}
)
self.view_set.request = request
with self.assertRaises(serializers.ValidationError):
self.view_set._process_data_params(DataParamsSerializer)
def test_process_data_params_with_partial_update(self):
"""
Частичное обновление (PATCH) - partial=True
Проверяет, что при PATCH запросе partial=True
"""
class PartialViewSet(MockViewSetForDataParams):
def _get_request_method(self):
return 'PATCH'
view_set = PartialViewSet()
request = self.create_i_ivanov_request(
'PATCH',
'/test',
data={'name': 'Updated Name'}
)
view_set.request = request
result = view_set._process_data_params(DataParamsSerializer)
self.assertEqual(result['name'], 'Updated Name')
self.assertNotIn('age', result)
self.assertNotIn('email', result)
def test_process_data_params_required_field_missing_partial(self):
"""
Отсутствие обязательного поля при partial=True
Проверяет, что при partial=True можно не указывать обязательные поля
"""
class PartialViewSet(MockViewSetForDataParams):
def _get_request_method(self):
return 'PATCH'
view_set = PartialViewSet()
request = self.create_i_ivanov_request(
'PATCH',
'/test',
data={'email': 'test@example.com'} # только опциональное поле
)
view_set.request = request
result = view_set._process_data_params(DataParamsSerializer)
self.assertEqual(result['email'], 'test@example.com')
self.assertNotIn('name', result)
self.assertNotIn('age', result)
def test_process_data_params_required_field_missing_full_update(self):
"""
Отсутствие обязательного поля при полном обновлении (PUT)
Проверяет, что при PUT обязательные поля должны быть указаны
"""
class PutViewSet(MockViewSetForDataParams):
def _get_request_method(self):
return 'PUT'
view_set = PutViewSet()
request = self.create_i_ivanov_request(
'PUT',
'/test',
data={'email': 'test@example.com'} # name и age отсутствуют
)
view_set.request = request
with self.assertRaises(serializers.ValidationError):
view_set._process_data_params(DataParamsSerializer)
def test_process_data_params_with_extra_fields(self):
"""
Лишние поля в данных - сериализатор их игнорирует
Проверяет, что сериализатор игнорирует поля, не определенные в нем
"""
# Arrange
request = self.create_i_ivanov_request(
'POST',
'/test',
data={
'name': 'Ivan Ivanov',
'age': 30,
'extra_field': 'should_be_ignored'
}
)
self.view_set.request = request
result = self.view_set._process_data_params(DataParamsSerializer)
self.assertEqual(result['name'], 'Ivan Ivanov')
self.assertEqual(result['age'], 30)
self.assertNotIn('extra_field', result)
def test_process_data_params_with_petrov_user(self):
"""
Запрос от Петрова с данными
Проверяет, что data params работают для любого пользователя
"""
request = self.create_p_petrov_request(
'POST',
'/test',
data={'name': 'Petr Petrov', 'age': 25}
)
self.view_set.request = request
result = self.view_set._process_data_params(DataParamsSerializer)
self.assertEqual(result['name'], 'Petr Petrov')
self.assertEqual(result['age'], 25)
def test_process_data_params_with_empty_data(self):
"""
Пустые данные
Проверяет обработку пустого request.data
"""
request = self.create_i_ivanov_request('POST', '/test', data={})
self.view_set.request = request
with self.assertRaises(serializers.ValidationError):
self.view_set._process_data_params(DataParamsSerializer)
def test_process_data_params_with_null_values(self):
"""
Null значения в данных
Проверяет, что null значения обрабатываются корректно
"""
request = self.create_i_ivanov_request(
'POST',
'/test',
data={'name': None, 'age': 30}
)
self.view_set.request = request
with self.assertRaises(serializers.ValidationError):
self.view_set._process_data_params(DataParamsSerializer)

View File

@@ -1,72 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreViewSetProcessPkParamTest
"""
from uuid import UUID
from django.test import SimpleTestCase
from net.xeaf.rack.core.views import CoreViewSet
from net.xeaf.rack.utils.exceptions import BadRequestException
class MockViewSet(CoreViewSet):
"""
Тестовый класс представления
"""
pass
class CoreViewSetProcessPkParamTests(SimpleTestCase):
"""
Тесты для CoreViewSet._process_pk_param
"""
def test_process_pk_param_valid_uuid(self):
"""
Валидный UUID передан
"""
view_set = MockViewSet()
valid_uuid = "bd428358-6944-4657-8044-a3dc1c4dff8a"
result = view_set._process_pk_param(pk=valid_uuid)
self.assertIsInstance(result, UUID)
self.assertEqual(str(result), valid_uuid)
def test_process_pk_param_missing_raises_error(self):
"""
Параметр pk отсутствует - ошибка
"""
view_set = MockViewSet()
with self.assertRaises(BadRequestException):
view_set._process_pk_param()
def test_process_pk_param_invalid_uuid_raises_error(self):
"""
Невалидный UUID - ошибка
"""
view_set = MockViewSet()
with self.assertRaises(ValueError):
view_set._process_pk_param(pk="invalid-uuid")
def test_process_pk_param_ignores_extra_kwargs(self):
"""
Лишние параметры игнорируются
"""
view_set = MockViewSet()
valid_uuid = "bd428358-6944-4657-8044-a3dc1c4dff8a"
result = view_set._process_pk_param(pk=valid_uuid, extra="value")
self.assertIsInstance(result, UUID)
self.assertEqual(str(result), valid_uuid)

View File

@@ -1,278 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreViewSetProcessQueryParamsTests
"""
from rest_framework import serializers
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.core.views import CoreViewSet
from net.xeaf.rack.management.userstory import AccountTestData
from net.xeaf.rack.management.userstory import SessionTestData
class QueryParamsSerializer(serializers.Serializer):
"""
Сериализатор параметров тестового запроса
"""
limit = serializers.IntegerField(required=False, default=10)
offset = serializers.IntegerField(required=False, default=0)
search = serializers.CharField(required=False, default='', allow_blank=True)
class MockViewSetForQueryParams(CoreViewSet):
"""
Тестовый класс представления
"""
permission_classes = ()
""" Классы проверки прав доступа """
def _get_request_session(self):
"""
Возвращает сессию пользователя из запроса
:return: Сессия пользователя
"""
return None
class CoreViewSetProcessQueryParamsTests(CoreTestCase):
"""
Тесты для CoreViewSet._process_query_params
"""
def setUp(self):
"""
Подготовка к тестированию
"""
super().setUp()
self.view_set = MockViewSetForQueryParams()
def test_process_query_params_no_serializer(self):
"""
Без сериализатора - возвращает словарь с параметрами
Проверяет, что параметры извлекаются из query_params и мержатся с kwargs
"""
request = self.create_i_ivanov_request(
'GET',
'/test',
query_params={
'param1': 'value1',
'param2': 'value2'
}
)
self.view_set.request = request
result = self.view_set._process_query_params(
None,
custom_param='custom_value'
)
self.assertEqual(result['param1'], 'value1')
self.assertEqual(result['param2'], 'value2')
self.assertEqual(result['custom_param'], 'custom_value')
self.assertNotIn('request', result)
def test_process_query_params_with_serializer(self):
"""
С сериализатором - валидирует и возвращает данные
Проверяет, что параметры проходят валидацию через сериализатор
"""
request = self.create_i_ivanov_request(
'GET',
'/test',
query_params={
'limit': '20',
'offset': '5',
'search': 'test'
}
)
self.view_set.request = request
result = self.view_set._process_query_params(QueryParamsSerializer)
self.assertEqual(result['limit'], 20)
self.assertEqual(result['offset'], 5)
self.assertEqual(result['search'], 'test')
def test_process_query_params_with_serializer_defaults(self):
"""
С сериализатором - значения по умолчанию
Проверяет, что отсутствующие параметры заполняются значениями по умолчанию
"""
request = self.create_i_ivanov_request(
'GET',
'/test',
query_params={}
)
self.view_set.request = request
result = self.view_set._process_query_params(QueryParamsSerializer)
self.assertEqual(result['limit'], 10) # default
self.assertEqual(result['offset'], 0) # default
self.assertEqual(result['search'], '') # allow_blank=True
def test_process_query_params_with_serializer_invalid_raises_error(self):
"""
Невалидные параметры - ошибка валидации
Проверяет, что сериализатор выбрасывает ValidationError
"""
request = self.create_i_ivanov_request(
'GET',
'/test',
query_params={
'limit': 'invalid', # невалидное значение
'offset': '5'
}
)
self.view_set.request = request
with self.assertRaises(serializers.ValidationError):
self.view_set._process_query_params(QueryParamsSerializer)
def test_process_query_params_merges_with_kwargs(self):
"""
Параметры из kwargs переопределяют параметры запроса
Проверяет, что kwargs имеют приоритет над query_params
"""
request = self.create_i_ivanov_request(
'GET',
'/test',
query_params={'limit': '20'}
)
self.view_set.request = request
result = self.view_set._process_query_params(
None,
limit=50 # переопределяем
)
self.assertEqual(result['limit'], 50)
def test_process_query_params_with_serializer_ignores_extra_kwargs(self):
"""
С сериализатором - только валидированные параметры
Проверяет, что сериализатор игнорирует параметры, не определенные в нем
"""
request = self.create_i_ivanov_request(
'GET',
'/test',
query_params={
'limit': '20',
'offset': '5'
}
)
self.view_set.request = request
result = self.view_set._process_query_params(
QueryParamsSerializer,
extra_param='extra_value' # не входит в сериализатор
)
self.assertNotIn('extra_param', result)
self.assertEqual(result['limit'], 20)
self.assertEqual(result['offset'], 5)
def test_process_query_params_empty_params(self):
"""
Пустые параметры запроса
Проверяет обработку пустого query_params
"""
request = self.create_i_ivanov_request('GET', '/test')
self.view_set.request = request
result = self.view_set._process_query_params(None)
self.assertEqual(result, {})
def test_process_query_params_filters_request_param(self):
"""
Фильтрация параметра 'request'
Проверяет, что параметр 'request' не попадает в результат
"""
request = self.create_i_ivanov_request(
'GET',
'/test',
query_params={
'request': 'should_be_filtered',
'param': 'value'
}
)
self.view_set.request = request
result = self.view_set._process_query_params(None)
self.assertNotIn('request', result)
self.assertEqual(result['param'], 'value')
def test_process_query_params_with_petrov_user(self):
"""
Запрос от Петрова с параметрами
Проверяет, что query_params работают для любого пользователя
"""
request = self.create_p_petrov_request(
'GET',
'/test',
query_params={
'limit': '15',
'offset': '3'
}
)
self.view_set.request = request
result = self.view_set._process_query_params(QueryParamsSerializer)
self.assertEqual(result['limit'], 15)
self.assertEqual(result['offset'], 3)
def test_process_query_params_with_non_get_method(self):
"""
Не-GET запрос с query_params
Проверяет, что query_params работают и для не-GET запросов
"""
request = self.create_i_ivanov_request(
'POST',
'/test',
data={'name': 'Test'},
query_params={'filter': 'active'}
)
self.view_set.request = request
result = self.view_set._process_query_params(None)
self.assertEqual(result['filter'], 'active')
def test_process_query_params_with_serializer_and_ivanov_session(self):
"""
Проверка, что сессия Ивана доступна в контексте
Используем get_ivanov_session() для проверки
"""
ivanov_session = self.get_i_ivanov_session()
request = self.create_request(
'GET',
'/test',
query_params={'limit': '20'},
auth=ivanov_session
)
self.view_set.request = request
result = self.view_set._process_query_params(QueryParamsSerializer)
self.assertEqual(result['limit'], 20)
# Проверяем, что сессия действительно Ивана
self.assertEqual(ivanov_session.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
self.assertEqual(str(ivanov_session.account.id), str(AccountTestData.I_IVANOV_ID))

View File

@@ -1,136 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreViewSetRequestContextTests
"""
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.core.views import CoreViewSet
from net.xeaf.rack.management.userstory import AccountTestData
from net.xeaf.rack.management.userstory import SessionTestData
from net.xeaf.rack.utils.exceptions import UnauthorizedException
class MockViewSetWithAuth(CoreViewSet):
"""
ViewSet для тестов аутентификации
"""
permission_classes = ()
class CoreViewSetRequestContextTests(CoreTestCase):
"""
Тесты методов получения сессии и аккаунта
"""
def setUp(self):
"""
Подготовка к тестированию
"""
super().setUp()
self.view_set = MockViewSetWithAuth()
def test_get_request_session_authenticated(self):
"""
Получение сессии для аутентифицированного запроса
"""
session = self.get_i_ivanov_session()
request = self.create_request('GET', '/test', auth=session)
self.view_set.request = request
result = self.view_set._get_request_session()
self.assertIsNotNone(result)
# noinspection PyUnresolvedReferences
self.assertEqual(result.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
# noinspection PyUnresolvedReferences
self.assertEqual(str(result.account.id), str(AccountTestData.I_IVANOV_ID))
def test_get_request_account_authenticated(self):
"""
Получение аккаунта для аутентифицированного запроса
"""
session = self.get_i_ivanov_session()
request = self.create_request('GET', '/test', auth=session)
self.view_set.request = request
result = self.view_set._get_request_account()
self.assertIsNotNone(result)
self.assertEqual(str(result.id), str(AccountTestData.I_IVANOV_ID))
self.assertEqual(result.username, AccountTestData.I_IVANOV_USERNAME)
def test_get_request_account_unauthenticated_raises_error(self):
"""
Не аутентифицированный запрос - ошибка UnauthorizedException
"""
request = self.create_request('GET', '/test')
self.view_set.request = request
with self.assertRaises(UnauthorizedException):
self.view_set._get_request_account()
def test_get_request_context_with_auth(self):
"""
Получение контекста запроса с аутентификацией
"""
session = self.get_i_ivanov_session()
request = self.create_request('GET', '/test', auth=session)
self.view_set.request = request
context = self.view_set._get_request_context()
self.assertIn('account', context)
self.assertIn('request', context)
self.assertEqual(str(context['account'].id), str(AccountTestData.I_IVANOV_ID))
self.assertEqual(context['request'], request)
def test_get_request_context_without_auth(self):
"""
Получение контекста без аутентификации - account = None
"""
request = self.create_request('GET', '/test')
self.view_set.request = request
context = self.view_set._get_request_context()
self.assertIn('account', context)
self.assertIsNone(context['account'])
self.assertIn('request', context)
self.assertEqual(context['request'], request)
def test_get_request_session_petrov_authenticated(self):
"""
Получение сессии Петрова
"""
session = self.get_p_petrov_session()
request = self.create_request('GET', '/test', auth=session)
self.view_set.request = request
result = self.view_set._get_request_session()
self.assertIsNotNone(result)
# noinspection PyUnresolvedReferences
self.assertEqual(result.token, SessionTestData.P_PETROV_SESSION_TOKEN)
# noinspection PyUnresolvedReferences
self.assertEqual(str(result.account.id), str(AccountTestData.P_PETROV_ID))
def test_get_request_account_petrov_authenticated(self):
"""
Получение аккаунта Петрова
"""
session = self.get_p_petrov_session()
request = self.create_request('GET', '/test', auth=session)
self.view_set.request = request
result = self.view_set._get_request_account()
self.assertIsNotNone(result)
self.assertEqual(str(result.id), str(AccountTestData.P_PETROV_ID))
self.assertEqual(result.username, AccountTestData.P_PETROV_USERNAME)

View File

@@ -1,13 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов для классов перечислений
"""
from .ui_language_tests import UILanguageTests
from .view_set_action_tests import ViewSetActionTests

View File

@@ -1,112 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса UILanguageTests
"""
from django.test import TestCase
from net.xeaf.rack.enums import UILanguage
class UILanguageTests(TestCase):
"""
Тесты для перечисления UILanguage
"""
def test_has_expected_values(self):
"""
Проверяем, что в enum есть ожидаемые языки
"""
expected = {"en", "ru"}
self.assertEqual(set(UILanguage.values()), expected)
def test_values_format(self):
"""
Проверяем формат значений
"""
for code in UILanguage.values():
self.assertEqual(len(code), 2)
self.assertTrue(code.islower())
def test_from_string_returns_english_for_en(self):
"""
Проверяем, что from_string возвращает ENGLISH для 'en'
"""
result = UILanguage.from_string('en')
self.assertEqual(result, UILanguage.ENGLISH)
def test_from_string_returns_russian_for_ru(self):
"""
Проверяем, что from_string возвращает RUSSIAN для 'ru'
"""
result = UILanguage.from_string('ru')
self.assertEqual(result, UILanguage.RUSSIAN)
def test_from_string_handles_uppercase(self):
"""
Проверяем, что from_string корректно обрабатывает заглавные буквы
"""
result = UILanguage.from_string('EN')
self.assertEqual(result, UILanguage.ENGLISH)
result = UILanguage.from_string('RU')
self.assertEqual(result, UILanguage.RUSSIAN)
def test_from_string_handles_regional_variants(self):
"""
Проверяем, что from_string корректно обрабатывает региональные варианты и лишние суффиксы
"""
test_cases = [
('en-us', UILanguage.ENGLISH),
('en-US', UILanguage.ENGLISH),
('en-gb', UILanguage.ENGLISH),
('en-GB', UILanguage.ENGLISH),
('en-us-extra', UILanguage.ENGLISH), # лишний суффикс
('en-us-extra-stuff', UILanguage.ENGLISH), # несколько суффиксов
('ru-ru', UILanguage.RUSSIAN),
('ru-RU', UILanguage.RUSSIAN),
]
for value, expected in test_cases:
with self.subTest(value=value):
result = UILanguage.from_string(value)
self.assertEqual(result, expected)
def test_from_string_raises_value_error_for_unknown(self):
"""
Проверяем, что from_string выбрасывает ValueError для неизвестного языка
"""
with self.assertRaises(ValueError):
UILanguage.from_string('fr')
with self.assertRaises(ValueError):
UILanguage.from_string('de')
with self.assertRaises(ValueError):
UILanguage.from_string('es')
def test_from_string_raises_value_error_for_invalid_format(self):
"""
Проверяем, что from_string выбрасывает ValueError для некорректного формата
"""
with self.assertRaises(ValueError):
UILanguage.from_string('123')
with self.assertRaises(ValueError):
UILanguage.from_string('')
with self.assertRaises(ValueError):
UILanguage.from_string('fr-FR') # 'fr' не поддерживается

View File

@@ -1,41 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ViewSetActionTests
"""
from django.test import SimpleTestCase
from net.xeaf.rack.enums import ViewSetAction
class ViewSetActionTests(SimpleTestCase):
"""
Тесты для ViewSetAction
"""
def test_get_method_returns_correct_mapping(self):
"""
Проверяет, что get_method возвращает правильные HTTP-методы для всех действий
"""
self.assertEqual(ViewSetAction.get_method(ViewSetAction.LIST), "item_list")
self.assertEqual(ViewSetAction.get_method(ViewSetAction.RETRIEVE), "item_retrieve")
self.assertEqual(ViewSetAction.get_method(ViewSetAction.CREATE), "item_create")
self.assertEqual(ViewSetAction.get_method(ViewSetAction.UPDATE), "item_update")
self.assertEqual(ViewSetAction.get_method(ViewSetAction.PARTIAL_UPDATE), "item_partial_update")
self.assertEqual(ViewSetAction.get_method(ViewSetAction.DESTROY), "item_destroy")
def test_get_method_with_invalid_action_raises_error(self):
"""
Проверяет поведение метода при передаче невалидного значения
"""
invalid_action = "invalid_action"
with self.assertRaises(Exception): # Будет вызван MatchError или ValueError в зависимости от настроек linter/python
ViewSetAction.get_method(invalid_action) # type: ignore

View File

@@ -1,21 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов для классов менеджеров
"""
from .configuration_manager_tests import ConfigurationManagerTests
from .crypto_manager_tests import CryptoManagerTests
from .locale_manager_tests import LocaleManagerTests
from .request_manager_get_request_account_tests import RequestManagerGetRequestAccountTests
from .request_manager_get_request_context_tests import RequestManagerGetRequestContextTests
from .request_manager_get_request_id_tests import RequestManagerGetRequestIdTests
from .request_manager_get_request_method_tests import RequestManagerGetRequestMethodTests
from .request_manager_get_request_session_tests import RequestManagerGetRequestSessionTests
from .sesson_manager_tests import SessionManagerTests

View File

@@ -1,116 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CryptoTests
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings
from django.test import SimpleTestCase
from net.xeaf.rack.managers import ConfigurationManager
class ConfigurationManagerTests(SimpleTestCase):
"""
Тесты для класса ConfigurationManager
"""
def test_get_string_returns_default_if_setting_missing(self) -> None:
"""
Должен вернуть дефолтное значение, если настройки нет в settings.py
"""
result = ConfigurationManager.get_string("NON_EXISTENT_STR_KEY", default="default_val")
self.assertEqual(result, "default_val")
@override_settings(EXISTING_STR_KEY="custom_val")
def test_get_string_returns_custom_value(self) -> None:
"""
Должен успешно прочитать кастомную строку из настроек проекта
"""
result = ConfigurationManager.get_string("EXISTING_STR_KEY", default="default_val")
self.assertEqual(result, "custom_val")
@override_settings(INVALID_STR_KEY=123)
def test_get_string_raises_exception_on_type_mismatch(self) -> None:
"""
Должен взорваться исключением ImproperlyConfigured, если вместо строки пришел int
"""
with self.assertRaises(ImproperlyConfigured) as context:
ConfigurationManager.get_string("INVALID_STR_KEY", default="default_val")
self.assertIn("must be of type str, got int", str(context.exception))
def test_get_int_returns_default_if_setting_missing(self) -> None:
"""
Должен вернуть дефолтное число, если настройки нет в файле конфигурации
"""
result = ConfigurationManager.get_int("NON_EXISTENT_INT_KEY", default=42)
self.assertEqual(result, 42)
@override_settings(EXISTING_INT_KEY=100)
def test_get_int_returns_custom_value(self) -> None:
"""
Должен успешно вернуть настроенный int
"""
result = ConfigurationManager.get_int("EXISTING_INT_KEY", default=42)
self.assertEqual(result, 100)
@override_settings(INVALID_INT_KEY="not_an_int")
def test_get_int_raises_exception_on_type_mismatch(self) -> None:
"""
Должен упасть, если в числовую настройку хост-проект прокинул строку
"""
with self.assertRaises(ImproperlyConfigured) as context:
ConfigurationManager.get_int("INVALID_INT_KEY", default=42)
self.assertIn("must be of type int, got str", str(context.exception))
@override_settings(BOOLEAN_INSTEAD_OF_INT=True)
def test_get_int_raises_exception_on_boolean_value(self) -> None:
"""
Хирургический тест: должен упасть, если вместо int передан bool (True/False)
"""
with self.assertRaises(ImproperlyConfigured) as context:
ConfigurationManager.get_int("BOOLEAN_INSTEAD_OF_INT", default=42)
self.assertIn("must be of type int, got bool", str(context.exception))
def test_get_bool_returns_default_if_setting_missing(self) -> None:
"""
Должен вернуть дефолтный булевый флаг при отсутствии настройки
"""
result = ConfigurationManager.get_bool("NON_EXISTENT_BOOL_KEY", default=True)
self.assertTrue(result)
@override_settings(EXISTING_BOOL_KEY=False)
def test_get_bool_returns_custom_value(self) -> None:
"""
Должен успешно прочитать переопределенный булевый флаг
"""
result = ConfigurationManager.get_bool("EXISTING_BOOL_KEY", default=True)
self.assertFalse(result)
@override_settings(INT_INSTEAD_OF_BOOL=1)
def test_get_bool_raises_exception_on_int_value(self) -> None:
"""
Должен выбросить исключение, если вместо честного bool передан int
"""
with self.assertRaises(ImproperlyConfigured) as context:
ConfigurationManager.get_bool("INT_INSTEAD_OF_BOOL", default=True)
self.assertIn("must be of type bool, got int", str(context.exception))

View File

@@ -1,163 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CryptoTests
"""
from django.db import models
from django.test import SimpleTestCase
from net.xeaf.rack.managers import CryptoManager
from net.xeaf.rack.models import SessionModel
class CryptoManagerTests(SimpleTestCase):
"""
Тесты для класса CryptoManager
"""
def test_gen_token(self):
"""
Тест генерации токена
"""
token = CryptoManager.gen_token()
self.assertEqual(len(token), CryptoManager.DEFAULT_TOKEN_LENGTH)
self._assert_lower_hex(token)
def test_gen_token_length(self):
"""
Тест генерации токена с указанием длины
"""
token = CryptoManager.gen_token(length=15)
self.assertEqual(len(token), 15)
def test_gen_token_different(self):
"""
Тест генерации разных токенов
"""
token1 = CryptoManager.gen_token()
token2 = CryptoManager.gen_token()
self.assertNotEqual(token1, token2)
def test_gen_pin_code(self):
"""
Тест генерации PIN-кода
"""
pin_code = CryptoManager.gen_pin_code()
self.assertEqual(len(pin_code), CryptoManager.DEFAULT_PIN_CODE_LENGTH)
self.assertTrue(all(c in CryptoManager.DEFAULT_PIN_CODE_ALPHABET for c in pin_code))
def test_gen_pin_code_length(self):
"""
Тест генерации PIN-кода с указанием длины
"""
pin_code = CryptoManager.gen_pin_code(length=8)
self.assertEqual(len(pin_code), 8)
self.assertTrue(all(c in CryptoManager.DEFAULT_PIN_CODE_ALPHABET for c in pin_code))
def test_gen_pin_code_alphabet(self):
"""
Тест генерации PIN-кода с указанием алфавита
"""
letters = 'ABCD'
pin_code = CryptoManager.gen_pin_code(alphabet=letters)
self.assertEqual(len(pin_code), CryptoManager.DEFAULT_PIN_CODE_LENGTH)
self.assertTrue(all(c in letters for c in pin_code))
def test_gen_pin_code_different(self):
"""
Тест генерации разных PIN-кодов
"""
pin_code_1 = CryptoManager.gen_pin_code()
pin_code_2 = CryptoManager.gen_pin_code()
self.assertNotEqual(pin_code_1, pin_code_2)
def test_gen_slug(self):
"""
Тест генерации slug
"""
pfx = 'test'
slug = CryptoManager.gen_slug(pfx)
self.assertEqual(len(slug), CryptoManager.DEFAULT_SLUG_LENGTH)
self.assertEqual(slug[:len(pfx)], pfx)
self._assert_lower_hex(slug[len(pfx):])
def test_gen_slug_length(self):
"""
Тест генерации slug с указанием длины
"""
pfx = 'test'
slug = CryptoManager.gen_slug(pfx, length=16)
self.assertEqual(len(slug), 16)
def test_gen_slug_prefix(self):
"""
Тест генерации slug с длинным префиксом
"""
pfx = 'test_test_test'
slug = CryptoManager.gen_slug(pfx)
self.assertEqual(slug, pfx)
def test_slug_different(self):
"""
Тест генерации разных slug
"""
slug_1 = CryptoManager.gen_slug('test')
slug_2 = CryptoManager.gen_slug('test')
self.assertNotEqual(slug_1, slug_2)
def _assert_lower_hex(self, value: str):
"""
Проверяет, что строка состоит из символов в нижнем регистре и шестнадцатеричных цифр
:param value: Проверяемая строка
"""
self.assertRegex(value, r'^[0-9a-f]+$')
def test_crypto_manager_should_generate_default_length_by_default(self) -> None:
"""
Гарантируем, что генератор без параметров использует базовую длину
"""
generated_token = CryptoManager.gen_token()
self.assertEqual(len(generated_token), CryptoManager.DEFAULT_TOKEN_LENGTH)
def test_session_model_field_must_match_max_token_length_constant(self) -> None:
"""
Защита схемы БД: max_length в миграциях равен MAX_TOKEN_LENGTH
"""
field = SessionModel._meta.get_field("token")
self.assertIsInstance(field, models.CharField)
db_max_length = field.max_length
self.assertIsInstance(db_max_length, int)
self.assertEqual(db_max_length, SessionModel.MAX_TOKEN_LENGTH)
def test_crypto_manager_should_raise_value_error_if_length_exceeds_max(self) -> None:
"""
Проверка Fail-Fast контракта: при превышении лимита СУБД выбрасывается ValueError
"""
excessive_length = SessionModel.MAX_TOKEN_LENGTH + 1
# Система обязана громко упасть с ValueError
with self.assertRaises(ValueError) as context:
CryptoManager.gen_token(length=excessive_length)
self.assertIn(f"Token length must be less than {SessionModel.MAX_TOKEN_LENGTH}", str(context.exception))

View File

@@ -1,245 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса LocaleManagerTest
"""
from django.conf import settings
from django.test import TestCase
from django.utils import translation
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
from net.xeaf.rack.enums import UILanguage
from net.xeaf.rack.managers import LocaleManager
class LocaleManagerTests(TestCase):
"""
Тесты для класса LocaleManager
"""
def setUp(self):
"""
Подготовка данных для тестов
"""
self.factory = APIRequestFactory()
self.default_language = UILanguage.from_string(settings.LANGUAGE_CODE)
def test_get_default_language_returns_settings_value(self):
"""
Проверяем, что метод возвращает язык из настроек
"""
result = LocaleManager.get_default_language()
expected = UILanguage.from_string(settings.LANGUAGE_CODE)
self.assertEqual(result, expected)
def test_get_accepted_language_returns_english_for_en(self):
"""
Проверяем, что для 'en' возвращается UILanguage.ENGLISH
"""
result = LocaleManager.get_accepted_language('en')
self.assertEqual(result, UILanguage.ENGLISH)
def test_get_accepted_language_returns_russian_for_ru(self):
"""
Проверяем, что для 'ru' возвращается UILanguage.RUSSIAN
"""
result = LocaleManager.get_accepted_language('ru')
self.assertEqual(result, UILanguage.RUSSIAN)
def test_get_accepted_language_handles_uppercase(self):
"""
Проверяем, что метод корректно обрабатывает заглавные буквы
"""
result = LocaleManager.get_accepted_language('EN')
self.assertEqual(result, UILanguage.ENGLISH)
def test_get_accepted_language_returns_default_for_unknown(self):
"""
Проверяем, что для неизвестного кода возвращается язык по умолчанию
"""
result = LocaleManager.get_accepted_language('fr')
self.assertEqual(result, self.default_language)
def test_get_accepted_language_returns_default_for_empty_string(self):
"""
Проверяем, что для пустой строки возвращается язык по умолчанию
"""
result = LocaleManager.get_accepted_language('')
self.assertEqual(result, self.default_language)
def test_get_request_language_returns_default_for_none_request(self):
"""
Проверяем, что для None запроса возвращается язык по умолчанию
"""
result = LocaleManager.get_request_language(None)
self.assertEqual(result, self.default_language)
def test_get_request_language_from_query_param_on_get_request(self):
"""
Проверяем, что для GET запроса язык берется из query параметра
"""
request = self.factory.get('/?x-ui-language=ru')
request.method = 'GET'
request = Request(request)
result = LocaleManager.get_request_language(request)
self.assertEqual(result, UILanguage.RUSSIAN)
def test_get_request_language_ignores_query_param_on_non_get(self):
"""
Проверяем, что для не-GET запроса query параметр игнорируется
"""
request = self.factory.post('/?x-ui-language=ru')
request.method = 'POST'
request.META['HTTP_ACCEPT_LANGUAGE'] = 'en-US,en;q=0.9'
request = Request(request)
result = LocaleManager.get_request_language(request)
self.assertEqual(result, UILanguage.ENGLISH)
def test_get_request_language_from_accept_header(self):
"""
Проверяем, что язык берется из заголовка Accept-Language
"""
request = self.factory.get('/')
request.method = 'GET'
request.META['HTTP_ACCEPT_LANGUAGE'] = 'ru-RU,ru;q=0.9,en;q=0.8'
request = Request(request)
result = LocaleManager.get_request_language(request)
self.assertEqual(result, UILanguage.RUSSIAN)
def test_get_request_language_returns_default_if_header_missing(self):
"""
Проверяем, что при отсутствии заголовка возвращается язык по умолчанию
"""
request = self.factory.get('/')
request.method = 'GET'
request = Request(request)
result = LocaleManager.get_request_language(request)
self.assertEqual(result, self.default_language)
def test_get_request_language_prefers_query_param_over_header(self):
"""
Проверяем, что query параметр имеет приоритет над заголовком
"""
request = self.factory.get('/?x-ui-language=en')
request.method = 'GET'
request.META['HTTP_ACCEPT_LANGUAGE'] = 'ru-RU,ru;q=0.9'
request = Request(request)
result = LocaleManager.get_request_language(request)
self.assertEqual(result, UILanguage.ENGLISH)
def test_set_request_language_activates_translation(self):
"""
Проверяем, что метод активирует перевод для указанного языка
"""
request = self.factory.get('/')
request = Request(request)
LocaleManager.set_request_language(request, UILanguage.RUSSIAN)
from django.utils import translation
self.assertEqual(translation.get_language(), 'ru')
def test_set_request_language_sets_language_code_in_request(self):
"""
Проверяем, что метод устанавливает LANGUAGE_CODE в запросе
"""
request = self.factory.get('/')
request = Request(request)
LocaleManager.set_request_language(request, UILanguage.ENGLISH)
self.assertEqual(request.LANGUAGE_CODE, 'EN')
def test_set_request_language_handles_none_request(self):
"""
Проверяем, что метод корректно обрабатывает None в запросе
"""
LocaleManager.set_request_language(None, UILanguage.RUSSIAN)
self.assertEqual(translation.get_language(), 'ru')
def test_process_accepted_language_returns_first_matching_language(self):
"""
Проверяем, что метод возвращает первый подходящий язык из заголовка
"""
request = self.factory.get('/')
request.META['HTTP_ACCEPT_LANGUAGE'] = 'fr-FR,fr;q=0.9,ru-RU,ru;q=0.8,en;q=0.7'
request = Request(request)
result = LocaleManager.process_accepted_language(request)
self.assertEqual(result, 'ru')
def test_process_accepted_language_returns_default_if_no_match(self):
"""
Проверяем, что если ни один язык не найден, возвращается язык по умолчанию
"""
request = self.factory.get('/')
request.META['HTTP_ACCEPT_LANGUAGE'] = 'fr-FR,fr;q=0.9,de;q=0.8'
request = Request(request)
result = LocaleManager.process_accepted_language(request)
self.assertEqual(result, str(self.default_language).lower())
def test_process_accepted_language_raises_keyerror_if_header_missing(self):
"""
Проверяем, что при отсутствии заголовка выбрасывается KeyError
"""
request = self.factory.get('/')
request = Request(request)
with self.assertRaises(KeyError):
LocaleManager.process_accepted_language(request)
def test_adjust_request_language_sets_language_from_request(self):
"""
Проверяем, что метод настраивает язык на основе запроса
"""
request = self.factory.get('/?x-ui-language=ru')
request.method = 'GET'
request = Request(request)
LocaleManager.adjust_request_language(request)
from django.utils import translation
self.assertEqual(translation.get_language(), 'ru')
self.assertEqual(request.LANGUAGE_CODE, 'RU')
def test_adjust_request_language_uses_default_if_no_language_found(self):
"""
Проверяем, что если язык не найден, используется язык по умолчанию
"""
request = self.factory.get('/')
request.method = 'GET'
request = Request(request)
LocaleManager.adjust_request_language(request)
from django.utils import translation
expected = str(self.default_language).lower()
self.assertEqual(translation.get_language(), expected)
def test_adjust_request_language_handles_empty_request(self):
"""
Проверяем, что метод корректно обрабатывает пустой запрос
"""
request = self.factory.get('/')
request.method = 'GET'
request = Request(request)
# Метод не должен выбрасывать исключений
try:
LocaleManager.adjust_request_language(request)
except Exception as e:
self.fail(f"adjust_request_language raised {e}")

View File

@@ -1,71 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса RequestManagerGetRequestAccountTests
"""
from rest_framework.request import Request
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.management.userstory import AccountTestData
from net.xeaf.rack.managers import RequestManager
from net.xeaf.rack.models import SessionModel
from net.xeaf.rack.utils.exceptions import UnauthorizedException
class RequestManagerGetRequestAccountTests(CoreTestCase):
"""
Тесты для RequestManager.get_request_account()
"""
def _create_test_request(self, session: SessionModel | None) -> Request:
"""
Создает запрос для тестирования
:param session: Модель сессии
:return: Объект запроса
"""
return self.create_request(method=HttpMethod.GET, path="/test", auth=session)
def test_get_request_account_authenticated(self):
"""
Получение аккаунта из аутентифицированного запроса
"""
session = self.get_i_ivanov_session()
request = self._create_test_request(session=session)
result = RequestManager.get_request_account(request)
self.assertIsNotNone(result)
self.assertEqual(str(result.id), str(AccountTestData.I_IVANOV_ID))
self.assertEqual(result.username, AccountTestData.I_IVANOV_USERNAME)
def test_get_request_account_unauthenticated_raises_error(self):
"""
Запрос без аутентификации - ошибка UnauthorizedException
"""
request = self._create_test_request(session=None)
with self.assertRaises(UnauthorizedException):
RequestManager.get_request_account(request)
def test_get_request_account_petrov(self):
"""
Получение аккаунта Петрова
"""
session = self.get_p_petrov_session()
request = self._create_test_request(session=session)
result = RequestManager.get_request_account(request)
self.assertIsNotNone(result)
self.assertEqual(str(result.id), str(AccountTestData.P_PETROV_ID))
self.assertEqual(result.username, AccountTestData.P_PETROV_USERNAME)

View File

@@ -1,83 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса RequestManagerGetRequestContextTests
"""
from typing import Any
from uuid import UUID
from rest_framework.request import Request
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.management.userstory import AccountTestData
from net.xeaf.rack.managers import RequestManager
from net.xeaf.rack.models import SessionModel
class RequestManagerGetRequestContextTests(CoreTestCase):
"""
Тесты для RequestManager.get_request_session()
"""
def _create_test_request(self, session: SessionModel | None, headers: dict[str, Any] | None = None) -> Request:
"""
Создает запрос для тестирования
:param session: Модель сессии
:param headers: Заголовки запроса
:return: Объект запроса
"""
return self.create_request(method=HttpMethod.GET, path="/test", headers=headers, auth=session)
def test_get_request_context_with_auth(self):
"""
RM-005-1: Контекст с аутентификацией
"""
session = self.get_i_ivanov_session()
request = self._create_test_request(session=session)
result = RequestManager.get_request_context(request)
self.assertIn('id', result)
self.assertIn('account', result)
self.assertIn('request', result)
self.assertIsInstance(result['id'], UUID)
self.assertEqual(str(result['account'].id), str(AccountTestData.I_IVANOV_ID))
self.assertEqual(result['request'], request)
def test_get_request_context_without_auth(self):
"""
Контекст без аутентификации - account = None
"""
request = self._create_test_request(session=None)
result = RequestManager.get_request_context(request)
self.assertIn('id', result)
self.assertIn('account', result)
self.assertIn('request', result)
self.assertIsInstance(result['id'], UUID)
self.assertIsNone(result['account'])
self.assertEqual(result['request'], request)
def test_get_request_context_with_request_id(self):
"""
Контекст с указанным Request-ID
"""
request_id = "bd428358-6944-4657-8044-a3dc1c4dff8a"
request = self._create_test_request(session=None, headers={RequestManager.REQUEST_ID_META: request_id})
result = RequestManager.get_request_context(request)
self.assertIsInstance(result['id'], UUID)
self.assertEqual(str(result['id']), request_id)

View File

@@ -1,66 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса RequestManagerGetRequestIdTests
"""
from uuid import UUID
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.managers import RequestManager
from net.xeaf.rack.utils.exceptions import BadRequestException
class RequestManagerGetRequestIdTests(CoreTestCase):
"""
Тесты для RequestManager.get_request_id()
"""
def _create_test_request(self, request_id: str | None = None):
"""
Создание тестового запроса с указанным Request-ID
"""
headers = {RequestManager.REQUEST_ID_META: request_id} if request_id else {}
return self.create_request(
method=HttpMethod.GET,
path="/test",
headers=headers
)
def test_get_request_id_valid(self):
"""
Получение валидного Request-ID из заголовка
"""
request_id = "bd428358-6944-4657-8044-a3dc1c4dff8a"
request = self._create_test_request(request_id)
result = RequestManager.get_request_id(request)
self.assertIsInstance(result, UUID)
self.assertEqual(str(result), request_id)
def test_get_request_id_missing(self):
"""
Заголовок X-Request-ID отсутствует - новый UUID
"""
request = self._create_test_request()
result = RequestManager.get_request_id(request)
self.assertIsInstance(result, UUID)
def test_get_request_id_invalid(self):
"""
Невалидный X-Request-ID - возникает исключение BadRequestException
"""
request = self._create_test_request('invalid-uuid')
with self.assertRaises(BadRequestException):
RequestManager.get_request_id(request)

View File

@@ -1,92 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса RequestManagerGetRequestMethodTests
"""
from django.core.exceptions import ImproperlyConfigured
from rest_framework.request import Request
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.managers import RequestManager
class RequestManagerGetRequestMethodTests(CoreTestCase):
"""
Тесты для RequestManager.get_request_method()
"""
def _create_test_request(self, method: HttpMethod) -> Request:
"""
Создает запрос для тестирования
:param method: Метод запроса
:return: Объект запроса
"""
return self.create_request(method=method, path="/test")
def test_get_request_method_get(self):
"""
GET запрос
"""
request = self._create_test_request(HttpMethod.GET)
result = RequestManager.get_request_method(request)
self.assertEqual(result, HttpMethod.GET)
def test_get_request_method_post(self):
"""
POST запрос
"""
request = self._create_test_request(HttpMethod.POST)
result = RequestManager.get_request_method(request)
self.assertEqual(result, HttpMethod.POST)
def test_get_request_method_put(self):
"""
PUT запрос
"""
request = self._create_test_request(HttpMethod.PUT)
result = RequestManager.get_request_method(request)
self.assertEqual(result, HttpMethod.PUT)
def test_get_request_method_patch(self):
"""
PATCH запрос
"""
request = self._create_test_request(HttpMethod.PATCH)
result = RequestManager.get_request_method(request)
self.assertEqual(result, HttpMethod.PATCH)
def test_get_request_method_delete(self):
"""
DELETE запрос
"""
request = self._create_test_request(HttpMethod.DELETE)
result = RequestManager.get_request_method(request)
self.assertEqual(result, HttpMethod.DELETE)
def test_get_request_method_missing_raises_error(self):
"""
Метод отсутствует - ошибка ImproperlyConfigured
"""
request = self._create_test_request(HttpMethod.DELETE)
request.method = None
with self.assertRaises(ImproperlyConfigured):
RequestManager.get_request_method(request)

View File

@@ -1,75 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса RequestManagerGetRequestSessionTests
"""
from rest_framework.request import Request
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.management.userstory import AccountTestData
from net.xeaf.rack.management.userstory import SessionTestData
from net.xeaf.rack.managers import RequestManager
from net.xeaf.rack.models import SessionModel
class RequestManagerGetRequestSessionTests(CoreTestCase):
"""
Тесты для RequestManager.get_request_session()
"""
def _create_test_request(self, session: SessionModel | None) -> Request:
"""
Создает запрос для тестирования
:param session: Модель сессии
:return: Объект запроса
"""
return self.create_request(method=HttpMethod.GET, path="/test", auth=session)
# noinspection DuplicatedCode
def test_get_request_session_authenticated(self):
"""
Получение сессии из аутентифицированного запроса
"""
session = self.get_i_ivanov_session()
request = self._create_test_request(session=session)
result = RequestManager.get_request_session(request)
self.assertIsNotNone(result)
self.assertIsInstance(result, SessionModel)
self.assertEqual(result.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
self.assertEqual(str(result.account.id), str(AccountTestData.I_IVANOV_ID))
def test_get_request_session_unauthenticated(self):
"""
Запрос без аутентификации - возвращает None
"""
request = self._create_test_request(session=None)
result = RequestManager.get_request_session(request)
self.assertIsNone(result)
# noinspection DuplicatedCode
def test_get_request_session_petrov(self):
"""
Получение сессии Петрова
"""
session = self.get_p_petrov_session()
request = self._create_test_request(session=session)
result = RequestManager.get_request_session(request)
self.assertIsNotNone(result)
self.assertIsInstance(result, SessionModel)
self.assertEqual(result.token, SessionTestData.P_PETROV_SESSION_TOKEN)
self.assertEqual(str(result.account.id), str(AccountTestData.P_PETROV_ID))

View File

@@ -1,216 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса SessionManagerTest
"""
import datetime
from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings
from django.utils import timezone
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.enums import AuthRejectReason
from net.xeaf.rack.management.userstory import AccountTestData
from net.xeaf.rack.management.userstory import SessionTestData
from net.xeaf.rack.managers import SessionManager
from net.xeaf.rack.models import AccountModel
from net.xeaf.rack.models import SessionModel
from net.xeaf.rack.utils.exceptions import UnauthorizedException
class SessionManagerTests(CoreTestCase):
"""
Тесты для класса SessionManager
"""
def test_should_successfully_authorize_ivanov_with_real_password(self) -> None:
"""
Проверка успешного создания сессии для эталонного аккаунта
"""
session = SessionManager.authenticate(
username="i.ivanov@xeaf.net",
password=AccountTestData.DEFAULT_PASSWORD,
device_id="mobile_app"
)
# Проверяем, что вернулась живая сессия со сгенерированным токеном
self.assertIsNotNone(session)
self.assertEqual(len(session.token), 40)
self.assertEqual(session.account_id, AccountTestData.I_IVANOV_ID)
self.assertEqual(session.actual_account_id, AccountTestData.I_IVANOV_ID)
self.assertEqual(session.device_id, "mobile_app")
# Честно проверяем запись физически внутри БД
db_session = SessionModel.objects.get(token=session.token)
self.assertEqual(db_session.account_id, AccountTestData.I_IVANOV_ID)
def test_should_raise_error_when_password_is_incorrect(self) -> None:
"""
Менеджер должен выкинуть исключение ядра при неверном пароле
"""
with self.assertRaises(UnauthorizedException) as context:
SessionManager.authenticate(
username="i.ivanov@xeaf.net",
password="wrong_password_here"
)
self.assertIsInstance(context.exception.meta, dict)
self.assertEqual(context.exception.meta['reason'], AuthRejectReason.INVALID_USERNAME_OR_PASSWORD)
def test_should_raise_error_when_username_does_not_exist(self) -> None:
"""
Менеджер должен выкинуть исключение ядра, если email не найден
"""
with self.assertRaises(UnauthorizedException) as context:
SessionManager.authenticate(
username="unknown.user@xeaf.net",
password=AccountTestData.DEFAULT_PASSWORD
)
self.assertIsInstance(context.exception.meta, dict)
self.assertEqual(context.exception.meta['reason'], AuthRejectReason.INVALID_USERNAME_OR_PASSWORD)
def test_should_raise_error_if_account_is_inactive(self) -> None:
"""
Проверка деструктивного изменения БД: отключение аккаунта блокирует вход
"""
petrov = AccountModel.objects.get(id=AccountTestData.P_PETROV_ID)
petrov.is_active = False
petrov.save()
with self.assertRaises(UnauthorizedException) as context:
SessionManager.authenticate(
username="p.petrov@xeaf.net",
password=AccountTestData.DEFAULT_PASSWORD
)
self.assertIsInstance(context.exception.meta, dict)
self.assertEqual(context.exception.meta['reason'], AuthRejectReason.ACCOUNT_IS_NOT_ACTIVE)
def test_should_find_real_ivanov_session_by_token(self) -> None:
"""
Поиск существующей и активной сессии по токену из фикстуры
"""
session = SessionManager.find_by_token(SessionTestData.I_IVANOV_SESSION_TOKEN)
self.assertIsNotNone(session)
self.assertIsInstance(session, SessionModel)
self.assertEqual(session.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
self.assertEqual(session.account_id, AccountTestData.I_IVANOV_ID)
def test_should_return_none_for_non_existent_token(self) -> None:
"""
Запрос несуществующего токена должен возвращать None
"""
result = SessionManager.find_by_token("non_existent_token_garbage_data")
self.assertIsNone(result)
def test_should_return_none_if_session_is_expired_in_db(self) -> None:
"""
Проверка деструктивного изменения БД: просроченная сессия не считывается
"""
session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
# Искусственно сдвигаем дату экспирации в далекое прошлое прямо в базе данных
session.expires_at = timezone.now() - datetime.timedelta(days=10)
session.save()
result = SessionManager.find_by_token(SessionTestData.P_PETROV_SESSION_TOKEN)
self.assertIsNone(result)
def test_should_fail_finding_if_session_was_physically_deleted_from_db(self) -> None:
"""
Полное физическое удаление строки из БД корректно обрабатывается
"""
SessionModel.objects.filter(token=SessionTestData.I_IVANOV_SESSION_TOKEN).delete()
result = SessionManager.find_by_token(SessionTestData.I_IVANOV_SESSION_TOKEN)
self.assertIsNone(result)
@override_settings(SESSION_TTL=1800)
def test_should_open_session_and_apply_override_settings_ttl(self) -> None:
"""
Открытие сессии должно использовать живой ConfigurationManager без заглушек
"""
ivanov = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
petrov = AccountModel.objects.get(id=AccountTestData.P_PETROV_ID)
# Создаем сессию имперсонации (Иванов под видом Петра)
session = SessionManager.open_session(account=ivanov, actual_account=petrov, device_id="tablet_v1")
# Проверяем, что запись успешно материализовалась в ОЗУ
db_session = SessionModel.objects.get(token=session.token)
self.assertEqual(db_session.account, ivanov)
self.assertEqual(db_session.actual_account, petrov)
self.assertEqual(db_session.device_id, "tablet_v1")
self.assertGreater(db_session.expires_at, timezone.now())
def test_should_physically_expire_session_on_close_session(self) -> None:
"""
Закрытие сессии переводит модель в статус expired прямо в БД
"""
session = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
SessionManager.close_session(session)
# Перечитываем чистую запись из БД
db_session = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
self.assertTrue(db_session.is_expired())
@override_settings(SESSION_TTL=9999)
def test_should_extend_expiration_time_on_renew_session(self) -> None:
"""
Обновление сессии сдвигает expires_at вперед на величину из настроек
"""
session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
initial_expires_at = session.expires_at
SessionManager.renew_session(session)
db_session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
self.assertGreater(db_session.expires_at, initial_expires_at)
def test_protected_create_session_model_writes_correct_record(self) -> None:
"""
Прямой вызов защищенного метода фабрики для хирургического теста записи
"""
ivanov = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
# Запускаем защищенный метод фабрики в обход цепочек обновления TTL
session = SessionManager._create_session_model(account=ivanov, actual_account=ivanov)
# Доказываем линтеру, что мы получили объект, и проверяем его наличие в БД
self.assertIsInstance(session, SessionModel)
self.assertTrue(SessionModel.objects.filter(token=session.token).exists())
@override_settings(AUTH_SESSION_MODEL="invalid_app.MissingModel")
def test_protected_get_session_model_class_raises_on_missing_model(self) -> None:
"""
Защищенный метод разрешения класса должен падать, если модель не найдена
"""
# Сбрасываем внутренний кэш класса в синглтоне менеджера перед тестом
SessionManager._SessionManager__model_class = None
with self.assertRaises(ImproperlyConfigured) as context:
SessionManager._get_session_model_class()
self.assertIn("must be set in settings.py", str(context.exception))

View File

@@ -1,12 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов классов моделей данных
"""
from .account_model_tests import AccountModelTests
from .session_model_tests import SessionModelTests

View File

@@ -1,56 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса AccountModelTests
"""
from django.db.utils import IntegrityError
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.management.userstory import AccountTestData
from net.xeaf.rack.models import AccountModel
class AccountModelTests(CoreTestCase):
"""
Тесты для класса AccountModel
"""
def test_user_story_accounts_exist(self):
"""
Проверяет, что эталонные пользователи успешно созданы в User Story
"""
# Ищем Ивана Иванова по фиксированному UUID
ivanov = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
self.assertEqual(ivanov.username, "i.ivanov@xeaf.net")
self.assertEqual(ivanov.full_name, "Иван И. Иванов")
self.assertEqual(ivanov.get_short_name(), "Иван")
# Проверяем, что наше исправление JSONField (default=dict) работает:
# Поле extra должно быть пустым словарем, а не None, готовым к .update()
self.assertEqual(ivanov.extra, {})
def test_password_hashing_works(self):
"""Проверяет, что пароли пользователей в User Story захешированы, а не лежат строкой"""
ivanov = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
# Проверяем, что стандартный джанговский механизм проверки пароля работает
self.assertTrue(ivanov.check_password(AccountTestData.DEFAULT_PASSWORD))
# Гарантируем, что в базе лежит хэш, а не сырая строка
self.assertNotEqual(ivanov.password, AccountTestData.DEFAULT_PASSWORD)
def test_username_uniqueness_constraint(self):
"""Проверяет жесткое ограничение уникальности email-username"""
# Попытка создать пользователя с дублирующим email Ивана должна намертво упасть
duplicate_account = AccountModel()
duplicate_account.username = "i.ivanov@xeaf.net"
with self.assertRaises(IntegrityError):
duplicate_account.save()

View File

@@ -1,17 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов классов расширений моделей данных
"""
from .created_at_mixin_tests import CreatedAtMixinTests
from .deleted_at_mixin_tests import DeletedAtMixinTests
from .description_mixin_tests import DescriptionMixinTests
from .expired_at_mixin_tests import ExpiresAtMixinTests
from .slug_mixin_tests import SlugMixinTests
from .updated_at_mixin_tests import UpdatedAtMixinTests
from .uuid_id_mixin_tests import UuidIdMixinTests

View File

@@ -1,129 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CreatedAtMixinTests
"""
import time
from datetime import datetime
from django.db import connection
from django.db import models
from django.test import TransactionTestCase
from net.xeaf.rack.models.mixins import CreatedAtMixin
class CreatedAtMixinTests(TransactionTestCase):
"""
Тесты для миксина CreatedAtMixin
"""
def setUp(self):
"""
Создаем тестовую модель и таблицу для каждого теста
"""
# Отключаем foreign keys
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = OFF;')
# Создаем тестовую модель
class TestCreatedAtModel(CreatedAtMixin):
name = models.CharField(max_length=100)
class Meta:
app_label = 'test'
db_table = 'test_created_at_mixin'
self.TestModel = TestCreatedAtModel
# Создаем таблицу
with connection.schema_editor() as editor:
editor.create_model(TestCreatedAtModel)
def tearDown(self):
"""
Удаляем таблицу после каждого теста
"""
# Удаляем таблицу
with connection.schema_editor() as editor:
editor.delete_model(self.TestModel)
# Включаем foreign keys обратно
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = ON;')
def test_created_at_is_auto_now_add(self):
"""
Проверяем, что created_at автоматически заполняется при создании
"""
instance = self.TestModel.objects.create(name='Test Instance')
self.assertIsNotNone(instance.created_at)
self.assertIsInstance(instance.created_at, datetime)
def test_created_at_is_auto_now_add_true(self):
"""
Проверяем, что auto_now_add=True
"""
field = self.TestModel._meta.get_field('created_at')
self.assertIsInstance(field, models.DateTimeField)
self.assertTrue(field.auto_now_add)
def test_created_at_editable_false(self):
"""
Проверяем, что editable=False
"""
field = self.TestModel._meta.get_field('created_at')
self.assertFalse(field.editable)
def test_different_instances_have_different_created_at(self):
"""
Проверяем, что у разных экземпляров разное время создания
"""
instance1 = self.TestModel.objects.create(name='First')
time.sleep(0.1)
instance2 = self.TestModel.objects.create(name='Second')
self.assertNotEqual(instance1.created_at, instance2.created_at)
self.assertLess(instance1.created_at, instance2.created_at)
def test_created_at_preserved_on_update(self):
"""
Проверяем, что created_at не меняется при обновлении
"""
instance = self.TestModel.objects.create(name='Test Instance')
old_created_at = instance.created_at
instance.name = 'Updated Name'
instance.save()
instance.refresh_from_db()
self.assertEqual(instance.created_at, old_created_at)
def test_created_at_not_null(self):
"""
Проверяем, что created_at не может быть NULL
"""
field = self.TestModel._meta.get_field('created_at')
self.assertFalse(field.null)
def test_verbose_name(self):
"""
Проверяем verbose_name поля
"""
field = self.TestModel._meta.get_field('created_at')
self.assertIsInstance(field, models.Field)
self.assertEqual(field.verbose_name, "rack.mixin.created_at")

View File

@@ -1,251 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CreatedAtMixinTests
"""
import time
from datetime import timedelta
from django.db import connection
from django.db import models
from django.test import TransactionTestCase
from django.utils import timezone
from net.xeaf.rack.models.mixins import DeletedAtMixin
class DeletedAtMixinTests(TransactionTestCase):
"""
Тесты для миксина DeletedAtMixin
"""
def setUp(self):
"""
Создаем тестовую модель и таблицу для каждого теста
"""
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = OFF;')
class TestDeletedAtModelModel(DeletedAtMixin):
name = models.CharField(max_length=100)
class Meta:
app_label = 'test'
db_table = 'test_deleted_at_mixin'
self.TestModel = TestDeletedAtModelModel
with connection.schema_editor() as editor:
editor.create_model(TestDeletedAtModelModel)
def tearDown(self):
"""
Удаляем таблицу после каждого теста
"""
with connection.schema_editor() as editor:
editor.delete_model(self.TestModel)
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = ON;')
def test_deleted_at_null_by_default(self):
"""
Проверяем, что при создании deleted_at = None
"""
instance = self.TestModel.objects.create(name='Test Instance')
self.assertIsNone(instance.deleted_at)
def test_soft_delete_sets_current_time(self):
"""
Проверяем, что soft_delete() устанавливает текущее время
"""
instance = self.TestModel.objects.create(name='Test Instance')
before = timezone.now()
instance.soft_delete(save=False)
self.assertIsNotNone(instance.deleted_at)
self.assertGreaterEqual(instance.deleted_at, before)
self.assertLessEqual(instance.deleted_at, timezone.now())
def test_soft_delete_saves_to_db_when_save_true(self):
"""
Проверяем, что при soft_delete(save=True) значение сохраняется в БД
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.soft_delete(save=True)
# Обновляем из БД
instance.refresh_from_db()
self.assertIsNotNone(instance.deleted_at)
def test_soft_delete_does_not_save_when_save_false(self):
"""
Проверяем, что при soft_delete(save=False) значение НЕ сохраняется в БД
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.soft_delete(save=False)
# Обновляем из БД
instance.refresh_from_db()
self.assertIsNone(instance.deleted_at)
def test_soft_delete_saves_by_default(self):
"""
Проверяем, что по умолчанию soft_delete() сохраняет в БД
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.soft_delete()
instance.refresh_from_db()
self.assertIsNotNone(instance.deleted_at)
def test_is_deleted_returns_false_when_not_deleted(self):
"""
Проверяем, что is_deleted() возвращает False для активной записи
"""
instance = self.TestModel.objects.create(name='Test Instance')
self.assertFalse(instance.is_deleted())
def test_is_deleted_returns_true_when_deleted(self):
"""
Проверяем, что is_deleted() возвращает True для удаленной записи
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.soft_delete(save=False)
self.assertTrue(instance.is_deleted())
def test_undelete_restores_record(self):
"""
Проверяем, что undelete() восстанавливает запись
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.soft_delete(save=False)
self.assertTrue(instance.is_deleted())
instance.undelete(save=False)
self.assertFalse(instance.is_deleted())
self.assertIsNone(instance.deleted_at)
def test_undelete_saves_to_db_when_save_true(self):
"""
Проверяем, что при undelete(save=True) значение сохраняется в БД
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.soft_delete(save=True)
self.assertTrue(instance.is_deleted())
instance.undelete(save=True)
instance.refresh_from_db()
self.assertFalse(instance.is_deleted())
self.assertIsNone(instance.deleted_at)
def test_undelete_does_not_save_when_save_false(self):
"""
Проверяем, что при undelete(save=False) значение НЕ сохраняется в БД
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.soft_delete(save=True)
self.assertTrue(instance.is_deleted())
instance.undelete(save=False)
instance.refresh_from_db()
# В БД все еще deleted_at не None
self.assertTrue(instance.is_deleted())
self.assertIsNotNone(instance.deleted_at)
def test_undelete_saves_by_default(self):
"""
Проверяем, что по умолчанию undelete() сохраняет в БД
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.soft_delete(save=True)
instance.undelete()
instance.refresh_from_db()
self.assertFalse(instance.is_deleted())
self.assertIsNone(instance.deleted_at)
def test_can_set_deleted_at_manually(self):
"""
Проверяем, что можно установить deleted_at вручную
"""
custom_time = timezone.now() - timedelta(days=1)
instance = self.TestModel.objects.create(name='Test Instance')
instance.deleted_at = custom_time
instance.save()
instance.refresh_from_db()
self.assertEqual(instance.deleted_at, custom_time)
def test_multiple_soft_deletes_update_time(self):
"""
Проверяем, что повторный вызов soft_delete() обновляет время
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.soft_delete(save=False)
first_time = instance.deleted_at
time.sleep(0.1)
instance.soft_delete(save=False)
second_time = instance.deleted_at
self.assertNotEqual(first_time, second_time)
self.assertGreater(second_time, first_time)
def test_undelete_after_manual_delete(self):
"""
Проверяем, что undelete() работает после ручной установки deleted_at
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.deleted_at = timezone.now() - timedelta(days=1)
self.assertTrue(instance.is_deleted())
instance.undelete(save=False)
self.assertFalse(instance.is_deleted())
self.assertIsNone(instance.deleted_at)
def test_soft_delete_twice_with_save(self):
"""
Проверяем, что повторный soft_delete с save=True корректно работает
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.soft_delete(save=True)
first_time = instance.deleted_at
time.sleep(0.1)
instance.soft_delete(save=True)
instance.refresh_from_db()
self.assertNotEqual(instance.deleted_at, first_time)
self.assertGreater(instance.deleted_at, first_time)

View File

@@ -1,59 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса DescriptionMixinTests
"""
from django.db import connection
from django.db import models
from django.test import TransactionTestCase
from net.xeaf.rack.models.mixins import DescriptionMixin
class DescriptionMixinTests(TransactionTestCase):
"""
Тесты для миксина DescriptionMixin
"""
def setUp(self):
"""
Создаем тестовую модель и таблицу для каждого теста
"""
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = OFF;')
class TestDescriptionModel(DescriptionMixin):
name = models.CharField(max_length=100)
class Meta:
app_label = 'test'
db_table = 'test_slug_mixin'
self.TestModel = TestDescriptionModel
with connection.schema_editor() as editor:
editor.create_model(TestDescriptionModel)
def tearDown(self):
"""
Удаляем таблицу после каждого теста
"""
with connection.schema_editor() as editor:
editor.delete_model(self.TestModel)
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = ON;')
def test_description_field_exists(self):
"""
Проверяем, что поле description добавлено в модель
"""
self.assertTrue(hasattr(self.TestModel, 'description'))

View File

@@ -1,332 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ExpiredAtMixinTests
"""
from datetime import timedelta
from django.db import connection
from django.db import models
from django.db.models.fields import DateTimeField
from django.test import TransactionTestCase
from django.utils import timezone
from net.xeaf.rack.models.mixins import ExpiresAtMixin
class ExpiresAtMixinTests(TransactionTestCase):
"""
Тесты для миксина ExpiresAtMixin
"""
def setUp(self):
"""
Создаем тестовую модель и таблицу для каждого теста
"""
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = OFF;')
class TestExpiredAtModel(ExpiresAtMixin):
name = models.CharField(max_length=100)
class Meta:
app_label = 'test'
db_table = 'test_expires_at_mixin'
self.TestModel = TestExpiredAtModel
with connection.schema_editor() as editor:
editor.create_model(TestExpiredAtModel)
def tearDown(self):
"""
Удаляем таблицу после каждого теста
"""
with connection.schema_editor() as editor:
editor.delete_model(self.TestModel)
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = ON;')
def test_expires_at_not_required(self):
"""
Проверяем, что expires_at не обязательное поле
"""
field = self.TestModel._meta.get_field('expires_at')
self.assertTrue(field.null)
self.assertIsInstance(field, DateTimeField)
self.assertTrue(field.blank)
def test_expires_at_with_none_never_expires(self):
"""
Проверяем, что при expires_at == None срок действия не ограничен
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=None
)
self.assertFalse(instance.is_expired())
def test_renew_sets_expiration_time(self):
"""
Проверяем, что renew() устанавливает время истечения
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
before = timezone.now()
instance.renew(seconds=3600, save=False)
self.assertIsNotNone(instance.expires_at)
self.assertGreaterEqual(instance.expires_at, before + timedelta(seconds=3600))
self.assertLessEqual(instance.expires_at, timezone.now() + timedelta(seconds=3600))
def test_renew_saves_to_db_when_save_true(self):
"""
Проверяем, что renew(save=True) сохраняет в БД
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
instance.renew(seconds=3600, save=True)
instance.refresh_from_db()
self.assertGreaterEqual(instance.expires_at, timezone.now())
def test_renew_does_not_save_when_save_false(self):
"""
Проверяем, что renew(save=False) НЕ сохраняет в БД
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
old_expires_at = instance.expires_at
instance.renew(seconds=3600, save=False)
instance.refresh_from_db()
self.assertEqual(instance.expires_at, old_expires_at)
def test_renew_raises_error_for_negative_seconds(self):
"""
Проверяем, что renew() выбрасывает ошибку для отрицательного значения
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
with self.assertRaises(ValueError) as context:
instance.renew(seconds=-1)
self.assertIn('greater than 0', str(context.exception))
def test_renew_raises_error_for_zero_seconds(self):
"""
Проверяем, что renew() выбрасывает ошибку для нулевого значения
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
with self.assertRaises(ValueError) as context:
instance.renew(seconds=0)
self.assertIn('greater than 0', str(context.exception))
def test_expire_sets_time_to_now(self):
"""
Проверяем, что expire() устанавливает время истечения на сейчас
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
before = timezone.now()
instance.expire(save=False)
self.assertIsNotNone(instance.expires_at)
self.assertGreaterEqual(instance.expires_at, before)
self.assertLessEqual(instance.expires_at, timezone.now())
def test_expire_saves_to_db_when_save_true(self):
"""
Проверяем, что expire(save=True) сохраняет в БД
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
instance.expire(save=True)
instance.refresh_from_db()
self.assertLessEqual(instance.expires_at, timezone.now())
def test_expire_does_not_save_when_save_false(self):
"""
Проверяем, что expire(save=False) НЕ сохраняет в БД
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
old_expires_at = instance.expires_at
instance.expire(save=False)
instance.refresh_from_db()
self.assertEqual(instance.expires_at, old_expires_at)
def test_is_expired_returns_false_when_not_expired(self):
"""
Проверяем, что is_expired() возвращает False для активной записи
"""
future_time = timezone.now() + timedelta(days=1)
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=future_time
)
self.assertFalse(instance.is_expired())
def test_is_expired_returns_true_when_expired(self):
"""
Проверяем, что is_expired() возвращает True для просроченной записи
"""
past_time = timezone.now() - timedelta(days=1)
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=past_time
)
self.assertTrue(instance.is_expired())
def test_is_expired_after_renew(self):
"""
Проверяем, что is_expired() корректно работает после renew()
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() - timedelta(days=1)
)
self.assertTrue(instance.is_expired())
instance.renew(seconds=3600, save=False)
self.assertFalse(instance.is_expired())
def test_is_expired_after_expire(self):
"""
Проверяем, что is_expired() корректно работает после expire()
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
self.assertFalse(instance.is_expired())
instance.expire(save=False)
self.assertTrue(instance.is_expired())
def test_renew_with_large_seconds(self):
"""
Проверяем, что renew() работает с большими значениями
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
seconds = 86400 * 365 # 1 год
instance.renew(seconds=seconds, save=False)
expected = timezone.now() + timedelta(seconds=seconds)
self.assertLessEqual(instance.expires_at, expected + timedelta(seconds=1))
self.assertGreaterEqual(instance.expires_at, expected - timedelta(seconds=1))
def test_renew_saves_by_default_false(self):
"""
Проверяем, что по умолчанию renew() НЕ сохраняет в БД
(save=False по умолчанию, в отличие от soft_delete)
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
old_expires_at = instance.expires_at
instance.renew(seconds=3600)
instance.refresh_from_db()
self.assertEqual(instance.expires_at, old_expires_at)
def test_expire_saves_by_default_false(self):
"""
Проверяем, что по умолчанию expire() НЕ сохраняет в БД
(save=False по умолчанию, в отличие от soft_delete)
"""
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=timezone.now() + timedelta(days=1)
)
old_expires_at = instance.expires_at
instance.expire()
instance.refresh_from_db()
self.assertEqual(instance.expires_at, old_expires_at)
def test_expires_at_editable_true(self):
"""
Проверяем, что поле expires_at редактируемое
"""
field = self.TestModel._meta.get_field('expires_at')
self.assertTrue(field.editable)
def test_can_set_expires_at_manually(self):
"""
Проверяем, что можно установить expires_at вручную
"""
custom_time = timezone.now() + timedelta(days=5)
instance = self.TestModel.objects.create(
name='Test Instance',
expires_at=custom_time
)
instance.save()
instance.refresh_from_db()
self.assertEqual(instance.expires_at, custom_time)

View File

@@ -1,59 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса SlugMixinTests
"""
from django.db import connection
from django.db import models
from django.test import TransactionTestCase
from net.xeaf.rack.models.mixins import SlugMixin
class SlugMixinTests(TransactionTestCase):
"""
Тесты для миксина SlugMixin
"""
def setUp(self):
"""
Создаем тестовую модель и таблицу для каждого теста
"""
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = OFF;')
class TestSlugModel(SlugMixin):
name = models.CharField(max_length=100)
class Meta:
app_label = 'test'
db_table = 'test_slug_mixin'
self.TestModel = TestSlugModel
with connection.schema_editor() as editor:
editor.create_model(TestSlugModel)
def tearDown(self):
"""
Удаляем таблицу после каждого теста
"""
with connection.schema_editor() as editor:
editor.delete_model(self.TestModel)
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = ON;')
def test_slug_field_exists(self):
"""
Проверяем, что поле slug добавлено в модель
"""
self.assertTrue(hasattr(self.TestModel, 'slug'))

View File

@@ -1,182 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса UpdatedAtMixinTests
"""
import time
from datetime import timedelta
from django.db import connection
from django.db import models
from django.test import TransactionTestCase
from django.utils import timezone
from net.xeaf.rack.models.mixins import UpdatedAtMixin
class UpdatedAtMixinTests(TransactionTestCase):
"""
Тесты для миксина UpdatedAtMixin
"""
def setUp(self):
"""
Создаем тестовую модель и таблицу для каждого теста
"""
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = OFF;')
class TestUpdatedAtModel(UpdatedAtMixin):
name = models.CharField(max_length=100)
class Meta:
app_label = 'test'
db_table = 'test_updated_at_mixin'
self.TestModel = TestUpdatedAtModel
with connection.schema_editor() as editor:
editor.create_model(TestUpdatedAtModel)
def tearDown(self):
"""
Удаляем таблицу после каждого теста
"""
with connection.schema_editor() as editor:
editor.delete_model(self.TestModel)
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = ON;')
def test_updated_at_null_by_default(self):
"""
Проверяем, что при создании updated_at = None
"""
instance = self.TestModel.objects.create(name='Test Instance')
self.assertIsNone(instance.updated_at)
def test_update_sets_current_time(self):
"""
Проверяем, что update() устанавливает текущее время
"""
instance = self.TestModel.objects.create(name='Test Instance')
before = timezone.now()
instance.update(save=False)
self.assertIsNotNone(instance.updated_at)
self.assertGreaterEqual(instance.updated_at, before)
self.assertLessEqual(instance.updated_at, timezone.now())
def test_update_saves_to_db_when_save_true(self):
"""
Проверяем, что update(save=True) сохраняет в БД
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.update(save=True)
instance.refresh_from_db()
self.assertIsNotNone(instance.updated_at)
def test_update_does_not_save_when_save_false(self):
"""
Проверяем, что update(save=False) НЕ сохраняет в БД
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.update(save=False)
instance.refresh_from_db()
self.assertIsNone(instance.updated_at)
def test_update_saves_by_default(self):
"""
Проверяем, что по умолчанию update() сохраняет в БД
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.update()
instance.refresh_from_db()
self.assertIsNotNone(instance.updated_at)
def test_is_updated_returns_false_when_not_updated(self):
"""
Проверяем, что is_updated() возвращает False для новой записи
"""
instance = self.TestModel.objects.create(name='Test Instance')
self.assertFalse(instance.is_updated())
def test_is_updated_returns_true_when_updated(self):
"""
Проверяем, что is_updated() возвращает True после update()
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.update(save=False)
self.assertTrue(instance.is_updated())
def test_is_updated_since_returns_false_when_not_updated(self):
"""
Проверяем, что is_updated_since() возвращает False для новой записи
"""
instance = self.TestModel.objects.create(name='Test Instance')
dt = timezone.now() - timedelta(minutes=5)
self.assertFalse(instance.is_updated_since(dt))
def test_is_updated_since_returns_true_after_update(self):
"""
Проверяем, что is_updated_since() возвращает True после update()
"""
instance = self.TestModel.objects.create(name='Test Instance')
dt = timezone.now()
time.sleep(0.1)
instance.update(save=False)
self.assertTrue(instance.is_updated_since(dt))
def test_is_updated_since_returns_false_if_updated_before_dt(self):
"""
Проверяем, что is_updated_since() возвращает False,
если обновление было раньше указанного времени
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.update(save=False)
old_updated_at = instance.updated_at
# Проверяем, что updated_at был раньше, чем через секунду
dt = old_updated_at + timedelta(seconds=1)
self.assertFalse(instance.is_updated_since(dt))
def test_multiple_updates(self):
"""
Проверяем, что повторный вызов update() обновляет время
"""
instance = self.TestModel.objects.create(name='Test Instance')
instance.update(save=False)
first_time = instance.updated_at
time.sleep(0.1)
instance.update(save=False)
second_time = instance.updated_at
self.assertNotEqual(first_time, second_time)
self.assertGreater(second_time, first_time)

View File

@@ -1,100 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса UuidIdMixinTests
"""
import uuid
from django.db import connection
from django.db import models
from django.test import TransactionTestCase
from net.xeaf.rack.models.mixins import UuidIdMixin
class UuidIdMixinTests(TransactionTestCase):
"""
Тесты для миксина UuidIdMixin
"""
def setUp(self):
"""
Создаем тестовую модель и таблицу для каждого теста
"""
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = OFF;')
class TestUuidModel(UuidIdMixin):
name = models.CharField(max_length=100)
class Meta:
app_label = 'test'
db_table = 'test_uuid_mixin'
self.TestModel = TestUuidModel
with connection.schema_editor() as editor:
editor.create_model(TestUuidModel)
def tearDown(self):
"""
Удаляем таблицу после каждого теста
"""
with connection.schema_editor() as editor:
editor.delete_model(self.TestModel)
with connection.cursor() as cursor:
cursor.execute('PRAGMA foreign_keys = ON;')
def test_id_is_uuid_by_default(self):
"""
Проверяем, что при создании id автоматически генерируется как UUID
"""
instance = self.TestModel.objects.create(name='Test Instance')
self.assertIsNotNone(instance.id)
self.assertIsInstance(instance.id, uuid.UUID)
def test_id_is_unique(self):
"""
Проверяем, что каждый экземпляр получает уникальный UUID
"""
instance1 = self.TestModel.objects.create(name='First')
instance2 = self.TestModel.objects.create(name='Second')
self.assertNotEqual(instance1.id, instance2.id)
def test_id_is_primary_key(self):
"""
Проверяем, что id является первичным ключом
"""
field = self.TestModel._meta.get_field('id')
self.assertTrue(field.primary_key)
def test_id_is_editable_false(self):
"""
Проверяем, что id нельзя редактировать
"""
field = self.TestModel._meta.get_field('id')
self.assertFalse(field.editable)
def test_id_can_be_used_in_queries(self):
"""
Проверяем, что можно искать по UUID
"""
instance = self.TestModel.objects.create(name='Test Instance')
found = self.TestModel.objects.get(id=instance.id)
self.assertEqual(found.id, instance.id)

View File

@@ -1,16 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов классов ответов по протоколу HTTP
"""
from .bool_response_tests import BoolResponseTests
from .data_response_tests import DataResponseTests
from .empty_response_tests import EmptyResponseTests
from .error_response_tests import ErrorResponseTests
from .exception_response_tests import ExceptionResponseTests
from .list_response_tests import ListResponseTests

View File

@@ -1,51 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса BoolResponseTests
"""
from django.test import SimpleTestCase
from rest_framework import status
from net.xeaf.rack.models.responses import BoolResponse
class BoolResponseTests(SimpleTestCase):
"""
Тесты для класса BoolResponse
"""
def test_init_with_true_value(self):
"""
Проверяет корректное создание ответа со значением True
"""
test_meta = {"action": "verify"}
response = BoolResponse(value=True, meta=test_meta)
# Проверяем заголовки и статус
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["status"], status.HTTP_200_OK)
# Булево значение должно лежать в ключе "data", так как код 200 (< 400)
self.assertIs(response.data["data"], True)
self.assertEqual(response.data["meta"], test_meta)
def test_init_with_false_value(self):
"""
Проверяет корректное создание ответа со значением False
"""
response = BoolResponse(value=False)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["status"], status.HTTP_200_OK)
# Проверяем, что False не интерпретируется как отсутствие данных
self.assertIs(response.data["data"], False)

View File

@@ -1,41 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса DataResponseTests
"""
from django.test import SimpleTestCase
from rest_framework import status
from net.xeaf.rack.models.responses import DataResponse
class DataResponseTests(SimpleTestCase):
"""
Тесты для класса DataResponse
"""
def test_init_with_dict_data(self):
"""
Проверяет корректное создание ответа со словарем данных и дефолтными статус-кодами
"""
test_dict = {"id": 42, "name": "Main Project", "is_active": True}
test_meta = {"version": "1.0.0"}
response = DataResponse(data=test_dict, meta=test_meta)
# Проверяем честный HTTP-статус в заголовках
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Проверяем контракт тела JSON
self.assertEqual(response.data["status"], status.HTTP_200_OK)
# Так как код 200 (< 400), объект обязан лежать именно в "data"
self.assertEqual(response.data["data"], test_dict)
# Проверяем проброс метаданных
self.assertEqual(response.data["meta"], test_meta)

View File

@@ -1,39 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса EmptyResponseTests
"""
from django.test import SimpleTestCase
from rest_framework import status
from net.xeaf.rack.models.responses import EmptyResponse
class EmptyResponseTests(SimpleTestCase):
"""
Тесты для класса EmptyResponse
"""
def test_init_sugar_configuration(self):
"""
Проверяет жестко зашитую конфигурацию пустого успешного ответа
"""
response = EmptyResponse()
# Проверяем честный HTTP-статус в заголовках
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Проверяем контракт тела ответа в JSON
self.assertEqual(response.data["status"], status.HTTP_200_OK)
self.assertIn("timestamp", response.data)
# Гарантируем, что ключи данных и деталей не создались, так как data=None
self.assertNotIn("data", response.data)
self.assertNotIn("detail", response.data)
self.assertNotIn("meta", response.data)

View File

@@ -1,65 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ErrorResponseTests
"""
from django.test import SimpleTestCase
from rest_framework import status
from net.xeaf.rack.models.responses import ErrorResponse
class ErrorResponseTests(SimpleTestCase):
"""
Тесты для класса ErrorResponse
"""
def test_calc_external_status_for_client_error(self):
"""
Ошибки типа 4xx (меньше 500) должны маскироваться под 200 OK
"""
# Вызываем protected метод класса напрямую
result = ErrorResponse._calc_external_status(status.HTTP_400_BAD_REQUEST)
self.assertEqual(result, status.HTTP_200_OK)
result_403 = ErrorResponse._calc_external_status(status.HTTP_403_FORBIDDEN)
self.assertEqual(result_403, status.HTTP_200_OK)
def test_calc_external_status_for_server_error(self):
"""
Ошибки типа 5xx (500 и выше) должны возвращать свой честный HTTP код
"""
result = ErrorResponse._calc_external_status(status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertEqual(result, status.HTTP_500_INTERNAL_SERVER_ERROR)
result_503 = ErrorResponse._calc_external_status(status.HTTP_503_SERVICE_UNAVAILABLE)
self.assertEqual(result_503, status.HTTP_503_SERVICE_UNAVAILABLE)
def test_init_integration(self):
"""
Проверяет базовый контракт сборки ответа с ошибкой
"""
error_msg = "Доступ запрещен"
internal_code = status.HTTP_403_FORBIDDEN
response = ErrorResponse(
status_code=internal_code,
detail=error_msg
)
# Проверяем, что метод класса отработал внутри init и вернул 200 OK в заголовки
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Проверяем структуру тела ответа
self.assertEqual(response.data["status"], internal_code)
# Так как код ошибки >= 400, данные обязаны лежать в ключе "detail"
self.assertEqual(response.data["detail"], error_msg)
self.assertNotIn("data", response.data)

View File

@@ -1,64 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ExceptionResponseTests
"""
from django.test import SimpleTestCase
from rest_framework import status
from net.xeaf.rack.core import CoreException
from net.xeaf.rack.models.responses import ExceptionResponse
class ExceptionResponseTests(SimpleTestCase):
"""
Тесты для класса ExceptionResponse
"""
def test_init_with_standard_exception(self):
"""
Если у исключения is_pure_status() == False,
внешний HTTP статус должен быть 200 OK, но внутренний — реальный код ошибки,
а данные обязаны лежать в ключе 'detail'
"""
# Симулируем обычную ошибку (is_pure_status по умолчанию выдает False)
exc = CoreException(status_code=status.HTTP_400_BAD_REQUEST, detail="Неверный логин")
response = ExceptionResponse(exc=exc)
# Проверяем маскировку под 200 OK для фронтенда в заголовках
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Проверяем JSON контракт
self.assertEqual(response.data["status"], status.HTTP_400_BAD_REQUEST)
# Так как реальный статус ошибки >= 400, данные ДОЛЖНЫ быть в detail
self.assertEqual(response.data["detail"], "Неверный логин")
self.assertNotIn("data", response.data)
def test_init_with_pure_http_exception(self):
"""
Если у исключения is_pure_status() == True,
и внешний HTTP статус, и внутренний код должны быть равны коду ошибки
"""
# Создаем тестовый класс, переопределяющий метод на True (как твой CriticalException)
class PureException(CoreException):
def is_pure_status(self) -> bool:
return True
exc = PureException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Крах БД")
response = ExceptionResponse(exc=exc)
# Проверяем, что маскировка отключилась и в заголовках честный HTTP статус
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
# Внутренний контракт тоже содержит реальный код ошибки
self.assertEqual(response.data["status"], status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertEqual(response.data["detail"], "Крах БД")

View File

@@ -1,41 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ListResponseTests
"""
from django.test import SimpleTestCase
from rest_framework import status
from net.xeaf.rack.models.responses import ListResponse
class ListResponseTests(SimpleTestCase):
"""
Тесты для класса ListResponse
"""
def test_init_with_list_data(self):
"""
Проверяет корректное создание ответа со списком и дефолтными статус-кодами
"""
test_list = [{"id": 1, "name": "Project A"}, {"id": 2, "name": "Project B"}]
test_meta = {"total_count": 2}
response = ListResponse(data=test_list, meta=test_meta)
# Проверяем честный HTTP-статус в заголовках
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Проверяем контракт тела JSON
self.assertEqual(response.data["status"], status.HTTP_200_OK)
# Так как код 200 (< 400), список обязан лежать именно в "data"
self.assertEqual(response.data["data"], test_list)
# Проверяем, что метаданные тоже успешно долетели через super()
self.assertEqual(response.data["meta"], test_meta)

View File

@@ -1,64 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса SessionModelTests
"""
from django.db.utils import IntegrityError
from django.utils import timezone
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 SessionModel
class SessionModelTests(CoreTestCase):
"""
Тесты для класса SessionModel
"""
def test_user_story_sessions_exist_and_linked(self):
"""
Проверяет, что сессии из User Story созданы и правильно связаны с аккаунтами
"""
# Ищем сессию Ивана по фиксированному токену
session = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
# Проверяем связи (включая твой крутой механизм имперсонации)
self.assertEqual(session.account_id, AccountTestData.I_IVANOV_ID)
self.assertEqual(session.actual_account_id, AccountTestData.I_IVANOV_ID)
# Проверяем дефолты текстовых полей, которые мы закладывали в модель
self.assertEqual(session.device_id, "WEB")
self.assertIsNone(session.fcm_token)
def test_session_expiration_logic(self):
"""
Проверяет, что метод renew() правильно выставил expires_at в будущем
"""
session = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
# Сессия должна быть активной прямо сейчас
self.assertGreater(session.expires_at, timezone.now())
def test_token_uniqueness_constraint(self):
"""
Проверяет жесткое ограничение уникальности токенов сессий
"""
# Попытка создать новую сессию с токеном Петра должна упасть
duplicate_session = SessionModel()
duplicate_session.token = SessionTestData.P_PETROV_SESSION_TOKEN
duplicate_session.account_id = AccountTestData.I_IVANOV_ID
duplicate_session.actual_account_id = AccountTestData.I_IVANOV_ID
duplicate_session.expire()
with self.assertRaises(IntegrityError):
duplicate_session.save()

View File

@@ -1,10 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов для служебных классов и утилит
"""

View File

@@ -1,12 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов классов авторизации
"""
from .expiring_token_authentication_tests import ExpiringTokenAuthenticationTests

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())