diff --git a/net/xeaf/rack/core/core_command.py b/net/xeaf/rack/core/core_command.py index a9bea28..6f49602 100644 --- a/net/xeaf/rack/core/core_command.py +++ b/net/xeaf/rack/core/core_command.py @@ -29,7 +29,7 @@ class CoreCommand(BaseCommand): call_command("flush", "--no-input") @classmethod - def is_debug_or_test(cls): + def _is_debug_or_test(cls): """ Возвращает признак режима отладки или тестов """ diff --git a/net/xeaf/rack/core/tests/__init__.py b/net/xeaf/rack/core/tests/__init__.py index aeb34e8..a6bf96e 100644 --- a/net/xeaf/rack/core/tests/__init__.py +++ b/net/xeaf/rack/core/tests/__init__.py @@ -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 diff --git a/net/xeaf/rack/core/tests/core_simple_test_case.py b/net/xeaf/rack/core/tests/core_simple_test_case.py new file mode 100644 index 0000000..30cc97a --- /dev/null +++ b/net/xeaf/rack/core/tests/core_simple_test_case.py @@ -0,0 +1,27 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: 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() diff --git a/net/xeaf/rack/core/tests/core_user_story_test_case.py b/net/xeaf/rack/core/tests/core_user_story_test_case.py new file mode 100644 index 0000000..9c1f6fe --- /dev/null +++ b/net/xeaf/rack/core/tests/core_user_story_test_case.py @@ -0,0 +1,78 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: 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 + ) diff --git a/net/xeaf/rack/core/tests/request_test_case_mixin.py b/net/xeaf/rack/core/tests/request_test_case_mixin.py new file mode 100644 index 0000000..85ed46f --- /dev/null +++ b/net/xeaf/rack/core/tests/request_test_case_mixin.py @@ -0,0 +1,164 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: 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 diff --git a/net/xeaf/rack/core/views/core_view_set.py b/net/xeaf/rack/core/views/core_view_set.py index 4e3d96c..4bf791f 100644 --- a/net/xeaf/rack/core/views/core_view_set.py +++ b/net/xeaf/rack/core/views/core_view_set.py @@ -7,6 +7,7 @@ """ Описание класса CoreViewSet """ + from typing import Any from typing import Type from uuid import UUID diff --git a/net/xeaf/rack/management/commands/rack_test_database.py b/net/xeaf/rack/management/commands/rack_test_database.py index c6758c8..b53a2a7 100644 --- a/net/xeaf/rack/management/commands/rack_test_database.py +++ b/net/xeaf/rack/management/commands/rack_test_database.py @@ -30,7 +30,7 @@ class Command(CoreCommand): :params options: Дополнительные параметры """ - if self.is_debug_or_test(): + if self._is_debug_or_test(): # Готовимся self._clear_database() diff --git a/net/xeaf/rack/management/commands/rack_wipe_database.py b/net/xeaf/rack/management/commands/rack_wipe_database.py index fb65e57..2fc7250 100644 --- a/net/xeaf/rack/management/commands/rack_wipe_database.py +++ b/net/xeaf/rack/management/commands/rack_wipe_database.py @@ -27,7 +27,7 @@ class Command(CoreCommand): :params options: Дополнительные параметры """ - if self.is_debug_or_test(): + if self._is_debug_or_test(): self._clear_database() else: self.stdout.write(self.style.ERROR("The rack_wipe_database command can only be executed in debug or test mode.")) diff --git a/net/xeaf/rack/management/userstory/account_test_data.py b/net/xeaf/rack/management/userstory/account_test_data.py index dd3b2b0..e112374 100644 --- a/net/xeaf/rack/management/userstory/account_test_data.py +++ b/net/xeaf/rack/management/userstory/account_test_data.py @@ -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!" """ Пароль по умолчанию для тестов """ @@ -40,17 +40,17 @@ class AccountTestData: Инициализирует Аккаунты пользователя """ - cls.init_i_ivanov_account() - cls.init_p_petrov_account() + cls.init_ivanov_account() + cls.init_petrov_account() @classmethod - def init_i_ivanov_account(cls): + def init_ivanov_account(cls): """ - Инициализирует Аккаунт пользователя Иван И. Иванов + Инициализирует Аккаунт пользователя Иван Иванов """ account = AccountModel() - account.id = cls.I_IVANOV_ID + account.id = cls.IVANOV_ID account.username = "i.ivanov@xeaf.net" account.nick_name = "Иван" account.full_name = "Иван И. Иванов" @@ -60,13 +60,13 @@ class AccountTestData: account.save() @classmethod - def init_p_petrov_account(cls): + def init_petrov_account(cls): """ - Инициализирует Аккаунт пользователя Петр П. Петров + Инициализирует Аккаунт пользователя Петр Петров """ account = AccountModel() - account.id = cls.P_PETROV_ID + account.id = cls.PETROV_ID account.username = "p.petrov@xeaf.net" account.nick_name = "Петр" account.full_name = "Петр П. Петров" @@ -76,17 +76,17 @@ class AccountTestData: account.save() @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 - 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) diff --git a/net/xeaf/rack/management/userstory/session_test_data.py b/net/xeaf/rack/management/userstory/session_test_data.py index b661c7a..5bb0a5e 100644 --- a/net/xeaf/rack/management/userstory/session_test_data.py +++ b/net/xeaf/rack/management/userstory/session_test_data.py @@ -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 """ Время жизни тестовой сессии """ @@ -32,51 +32,51 @@ class SessionTestData: Инициализирует сессии для тестовых пользователей """ - cls.init_i_ivanov_session() - cls.init_p_petrov_session() + cls.init_ivanov_session() + cls.init_petrov_session() @classmethod - def init_i_ivanov_session(cls): + def init_ivanov_session(cls): """ - Создает Сессию для пользователя Иван И. Иванов + Создает Сессию для пользователя Иван Иванов """ session = SessionModel() - session.token = cls.I_IVANOV_SESSION_TOKEN - session.account_id = AccountTestData.I_IVANOV_ID - session.actual_account_id = AccountTestData.I_IVANOV_ID + session.token = cls.IVANOV_SESSION_TOKEN + session.account_id = AccountTestData.IVANOV_ID + session.actual_account_id = AccountTestData.IVANOV_ID session.renew(cls.TEST_SESSION_TTL) session.save() @classmethod - def init_p_petrov_session(cls): + def init_petrov_session(cls): """ - Создает Сессию для пользователя Петр П. Петров + Создает Сессию для пользователя Петр Петров """ session = SessionModel() - session.token = cls.P_PETROV_SESSION_TOKEN - session.account_id = AccountTestData.P_PETROV_ID - session.actual_account_id = AccountTestData.P_PETROV_ID + session.token = cls.PETROV_SESSION_TOKEN + session.account_id = AccountTestData.PETROV_ID + session.actual_account_id = AccountTestData.PETROV_ID session.renew(cls.TEST_SESSION_TTL) session.save() @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 - 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) diff --git a/net/xeaf/rack/models/mixins/expired_at_mixin.py b/net/xeaf/rack/models/mixins/expired_at_mixin.py index 94748ca..de3456f 100644 --- a/net/xeaf/rack/models/mixins/expired_at_mixin.py +++ b/net/xeaf/rack/models/mixins/expired_at_mixin.py @@ -7,7 +7,7 @@ """ Описание класса ExpiredAtMixin """ - +from django.conf import settings from django.db import models 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( null=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 force: Признак принудительного обновления :param save: Признак необходимости сохранения """ @@ -39,7 +43,16 @@ class ExpiresAtMixin(models.Model): if seconds <= 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: self.save() diff --git a/net/xeaf/rack/tests/core/__init__.py b/net/xeaf/rack/tests/core/__init__.py deleted file mode 100644 index e07c41a..0000000 --- a/net/xeaf/rack/tests/core/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: MIT - -""" -Пакет описания тестов для базовых классов -""" - -from .core_enum_tests import CoreEnumTests -from .core_exception_tests import CoreExceptionTests -from .core_response_tests import CoreResponseTests - -from .views import * diff --git a/net/xeaf/rack/tests/core/core_enum_tests.py b/net/xeaf/rack/tests/core/core_enum_tests.py deleted file mode 100644 index 5048c31..0000000 --- a/net/xeaf/rack/tests/core/core_enum_tests.py +++ /dev/null @@ -1,234 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/core/core_exception_tests.py b/net/xeaf/rack/tests/core/core_exception_tests.py deleted file mode 100644 index fa408c7..0000000 --- a/net/xeaf/rack/tests/core/core_exception_tests.py +++ /dev/null @@ -1,141 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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()) diff --git a/net/xeaf/rack/tests/core/core_response_tests.py b/net/xeaf/rack/tests/core/core_response_tests.py deleted file mode 100644 index 7e15c16..0000000 --- a/net/xeaf/rack/tests/core/core_response_tests.py +++ /dev/null @@ -1,98 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/core/views/__init__.py b/net/xeaf/rack/tests/core/views/__init__.py deleted file mode 100644 index d19377f..0000000 --- a/net/xeaf/rack/tests/core/views/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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 diff --git a/net/xeaf/rack/tests/core/views/core_view_set_get_serializer_tests.py b/net/xeaf/rack/tests/core/views/core_view_set_get_serializer_tests.py deleted file mode 100644 index 9bbfd4d..0000000 --- a/net/xeaf/rack/tests/core/views/core_view_set_get_serializer_tests.py +++ /dev/null @@ -1,71 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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)) diff --git a/net/xeaf/rack/tests/core/views/core_view_set_is_partial_update.py b/net/xeaf/rack/tests/core/views/core_view_set_is_partial_update.py deleted file mode 100644 index 2310c89..0000000 --- a/net/xeaf/rack/tests/core/views/core_view_set_is_partial_update.py +++ /dev/null @@ -1,107 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/core/views/core_view_set_path_generation_tests.py b/net/xeaf/rack/tests/core/views/core_view_set_path_generation_tests.py deleted file mode 100644 index fd73262..0000000 --- a/net/xeaf/rack/tests/core/views/core_view_set_path_generation_tests.py +++ /dev/null @@ -1,163 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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//" - - @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//" - - @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//") - - 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//") - - 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) \ No newline at end of file diff --git a/net/xeaf/rack/tests/core/views/core_view_set_process_data_params.py b/net/xeaf/rack/tests/core/views/core_view_set_process_data_params.py deleted file mode 100644 index 58f9e21..0000000 --- a/net/xeaf/rack/tests/core/views/core_view_set_process_data_params.py +++ /dev/null @@ -1,320 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/core/views/core_view_set_process_pk_param_tests.py b/net/xeaf/rack/tests/core/views/core_view_set_process_pk_param_tests.py deleted file mode 100644 index be6c970..0000000 --- a/net/xeaf/rack/tests/core/views/core_view_set_process_pk_param_tests.py +++ /dev/null @@ -1,72 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/core/views/core_view_set_process_query_params_tests.py b/net/xeaf/rack/tests/core/views/core_view_set_process_query_params_tests.py deleted file mode 100644 index 81babe5..0000000 --- a/net/xeaf/rack/tests/core/views/core_view_set_process_query_params_tests.py +++ /dev/null @@ -1,278 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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)) diff --git a/net/xeaf/rack/tests/core/views/core_view_set_request_context_tests.py b/net/xeaf/rack/tests/core/views/core_view_set_request_context_tests.py deleted file mode 100644 index cfc0161..0000000 --- a/net/xeaf/rack/tests/core/views/core_view_set_request_context_tests.py +++ /dev/null @@ -1,136 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/enums/__init__.py b/net/xeaf/rack/tests/enums/__init__.py deleted file mode 100644 index 46738d4..0000000 --- a/net/xeaf/rack/tests/enums/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: MIT - -""" -Пакет описания тестов для классов перечислений -""" - -from .ui_language_tests import UILanguageTests -from .view_set_action_tests import ViewSetActionTests - diff --git a/net/xeaf/rack/tests/enums/ui_language_tests.py b/net/xeaf/rack/tests/enums/ui_language_tests.py deleted file mode 100644 index 7cf3975..0000000 --- a/net/xeaf/rack/tests/enums/ui_language_tests.py +++ /dev/null @@ -1,112 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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' не поддерживается diff --git a/net/xeaf/rack/tests/enums/view_set_action_tests.py b/net/xeaf/rack/tests/enums/view_set_action_tests.py deleted file mode 100644 index c83e827..0000000 --- a/net/xeaf/rack/tests/enums/view_set_action_tests.py +++ /dev/null @@ -1,41 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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 diff --git a/net/xeaf/rack/tests/managers/__init__.py b/net/xeaf/rack/tests/managers/__init__.py deleted file mode 100644 index 14ab749..0000000 --- a/net/xeaf/rack/tests/managers/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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 diff --git a/net/xeaf/rack/tests/managers/configuration_manager_tests.py b/net/xeaf/rack/tests/managers/configuration_manager_tests.py deleted file mode 100644 index 6b00807..0000000 --- a/net/xeaf/rack/tests/managers/configuration_manager_tests.py +++ /dev/null @@ -1,116 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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)) diff --git a/net/xeaf/rack/tests/managers/crypto_manager_tests.py b/net/xeaf/rack/tests/managers/crypto_manager_tests.py deleted file mode 100644 index ba11d2f..0000000 --- a/net/xeaf/rack/tests/managers/crypto_manager_tests.py +++ /dev/null @@ -1,163 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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)) diff --git a/net/xeaf/rack/tests/managers/locale_manager_tests.py b/net/xeaf/rack/tests/managers/locale_manager_tests.py deleted file mode 100644 index 3bf889d..0000000 --- a/net/xeaf/rack/tests/managers/locale_manager_tests.py +++ /dev/null @@ -1,245 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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}") diff --git a/net/xeaf/rack/tests/managers/request_manager_get_request_account_tests.py b/net/xeaf/rack/tests/managers/request_manager_get_request_account_tests.py deleted file mode 100644 index 8460d4a..0000000 --- a/net/xeaf/rack/tests/managers/request_manager_get_request_account_tests.py +++ /dev/null @@ -1,71 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/managers/request_manager_get_request_context_tests.py b/net/xeaf/rack/tests/managers/request_manager_get_request_context_tests.py deleted file mode 100644 index b5d83f3..0000000 --- a/net/xeaf/rack/tests/managers/request_manager_get_request_context_tests.py +++ /dev/null @@ -1,83 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/managers/request_manager_get_request_id_tests.py b/net/xeaf/rack/tests/managers/request_manager_get_request_id_tests.py deleted file mode 100644 index d9d0566..0000000 --- a/net/xeaf/rack/tests/managers/request_manager_get_request_id_tests.py +++ /dev/null @@ -1,66 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/managers/request_manager_get_request_method_tests.py b/net/xeaf/rack/tests/managers/request_manager_get_request_method_tests.py deleted file mode 100644 index 9f664d0..0000000 --- a/net/xeaf/rack/tests/managers/request_manager_get_request_method_tests.py +++ /dev/null @@ -1,92 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/managers/request_manager_get_request_session_tests.py b/net/xeaf/rack/tests/managers/request_manager_get_request_session_tests.py deleted file mode 100644 index 81fb81f..0000000 --- a/net/xeaf/rack/tests/managers/request_manager_get_request_session_tests.py +++ /dev/null @@ -1,75 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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)) diff --git a/net/xeaf/rack/tests/managers/sesson_manager_tests.py b/net/xeaf/rack/tests/managers/sesson_manager_tests.py deleted file mode 100644 index 127ff50..0000000 --- a/net/xeaf/rack/tests/managers/sesson_manager_tests.py +++ /dev/null @@ -1,216 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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)) diff --git a/net/xeaf/rack/tests/models/__init__.py b/net/xeaf/rack/tests/models/__init__.py deleted file mode 100644 index f7dd734..0000000 --- a/net/xeaf/rack/tests/models/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: MIT - -""" -Пакет описания тестов классов моделей данных -""" - -from .account_model_tests import AccountModelTests -from .session_model_tests import SessionModelTests diff --git a/net/xeaf/rack/tests/models/account_model_tests.py b/net/xeaf/rack/tests/models/account_model_tests.py deleted file mode 100644 index be393b3..0000000 --- a/net/xeaf/rack/tests/models/account_model_tests.py +++ /dev/null @@ -1,56 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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() - diff --git a/net/xeaf/rack/tests/models/mixins/__init__.py b/net/xeaf/rack/tests/models/mixins/__init__.py deleted file mode 100644 index afc66e2..0000000 --- a/net/xeaf/rack/tests/models/mixins/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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 diff --git a/net/xeaf/rack/tests/models/mixins/created_at_mixin_tests.py b/net/xeaf/rack/tests/models/mixins/created_at_mixin_tests.py deleted file mode 100644 index acaedf2..0000000 --- a/net/xeaf/rack/tests/models/mixins/created_at_mixin_tests.py +++ /dev/null @@ -1,129 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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") diff --git a/net/xeaf/rack/tests/models/mixins/deleted_at_mixin_tests.py b/net/xeaf/rack/tests/models/mixins/deleted_at_mixin_tests.py deleted file mode 100644 index 61a35fa..0000000 --- a/net/xeaf/rack/tests/models/mixins/deleted_at_mixin_tests.py +++ /dev/null @@ -1,251 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/models/mixins/description_mixin_tests.py b/net/xeaf/rack/tests/models/mixins/description_mixin_tests.py deleted file mode 100644 index ef03abc..0000000 --- a/net/xeaf/rack/tests/models/mixins/description_mixin_tests.py +++ /dev/null @@ -1,59 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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')) diff --git a/net/xeaf/rack/tests/models/mixins/expired_at_mixin_tests.py b/net/xeaf/rack/tests/models/mixins/expired_at_mixin_tests.py deleted file mode 100644 index fa76083..0000000 --- a/net/xeaf/rack/tests/models/mixins/expired_at_mixin_tests.py +++ /dev/null @@ -1,332 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/models/mixins/slug_mixin_tests.py b/net/xeaf/rack/tests/models/mixins/slug_mixin_tests.py deleted file mode 100644 index 1442e2d..0000000 --- a/net/xeaf/rack/tests/models/mixins/slug_mixin_tests.py +++ /dev/null @@ -1,59 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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')) diff --git a/net/xeaf/rack/tests/models/mixins/updated_at_mixin_tests.py b/net/xeaf/rack/tests/models/mixins/updated_at_mixin_tests.py deleted file mode 100644 index ab161a1..0000000 --- a/net/xeaf/rack/tests/models/mixins/updated_at_mixin_tests.py +++ /dev/null @@ -1,182 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/models/mixins/uuid_id_mixin_tests.py b/net/xeaf/rack/tests/models/mixins/uuid_id_mixin_tests.py deleted file mode 100644 index 7639c79..0000000 --- a/net/xeaf/rack/tests/models/mixins/uuid_id_mixin_tests.py +++ /dev/null @@ -1,100 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/models/responses/__init__.py b/net/xeaf/rack/tests/models/responses/__init__.py deleted file mode 100644 index e826be3..0000000 --- a/net/xeaf/rack/tests/models/responses/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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 diff --git a/net/xeaf/rack/tests/models/responses/bool_response_tests.py b/net/xeaf/rack/tests/models/responses/bool_response_tests.py deleted file mode 100644 index 81bdebf..0000000 --- a/net/xeaf/rack/tests/models/responses/bool_response_tests.py +++ /dev/null @@ -1,51 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) - diff --git a/net/xeaf/rack/tests/models/responses/data_response_tests.py b/net/xeaf/rack/tests/models/responses/data_response_tests.py deleted file mode 100644 index e028777..0000000 --- a/net/xeaf/rack/tests/models/responses/data_response_tests.py +++ /dev/null @@ -1,41 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/models/responses/empty_response_tests.py b/net/xeaf/rack/tests/models/responses/empty_response_tests.py deleted file mode 100644 index 9250f53..0000000 --- a/net/xeaf/rack/tests/models/responses/empty_response_tests.py +++ /dev/null @@ -1,39 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/models/responses/error_response_tests.py b/net/xeaf/rack/tests/models/responses/error_response_tests.py deleted file mode 100644 index 8aef431..0000000 --- a/net/xeaf/rack/tests/models/responses/error_response_tests.py +++ /dev/null @@ -1,65 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/models/responses/exception_response_tests.py b/net/xeaf/rack/tests/models/responses/exception_response_tests.py deleted file mode 100644 index 7d72a0f..0000000 --- a/net/xeaf/rack/tests/models/responses/exception_response_tests.py +++ /dev/null @@ -1,64 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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"], "Крах БД") diff --git a/net/xeaf/rack/tests/models/responses/list_response_tests.py b/net/xeaf/rack/tests/models/responses/list_response_tests.py deleted file mode 100644 index 9b09333..0000000 --- a/net/xeaf/rack/tests/models/responses/list_response_tests.py +++ /dev/null @@ -1,41 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/models/session_model_tests.py b/net/xeaf/rack/tests/models/session_model_tests.py deleted file mode 100644 index e156a2d..0000000 --- a/net/xeaf/rack/tests/models/session_model_tests.py +++ /dev/null @@ -1,64 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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() diff --git a/net/xeaf/rack/tests/utils/__init__.py b/net/xeaf/rack/tests/utils/__init__.py deleted file mode 100644 index 63e17a4..0000000 --- a/net/xeaf/rack/tests/utils/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: MIT - -""" -Пакет описания тестов для служебных классов и утилит -""" - diff --git a/net/xeaf/rack/tests/utils/auth/__init__.py b/net/xeaf/rack/tests/utils/auth/__init__.py deleted file mode 100644 index 0b1202b..0000000 --- a/net/xeaf/rack/tests/utils/auth/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: MIT - -""" -Пакет описания тестов классов авторизации -""" - -from .expiring_token_authentication_tests import ExpiringTokenAuthenticationTests - diff --git a/net/xeaf/rack/tests/utils/auth/expiring_token_authentication_tests.py b/net/xeaf/rack/tests/utils/auth/expiring_token_authentication_tests.py deleted file mode 100644 index 9dc0d32..0000000 --- a/net/xeaf/rack/tests/utils/auth/expiring_token_authentication_tests.py +++ /dev/null @@ -1,173 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/utils/date_utils_tests.py b/net/xeaf/rack/tests/utils/date_utils_tests.py deleted file mode 100644 index f2da732..0000000 --- a/net/xeaf/rack/tests/utils/date_utils_tests.py +++ /dev/null @@ -1,99 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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) diff --git a/net/xeaf/rack/tests/utils/exceptions/__init__.py b/net/xeaf/rack/tests/utils/exceptions/__init__.py deleted file mode 100644 index b3fe3ca..0000000 --- a/net/xeaf/rack/tests/utils/exceptions/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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 diff --git a/net/xeaf/rack/tests/utils/exceptions/bad_request_exception_tests.py b/net/xeaf/rack/tests/utils/exceptions/bad_request_exception_tests.py deleted file mode 100644 index 72756c1..0000000 --- a/net/xeaf/rack/tests/utils/exceptions/bad_request_exception_tests.py +++ /dev/null @@ -1,40 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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()) diff --git a/net/xeaf/rack/tests/utils/exceptions/critical_exception_tests.py b/net/xeaf/rack/tests/utils/exceptions/critical_exception_tests.py deleted file mode 100644 index bc741be..0000000 --- a/net/xeaf/rack/tests/utils/exceptions/critical_exception_tests.py +++ /dev/null @@ -1,40 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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()) diff --git a/net/xeaf/rack/tests/utils/exceptions/file_not_found_exception_tests.py b/net/xeaf/rack/tests/utils/exceptions/file_not_found_exception_tests.py deleted file mode 100644 index 7df91c1..0000000 --- a/net/xeaf/rack/tests/utils/exceptions/file_not_found_exception_tests.py +++ /dev/null @@ -1,40 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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()) diff --git a/net/xeaf/rack/tests/utils/exceptions/forbidden_exception_tests.py b/net/xeaf/rack/tests/utils/exceptions/forbidden_exception_tests.py deleted file mode 100644 index 2de1197..0000000 --- a/net/xeaf/rack/tests/utils/exceptions/forbidden_exception_tests.py +++ /dev/null @@ -1,40 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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()) diff --git a/net/xeaf/rack/tests/utils/exceptions/not_found_exception_tests.py b/net/xeaf/rack/tests/utils/exceptions/not_found_exception_tests.py deleted file mode 100644 index e90475d..0000000 --- a/net/xeaf/rack/tests/utils/exceptions/not_found_exception_tests.py +++ /dev/null @@ -1,40 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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()) diff --git a/net/xeaf/rack/tests/utils/exceptions/unauthorized_exception_tests.py b/net/xeaf/rack/tests/utils/exceptions/unauthorized_exception_tests.py deleted file mode 100644 index 11d35e3..0000000 --- a/net/xeaf/rack/tests/utils/exceptions/unauthorized_exception_tests.py +++ /dev/null @@ -1,40 +0,0 @@ -# DRF Rack -# Библиотека классов расширений для Django REST Framework -# -# Автор: Николай В. Анохин -# Все права защищены. Лицензия: 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())