Подготовка новых моделей тестов
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
Возвращает признак режима отладки или тестов
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
27
net/xeaf/rack/core/tests/core_simple_test_case.py
Normal file
27
net/xeaf/rack/core/tests/core_simple_test_case.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreUserStoryTestCase
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from .request_test_case_mixin import RequestTestCaseMixin
|
||||
|
||||
|
||||
class CoreSimpleTestCase(SimpleTestCase, RequestTestCaseMixin):
|
||||
"""
|
||||
Реализует методы для тестирования без использования БД
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Настройка перед тестированием
|
||||
"""
|
||||
|
||||
self.factory = APIRequestFactory()
|
||||
78
net/xeaf/rack/core/tests/core_user_story_test_case.py
Normal file
78
net/xeaf/rack/core/tests/core_user_story_test_case.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreUserStoryTestCase
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from net.xeaf.rack.enums import HttpMethod
|
||||
from net.xeaf.rack.management.userstory import SessionTestData
|
||||
from .request_test_case_mixin import RequestTestCaseMixin
|
||||
|
||||
|
||||
class CoreUserStoryTestCase(TestCase, RequestTestCaseMixin):
|
||||
"""
|
||||
Реализует методы для тестирования пользовательских историй
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Настройка перед тестированием
|
||||
"""
|
||||
|
||||
call_command("rack_test_database")
|
||||
self.factory = APIRequestFactory()
|
||||
|
||||
def create_i_ivanov_request(self, method: HttpMethod, path: str, headers: dict[str, str] | None = None, data: dict[str, Any] | None = None, query_params: dict[str, Any] | None = None) -> Request:
|
||||
"""
|
||||
Создает запрос от имени Ивана И. Иванова
|
||||
|
||||
:param method: HTTP метод (GET, POST, PUT, PATCH, DELETE)
|
||||
:param path: URL путь
|
||||
:param headers: Заголовки запроса
|
||||
:param data: Данные для тела запроса
|
||||
:param query_params: Параметры запроса
|
||||
|
||||
:return: Объект запроса с аутентификацией Ивана Иванова
|
||||
"""
|
||||
|
||||
return self.create_authenticated_request(
|
||||
method=method,
|
||||
path=path,
|
||||
headers=headers,
|
||||
data=data,
|
||||
query_params=query_params,
|
||||
token=SessionTestData.IVANOV_SESSION_TOKEN,
|
||||
)
|
||||
|
||||
def create_p_petrov_request(self, method: HttpMethod, path: str, headers: dict[str, str] | None = None, data: dict[str, Any] | None = None, query_params: dict[str, Any] | None = None) -> Request:
|
||||
"""
|
||||
Создает запрос от имени Петра П. Петрова
|
||||
|
||||
:param method: HTTP метод (GET, POST, PUT, PATCH, DELETE)
|
||||
:param path: URL путь
|
||||
:param headers: Заголовки запроса
|
||||
:param data: Данные для тела запроса
|
||||
:param query_params: Параметры запроса
|
||||
|
||||
:return: Объект запроса с аутентификацией Петра Петрова
|
||||
"""
|
||||
|
||||
return self.create_authenticated_request(
|
||||
method=method,
|
||||
path=path,
|
||||
headers=headers,
|
||||
data=data,
|
||||
query_params=query_params,
|
||||
token=SessionTestData.PETROV_SESSION_TOKEN
|
||||
)
|
||||
164
net/xeaf/rack/core/tests/request_test_case_mixin.py
Normal file
164
net/xeaf/rack/core/tests/request_test_case_mixin.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса RequestTestCaseMixin
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from net.xeaf.rack.enums import HttpMethod
|
||||
from net.xeaf.rack.managers import SessionManager
|
||||
from net.xeaf.rack.models import SessionModel
|
||||
|
||||
|
||||
class RequestTestCaseMixin:
|
||||
"""
|
||||
Реализует методы для тестирования запросов
|
||||
"""
|
||||
|
||||
factory: APIRequestFactory
|
||||
""" Фабрика запросов """
|
||||
|
||||
def create_request(self,
|
||||
method: HttpMethod,
|
||||
path: str,
|
||||
headers: dict[str, str] | None = None,
|
||||
data: dict[str, Any] | None = None,
|
||||
query_params: dict[str, str] | None = None,
|
||||
auth: SessionModel | None = None) -> Request:
|
||||
"""
|
||||
Создает объект запроса для тестирования
|
||||
|
||||
:param method: HTTP метод (GET, POST, PUT, PATCH, DELETE)
|
||||
:param path: URL путь
|
||||
:param headers: Заголовки запроса
|
||||
:param data: Данные для тела запроса
|
||||
:param query_params: Параметры запроса
|
||||
:param auth: Объект аутентификации (сессия)
|
||||
|
||||
:return: DRF Request объект
|
||||
"""
|
||||
|
||||
# Уточняем значения параметров
|
||||
headers = self.__prepare_headers(headers or {})
|
||||
query_params = query_params or {}
|
||||
data = data or {}
|
||||
|
||||
# Создаем внутренний объект запроса
|
||||
match method:
|
||||
case HttpMethod.GET:
|
||||
wsgi_request = self.factory.get(path, data=query_params, **headers)
|
||||
case HttpMethod.POST:
|
||||
wsgi_request = self.factory.post(path, data=data, format='json', **headers)
|
||||
case HttpMethod.PUT:
|
||||
wsgi_request = self.factory.put(path, data=data, format='json', **headers)
|
||||
case HttpMethod.PATCH:
|
||||
wsgi_request = self.factory.patch(path, data=data, format='json', **headers)
|
||||
case HttpMethod.DELETE:
|
||||
wsgi_request = self.factory.delete(path, **headers)
|
||||
case _:
|
||||
wsgi_request = self.factory.generic(
|
||||
method,
|
||||
path,
|
||||
content_type='application/json',
|
||||
data=json.dumps(data) if data else ''
|
||||
)
|
||||
|
||||
# Преобразуем к объекту запроса DRF
|
||||
drf_request = Request(wsgi_request, parsers=[JSONParser()])
|
||||
|
||||
if method != HttpMethod.GET:
|
||||
self.__assign_query_params(drf_request, query_params)
|
||||
self.__assign_auth(drf_request, auth)
|
||||
|
||||
return drf_request
|
||||
|
||||
def create_authenticated_request(self,
|
||||
method: HttpMethod,
|
||||
path: str,
|
||||
headers: dict[str, str] | None = None,
|
||||
data: dict[str, Any] | None = None,
|
||||
query_params: dict[str, str] | None = None,
|
||||
token: str | None = None) -> Request:
|
||||
"""
|
||||
Создает объект авторизованного запроса для тестирования
|
||||
|
||||
:param method: HTTP метод (GET, POST, PUT, PATCH, DELETE)
|
||||
:param path: URL путь
|
||||
:param headers: Заголовки запроса
|
||||
:param data: Данные для тела запроса
|
||||
:param query_params: Параметры запроса
|
||||
:param token: Токен сессии авторизации
|
||||
|
||||
:return: DRF Request объект
|
||||
"""
|
||||
|
||||
# Получаем сессию
|
||||
if token is None:
|
||||
session = None
|
||||
else:
|
||||
session = SessionManager.find_by_token(token)
|
||||
|
||||
# Возвращаем объект запроса
|
||||
return self.create_request(method=method, path=path, headers=headers, data=data, query_params=query_params, auth=session)
|
||||
|
||||
@classmethod
|
||||
def __prepare_headers(cls, headers: dict[str, str]) -> dict[str, str]:
|
||||
"""
|
||||
Подготавливаем объект заголовков для передачи в фабрику
|
||||
|
||||
:param headers: Заголовки запроса
|
||||
|
||||
:return: Объект заголовков для фабрики
|
||||
"""
|
||||
|
||||
wsgi_headers = {}
|
||||
for key, value in headers.items():
|
||||
if key.startswith('HTTP_'):
|
||||
wsgi_headers[key] = value
|
||||
else:
|
||||
norm_key = key.upper().replace('-', '_')
|
||||
if norm_key in ('CONTENT_TYPE', 'CONTENT_LENGTH'):
|
||||
wsgi_headers[norm_key.lower()] = value
|
||||
else:
|
||||
wsgi_headers[f'HTTP_{norm_key}'] = value
|
||||
|
||||
return wsgi_headers
|
||||
|
||||
@classmethod
|
||||
def __assign_query_params(cls, request: Request, query_params: dict[str, Any]) -> None:
|
||||
"""
|
||||
Присваивает параметры запроса в DRF Request объект
|
||||
|
||||
:param request: DRF Request объект
|
||||
:param query_params: Параметры запроса
|
||||
"""
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
request.query_params._mutable = True
|
||||
for key, value in query_params.items():
|
||||
request.query_params[key] = value
|
||||
request.query_params._mutable = False
|
||||
|
||||
@classmethod
|
||||
def __assign_auth(cls, request: Request, auth: SessionModel | None) -> None:
|
||||
"""
|
||||
Присваивает аутентификацию в DRF Request объект
|
||||
|
||||
:param request: Объект запроса
|
||||
:param auth: Объект сессии пользователя
|
||||
"""
|
||||
|
||||
if auth:
|
||||
request.auth = auth
|
||||
if hasattr(auth, 'account'):
|
||||
request.user = auth.account
|
||||
@@ -7,6 +7,7 @@
|
||||
"""
|
||||
Описание класса CoreViewSet
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Type
|
||||
from uuid import UUID
|
||||
|
||||
@@ -30,7 +30,7 @@ class Command(CoreCommand):
|
||||
:params options: Дополнительные параметры
|
||||
"""
|
||||
|
||||
if self.is_debug_or_test():
|
||||
if self._is_debug_or_test():
|
||||
|
||||
# Готовимся
|
||||
self._clear_database()
|
||||
|
||||
@@ -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."))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов для базовых классов
|
||||
"""
|
||||
|
||||
from .core_enum_tests import CoreEnumTests
|
||||
from .core_exception_tests import CoreExceptionTests
|
||||
from .core_response_tests import CoreResponseTests
|
||||
|
||||
from .views import *
|
||||
@@ -1,234 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreEnumTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from net.xeaf.rack.core import CoreEnum
|
||||
|
||||
|
||||
class TestColorEnum(CoreEnum):
|
||||
"""
|
||||
Тестовое перечисление для проверки
|
||||
"""
|
||||
|
||||
RED = "red"
|
||||
GREEN = "green"
|
||||
BLUE = "blue"
|
||||
|
||||
|
||||
class TestStatusEnum(CoreEnum):
|
||||
"""
|
||||
Еще одно тестовое перечисление
|
||||
"""
|
||||
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
PENDING = "pending"
|
||||
|
||||
|
||||
class TestEmptyEnum(CoreEnum):
|
||||
"""
|
||||
Пустое перечисление для граничных случаев
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreEnumTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса CoreEnum
|
||||
"""
|
||||
|
||||
def test_choices_returns_list_of_tuples(self):
|
||||
"""
|
||||
Проверяет, что choices() возвращает список кортежей (value, name)
|
||||
"""
|
||||
|
||||
expected = [
|
||||
("red", "RED"),
|
||||
("green", "GREEN"),
|
||||
("blue", "BLUE"),
|
||||
]
|
||||
self.assertEqual(TestColorEnum.choices(), expected)
|
||||
|
||||
def test_choices_contains_all_members(self):
|
||||
"""
|
||||
Проверяет, что choices() содержит все элементы перечисления
|
||||
"""
|
||||
|
||||
choices = TestColorEnum.choices()
|
||||
self.assertEqual(len(choices), 3)
|
||||
self.assertIn(("red", "RED"), choices)
|
||||
self.assertIn(("green", "GREEN"), choices)
|
||||
self.assertIn(("blue", "BLUE"), choices)
|
||||
|
||||
def test_choices_order_matches_definition(self):
|
||||
"""
|
||||
Проверяет, что порядок элементов сохраняется
|
||||
"""
|
||||
|
||||
choices = TestColorEnum.choices()
|
||||
self.assertEqual(choices[0][0], "red")
|
||||
self.assertEqual(choices[1][0], "green")
|
||||
self.assertEqual(choices[2][0], "blue")
|
||||
|
||||
def test_keys_returns_list_of_names(self):
|
||||
"""
|
||||
Проверяет, что keys() возвращает список имен
|
||||
"""
|
||||
|
||||
expected = ["RED", "GREEN", "BLUE"]
|
||||
self.assertEqual(TestColorEnum.keys(), expected)
|
||||
|
||||
def test_keys_contains_all_names(self):
|
||||
"""
|
||||
Проверяет, что keys() содержит все имена элементов
|
||||
"""
|
||||
|
||||
keys = TestColorEnum.keys()
|
||||
self.assertEqual(len(keys), 3)
|
||||
self.assertIn("RED", keys)
|
||||
self.assertIn("GREEN", keys)
|
||||
self.assertIn("BLUE", keys)
|
||||
|
||||
def test_values_returns_list_of_values(self):
|
||||
"""
|
||||
Проверяет, что values() возвращает список значений
|
||||
"""
|
||||
|
||||
expected = ["red", "green", "blue"]
|
||||
self.assertEqual(TestColorEnum.values(), expected)
|
||||
|
||||
def test_values_contains_all_values(self):
|
||||
"""
|
||||
Проверяет, что values() содержит все значения элементов
|
||||
"""
|
||||
|
||||
values = TestColorEnum.values()
|
||||
self.assertEqual(len(values), 3)
|
||||
self.assertIn("red", values)
|
||||
self.assertIn("green", values)
|
||||
self.assertIn("blue", values)
|
||||
|
||||
def test_choices_with_different_enum(self):
|
||||
"""
|
||||
Проверяет работу choices() с другим перечислением
|
||||
"""
|
||||
|
||||
expected = [
|
||||
("active", "ACTIVE"),
|
||||
("inactive", "INACTIVE"),
|
||||
("pending", "PENDING"),
|
||||
]
|
||||
self.assertEqual(TestStatusEnum.choices(), expected)
|
||||
|
||||
def test_keys_with_different_enum(self):
|
||||
"""
|
||||
Проверяет работу keys() с другим перечислением
|
||||
"""
|
||||
|
||||
expected = ["ACTIVE", "INACTIVE", "PENDING"]
|
||||
self.assertEqual(TestStatusEnum.keys(), expected)
|
||||
|
||||
def test_values_with_different_enum(self):
|
||||
"""
|
||||
Проверяет работу values() с другим перечислением
|
||||
"""
|
||||
|
||||
expected = ["active", "inactive", "pending"]
|
||||
self.assertEqual(TestStatusEnum.values(), expected)
|
||||
|
||||
def test_empty_enum_choices(self):
|
||||
"""
|
||||
Проверяет поведение choices() для пустого перечисления
|
||||
"""
|
||||
|
||||
self.assertEqual(TestEmptyEnum.choices(), [])
|
||||
|
||||
def test_empty_enum_keys(self):
|
||||
"""
|
||||
Проверяет поведение keys() для пустого перечисления
|
||||
"""
|
||||
|
||||
self.assertEqual(TestEmptyEnum.keys(), [])
|
||||
|
||||
def test_empty_enum_values(self):
|
||||
"""
|
||||
Проверяет поведение values() для пустого перечисления
|
||||
"""
|
||||
|
||||
self.assertEqual(TestEmptyEnum.values(), [])
|
||||
|
||||
def test_choices_returns_different_type(self):
|
||||
"""
|
||||
Проверяет, что choices() возвращает list, а не tuple
|
||||
"""
|
||||
|
||||
self.assertIsInstance(TestColorEnum.choices(), list)
|
||||
self.assertIsInstance(TestColorEnum.choices()[0], tuple)
|
||||
|
||||
def test_keys_returns_list(self):
|
||||
"""
|
||||
Проверяет, что keys() возвращает list
|
||||
"""
|
||||
|
||||
self.assertIsInstance(TestColorEnum.keys(), list)
|
||||
|
||||
def test_values_returns_list(self):
|
||||
"""
|
||||
Проверяет, что values() возвращает list
|
||||
"""
|
||||
|
||||
self.assertIsInstance(TestColorEnum.values(), list)
|
||||
|
||||
def test_choices_values_match(self):
|
||||
"""
|
||||
Проверяет согласованность choices() и values()
|
||||
"""
|
||||
|
||||
choices_values = [choice[0] for choice in TestColorEnum.choices()]
|
||||
self.assertEqual(choices_values, TestColorEnum.values())
|
||||
|
||||
def test_choices_keys_match(self):
|
||||
"""
|
||||
Проверяет согласованность choices() и keys()
|
||||
"""
|
||||
|
||||
choices_keys = [choice[1] for choice in TestColorEnum.choices()]
|
||||
self.assertEqual(choices_keys, TestColorEnum.keys())
|
||||
|
||||
def test_keys_and_values_length_match(self):
|
||||
"""
|
||||
Проверяет, что keys() и values() имеют одинаковую длину
|
||||
"""
|
||||
|
||||
self.assertEqual(len(TestColorEnum.keys()), len(TestColorEnum.values()))
|
||||
|
||||
def test_choices_length_matches_members(self):
|
||||
"""
|
||||
Проверяет, что количество choices совпадает с количеством элементов
|
||||
"""
|
||||
|
||||
self.assertEqual(len(TestColorEnum.choices()), len(TestColorEnum.__members__))
|
||||
|
||||
def test_all_elements_are_strings(self):
|
||||
"""
|
||||
Проверяет, что все значения являются строками (StrEnum)
|
||||
"""
|
||||
|
||||
for value in TestColorEnum.values():
|
||||
self.assertIsInstance(value, str)
|
||||
|
||||
for key in TestColorEnum.keys():
|
||||
self.assertIsInstance(key, str)
|
||||
|
||||
for choice in TestColorEnum.choices():
|
||||
self.assertIsInstance(choice[0], str)
|
||||
self.assertIsInstance(choice[1], str)
|
||||
@@ -1,141 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.core import CoreException
|
||||
|
||||
|
||||
class CoreExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса CoreException
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Заполняем параметры для теста
|
||||
"""
|
||||
|
||||
self.default_status = status.HTTP_400_BAD_REQUEST
|
||||
self.default_detail = "Тестовая ошибка"
|
||||
self.exc_instance = CoreException(status_code=self.default_status)
|
||||
|
||||
def test_init_with_basic_params(self):
|
||||
"""
|
||||
Проверяет базовую инициализацию без метаданных
|
||||
"""
|
||||
|
||||
exc = CoreException(status_code=self.default_status, detail=self.default_detail)
|
||||
|
||||
self.assertEqual(exc.status_code, self.default_status)
|
||||
self.assertEqual(exc.detail, self.default_detail)
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_init_with_meta_only(self):
|
||||
"""
|
||||
Проверяет инициализацию, когда передан только словарь meta
|
||||
"""
|
||||
|
||||
meta_data = {"action": "REDIRECT", "url": "/login/"}
|
||||
|
||||
exc = CoreException(status_code=self.default_status, meta=meta_data)
|
||||
|
||||
self.assertEqual(exc.status_code, self.default_status)
|
||||
self.assertEqual(exc.meta, meta_data)
|
||||
self.assertIsInstance(exc.meta, dict)
|
||||
self.assertEqual(exc.meta["action"], "REDIRECT")
|
||||
|
||||
def test_init_with_kwargs_only(self):
|
||||
"""
|
||||
Проверяет инициализацию, когда meta=None, но переданы именованные аргументы (kwargs)
|
||||
"""
|
||||
|
||||
exc = CoreException(status_code=self.default_status, foo="bar", code=123)
|
||||
|
||||
self.assertEqual(exc.status_code, self.default_status)
|
||||
self.assertIsNotNone(exc.meta)
|
||||
self.assertIsInstance(exc.meta, dict)
|
||||
self.assertEqual(exc.meta["foo"], "bar")
|
||||
self.assertEqual(exc.meta["code"], 123)
|
||||
|
||||
def test_process_meta_with_none(self):
|
||||
"""
|
||||
Если meta=None и kwargs пусты, self.meta должен остаться None
|
||||
"""
|
||||
|
||||
self.exc_instance.meta = None # Сбрасываем состояние перед проверкой
|
||||
|
||||
self.exc_instance._process_meta(meta=None)
|
||||
|
||||
self.assertIsNone(self.exc_instance.meta)
|
||||
|
||||
def test_process_meta_with_dict(self):
|
||||
"""
|
||||
Если передан только словарь meta, он должен записаться в self.meta
|
||||
"""
|
||||
|
||||
self.exc_instance.meta = None
|
||||
test_meta = {"source": "auth_service"}
|
||||
|
||||
self.exc_instance._process_meta(meta=test_meta)
|
||||
|
||||
self.assertEqual(self.exc_instance.meta, test_meta)
|
||||
|
||||
def test_process_meta_with_kwargs_only(self):
|
||||
"""
|
||||
Если meta=None, но есть kwargs, должен создаться словарь с этими kwargs
|
||||
"""
|
||||
|
||||
self.exc_instance.meta = None
|
||||
|
||||
self.exc_instance._process_meta(meta=None, request_id="req_123", user_id=99)
|
||||
|
||||
self.assertIsNotNone(self.exc_instance.meta)
|
||||
self.assertIsInstance(self.exc_instance.meta, dict)
|
||||
self.assertEqual(self.exc_instance.meta["request_id"], "req_123")
|
||||
self.assertEqual(self.exc_instance.meta["user_id"], 99)
|
||||
|
||||
def test_process_meta_combined(self):
|
||||
"""
|
||||
Если переданы и meta, и kwargs, они должны успешно объединиться (update)
|
||||
"""
|
||||
|
||||
self.exc_instance.meta = None
|
||||
initial_meta = {"version": "v1"}
|
||||
|
||||
self.exc_instance._process_meta(meta=initial_meta, debug=True, env="prod")
|
||||
|
||||
self.assertIsNotNone(self.exc_instance.meta)
|
||||
self.assertIsInstance(self.exc_instance.meta, dict)
|
||||
self.assertEqual(self.exc_instance.meta["version"], "v1")
|
||||
self.assertEqual(self.exc_instance.meta["debug"], True)
|
||||
self.assertEqual(self.exc_instance.meta["env"], "prod")
|
||||
|
||||
def test_process_meta_with_empty_dict_and_kwargs(self):
|
||||
"""
|
||||
Критический случай: meta пришел пустым словарем, но kwargs заполнены
|
||||
"""
|
||||
|
||||
self.exc_instance.meta = None
|
||||
|
||||
self.exc_instance._process_meta(meta={}, trace_id="xyz")
|
||||
|
||||
self.assertIsNotNone(self.exc_instance.meta)
|
||||
self.assertIsInstance(self.exc_instance.meta, dict)
|
||||
self.assertEqual(self.exc_instance.meta["trace_id"], "xyz")
|
||||
|
||||
def test_is_pure_status_defaults_to_false(self):
|
||||
"""
|
||||
Проверяет, что по умолчанию исключение возвращает False для чистого статуса
|
||||
"""
|
||||
|
||||
exc = CoreException(status_code=400)
|
||||
self.assertFalse(exc.is_pure_status())
|
||||
@@ -1,98 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreResponseTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.core import CoreResponse
|
||||
|
||||
|
||||
class CoreResponseTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса CoreResponse
|
||||
"""
|
||||
|
||||
def test_init_success_format(self):
|
||||
"""
|
||||
Проверяет формат успешного ответа (код < 400): данные улетают в 'data'
|
||||
"""
|
||||
|
||||
test_data = {"user_id": 42}
|
||||
internal_code = status.HTTP_200_OK
|
||||
external_code = status.HTTP_200_OK
|
||||
|
||||
response = CoreResponse(
|
||||
internal_status=internal_code,
|
||||
external_status=external_code,
|
||||
data=test_data
|
||||
)
|
||||
|
||||
# Проверяем внешний HTTP-статус в заголовках
|
||||
self.assertEqual(response.status_code, external_code)
|
||||
|
||||
# Проверяем структуру тела JSON-ответа (внутреннее свойство DRF .data)
|
||||
self.assertEqual(response.data["status"], internal_code)
|
||||
self.assertIn("timestamp", response.data)
|
||||
# Так как это успех, данные обязаны быть в ключе "data"
|
||||
self.assertEqual(response.data["data"], test_data)
|
||||
self.assertNotIn("detail", response.data)
|
||||
self.assertNotIn("meta", response.data)
|
||||
|
||||
def test_init_error_format(self):
|
||||
"""
|
||||
Проверяет формат ответа с ошибкой (код >= 400): данные улетают в 'detail'
|
||||
"""
|
||||
|
||||
error_msg = "Некорректные параметры запроса"
|
||||
internal_code = status.HTTP_400_BAD_REQUEST
|
||||
external_code = status.HTTP_200_OK # Маскируем под 200 OK для фронтенда
|
||||
|
||||
response = CoreResponse(
|
||||
internal_status=internal_code,
|
||||
external_status=external_code,
|
||||
data=error_msg
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, external_code)
|
||||
self.assertEqual(response.data["status"], internal_code)
|
||||
# Так как это ошибка, данные обязаны быть в ключе "detail"
|
||||
self.assertEqual(response.data["detail"], error_msg)
|
||||
self.assertNotIn("data", response.data)
|
||||
|
||||
def test_init_with_meta(self):
|
||||
"""
|
||||
Проверяет корректную интеграцию словаря meta в тело ответа
|
||||
"""
|
||||
|
||||
meta_data = {"page": 1, "has_next": False}
|
||||
|
||||
response = CoreResponse(
|
||||
internal_status=status.HTTP_200_OK,
|
||||
external_status=status.HTTP_200_OK,
|
||||
data={"items": []},
|
||||
meta=meta_data
|
||||
)
|
||||
|
||||
self.assertIn("meta", response.data)
|
||||
self.assertEqual(response.data["meta"], meta_data)
|
||||
|
||||
def test_init_with_empty_data(self):
|
||||
"""
|
||||
Проверяет, что если data=None или пустой, ключи 'data' и 'detail' не создаются
|
||||
"""
|
||||
|
||||
response = CoreResponse(
|
||||
internal_status=status.HTTP_204_NO_CONTENT,
|
||||
external_status=status.HTTP_204_NO_CONTENT,
|
||||
data=None
|
||||
)
|
||||
|
||||
self.assertNotIn("data", response.data)
|
||||
self.assertNotIn("detail", response.data)
|
||||
@@ -1,17 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов для базовых классов представлений
|
||||
"""
|
||||
|
||||
from .core_view_set_get_serializer_tests import CoreViewSetGetSerializerTests
|
||||
from .core_view_set_is_partial_update import CoreViewSetIsPartialUpdateTests
|
||||
from .core_view_set_process_data_params import CoreViewSetProcessDataParamsTests
|
||||
from .core_view_set_process_pk_param_tests import CoreViewSetProcessPkParamTests
|
||||
from .core_view_set_process_query_params_tests import CoreViewSetProcessQueryParamsTests
|
||||
from .core_view_set_path_generation_tests import CoreViewSetPathGenerationTests
|
||||
from .core_view_set_request_context_tests import CoreViewSetRequestContextTests
|
||||
@@ -1,71 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreViewSetGetSerializerTest
|
||||
"""
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import serializers
|
||||
|
||||
from net.xeaf.rack.core.views import CoreViewSet
|
||||
|
||||
|
||||
class MockSerializer(serializers.Serializer):
|
||||
"""
|
||||
Тестовый класс сериализатора
|
||||
"""
|
||||
|
||||
field = serializers.CharField()
|
||||
""" Поле сериализатора """
|
||||
|
||||
|
||||
class MockViewSet(CoreViewSet):
|
||||
"""
|
||||
Тестовый класс представления
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreViewSetGetSerializerTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты метода CoreViewSet._get_serializer
|
||||
"""
|
||||
|
||||
def test_get_serializer_with_serializer_class(self):
|
||||
"""
|
||||
Передан явный класс сериализатора
|
||||
"""
|
||||
|
||||
view_set = MockViewSet()
|
||||
custom_serializer = MockSerializer
|
||||
result = view_set._get_serializer(custom_serializer, None)
|
||||
|
||||
self.assertEqual(result, custom_serializer)
|
||||
|
||||
def test_get_serializer_with_default(self):
|
||||
"""
|
||||
Передан класс по умолчанию
|
||||
"""
|
||||
|
||||
view_set = MockViewSet()
|
||||
default_serializer = MockSerializer
|
||||
result = view_set._get_serializer(None, default_serializer)
|
||||
|
||||
self.assertEqual(result, default_serializer)
|
||||
|
||||
def test_get_serializer_no_serializer_raises_error(self):
|
||||
"""
|
||||
Не передан сериализатор - ошибка
|
||||
"""
|
||||
|
||||
view_set = MockViewSet()
|
||||
with self.assertRaises(ImproperlyConfigured) as context:
|
||||
view_set._get_serializer(None, None)
|
||||
|
||||
self.assertIn("must define 'serializer' attribute", str(context.exception))
|
||||
@@ -1,107 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreViewSetIsPartialUpdateTest
|
||||
"""
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.core.views import CoreViewSet
|
||||
|
||||
|
||||
class MockViewSetForIsPartialUpdate(CoreViewSet):
|
||||
"""
|
||||
ViewSet для тестов _is_partial_update
|
||||
"""
|
||||
|
||||
permission_classes = ()
|
||||
|
||||
|
||||
class CoreViewSetIsPartialUpdateTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для CoreViewSet._is_partial_update
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Подготовка к тестированию
|
||||
"""
|
||||
|
||||
super().setUp()
|
||||
self.view_set = MockViewSetForIsPartialUpdate()
|
||||
|
||||
def test_is_partial_update_for_patch(self):
|
||||
"""
|
||||
PATCH запрос - возвращает True
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'PATCH',
|
||||
'/test',
|
||||
data={'name': 'Updated'}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._is_partial_update()
|
||||
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_is_partial_update_for_put(self):
|
||||
"""
|
||||
PUT запрос - возвращает False
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'PUT',
|
||||
'/test',
|
||||
data={'name': 'Updated', 'age': 30}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._is_partial_update()
|
||||
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_is_partial_update_for_post(self):
|
||||
"""
|
||||
POST запрос - возвращает False
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={'name': 'New', 'age': 25}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._is_partial_update()
|
||||
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_is_partial_update_for_delete(self):
|
||||
"""
|
||||
DELETE запрос - возвращает False
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'DELETE',
|
||||
'/test/123/'
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._is_partial_update()
|
||||
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_is_partial_update_for_get(self):
|
||||
"""
|
||||
GET запрос - возвращает False
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'GET',
|
||||
'/test'
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._is_partial_update()
|
||||
|
||||
self.assertFalse(result)
|
||||
@@ -1,163 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreViewSetPathGenerationTests
|
||||
"""
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import SimpleTestCase
|
||||
from net.xeaf.rack.core.views import CoreViewSet
|
||||
from net.xeaf.rack.enums import ViewSetAction
|
||||
|
||||
|
||||
class CoreViewSetPathGenerationTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты генерации URL паттернов
|
||||
"""
|
||||
|
||||
class FullCRUDViewSet(CoreViewSet):
|
||||
"""
|
||||
ViewSet с полным CRUD
|
||||
"""
|
||||
|
||||
item_list_pattern = "test/"
|
||||
item_detail_pattern = "test/<uuid:pk>/"
|
||||
|
||||
@classmethod
|
||||
def item_list(cls, request):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def item_create(cls, request):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def item_retrieve(cls, request, pk):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def item_update(cls, request, pk):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def item_partial_update(cls, request, pk):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def item_destroy(cls, request, pk):
|
||||
pass
|
||||
|
||||
class ListOnlyViewSet(CoreViewSet):
|
||||
"""
|
||||
ViewSet только для списка
|
||||
"""
|
||||
|
||||
item_list_pattern = "test/"
|
||||
|
||||
@classmethod
|
||||
def item_list(cls, request):
|
||||
pass
|
||||
|
||||
class DetailOnlyViewSet(CoreViewSet):
|
||||
"""
|
||||
ViewSet только для деталей
|
||||
"""
|
||||
|
||||
item_detail_pattern = "test/<uuid:pk>/"
|
||||
|
||||
@classmethod
|
||||
def item_retrieve(cls, request, pk):
|
||||
pass
|
||||
|
||||
def test_get_paths_full_crud(self):
|
||||
"""
|
||||
Полный CRUD - все пути сгенерированы
|
||||
"""
|
||||
|
||||
paths = self.FullCRUDViewSet.get_paths()
|
||||
|
||||
self.assertEqual(len(paths), 2)
|
||||
|
||||
list_path = paths[0]
|
||||
detail_path = paths[1]
|
||||
|
||||
self.assertEqual(list_path.pattern._route, "test/")
|
||||
self.assertEqual(detail_path.pattern._route, "test/<uuid:pk>/")
|
||||
|
||||
def test_get_paths_list_only(self):
|
||||
"""
|
||||
Только список - только list path
|
||||
"""
|
||||
|
||||
paths = self.ListOnlyViewSet.get_paths()
|
||||
|
||||
self.assertEqual(len(paths), 1)
|
||||
|
||||
list_path = paths[0]
|
||||
self.assertEqual(list_path.pattern._route, "test/")
|
||||
|
||||
def test_get_paths_detail_only(self):
|
||||
"""
|
||||
Только детали - только detail path
|
||||
"""
|
||||
|
||||
paths = self.DetailOnlyViewSet.get_paths()
|
||||
|
||||
self.assertEqual(len(paths), 1)
|
||||
|
||||
detail_path = paths[0]
|
||||
self.assertEqual(detail_path.pattern._route, "test/<uuid:pk>/")
|
||||
|
||||
def test_get_paths_no_patterns(self):
|
||||
"""
|
||||
Нет паттернов - пустой список
|
||||
"""
|
||||
|
||||
class EmptyViewSet(CoreViewSet):
|
||||
pass
|
||||
|
||||
paths = EmptyViewSet.get_paths()
|
||||
|
||||
self.assertEqual(paths, [])
|
||||
|
||||
def test_get_paths_list_without_methods(self):
|
||||
"""
|
||||
Есть паттерн, но нет методов - пустой список
|
||||
"""
|
||||
|
||||
class NoMethodsViewSet(CoreViewSet):
|
||||
item_list_pattern = "test/"
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
NoMethodsViewSet.get_paths()
|
||||
|
||||
def test_path_method_valid_action(self):
|
||||
"""
|
||||
Получение метода для валидного действия
|
||||
"""
|
||||
|
||||
result = CoreViewSet._path_method(ViewSetAction.LIST)
|
||||
self.assertEqual(result, "item_list")
|
||||
|
||||
def test_path_method_exists_true(self):
|
||||
"""
|
||||
Проверка существования метода - True
|
||||
"""
|
||||
|
||||
result = self.FullCRUDViewSet._path_method_exists(ViewSetAction.LIST)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_path_method_exists_false(self):
|
||||
"""
|
||||
Проверка существования метода - False
|
||||
"""
|
||||
|
||||
class EmptyViewSet(CoreViewSet):
|
||||
pass
|
||||
|
||||
result = EmptyViewSet._path_method_exists(ViewSetAction.LIST)
|
||||
self.assertFalse(result)
|
||||
@@ -1,320 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreViewSetProcessDataParamsTestsTest
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.core.views import CoreViewSet
|
||||
|
||||
|
||||
class DataParamsSerializer(serializers.Serializer):
|
||||
"""
|
||||
Сериализатор для тестов data params
|
||||
"""
|
||||
|
||||
name = serializers.CharField(max_length=100)
|
||||
age = serializers.IntegerField(min_value=0, max_value=150)
|
||||
email = serializers.EmailField(required=False)
|
||||
|
||||
|
||||
class MockViewSetForDataParams(CoreViewSet):
|
||||
"""
|
||||
ViewSet для тестов data params
|
||||
"""
|
||||
|
||||
permission_classes = ()
|
||||
|
||||
def _get_request_method(self):
|
||||
"""
|
||||
Метод по умолчанию
|
||||
"""
|
||||
|
||||
return 'POST' # По умолчанию POST
|
||||
|
||||
def _get_request_session(self):
|
||||
"""
|
||||
Нет сессии
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class CoreViewSetProcessDataParamsTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для CoreViewSet._process_data_params
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Подготовка к тестам
|
||||
"""
|
||||
|
||||
super().setUp()
|
||||
self.view_set = MockViewSetForDataParams()
|
||||
|
||||
def test_process_data_params_no_serializer(self):
|
||||
"""
|
||||
Без сериализатора - возвращаются исходные данные
|
||||
"""
|
||||
|
||||
data = {'name': 'Test', 'age': 25}
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data=data
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_data_params(None)
|
||||
|
||||
self.assertEqual(result, data)
|
||||
|
||||
def test_process_data_params_with_serializer(self):
|
||||
"""
|
||||
С сериализатором - валидирует и возвращает данные
|
||||
Проверяет, что данные проходят валидацию через сериализатор
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={'name': 'Ivan Ivanov', 'age': 30}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
self.assertEqual(result['name'], 'Ivan Ivanov')
|
||||
self.assertEqual(result['age'], 30)
|
||||
self.assertIsInstance(result['age'], int)
|
||||
|
||||
def test_process_data_params_with_serializer_optional_fields(self):
|
||||
"""
|
||||
С сериализатором - опциональные поля
|
||||
Проверяет, что опциональные поля могут отсутствовать
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={'name': 'Ivan Ivanov', 'age': 30}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
self.assertEqual(result['name'], 'Ivan Ivanov')
|
||||
self.assertEqual(result['age'], 30)
|
||||
self.assertNotIn('email', result)
|
||||
|
||||
def test_process_data_params_with_serializer_optional_field_present(self):
|
||||
"""
|
||||
Опциональное поле передано
|
||||
Проверяет, что опциональное поле обрабатывается корректно
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={
|
||||
'name': 'Ivan Ivanov',
|
||||
'age': 30,
|
||||
'email': 'ivan@example.com'
|
||||
}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
self.assertEqual(result['name'], 'Ivan Ivanov')
|
||||
self.assertEqual(result['age'], 30)
|
||||
self.assertEqual(result['email'], 'ivan@example.com')
|
||||
|
||||
def test_process_data_params_with_serializer_invalid_raises_error(self):
|
||||
"""
|
||||
Невалидные данные - ошибка валидации
|
||||
Проверяет, что сериализатор выбрасывает ValidationError
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={'name': '', 'age': 200} # name пустой, age > 150
|
||||
)
|
||||
self.view_set.request = request
|
||||
|
||||
with self.assertRaises(serializers.ValidationError):
|
||||
self.view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
def test_process_data_params_with_serializer_missing_required(self):
|
||||
"""
|
||||
Отсутствие обязательного поля
|
||||
Проверяет, что при отсутствии required поля выбрасывается ошибка
|
||||
"""
|
||||
# Arrange
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={'age': 30} # name отсутствует
|
||||
)
|
||||
self.view_set.request = request
|
||||
|
||||
with self.assertRaises(serializers.ValidationError):
|
||||
self.view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
def test_process_data_params_with_serializer_invalid_email(self):
|
||||
"""
|
||||
Невалидный email
|
||||
Проверяет, что невалидный email вызывает ошибку
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={
|
||||
'name': 'Ivan Ivanov',
|
||||
'age': 30,
|
||||
'email': 'invalid-email' # невалидный email
|
||||
}
|
||||
)
|
||||
self.view_set.request = request
|
||||
|
||||
with self.assertRaises(serializers.ValidationError):
|
||||
self.view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
def test_process_data_params_with_partial_update(self):
|
||||
"""
|
||||
Частичное обновление (PATCH) - partial=True
|
||||
Проверяет, что при PATCH запросе partial=True
|
||||
"""
|
||||
|
||||
class PartialViewSet(MockViewSetForDataParams):
|
||||
def _get_request_method(self):
|
||||
return 'PATCH'
|
||||
|
||||
view_set = PartialViewSet()
|
||||
request = self.create_i_ivanov_request(
|
||||
'PATCH',
|
||||
'/test',
|
||||
data={'name': 'Updated Name'}
|
||||
)
|
||||
view_set.request = request
|
||||
result = view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
self.assertEqual(result['name'], 'Updated Name')
|
||||
self.assertNotIn('age', result)
|
||||
self.assertNotIn('email', result)
|
||||
|
||||
def test_process_data_params_required_field_missing_partial(self):
|
||||
"""
|
||||
Отсутствие обязательного поля при partial=True
|
||||
Проверяет, что при partial=True можно не указывать обязательные поля
|
||||
"""
|
||||
|
||||
class PartialViewSet(MockViewSetForDataParams):
|
||||
def _get_request_method(self):
|
||||
return 'PATCH'
|
||||
|
||||
view_set = PartialViewSet()
|
||||
request = self.create_i_ivanov_request(
|
||||
'PATCH',
|
||||
'/test',
|
||||
data={'email': 'test@example.com'} # только опциональное поле
|
||||
)
|
||||
view_set.request = request
|
||||
result = view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
self.assertEqual(result['email'], 'test@example.com')
|
||||
self.assertNotIn('name', result)
|
||||
self.assertNotIn('age', result)
|
||||
|
||||
def test_process_data_params_required_field_missing_full_update(self):
|
||||
"""
|
||||
Отсутствие обязательного поля при полном обновлении (PUT)
|
||||
Проверяет, что при PUT обязательные поля должны быть указаны
|
||||
"""
|
||||
|
||||
class PutViewSet(MockViewSetForDataParams):
|
||||
def _get_request_method(self):
|
||||
return 'PUT'
|
||||
|
||||
view_set = PutViewSet()
|
||||
request = self.create_i_ivanov_request(
|
||||
'PUT',
|
||||
'/test',
|
||||
data={'email': 'test@example.com'} # name и age отсутствуют
|
||||
)
|
||||
view_set.request = request
|
||||
|
||||
with self.assertRaises(serializers.ValidationError):
|
||||
view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
def test_process_data_params_with_extra_fields(self):
|
||||
"""
|
||||
Лишние поля в данных - сериализатор их игнорирует
|
||||
Проверяет, что сериализатор игнорирует поля, не определенные в нем
|
||||
"""
|
||||
# Arrange
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={
|
||||
'name': 'Ivan Ivanov',
|
||||
'age': 30,
|
||||
'extra_field': 'should_be_ignored'
|
||||
}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
self.assertEqual(result['name'], 'Ivan Ivanov')
|
||||
self.assertEqual(result['age'], 30)
|
||||
self.assertNotIn('extra_field', result)
|
||||
|
||||
def test_process_data_params_with_petrov_user(self):
|
||||
"""
|
||||
Запрос от Петрова с данными
|
||||
Проверяет, что data params работают для любого пользователя
|
||||
"""
|
||||
|
||||
request = self.create_p_petrov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={'name': 'Petr Petrov', 'age': 25}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
self.assertEqual(result['name'], 'Petr Petrov')
|
||||
self.assertEqual(result['age'], 25)
|
||||
|
||||
def test_process_data_params_with_empty_data(self):
|
||||
"""
|
||||
Пустые данные
|
||||
Проверяет обработку пустого request.data
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request('POST', '/test', data={})
|
||||
self.view_set.request = request
|
||||
|
||||
with self.assertRaises(serializers.ValidationError):
|
||||
self.view_set._process_data_params(DataParamsSerializer)
|
||||
|
||||
def test_process_data_params_with_null_values(self):
|
||||
"""
|
||||
Null значения в данных
|
||||
Проверяет, что null значения обрабатываются корректно
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={'name': None, 'age': 30}
|
||||
)
|
||||
self.view_set.request = request
|
||||
|
||||
with self.assertRaises(serializers.ValidationError):
|
||||
self.view_set._process_data_params(DataParamsSerializer)
|
||||
@@ -1,72 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreViewSetProcessPkParamTest
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from net.xeaf.rack.core.views import CoreViewSet
|
||||
from net.xeaf.rack.utils.exceptions import BadRequestException
|
||||
|
||||
|
||||
class MockViewSet(CoreViewSet):
|
||||
"""
|
||||
Тестовый класс представления
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CoreViewSetProcessPkParamTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для CoreViewSet._process_pk_param
|
||||
"""
|
||||
|
||||
def test_process_pk_param_valid_uuid(self):
|
||||
"""
|
||||
Валидный UUID передан
|
||||
"""
|
||||
|
||||
view_set = MockViewSet()
|
||||
valid_uuid = "bd428358-6944-4657-8044-a3dc1c4dff8a"
|
||||
result = view_set._process_pk_param(pk=valid_uuid)
|
||||
|
||||
self.assertIsInstance(result, UUID)
|
||||
self.assertEqual(str(result), valid_uuid)
|
||||
|
||||
def test_process_pk_param_missing_raises_error(self):
|
||||
"""
|
||||
Параметр pk отсутствует - ошибка
|
||||
"""
|
||||
|
||||
view_set = MockViewSet()
|
||||
with self.assertRaises(BadRequestException):
|
||||
view_set._process_pk_param()
|
||||
|
||||
def test_process_pk_param_invalid_uuid_raises_error(self):
|
||||
"""
|
||||
Невалидный UUID - ошибка
|
||||
"""
|
||||
|
||||
view_set = MockViewSet()
|
||||
with self.assertRaises(ValueError):
|
||||
view_set._process_pk_param(pk="invalid-uuid")
|
||||
|
||||
def test_process_pk_param_ignores_extra_kwargs(self):
|
||||
"""
|
||||
Лишние параметры игнорируются
|
||||
"""
|
||||
|
||||
view_set = MockViewSet()
|
||||
valid_uuid = "bd428358-6944-4657-8044-a3dc1c4dff8a"
|
||||
result = view_set._process_pk_param(pk=valid_uuid, extra="value")
|
||||
|
||||
self.assertIsInstance(result, UUID)
|
||||
self.assertEqual(str(result), valid_uuid)
|
||||
@@ -1,278 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreViewSetProcessQueryParamsTests
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.core.views import CoreViewSet
|
||||
from net.xeaf.rack.management.userstory import AccountTestData
|
||||
from net.xeaf.rack.management.userstory import SessionTestData
|
||||
|
||||
|
||||
class QueryParamsSerializer(serializers.Serializer):
|
||||
"""
|
||||
Сериализатор параметров тестового запроса
|
||||
"""
|
||||
|
||||
limit = serializers.IntegerField(required=False, default=10)
|
||||
offset = serializers.IntegerField(required=False, default=0)
|
||||
search = serializers.CharField(required=False, default='', allow_blank=True)
|
||||
|
||||
|
||||
class MockViewSetForQueryParams(CoreViewSet):
|
||||
"""
|
||||
Тестовый класс представления
|
||||
"""
|
||||
|
||||
permission_classes = ()
|
||||
""" Классы проверки прав доступа """
|
||||
|
||||
def _get_request_session(self):
|
||||
"""
|
||||
Возвращает сессию пользователя из запроса
|
||||
|
||||
:return: Сессия пользователя
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class CoreViewSetProcessQueryParamsTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для CoreViewSet._process_query_params
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Подготовка к тестированию
|
||||
"""
|
||||
|
||||
super().setUp()
|
||||
self.view_set = MockViewSetForQueryParams()
|
||||
|
||||
def test_process_query_params_no_serializer(self):
|
||||
"""
|
||||
Без сериализатора - возвращает словарь с параметрами
|
||||
Проверяет, что параметры извлекаются из query_params и мержатся с kwargs
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'GET',
|
||||
'/test',
|
||||
query_params={
|
||||
'param1': 'value1',
|
||||
'param2': 'value2'
|
||||
}
|
||||
)
|
||||
self.view_set.request = request
|
||||
|
||||
result = self.view_set._process_query_params(
|
||||
None,
|
||||
custom_param='custom_value'
|
||||
)
|
||||
|
||||
self.assertEqual(result['param1'], 'value1')
|
||||
self.assertEqual(result['param2'], 'value2')
|
||||
self.assertEqual(result['custom_param'], 'custom_value')
|
||||
self.assertNotIn('request', result)
|
||||
|
||||
def test_process_query_params_with_serializer(self):
|
||||
"""
|
||||
С сериализатором - валидирует и возвращает данные
|
||||
Проверяет, что параметры проходят валидацию через сериализатор
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'GET',
|
||||
'/test',
|
||||
query_params={
|
||||
'limit': '20',
|
||||
'offset': '5',
|
||||
'search': 'test'
|
||||
}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_query_params(QueryParamsSerializer)
|
||||
|
||||
self.assertEqual(result['limit'], 20)
|
||||
self.assertEqual(result['offset'], 5)
|
||||
self.assertEqual(result['search'], 'test')
|
||||
|
||||
def test_process_query_params_with_serializer_defaults(self):
|
||||
"""
|
||||
С сериализатором - значения по умолчанию
|
||||
Проверяет, что отсутствующие параметры заполняются значениями по умолчанию
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'GET',
|
||||
'/test',
|
||||
query_params={}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_query_params(QueryParamsSerializer)
|
||||
|
||||
self.assertEqual(result['limit'], 10) # default
|
||||
self.assertEqual(result['offset'], 0) # default
|
||||
self.assertEqual(result['search'], '') # allow_blank=True
|
||||
|
||||
def test_process_query_params_with_serializer_invalid_raises_error(self):
|
||||
"""
|
||||
Невалидные параметры - ошибка валидации
|
||||
Проверяет, что сериализатор выбрасывает ValidationError
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'GET',
|
||||
'/test',
|
||||
query_params={
|
||||
'limit': 'invalid', # невалидное значение
|
||||
'offset': '5'
|
||||
}
|
||||
)
|
||||
self.view_set.request = request
|
||||
|
||||
with self.assertRaises(serializers.ValidationError):
|
||||
self.view_set._process_query_params(QueryParamsSerializer)
|
||||
|
||||
def test_process_query_params_merges_with_kwargs(self):
|
||||
"""
|
||||
Параметры из kwargs переопределяют параметры запроса
|
||||
Проверяет, что kwargs имеют приоритет над query_params
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'GET',
|
||||
'/test',
|
||||
query_params={'limit': '20'}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_query_params(
|
||||
None,
|
||||
limit=50 # переопределяем
|
||||
)
|
||||
|
||||
self.assertEqual(result['limit'], 50)
|
||||
|
||||
def test_process_query_params_with_serializer_ignores_extra_kwargs(self):
|
||||
"""
|
||||
С сериализатором - только валидированные параметры
|
||||
Проверяет, что сериализатор игнорирует параметры, не определенные в нем
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'GET',
|
||||
'/test',
|
||||
query_params={
|
||||
'limit': '20',
|
||||
'offset': '5'
|
||||
}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_query_params(
|
||||
QueryParamsSerializer,
|
||||
extra_param='extra_value' # не входит в сериализатор
|
||||
)
|
||||
|
||||
self.assertNotIn('extra_param', result)
|
||||
self.assertEqual(result['limit'], 20)
|
||||
self.assertEqual(result['offset'], 5)
|
||||
|
||||
def test_process_query_params_empty_params(self):
|
||||
"""
|
||||
Пустые параметры запроса
|
||||
Проверяет обработку пустого query_params
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request('GET', '/test')
|
||||
self.view_set.request = request
|
||||
|
||||
result = self.view_set._process_query_params(None)
|
||||
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_process_query_params_filters_request_param(self):
|
||||
"""
|
||||
Фильтрация параметра 'request'
|
||||
Проверяет, что параметр 'request' не попадает в результат
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'GET',
|
||||
'/test',
|
||||
query_params={
|
||||
'request': 'should_be_filtered',
|
||||
'param': 'value'
|
||||
}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_query_params(None)
|
||||
|
||||
self.assertNotIn('request', result)
|
||||
self.assertEqual(result['param'], 'value')
|
||||
|
||||
def test_process_query_params_with_petrov_user(self):
|
||||
"""
|
||||
Запрос от Петрова с параметрами
|
||||
Проверяет, что query_params работают для любого пользователя
|
||||
"""
|
||||
|
||||
request = self.create_p_petrov_request(
|
||||
'GET',
|
||||
'/test',
|
||||
query_params={
|
||||
'limit': '15',
|
||||
'offset': '3'
|
||||
}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_query_params(QueryParamsSerializer)
|
||||
|
||||
self.assertEqual(result['limit'], 15)
|
||||
self.assertEqual(result['offset'], 3)
|
||||
|
||||
def test_process_query_params_with_non_get_method(self):
|
||||
"""
|
||||
Не-GET запрос с query_params
|
||||
Проверяет, что query_params работают и для не-GET запросов
|
||||
"""
|
||||
|
||||
request = self.create_i_ivanov_request(
|
||||
'POST',
|
||||
'/test',
|
||||
data={'name': 'Test'},
|
||||
query_params={'filter': 'active'}
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_query_params(None)
|
||||
|
||||
self.assertEqual(result['filter'], 'active')
|
||||
|
||||
def test_process_query_params_with_serializer_and_ivanov_session(self):
|
||||
"""
|
||||
Проверка, что сессия Ивана доступна в контексте
|
||||
Используем get_ivanov_session() для проверки
|
||||
"""
|
||||
|
||||
ivanov_session = self.get_i_ivanov_session()
|
||||
request = self.create_request(
|
||||
'GET',
|
||||
'/test',
|
||||
query_params={'limit': '20'},
|
||||
auth=ivanov_session
|
||||
)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._process_query_params(QueryParamsSerializer)
|
||||
|
||||
self.assertEqual(result['limit'], 20)
|
||||
|
||||
# Проверяем, что сессия действительно Ивана
|
||||
self.assertEqual(ivanov_session.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
self.assertEqual(str(ivanov_session.account.id), str(AccountTestData.I_IVANOV_ID))
|
||||
@@ -1,136 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CoreViewSetRequestContextTests
|
||||
"""
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.core.views import CoreViewSet
|
||||
from net.xeaf.rack.management.userstory import AccountTestData
|
||||
from net.xeaf.rack.management.userstory import SessionTestData
|
||||
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||
|
||||
|
||||
class MockViewSetWithAuth(CoreViewSet):
|
||||
"""
|
||||
ViewSet для тестов аутентификации
|
||||
"""
|
||||
|
||||
permission_classes = ()
|
||||
|
||||
|
||||
class CoreViewSetRequestContextTests(CoreTestCase):
|
||||
"""
|
||||
Тесты методов получения сессии и аккаунта
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Подготовка к тестированию
|
||||
"""
|
||||
|
||||
super().setUp()
|
||||
self.view_set = MockViewSetWithAuth()
|
||||
|
||||
def test_get_request_session_authenticated(self):
|
||||
"""
|
||||
Получение сессии для аутентифицированного запроса
|
||||
"""
|
||||
|
||||
session = self.get_i_ivanov_session()
|
||||
request = self.create_request('GET', '/test', auth=session)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._get_request_session()
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.assertEqual(result.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.assertEqual(str(result.account.id), str(AccountTestData.I_IVANOV_ID))
|
||||
|
||||
def test_get_request_account_authenticated(self):
|
||||
"""
|
||||
Получение аккаунта для аутентифицированного запроса
|
||||
"""
|
||||
|
||||
session = self.get_i_ivanov_session()
|
||||
request = self.create_request('GET', '/test', auth=session)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._get_request_account()
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(str(result.id), str(AccountTestData.I_IVANOV_ID))
|
||||
self.assertEqual(result.username, AccountTestData.I_IVANOV_USERNAME)
|
||||
|
||||
def test_get_request_account_unauthenticated_raises_error(self):
|
||||
"""
|
||||
Не аутентифицированный запрос - ошибка UnauthorizedException
|
||||
"""
|
||||
|
||||
request = self.create_request('GET', '/test')
|
||||
self.view_set.request = request
|
||||
|
||||
with self.assertRaises(UnauthorizedException):
|
||||
self.view_set._get_request_account()
|
||||
|
||||
def test_get_request_context_with_auth(self):
|
||||
"""
|
||||
Получение контекста запроса с аутентификацией
|
||||
"""
|
||||
|
||||
session = self.get_i_ivanov_session()
|
||||
request = self.create_request('GET', '/test', auth=session)
|
||||
self.view_set.request = request
|
||||
context = self.view_set._get_request_context()
|
||||
|
||||
self.assertIn('account', context)
|
||||
self.assertIn('request', context)
|
||||
self.assertEqual(str(context['account'].id), str(AccountTestData.I_IVANOV_ID))
|
||||
self.assertEqual(context['request'], request)
|
||||
|
||||
def test_get_request_context_without_auth(self):
|
||||
"""
|
||||
Получение контекста без аутентификации - account = None
|
||||
"""
|
||||
|
||||
request = self.create_request('GET', '/test')
|
||||
self.view_set.request = request
|
||||
context = self.view_set._get_request_context()
|
||||
|
||||
self.assertIn('account', context)
|
||||
self.assertIsNone(context['account'])
|
||||
self.assertIn('request', context)
|
||||
self.assertEqual(context['request'], request)
|
||||
|
||||
def test_get_request_session_petrov_authenticated(self):
|
||||
"""
|
||||
Получение сессии Петрова
|
||||
"""
|
||||
|
||||
session = self.get_p_petrov_session()
|
||||
request = self.create_request('GET', '/test', auth=session)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._get_request_session()
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.assertEqual(result.token, SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.assertEqual(str(result.account.id), str(AccountTestData.P_PETROV_ID))
|
||||
|
||||
def test_get_request_account_petrov_authenticated(self):
|
||||
"""
|
||||
Получение аккаунта Петрова
|
||||
"""
|
||||
session = self.get_p_petrov_session()
|
||||
request = self.create_request('GET', '/test', auth=session)
|
||||
self.view_set.request = request
|
||||
result = self.view_set._get_request_account()
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(str(result.id), str(AccountTestData.P_PETROV_ID))
|
||||
self.assertEqual(result.username, AccountTestData.P_PETROV_USERNAME)
|
||||
@@ -1,13 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов для классов перечислений
|
||||
"""
|
||||
|
||||
from .ui_language_tests import UILanguageTests
|
||||
from .view_set_action_tests import ViewSetActionTests
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса UILanguageTests
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from net.xeaf.rack.enums import UILanguage
|
||||
|
||||
|
||||
class UILanguageTests(TestCase):
|
||||
"""
|
||||
Тесты для перечисления UILanguage
|
||||
"""
|
||||
|
||||
def test_has_expected_values(self):
|
||||
"""
|
||||
Проверяем, что в enum есть ожидаемые языки
|
||||
"""
|
||||
|
||||
expected = {"en", "ru"}
|
||||
self.assertEqual(set(UILanguage.values()), expected)
|
||||
|
||||
def test_values_format(self):
|
||||
"""
|
||||
Проверяем формат значений
|
||||
"""
|
||||
|
||||
for code in UILanguage.values():
|
||||
self.assertEqual(len(code), 2)
|
||||
self.assertTrue(code.islower())
|
||||
|
||||
def test_from_string_returns_english_for_en(self):
|
||||
"""
|
||||
Проверяем, что from_string возвращает ENGLISH для 'en'
|
||||
"""
|
||||
|
||||
result = UILanguage.from_string('en')
|
||||
self.assertEqual(result, UILanguage.ENGLISH)
|
||||
|
||||
def test_from_string_returns_russian_for_ru(self):
|
||||
"""
|
||||
Проверяем, что from_string возвращает RUSSIAN для 'ru'
|
||||
"""
|
||||
|
||||
result = UILanguage.from_string('ru')
|
||||
self.assertEqual(result, UILanguage.RUSSIAN)
|
||||
|
||||
def test_from_string_handles_uppercase(self):
|
||||
"""
|
||||
Проверяем, что from_string корректно обрабатывает заглавные буквы
|
||||
"""
|
||||
|
||||
result = UILanguage.from_string('EN')
|
||||
self.assertEqual(result, UILanguage.ENGLISH)
|
||||
|
||||
result = UILanguage.from_string('RU')
|
||||
self.assertEqual(result, UILanguage.RUSSIAN)
|
||||
|
||||
def test_from_string_handles_regional_variants(self):
|
||||
"""
|
||||
Проверяем, что from_string корректно обрабатывает региональные варианты и лишние суффиксы
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
('en-us', UILanguage.ENGLISH),
|
||||
('en-US', UILanguage.ENGLISH),
|
||||
('en-gb', UILanguage.ENGLISH),
|
||||
('en-GB', UILanguage.ENGLISH),
|
||||
('en-us-extra', UILanguage.ENGLISH), # лишний суффикс
|
||||
('en-us-extra-stuff', UILanguage.ENGLISH), # несколько суффиксов
|
||||
('ru-ru', UILanguage.RUSSIAN),
|
||||
('ru-RU', UILanguage.RUSSIAN),
|
||||
]
|
||||
|
||||
for value, expected in test_cases:
|
||||
with self.subTest(value=value):
|
||||
result = UILanguage.from_string(value)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_from_string_raises_value_error_for_unknown(self):
|
||||
"""
|
||||
Проверяем, что from_string выбрасывает ValueError для неизвестного языка
|
||||
"""
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
UILanguage.from_string('fr')
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
UILanguage.from_string('de')
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
UILanguage.from_string('es')
|
||||
|
||||
def test_from_string_raises_value_error_for_invalid_format(self):
|
||||
"""
|
||||
Проверяем, что from_string выбрасывает ValueError для некорректного формата
|
||||
"""
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
UILanguage.from_string('123')
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
UILanguage.from_string('')
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
UILanguage.from_string('fr-FR') # 'fr' не поддерживается
|
||||
@@ -1,41 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ViewSetActionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from net.xeaf.rack.enums import ViewSetAction
|
||||
|
||||
|
||||
class ViewSetActionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для ViewSetAction
|
||||
"""
|
||||
|
||||
def test_get_method_returns_correct_mapping(self):
|
||||
"""
|
||||
Проверяет, что get_method возвращает правильные HTTP-методы для всех действий
|
||||
"""
|
||||
|
||||
self.assertEqual(ViewSetAction.get_method(ViewSetAction.LIST), "item_list")
|
||||
self.assertEqual(ViewSetAction.get_method(ViewSetAction.RETRIEVE), "item_retrieve")
|
||||
self.assertEqual(ViewSetAction.get_method(ViewSetAction.CREATE), "item_create")
|
||||
self.assertEqual(ViewSetAction.get_method(ViewSetAction.UPDATE), "item_update")
|
||||
self.assertEqual(ViewSetAction.get_method(ViewSetAction.PARTIAL_UPDATE), "item_partial_update")
|
||||
self.assertEqual(ViewSetAction.get_method(ViewSetAction.DESTROY), "item_destroy")
|
||||
|
||||
def test_get_method_with_invalid_action_raises_error(self):
|
||||
"""
|
||||
Проверяет поведение метода при передаче невалидного значения
|
||||
"""
|
||||
|
||||
invalid_action = "invalid_action"
|
||||
|
||||
with self.assertRaises(Exception): # Будет вызван MatchError или ValueError в зависимости от настроек linter/python
|
||||
ViewSetAction.get_method(invalid_action) # type: ignore
|
||||
@@ -1,21 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов для классов менеджеров
|
||||
"""
|
||||
|
||||
from .configuration_manager_tests import ConfigurationManagerTests
|
||||
from .crypto_manager_tests import CryptoManagerTests
|
||||
from .locale_manager_tests import LocaleManagerTests
|
||||
|
||||
from .request_manager_get_request_account_tests import RequestManagerGetRequestAccountTests
|
||||
from .request_manager_get_request_context_tests import RequestManagerGetRequestContextTests
|
||||
from .request_manager_get_request_id_tests import RequestManagerGetRequestIdTests
|
||||
from .request_manager_get_request_method_tests import RequestManagerGetRequestMethodTests
|
||||
from .request_manager_get_request_session_tests import RequestManagerGetRequestSessionTests
|
||||
|
||||
from .sesson_manager_tests import SessionManagerTests
|
||||
@@ -1,116 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CryptoTests
|
||||
"""
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import override_settings
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from net.xeaf.rack.managers import ConfigurationManager
|
||||
|
||||
|
||||
class ConfigurationManagerTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса ConfigurationManager
|
||||
"""
|
||||
|
||||
def test_get_string_returns_default_if_setting_missing(self) -> None:
|
||||
"""
|
||||
Должен вернуть дефолтное значение, если настройки нет в settings.py
|
||||
"""
|
||||
|
||||
result = ConfigurationManager.get_string("NON_EXISTENT_STR_KEY", default="default_val")
|
||||
self.assertEqual(result, "default_val")
|
||||
|
||||
@override_settings(EXISTING_STR_KEY="custom_val")
|
||||
def test_get_string_returns_custom_value(self) -> None:
|
||||
"""
|
||||
Должен успешно прочитать кастомную строку из настроек проекта
|
||||
"""
|
||||
|
||||
result = ConfigurationManager.get_string("EXISTING_STR_KEY", default="default_val")
|
||||
self.assertEqual(result, "custom_val")
|
||||
|
||||
@override_settings(INVALID_STR_KEY=123)
|
||||
def test_get_string_raises_exception_on_type_mismatch(self) -> None:
|
||||
"""
|
||||
Должен взорваться исключением ImproperlyConfigured, если вместо строки пришел int
|
||||
"""
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured) as context:
|
||||
ConfigurationManager.get_string("INVALID_STR_KEY", default="default_val")
|
||||
|
||||
self.assertIn("must be of type str, got int", str(context.exception))
|
||||
|
||||
def test_get_int_returns_default_if_setting_missing(self) -> None:
|
||||
"""
|
||||
Должен вернуть дефолтное число, если настройки нет в файле конфигурации
|
||||
"""
|
||||
|
||||
result = ConfigurationManager.get_int("NON_EXISTENT_INT_KEY", default=42)
|
||||
self.assertEqual(result, 42)
|
||||
|
||||
@override_settings(EXISTING_INT_KEY=100)
|
||||
def test_get_int_returns_custom_value(self) -> None:
|
||||
"""
|
||||
Должен успешно вернуть настроенный int
|
||||
"""
|
||||
|
||||
result = ConfigurationManager.get_int("EXISTING_INT_KEY", default=42)
|
||||
self.assertEqual(result, 100)
|
||||
|
||||
@override_settings(INVALID_INT_KEY="not_an_int")
|
||||
def test_get_int_raises_exception_on_type_mismatch(self) -> None:
|
||||
"""
|
||||
Должен упасть, если в числовую настройку хост-проект прокинул строку
|
||||
"""
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured) as context:
|
||||
ConfigurationManager.get_int("INVALID_INT_KEY", default=42)
|
||||
|
||||
self.assertIn("must be of type int, got str", str(context.exception))
|
||||
|
||||
@override_settings(BOOLEAN_INSTEAD_OF_INT=True)
|
||||
def test_get_int_raises_exception_on_boolean_value(self) -> None:
|
||||
"""
|
||||
Хирургический тест: должен упасть, если вместо int передан bool (True/False)
|
||||
"""
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured) as context:
|
||||
ConfigurationManager.get_int("BOOLEAN_INSTEAD_OF_INT", default=42)
|
||||
|
||||
self.assertIn("must be of type int, got bool", str(context.exception))
|
||||
|
||||
def test_get_bool_returns_default_if_setting_missing(self) -> None:
|
||||
"""
|
||||
Должен вернуть дефолтный булевый флаг при отсутствии настройки
|
||||
"""
|
||||
|
||||
result = ConfigurationManager.get_bool("NON_EXISTENT_BOOL_KEY", default=True)
|
||||
self.assertTrue(result)
|
||||
|
||||
@override_settings(EXISTING_BOOL_KEY=False)
|
||||
def test_get_bool_returns_custom_value(self) -> None:
|
||||
"""
|
||||
Должен успешно прочитать переопределенный булевый флаг
|
||||
"""
|
||||
|
||||
result = ConfigurationManager.get_bool("EXISTING_BOOL_KEY", default=True)
|
||||
self.assertFalse(result)
|
||||
|
||||
@override_settings(INT_INSTEAD_OF_BOOL=1)
|
||||
def test_get_bool_raises_exception_on_int_value(self) -> None:
|
||||
"""
|
||||
Должен выбросить исключение, если вместо честного bool передан int
|
||||
"""
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured) as context:
|
||||
ConfigurationManager.get_bool("INT_INSTEAD_OF_BOOL", default=True)
|
||||
|
||||
self.assertIn("must be of type bool, got int", str(context.exception))
|
||||
@@ -1,163 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CryptoTests
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from net.xeaf.rack.managers import CryptoManager
|
||||
from net.xeaf.rack.models import SessionModel
|
||||
|
||||
|
||||
class CryptoManagerTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса CryptoManager
|
||||
"""
|
||||
|
||||
def test_gen_token(self):
|
||||
"""
|
||||
Тест генерации токена
|
||||
"""
|
||||
|
||||
token = CryptoManager.gen_token()
|
||||
self.assertEqual(len(token), CryptoManager.DEFAULT_TOKEN_LENGTH)
|
||||
self._assert_lower_hex(token)
|
||||
|
||||
def test_gen_token_length(self):
|
||||
"""
|
||||
Тест генерации токена с указанием длины
|
||||
"""
|
||||
|
||||
token = CryptoManager.gen_token(length=15)
|
||||
self.assertEqual(len(token), 15)
|
||||
|
||||
def test_gen_token_different(self):
|
||||
"""
|
||||
Тест генерации разных токенов
|
||||
"""
|
||||
|
||||
token1 = CryptoManager.gen_token()
|
||||
token2 = CryptoManager.gen_token()
|
||||
self.assertNotEqual(token1, token2)
|
||||
|
||||
def test_gen_pin_code(self):
|
||||
"""
|
||||
Тест генерации PIN-кода
|
||||
"""
|
||||
|
||||
pin_code = CryptoManager.gen_pin_code()
|
||||
self.assertEqual(len(pin_code), CryptoManager.DEFAULT_PIN_CODE_LENGTH)
|
||||
self.assertTrue(all(c in CryptoManager.DEFAULT_PIN_CODE_ALPHABET for c in pin_code))
|
||||
|
||||
def test_gen_pin_code_length(self):
|
||||
"""
|
||||
Тест генерации PIN-кода с указанием длины
|
||||
"""
|
||||
|
||||
pin_code = CryptoManager.gen_pin_code(length=8)
|
||||
self.assertEqual(len(pin_code), 8)
|
||||
self.assertTrue(all(c in CryptoManager.DEFAULT_PIN_CODE_ALPHABET for c in pin_code))
|
||||
|
||||
def test_gen_pin_code_alphabet(self):
|
||||
"""
|
||||
Тест генерации PIN-кода с указанием алфавита
|
||||
"""
|
||||
|
||||
letters = 'ABCD'
|
||||
pin_code = CryptoManager.gen_pin_code(alphabet=letters)
|
||||
self.assertEqual(len(pin_code), CryptoManager.DEFAULT_PIN_CODE_LENGTH)
|
||||
self.assertTrue(all(c in letters for c in pin_code))
|
||||
|
||||
def test_gen_pin_code_different(self):
|
||||
"""
|
||||
Тест генерации разных PIN-кодов
|
||||
"""
|
||||
|
||||
pin_code_1 = CryptoManager.gen_pin_code()
|
||||
pin_code_2 = CryptoManager.gen_pin_code()
|
||||
self.assertNotEqual(pin_code_1, pin_code_2)
|
||||
|
||||
def test_gen_slug(self):
|
||||
"""
|
||||
Тест генерации slug
|
||||
"""
|
||||
|
||||
pfx = 'test'
|
||||
slug = CryptoManager.gen_slug(pfx)
|
||||
self.assertEqual(len(slug), CryptoManager.DEFAULT_SLUG_LENGTH)
|
||||
self.assertEqual(slug[:len(pfx)], pfx)
|
||||
self._assert_lower_hex(slug[len(pfx):])
|
||||
|
||||
def test_gen_slug_length(self):
|
||||
"""
|
||||
Тест генерации slug с указанием длины
|
||||
"""
|
||||
|
||||
pfx = 'test'
|
||||
slug = CryptoManager.gen_slug(pfx, length=16)
|
||||
self.assertEqual(len(slug), 16)
|
||||
|
||||
def test_gen_slug_prefix(self):
|
||||
"""
|
||||
Тест генерации slug с длинным префиксом
|
||||
"""
|
||||
|
||||
pfx = 'test_test_test'
|
||||
slug = CryptoManager.gen_slug(pfx)
|
||||
self.assertEqual(slug, pfx)
|
||||
|
||||
def test_slug_different(self):
|
||||
"""
|
||||
Тест генерации разных slug
|
||||
"""
|
||||
|
||||
slug_1 = CryptoManager.gen_slug('test')
|
||||
slug_2 = CryptoManager.gen_slug('test')
|
||||
self.assertNotEqual(slug_1, slug_2)
|
||||
|
||||
def _assert_lower_hex(self, value: str):
|
||||
"""
|
||||
Проверяет, что строка состоит из символов в нижнем регистре и шестнадцатеричных цифр
|
||||
|
||||
:param value: Проверяемая строка
|
||||
"""
|
||||
|
||||
self.assertRegex(value, r'^[0-9a-f]+$')
|
||||
|
||||
def test_crypto_manager_should_generate_default_length_by_default(self) -> None:
|
||||
"""
|
||||
Гарантируем, что генератор без параметров использует базовую длину
|
||||
"""
|
||||
|
||||
generated_token = CryptoManager.gen_token()
|
||||
self.assertEqual(len(generated_token), CryptoManager.DEFAULT_TOKEN_LENGTH)
|
||||
|
||||
def test_session_model_field_must_match_max_token_length_constant(self) -> None:
|
||||
"""
|
||||
Защита схемы БД: max_length в миграциях равен MAX_TOKEN_LENGTH
|
||||
"""
|
||||
|
||||
field = SessionModel._meta.get_field("token")
|
||||
self.assertIsInstance(field, models.CharField)
|
||||
db_max_length = field.max_length
|
||||
self.assertIsInstance(db_max_length, int)
|
||||
self.assertEqual(db_max_length, SessionModel.MAX_TOKEN_LENGTH)
|
||||
|
||||
def test_crypto_manager_should_raise_value_error_if_length_exceeds_max(self) -> None:
|
||||
"""
|
||||
Проверка Fail-Fast контракта: при превышении лимита СУБД выбрасывается ValueError
|
||||
"""
|
||||
|
||||
excessive_length = SessionModel.MAX_TOKEN_LENGTH + 1
|
||||
|
||||
# Система обязана громко упасть с ValueError
|
||||
with self.assertRaises(ValueError) as context:
|
||||
CryptoManager.gen_token(length=excessive_length)
|
||||
|
||||
self.assertIn(f"Token length must be less than {SessionModel.MAX_TOKEN_LENGTH}", str(context.exception))
|
||||
@@ -1,245 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса LocaleManagerTest
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.utils import translation
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from net.xeaf.rack.enums import UILanguage
|
||||
from net.xeaf.rack.managers import LocaleManager
|
||||
|
||||
|
||||
class LocaleManagerTests(TestCase):
|
||||
"""
|
||||
Тесты для класса LocaleManager
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Подготовка данных для тестов
|
||||
"""
|
||||
self.factory = APIRequestFactory()
|
||||
self.default_language = UILanguage.from_string(settings.LANGUAGE_CODE)
|
||||
|
||||
def test_get_default_language_returns_settings_value(self):
|
||||
"""
|
||||
Проверяем, что метод возвращает язык из настроек
|
||||
"""
|
||||
result = LocaleManager.get_default_language()
|
||||
|
||||
expected = UILanguage.from_string(settings.LANGUAGE_CODE)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_get_accepted_language_returns_english_for_en(self):
|
||||
"""
|
||||
Проверяем, что для 'en' возвращается UILanguage.ENGLISH
|
||||
"""
|
||||
result = LocaleManager.get_accepted_language('en')
|
||||
self.assertEqual(result, UILanguage.ENGLISH)
|
||||
|
||||
def test_get_accepted_language_returns_russian_for_ru(self):
|
||||
"""
|
||||
Проверяем, что для 'ru' возвращается UILanguage.RUSSIAN
|
||||
"""
|
||||
result = LocaleManager.get_accepted_language('ru')
|
||||
self.assertEqual(result, UILanguage.RUSSIAN)
|
||||
|
||||
def test_get_accepted_language_handles_uppercase(self):
|
||||
"""
|
||||
Проверяем, что метод корректно обрабатывает заглавные буквы
|
||||
"""
|
||||
result = LocaleManager.get_accepted_language('EN')
|
||||
self.assertEqual(result, UILanguage.ENGLISH)
|
||||
|
||||
def test_get_accepted_language_returns_default_for_unknown(self):
|
||||
"""
|
||||
Проверяем, что для неизвестного кода возвращается язык по умолчанию
|
||||
"""
|
||||
result = LocaleManager.get_accepted_language('fr')
|
||||
self.assertEqual(result, self.default_language)
|
||||
|
||||
def test_get_accepted_language_returns_default_for_empty_string(self):
|
||||
"""
|
||||
Проверяем, что для пустой строки возвращается язык по умолчанию
|
||||
"""
|
||||
result = LocaleManager.get_accepted_language('')
|
||||
self.assertEqual(result, self.default_language)
|
||||
|
||||
def test_get_request_language_returns_default_for_none_request(self):
|
||||
"""
|
||||
Проверяем, что для None запроса возвращается язык по умолчанию
|
||||
"""
|
||||
result = LocaleManager.get_request_language(None)
|
||||
self.assertEqual(result, self.default_language)
|
||||
|
||||
def test_get_request_language_from_query_param_on_get_request(self):
|
||||
"""
|
||||
Проверяем, что для GET запроса язык берется из query параметра
|
||||
"""
|
||||
request = self.factory.get('/?x-ui-language=ru')
|
||||
request.method = 'GET'
|
||||
request = Request(request)
|
||||
|
||||
result = LocaleManager.get_request_language(request)
|
||||
self.assertEqual(result, UILanguage.RUSSIAN)
|
||||
|
||||
def test_get_request_language_ignores_query_param_on_non_get(self):
|
||||
"""
|
||||
Проверяем, что для не-GET запроса query параметр игнорируется
|
||||
"""
|
||||
request = self.factory.post('/?x-ui-language=ru')
|
||||
request.method = 'POST'
|
||||
request.META['HTTP_ACCEPT_LANGUAGE'] = 'en-US,en;q=0.9'
|
||||
request = Request(request)
|
||||
|
||||
result = LocaleManager.get_request_language(request)
|
||||
self.assertEqual(result, UILanguage.ENGLISH)
|
||||
|
||||
def test_get_request_language_from_accept_header(self):
|
||||
"""
|
||||
Проверяем, что язык берется из заголовка Accept-Language
|
||||
"""
|
||||
request = self.factory.get('/')
|
||||
request.method = 'GET'
|
||||
request.META['HTTP_ACCEPT_LANGUAGE'] = 'ru-RU,ru;q=0.9,en;q=0.8'
|
||||
request = Request(request)
|
||||
|
||||
result = LocaleManager.get_request_language(request)
|
||||
self.assertEqual(result, UILanguage.RUSSIAN)
|
||||
|
||||
def test_get_request_language_returns_default_if_header_missing(self):
|
||||
"""
|
||||
Проверяем, что при отсутствии заголовка возвращается язык по умолчанию
|
||||
"""
|
||||
request = self.factory.get('/')
|
||||
request.method = 'GET'
|
||||
request = Request(request)
|
||||
|
||||
result = LocaleManager.get_request_language(request)
|
||||
self.assertEqual(result, self.default_language)
|
||||
|
||||
def test_get_request_language_prefers_query_param_over_header(self):
|
||||
"""
|
||||
Проверяем, что query параметр имеет приоритет над заголовком
|
||||
"""
|
||||
request = self.factory.get('/?x-ui-language=en')
|
||||
request.method = 'GET'
|
||||
request.META['HTTP_ACCEPT_LANGUAGE'] = 'ru-RU,ru;q=0.9'
|
||||
request = Request(request)
|
||||
|
||||
result = LocaleManager.get_request_language(request)
|
||||
self.assertEqual(result, UILanguage.ENGLISH)
|
||||
|
||||
def test_set_request_language_activates_translation(self):
|
||||
"""
|
||||
Проверяем, что метод активирует перевод для указанного языка
|
||||
"""
|
||||
request = self.factory.get('/')
|
||||
request = Request(request)
|
||||
|
||||
LocaleManager.set_request_language(request, UILanguage.RUSSIAN)
|
||||
|
||||
from django.utils import translation
|
||||
self.assertEqual(translation.get_language(), 'ru')
|
||||
|
||||
def test_set_request_language_sets_language_code_in_request(self):
|
||||
"""
|
||||
Проверяем, что метод устанавливает LANGUAGE_CODE в запросе
|
||||
"""
|
||||
request = self.factory.get('/')
|
||||
request = Request(request)
|
||||
|
||||
LocaleManager.set_request_language(request, UILanguage.ENGLISH)
|
||||
|
||||
self.assertEqual(request.LANGUAGE_CODE, 'EN')
|
||||
|
||||
def test_set_request_language_handles_none_request(self):
|
||||
"""
|
||||
Проверяем, что метод корректно обрабатывает None в запросе
|
||||
"""
|
||||
|
||||
LocaleManager.set_request_language(None, UILanguage.RUSSIAN)
|
||||
self.assertEqual(translation.get_language(), 'ru')
|
||||
|
||||
def test_process_accepted_language_returns_first_matching_language(self):
|
||||
"""
|
||||
Проверяем, что метод возвращает первый подходящий язык из заголовка
|
||||
"""
|
||||
request = self.factory.get('/')
|
||||
request.META['HTTP_ACCEPT_LANGUAGE'] = 'fr-FR,fr;q=0.9,ru-RU,ru;q=0.8,en;q=0.7'
|
||||
request = Request(request)
|
||||
|
||||
result = LocaleManager.process_accepted_language(request)
|
||||
self.assertEqual(result, 'ru')
|
||||
|
||||
def test_process_accepted_language_returns_default_if_no_match(self):
|
||||
"""
|
||||
Проверяем, что если ни один язык не найден, возвращается язык по умолчанию
|
||||
"""
|
||||
request = self.factory.get('/')
|
||||
request.META['HTTP_ACCEPT_LANGUAGE'] = 'fr-FR,fr;q=0.9,de;q=0.8'
|
||||
request = Request(request)
|
||||
|
||||
result = LocaleManager.process_accepted_language(request)
|
||||
self.assertEqual(result, str(self.default_language).lower())
|
||||
|
||||
def test_process_accepted_language_raises_keyerror_if_header_missing(self):
|
||||
"""
|
||||
Проверяем, что при отсутствии заголовка выбрасывается KeyError
|
||||
"""
|
||||
request = self.factory.get('/')
|
||||
request = Request(request)
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
LocaleManager.process_accepted_language(request)
|
||||
|
||||
def test_adjust_request_language_sets_language_from_request(self):
|
||||
"""
|
||||
Проверяем, что метод настраивает язык на основе запроса
|
||||
"""
|
||||
request = self.factory.get('/?x-ui-language=ru')
|
||||
request.method = 'GET'
|
||||
request = Request(request)
|
||||
|
||||
LocaleManager.adjust_request_language(request)
|
||||
|
||||
from django.utils import translation
|
||||
self.assertEqual(translation.get_language(), 'ru')
|
||||
self.assertEqual(request.LANGUAGE_CODE, 'RU')
|
||||
|
||||
def test_adjust_request_language_uses_default_if_no_language_found(self):
|
||||
"""
|
||||
Проверяем, что если язык не найден, используется язык по умолчанию
|
||||
"""
|
||||
request = self.factory.get('/')
|
||||
request.method = 'GET'
|
||||
request = Request(request)
|
||||
|
||||
LocaleManager.adjust_request_language(request)
|
||||
|
||||
from django.utils import translation
|
||||
expected = str(self.default_language).lower()
|
||||
self.assertEqual(translation.get_language(), expected)
|
||||
|
||||
def test_adjust_request_language_handles_empty_request(self):
|
||||
"""
|
||||
Проверяем, что метод корректно обрабатывает пустой запрос
|
||||
"""
|
||||
request = self.factory.get('/')
|
||||
request.method = 'GET'
|
||||
request = Request(request)
|
||||
|
||||
# Метод не должен выбрасывать исключений
|
||||
try:
|
||||
LocaleManager.adjust_request_language(request)
|
||||
except Exception as e:
|
||||
self.fail(f"adjust_request_language raised {e}")
|
||||
@@ -1,71 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса RequestManagerGetRequestAccountTests
|
||||
"""
|
||||
|
||||
from rest_framework.request import Request
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.enums import HttpMethod
|
||||
from net.xeaf.rack.management.userstory import AccountTestData
|
||||
from net.xeaf.rack.managers import RequestManager
|
||||
from net.xeaf.rack.models import SessionModel
|
||||
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||
|
||||
|
||||
class RequestManagerGetRequestAccountTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для RequestManager.get_request_account()
|
||||
"""
|
||||
|
||||
def _create_test_request(self, session: SessionModel | None) -> Request:
|
||||
"""
|
||||
Создает запрос для тестирования
|
||||
|
||||
:param session: Модель сессии
|
||||
|
||||
:return: Объект запроса
|
||||
"""
|
||||
|
||||
return self.create_request(method=HttpMethod.GET, path="/test", auth=session)
|
||||
|
||||
def test_get_request_account_authenticated(self):
|
||||
"""
|
||||
Получение аккаунта из аутентифицированного запроса
|
||||
"""
|
||||
|
||||
session = self.get_i_ivanov_session()
|
||||
request = self._create_test_request(session=session)
|
||||
result = RequestManager.get_request_account(request)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(str(result.id), str(AccountTestData.I_IVANOV_ID))
|
||||
self.assertEqual(result.username, AccountTestData.I_IVANOV_USERNAME)
|
||||
|
||||
def test_get_request_account_unauthenticated_raises_error(self):
|
||||
"""
|
||||
Запрос без аутентификации - ошибка UnauthorizedException
|
||||
"""
|
||||
|
||||
request = self._create_test_request(session=None)
|
||||
|
||||
with self.assertRaises(UnauthorizedException):
|
||||
RequestManager.get_request_account(request)
|
||||
|
||||
def test_get_request_account_petrov(self):
|
||||
"""
|
||||
Получение аккаунта Петрова
|
||||
"""
|
||||
|
||||
session = self.get_p_petrov_session()
|
||||
request = self._create_test_request(session=session)
|
||||
result = RequestManager.get_request_account(request)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(str(result.id), str(AccountTestData.P_PETROV_ID))
|
||||
self.assertEqual(result.username, AccountTestData.P_PETROV_USERNAME)
|
||||
@@ -1,83 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса RequestManagerGetRequestContextTests
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from rest_framework.request import Request
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.enums import HttpMethod
|
||||
from net.xeaf.rack.management.userstory import AccountTestData
|
||||
from net.xeaf.rack.managers import RequestManager
|
||||
from net.xeaf.rack.models import SessionModel
|
||||
|
||||
|
||||
class RequestManagerGetRequestContextTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для RequestManager.get_request_session()
|
||||
"""
|
||||
|
||||
def _create_test_request(self, session: SessionModel | None, headers: dict[str, Any] | None = None) -> Request:
|
||||
"""
|
||||
Создает запрос для тестирования
|
||||
|
||||
:param session: Модель сессии
|
||||
:param headers: Заголовки запроса
|
||||
|
||||
:return: Объект запроса
|
||||
"""
|
||||
|
||||
return self.create_request(method=HttpMethod.GET, path="/test", headers=headers, auth=session)
|
||||
|
||||
def test_get_request_context_with_auth(self):
|
||||
"""
|
||||
RM-005-1: Контекст с аутентификацией
|
||||
"""
|
||||
session = self.get_i_ivanov_session()
|
||||
request = self._create_test_request(session=session)
|
||||
|
||||
result = RequestManager.get_request_context(request)
|
||||
|
||||
self.assertIn('id', result)
|
||||
self.assertIn('account', result)
|
||||
self.assertIn('request', result)
|
||||
|
||||
self.assertIsInstance(result['id'], UUID)
|
||||
self.assertEqual(str(result['account'].id), str(AccountTestData.I_IVANOV_ID))
|
||||
self.assertEqual(result['request'], request)
|
||||
|
||||
def test_get_request_context_without_auth(self):
|
||||
"""
|
||||
Контекст без аутентификации - account = None
|
||||
"""
|
||||
|
||||
request = self._create_test_request(session=None)
|
||||
result = RequestManager.get_request_context(request)
|
||||
|
||||
self.assertIn('id', result)
|
||||
self.assertIn('account', result)
|
||||
self.assertIn('request', result)
|
||||
|
||||
self.assertIsInstance(result['id'], UUID)
|
||||
self.assertIsNone(result['account'])
|
||||
self.assertEqual(result['request'], request)
|
||||
|
||||
def test_get_request_context_with_request_id(self):
|
||||
"""
|
||||
Контекст с указанным Request-ID
|
||||
"""
|
||||
|
||||
request_id = "bd428358-6944-4657-8044-a3dc1c4dff8a"
|
||||
request = self._create_test_request(session=None, headers={RequestManager.REQUEST_ID_META: request_id})
|
||||
result = RequestManager.get_request_context(request)
|
||||
|
||||
self.assertIsInstance(result['id'], UUID)
|
||||
self.assertEqual(str(result['id']), request_id)
|
||||
@@ -1,66 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса RequestManagerGetRequestIdTests
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.enums import HttpMethod
|
||||
from net.xeaf.rack.managers import RequestManager
|
||||
from net.xeaf.rack.utils.exceptions import BadRequestException
|
||||
|
||||
|
||||
class RequestManagerGetRequestIdTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для RequestManager.get_request_id()
|
||||
"""
|
||||
|
||||
def _create_test_request(self, request_id: str | None = None):
|
||||
"""
|
||||
Создание тестового запроса с указанным Request-ID
|
||||
"""
|
||||
|
||||
headers = {RequestManager.REQUEST_ID_META: request_id} if request_id else {}
|
||||
return self.create_request(
|
||||
method=HttpMethod.GET,
|
||||
path="/test",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
def test_get_request_id_valid(self):
|
||||
"""
|
||||
Получение валидного Request-ID из заголовка
|
||||
"""
|
||||
|
||||
request_id = "bd428358-6944-4657-8044-a3dc1c4dff8a"
|
||||
request = self._create_test_request(request_id)
|
||||
|
||||
result = RequestManager.get_request_id(request)
|
||||
|
||||
self.assertIsInstance(result, UUID)
|
||||
self.assertEqual(str(result), request_id)
|
||||
|
||||
def test_get_request_id_missing(self):
|
||||
"""
|
||||
Заголовок X-Request-ID отсутствует - новый UUID
|
||||
"""
|
||||
|
||||
request = self._create_test_request()
|
||||
result = RequestManager.get_request_id(request)
|
||||
|
||||
self.assertIsInstance(result, UUID)
|
||||
|
||||
def test_get_request_id_invalid(self):
|
||||
"""
|
||||
Невалидный X-Request-ID - возникает исключение BadRequestException
|
||||
"""
|
||||
|
||||
request = self._create_test_request('invalid-uuid')
|
||||
with self.assertRaises(BadRequestException):
|
||||
RequestManager.get_request_id(request)
|
||||
@@ -1,92 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса RequestManagerGetRequestMethodTests
|
||||
"""
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from rest_framework.request import Request
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.enums import HttpMethod
|
||||
from net.xeaf.rack.managers import RequestManager
|
||||
|
||||
|
||||
class RequestManagerGetRequestMethodTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для RequestManager.get_request_method()
|
||||
"""
|
||||
|
||||
def _create_test_request(self, method: HttpMethod) -> Request:
|
||||
"""
|
||||
Создает запрос для тестирования
|
||||
|
||||
:param method: Метод запроса
|
||||
:return: Объект запроса
|
||||
"""
|
||||
|
||||
return self.create_request(method=method, path="/test")
|
||||
|
||||
def test_get_request_method_get(self):
|
||||
"""
|
||||
GET запрос
|
||||
"""
|
||||
|
||||
request = self._create_test_request(HttpMethod.GET)
|
||||
result = RequestManager.get_request_method(request)
|
||||
self.assertEqual(result, HttpMethod.GET)
|
||||
|
||||
def test_get_request_method_post(self):
|
||||
"""
|
||||
POST запрос
|
||||
"""
|
||||
|
||||
request = self._create_test_request(HttpMethod.POST)
|
||||
result = RequestManager.get_request_method(request)
|
||||
|
||||
self.assertEqual(result, HttpMethod.POST)
|
||||
|
||||
def test_get_request_method_put(self):
|
||||
"""
|
||||
PUT запрос
|
||||
"""
|
||||
|
||||
request = self._create_test_request(HttpMethod.PUT)
|
||||
result = RequestManager.get_request_method(request)
|
||||
|
||||
self.assertEqual(result, HttpMethod.PUT)
|
||||
|
||||
def test_get_request_method_patch(self):
|
||||
"""
|
||||
PATCH запрос
|
||||
"""
|
||||
|
||||
request = self._create_test_request(HttpMethod.PATCH)
|
||||
result = RequestManager.get_request_method(request)
|
||||
|
||||
self.assertEqual(result, HttpMethod.PATCH)
|
||||
|
||||
def test_get_request_method_delete(self):
|
||||
"""
|
||||
DELETE запрос
|
||||
"""
|
||||
|
||||
request = self._create_test_request(HttpMethod.DELETE)
|
||||
result = RequestManager.get_request_method(request)
|
||||
|
||||
self.assertEqual(result, HttpMethod.DELETE)
|
||||
|
||||
def test_get_request_method_missing_raises_error(self):
|
||||
"""
|
||||
Метод отсутствует - ошибка ImproperlyConfigured
|
||||
"""
|
||||
|
||||
request = self._create_test_request(HttpMethod.DELETE)
|
||||
request.method = None
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
RequestManager.get_request_method(request)
|
||||
@@ -1,75 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса RequestManagerGetRequestSessionTests
|
||||
"""
|
||||
|
||||
from rest_framework.request import Request
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.enums import HttpMethod
|
||||
from net.xeaf.rack.management.userstory import AccountTestData
|
||||
from net.xeaf.rack.management.userstory import SessionTestData
|
||||
from net.xeaf.rack.managers import RequestManager
|
||||
from net.xeaf.rack.models import SessionModel
|
||||
|
||||
|
||||
class RequestManagerGetRequestSessionTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для RequestManager.get_request_session()
|
||||
"""
|
||||
|
||||
def _create_test_request(self, session: SessionModel | None) -> Request:
|
||||
"""
|
||||
Создает запрос для тестирования
|
||||
|
||||
:param session: Модель сессии
|
||||
|
||||
:return: Объект запроса
|
||||
"""
|
||||
|
||||
return self.create_request(method=HttpMethod.GET, path="/test", auth=session)
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
def test_get_request_session_authenticated(self):
|
||||
"""
|
||||
Получение сессии из аутентифицированного запроса
|
||||
"""
|
||||
|
||||
session = self.get_i_ivanov_session()
|
||||
request = self._create_test_request(session=session)
|
||||
result = RequestManager.get_request_session(request)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIsInstance(result, SessionModel)
|
||||
self.assertEqual(result.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
self.assertEqual(str(result.account.id), str(AccountTestData.I_IVANOV_ID))
|
||||
|
||||
def test_get_request_session_unauthenticated(self):
|
||||
"""
|
||||
Запрос без аутентификации - возвращает None
|
||||
"""
|
||||
|
||||
request = self._create_test_request(session=None)
|
||||
result = RequestManager.get_request_session(request)
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
def test_get_request_session_petrov(self):
|
||||
"""
|
||||
Получение сессии Петрова
|
||||
"""
|
||||
|
||||
session = self.get_p_petrov_session()
|
||||
request = self._create_test_request(session=session)
|
||||
result = RequestManager.get_request_session(request)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIsInstance(result, SessionModel)
|
||||
self.assertEqual(result.token, SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||
self.assertEqual(str(result.account.id), str(AccountTestData.P_PETROV_ID))
|
||||
@@ -1,216 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса SessionManagerTest
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.enums import AuthRejectReason
|
||||
from net.xeaf.rack.management.userstory import AccountTestData
|
||||
from net.xeaf.rack.management.userstory import SessionTestData
|
||||
from net.xeaf.rack.managers import SessionManager
|
||||
from net.xeaf.rack.models import AccountModel
|
||||
from net.xeaf.rack.models import SessionModel
|
||||
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||
|
||||
|
||||
class SessionManagerTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для класса SessionManager
|
||||
"""
|
||||
|
||||
def test_should_successfully_authorize_ivanov_with_real_password(self) -> None:
|
||||
"""
|
||||
Проверка успешного создания сессии для эталонного аккаунта
|
||||
"""
|
||||
|
||||
session = SessionManager.authenticate(
|
||||
username="i.ivanov@xeaf.net",
|
||||
password=AccountTestData.DEFAULT_PASSWORD,
|
||||
device_id="mobile_app"
|
||||
)
|
||||
|
||||
# Проверяем, что вернулась живая сессия со сгенерированным токеном
|
||||
self.assertIsNotNone(session)
|
||||
self.assertEqual(len(session.token), 40)
|
||||
self.assertEqual(session.account_id, AccountTestData.I_IVANOV_ID)
|
||||
self.assertEqual(session.actual_account_id, AccountTestData.I_IVANOV_ID)
|
||||
self.assertEqual(session.device_id, "mobile_app")
|
||||
|
||||
# Честно проверяем запись физически внутри БД
|
||||
db_session = SessionModel.objects.get(token=session.token)
|
||||
self.assertEqual(db_session.account_id, AccountTestData.I_IVANOV_ID)
|
||||
|
||||
def test_should_raise_error_when_password_is_incorrect(self) -> None:
|
||||
"""
|
||||
Менеджер должен выкинуть исключение ядра при неверном пароле
|
||||
"""
|
||||
|
||||
with self.assertRaises(UnauthorizedException) as context:
|
||||
SessionManager.authenticate(
|
||||
username="i.ivanov@xeaf.net",
|
||||
password="wrong_password_here"
|
||||
)
|
||||
|
||||
self.assertIsInstance(context.exception.meta, dict)
|
||||
self.assertEqual(context.exception.meta['reason'], AuthRejectReason.INVALID_USERNAME_OR_PASSWORD)
|
||||
|
||||
def test_should_raise_error_when_username_does_not_exist(self) -> None:
|
||||
"""
|
||||
Менеджер должен выкинуть исключение ядра, если email не найден
|
||||
"""
|
||||
|
||||
with self.assertRaises(UnauthorizedException) as context:
|
||||
SessionManager.authenticate(
|
||||
username="unknown.user@xeaf.net",
|
||||
password=AccountTestData.DEFAULT_PASSWORD
|
||||
)
|
||||
|
||||
self.assertIsInstance(context.exception.meta, dict)
|
||||
self.assertEqual(context.exception.meta['reason'], AuthRejectReason.INVALID_USERNAME_OR_PASSWORD)
|
||||
|
||||
def test_should_raise_error_if_account_is_inactive(self) -> None:
|
||||
"""
|
||||
Проверка деструктивного изменения БД: отключение аккаунта блокирует вход
|
||||
"""
|
||||
|
||||
petrov = AccountModel.objects.get(id=AccountTestData.P_PETROV_ID)
|
||||
petrov.is_active = False
|
||||
petrov.save()
|
||||
|
||||
with self.assertRaises(UnauthorizedException) as context:
|
||||
SessionManager.authenticate(
|
||||
username="p.petrov@xeaf.net",
|
||||
password=AccountTestData.DEFAULT_PASSWORD
|
||||
)
|
||||
|
||||
self.assertIsInstance(context.exception.meta, dict)
|
||||
self.assertEqual(context.exception.meta['reason'], AuthRejectReason.ACCOUNT_IS_NOT_ACTIVE)
|
||||
|
||||
def test_should_find_real_ivanov_session_by_token(self) -> None:
|
||||
"""
|
||||
Поиск существующей и активной сессии по токену из фикстуры
|
||||
"""
|
||||
|
||||
session = SessionManager.find_by_token(SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
self.assertIsNotNone(session)
|
||||
self.assertIsInstance(session, SessionModel)
|
||||
self.assertEqual(session.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
self.assertEqual(session.account_id, AccountTestData.I_IVANOV_ID)
|
||||
|
||||
def test_should_return_none_for_non_existent_token(self) -> None:
|
||||
"""
|
||||
Запрос несуществующего токена должен возвращать None
|
||||
"""
|
||||
|
||||
result = SessionManager.find_by_token("non_existent_token_garbage_data")
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_should_return_none_if_session_is_expired_in_db(self) -> None:
|
||||
"""
|
||||
Проверка деструктивного изменения БД: просроченная сессия не считывается
|
||||
"""
|
||||
|
||||
session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||
|
||||
# Искусственно сдвигаем дату экспирации в далекое прошлое прямо в базе данных
|
||||
session.expires_at = timezone.now() - datetime.timedelta(days=10)
|
||||
session.save()
|
||||
|
||||
result = SessionManager.find_by_token(SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_should_fail_finding_if_session_was_physically_deleted_from_db(self) -> None:
|
||||
"""
|
||||
Полное физическое удаление строки из БД корректно обрабатывается
|
||||
"""
|
||||
|
||||
SessionModel.objects.filter(token=SessionTestData.I_IVANOV_SESSION_TOKEN).delete()
|
||||
|
||||
result = SessionManager.find_by_token(SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@override_settings(SESSION_TTL=1800)
|
||||
def test_should_open_session_and_apply_override_settings_ttl(self) -> None:
|
||||
"""
|
||||
Открытие сессии должно использовать живой ConfigurationManager без заглушек
|
||||
"""
|
||||
|
||||
ivanov = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
|
||||
petrov = AccountModel.objects.get(id=AccountTestData.P_PETROV_ID)
|
||||
|
||||
# Создаем сессию имперсонации (Иванов под видом Петра)
|
||||
session = SessionManager.open_session(account=ivanov, actual_account=petrov, device_id="tablet_v1")
|
||||
|
||||
# Проверяем, что запись успешно материализовалась в ОЗУ
|
||||
db_session = SessionModel.objects.get(token=session.token)
|
||||
self.assertEqual(db_session.account, ivanov)
|
||||
self.assertEqual(db_session.actual_account, petrov)
|
||||
self.assertEqual(db_session.device_id, "tablet_v1")
|
||||
self.assertGreater(db_session.expires_at, timezone.now())
|
||||
|
||||
def test_should_physically_expire_session_on_close_session(self) -> None:
|
||||
"""
|
||||
Закрытие сессии переводит модель в статус expired прямо в БД
|
||||
"""
|
||||
|
||||
session = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
SessionManager.close_session(session)
|
||||
|
||||
# Перечитываем чистую запись из БД
|
||||
db_session = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
self.assertTrue(db_session.is_expired())
|
||||
|
||||
@override_settings(SESSION_TTL=9999)
|
||||
def test_should_extend_expiration_time_on_renew_session(self) -> None:
|
||||
"""
|
||||
Обновление сессии сдвигает expires_at вперед на величину из настроек
|
||||
"""
|
||||
|
||||
session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||
initial_expires_at = session.expires_at
|
||||
|
||||
SessionManager.renew_session(session)
|
||||
|
||||
db_session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||
self.assertGreater(db_session.expires_at, initial_expires_at)
|
||||
|
||||
def test_protected_create_session_model_writes_correct_record(self) -> None:
|
||||
"""
|
||||
Прямой вызов защищенного метода фабрики для хирургического теста записи
|
||||
"""
|
||||
|
||||
ivanov = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
|
||||
|
||||
# Запускаем защищенный метод фабрики в обход цепочек обновления TTL
|
||||
session = SessionManager._create_session_model(account=ivanov, actual_account=ivanov)
|
||||
|
||||
# Доказываем линтеру, что мы получили объект, и проверяем его наличие в БД
|
||||
self.assertIsInstance(session, SessionModel)
|
||||
self.assertTrue(SessionModel.objects.filter(token=session.token).exists())
|
||||
|
||||
@override_settings(AUTH_SESSION_MODEL="invalid_app.MissingModel")
|
||||
def test_protected_get_session_model_class_raises_on_missing_model(self) -> None:
|
||||
"""
|
||||
Защищенный метод разрешения класса должен падать, если модель не найдена
|
||||
"""
|
||||
|
||||
# Сбрасываем внутренний кэш класса в синглтоне менеджера перед тестом
|
||||
SessionManager._SessionManager__model_class = None
|
||||
|
||||
with self.assertRaises(ImproperlyConfigured) as context:
|
||||
SessionManager._get_session_model_class()
|
||||
|
||||
self.assertIn("must be set in settings.py", str(context.exception))
|
||||
@@ -1,12 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов классов моделей данных
|
||||
"""
|
||||
|
||||
from .account_model_tests import AccountModelTests
|
||||
from .session_model_tests import SessionModelTests
|
||||
@@ -1,56 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса AccountModelTests
|
||||
"""
|
||||
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.management.userstory import AccountTestData
|
||||
from net.xeaf.rack.models import AccountModel
|
||||
|
||||
|
||||
class AccountModelTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для класса AccountModel
|
||||
"""
|
||||
|
||||
def test_user_story_accounts_exist(self):
|
||||
"""
|
||||
Проверяет, что эталонные пользователи успешно созданы в User Story
|
||||
"""
|
||||
|
||||
# Ищем Ивана Иванова по фиксированному UUID
|
||||
ivanov = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
|
||||
|
||||
self.assertEqual(ivanov.username, "i.ivanov@xeaf.net")
|
||||
self.assertEqual(ivanov.full_name, "Иван И. Иванов")
|
||||
self.assertEqual(ivanov.get_short_name(), "Иван")
|
||||
|
||||
# Проверяем, что наше исправление JSONField (default=dict) работает:
|
||||
# Поле extra должно быть пустым словарем, а не None, готовым к .update()
|
||||
self.assertEqual(ivanov.extra, {})
|
||||
|
||||
def test_password_hashing_works(self):
|
||||
"""Проверяет, что пароли пользователей в User Story захешированы, а не лежат строкой"""
|
||||
ivanov = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
|
||||
|
||||
# Проверяем, что стандартный джанговский механизм проверки пароля работает
|
||||
self.assertTrue(ivanov.check_password(AccountTestData.DEFAULT_PASSWORD))
|
||||
# Гарантируем, что в базе лежит хэш, а не сырая строка
|
||||
self.assertNotEqual(ivanov.password, AccountTestData.DEFAULT_PASSWORD)
|
||||
|
||||
def test_username_uniqueness_constraint(self):
|
||||
"""Проверяет жесткое ограничение уникальности email-username"""
|
||||
# Попытка создать пользователя с дублирующим email Ивана должна намертво упасть
|
||||
duplicate_account = AccountModel()
|
||||
duplicate_account.username = "i.ivanov@xeaf.net"
|
||||
|
||||
with self.assertRaises(IntegrityError):
|
||||
duplicate_account.save()
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов классов расширений моделей данных
|
||||
"""
|
||||
|
||||
from .created_at_mixin_tests import CreatedAtMixinTests
|
||||
from .deleted_at_mixin_tests import DeletedAtMixinTests
|
||||
from .description_mixin_tests import DescriptionMixinTests
|
||||
from .expired_at_mixin_tests import ExpiresAtMixinTests
|
||||
from .slug_mixin_tests import SlugMixinTests
|
||||
from .updated_at_mixin_tests import UpdatedAtMixinTests
|
||||
from .uuid_id_mixin_tests import UuidIdMixinTests
|
||||
@@ -1,129 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CreatedAtMixinTests
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from net.xeaf.rack.models.mixins import CreatedAtMixin
|
||||
|
||||
|
||||
class CreatedAtMixinTests(TransactionTestCase):
|
||||
"""
|
||||
Тесты для миксина CreatedAtMixin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Создаем тестовую модель и таблицу для каждого теста
|
||||
"""
|
||||
|
||||
# Отключаем foreign keys
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||
|
||||
# Создаем тестовую модель
|
||||
class TestCreatedAtModel(CreatedAtMixin):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
app_label = 'test'
|
||||
db_table = 'test_created_at_mixin'
|
||||
|
||||
self.TestModel = TestCreatedAtModel
|
||||
|
||||
# Создаем таблицу
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(TestCreatedAtModel)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Удаляем таблицу после каждого теста
|
||||
"""
|
||||
|
||||
# Удаляем таблицу
|
||||
with connection.schema_editor() as editor:
|
||||
editor.delete_model(self.TestModel)
|
||||
|
||||
# Включаем foreign keys обратно
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = ON;')
|
||||
|
||||
def test_created_at_is_auto_now_add(self):
|
||||
"""
|
||||
Проверяем, что created_at автоматически заполняется при создании
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
self.assertIsNotNone(instance.created_at)
|
||||
self.assertIsInstance(instance.created_at, datetime)
|
||||
|
||||
def test_created_at_is_auto_now_add_true(self):
|
||||
"""
|
||||
Проверяем, что auto_now_add=True
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('created_at')
|
||||
self.assertIsInstance(field, models.DateTimeField)
|
||||
self.assertTrue(field.auto_now_add)
|
||||
|
||||
def test_created_at_editable_false(self):
|
||||
"""
|
||||
Проверяем, что editable=False
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('created_at')
|
||||
self.assertFalse(field.editable)
|
||||
|
||||
def test_different_instances_have_different_created_at(self):
|
||||
"""
|
||||
Проверяем, что у разных экземпляров разное время создания
|
||||
"""
|
||||
|
||||
instance1 = self.TestModel.objects.create(name='First')
|
||||
time.sleep(0.1)
|
||||
instance2 = self.TestModel.objects.create(name='Second')
|
||||
|
||||
self.assertNotEqual(instance1.created_at, instance2.created_at)
|
||||
self.assertLess(instance1.created_at, instance2.created_at)
|
||||
|
||||
def test_created_at_preserved_on_update(self):
|
||||
"""
|
||||
Проверяем, что created_at не меняется при обновлении
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
old_created_at = instance.created_at
|
||||
|
||||
instance.name = 'Updated Name'
|
||||
instance.save()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.created_at, old_created_at)
|
||||
|
||||
def test_created_at_not_null(self):
|
||||
"""
|
||||
Проверяем, что created_at не может быть NULL
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('created_at')
|
||||
self.assertFalse(field.null)
|
||||
|
||||
def test_verbose_name(self):
|
||||
"""
|
||||
Проверяем verbose_name поля
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('created_at')
|
||||
self.assertIsInstance(field, models.Field)
|
||||
self.assertEqual(field.verbose_name, "rack.mixin.created_at")
|
||||
@@ -1,251 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CreatedAtMixinTests
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.test import TransactionTestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.models.mixins import DeletedAtMixin
|
||||
|
||||
|
||||
class DeletedAtMixinTests(TransactionTestCase):
|
||||
"""
|
||||
Тесты для миксина DeletedAtMixin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Создаем тестовую модель и таблицу для каждого теста
|
||||
"""
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||
|
||||
class TestDeletedAtModelModel(DeletedAtMixin):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
app_label = 'test'
|
||||
db_table = 'test_deleted_at_mixin'
|
||||
|
||||
self.TestModel = TestDeletedAtModelModel
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(TestDeletedAtModelModel)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Удаляем таблицу после каждого теста
|
||||
"""
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.delete_model(self.TestModel)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = ON;')
|
||||
|
||||
def test_deleted_at_null_by_default(self):
|
||||
"""
|
||||
Проверяем, что при создании deleted_at = None
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_soft_delete_sets_current_time(self):
|
||||
"""
|
||||
Проверяем, что soft_delete() устанавливает текущее время
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
before = timezone.now()
|
||||
|
||||
instance.soft_delete(save=False)
|
||||
|
||||
self.assertIsNotNone(instance.deleted_at)
|
||||
self.assertGreaterEqual(instance.deleted_at, before)
|
||||
self.assertLessEqual(instance.deleted_at, timezone.now())
|
||||
|
||||
def test_soft_delete_saves_to_db_when_save_true(self):
|
||||
"""
|
||||
Проверяем, что при soft_delete(save=True) значение сохраняется в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=True)
|
||||
|
||||
# Обновляем из БД
|
||||
instance.refresh_from_db()
|
||||
self.assertIsNotNone(instance.deleted_at)
|
||||
|
||||
def test_soft_delete_does_not_save_when_save_false(self):
|
||||
"""
|
||||
Проверяем, что при soft_delete(save=False) значение НЕ сохраняется в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=False)
|
||||
|
||||
# Обновляем из БД
|
||||
instance.refresh_from_db()
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_soft_delete_saves_by_default(self):
|
||||
"""
|
||||
Проверяем, что по умолчанию soft_delete() сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete()
|
||||
|
||||
instance.refresh_from_db()
|
||||
self.assertIsNotNone(instance.deleted_at)
|
||||
|
||||
def test_is_deleted_returns_false_when_not_deleted(self):
|
||||
"""
|
||||
Проверяем, что is_deleted() возвращает False для активной записи
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
self.assertFalse(instance.is_deleted())
|
||||
|
||||
def test_is_deleted_returns_true_when_deleted(self):
|
||||
"""
|
||||
Проверяем, что is_deleted() возвращает True для удаленной записи
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=False)
|
||||
self.assertTrue(instance.is_deleted())
|
||||
|
||||
def test_undelete_restores_record(self):
|
||||
"""
|
||||
Проверяем, что undelete() восстанавливает запись
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=False)
|
||||
self.assertTrue(instance.is_deleted())
|
||||
|
||||
instance.undelete(save=False)
|
||||
self.assertFalse(instance.is_deleted())
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_undelete_saves_to_db_when_save_true(self):
|
||||
"""
|
||||
Проверяем, что при undelete(save=True) значение сохраняется в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=True)
|
||||
self.assertTrue(instance.is_deleted())
|
||||
|
||||
instance.undelete(save=True)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertFalse(instance.is_deleted())
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_undelete_does_not_save_when_save_false(self):
|
||||
"""
|
||||
Проверяем, что при undelete(save=False) значение НЕ сохраняется в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=True)
|
||||
self.assertTrue(instance.is_deleted())
|
||||
|
||||
instance.undelete(save=False)
|
||||
instance.refresh_from_db()
|
||||
|
||||
# В БД все еще deleted_at не None
|
||||
self.assertTrue(instance.is_deleted())
|
||||
self.assertIsNotNone(instance.deleted_at)
|
||||
|
||||
def test_undelete_saves_by_default(self):
|
||||
"""
|
||||
Проверяем, что по умолчанию undelete() сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.soft_delete(save=True)
|
||||
|
||||
instance.undelete()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertFalse(instance.is_deleted())
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_can_set_deleted_at_manually(self):
|
||||
"""
|
||||
Проверяем, что можно установить deleted_at вручную
|
||||
"""
|
||||
|
||||
custom_time = timezone.now() - timedelta(days=1)
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.deleted_at = custom_time
|
||||
instance.save()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.deleted_at, custom_time)
|
||||
|
||||
def test_multiple_soft_deletes_update_time(self):
|
||||
"""
|
||||
Проверяем, что повторный вызов soft_delete() обновляет время
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
|
||||
instance.soft_delete(save=False)
|
||||
first_time = instance.deleted_at
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
instance.soft_delete(save=False)
|
||||
second_time = instance.deleted_at
|
||||
|
||||
self.assertNotEqual(first_time, second_time)
|
||||
self.assertGreater(second_time, first_time)
|
||||
|
||||
def test_undelete_after_manual_delete(self):
|
||||
"""
|
||||
Проверяем, что undelete() работает после ручной установки deleted_at
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.deleted_at = timezone.now() - timedelta(days=1)
|
||||
self.assertTrue(instance.is_deleted())
|
||||
|
||||
instance.undelete(save=False)
|
||||
self.assertFalse(instance.is_deleted())
|
||||
self.assertIsNone(instance.deleted_at)
|
||||
|
||||
def test_soft_delete_twice_with_save(self):
|
||||
"""
|
||||
Проверяем, что повторный soft_delete с save=True корректно работает
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
|
||||
instance.soft_delete(save=True)
|
||||
first_time = instance.deleted_at
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
instance.soft_delete(save=True)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertNotEqual(instance.deleted_at, first_time)
|
||||
self.assertGreater(instance.deleted_at, first_time)
|
||||
@@ -1,59 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса DescriptionMixinTests
|
||||
"""
|
||||
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from net.xeaf.rack.models.mixins import DescriptionMixin
|
||||
|
||||
|
||||
class DescriptionMixinTests(TransactionTestCase):
|
||||
"""
|
||||
Тесты для миксина DescriptionMixin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Создаем тестовую модель и таблицу для каждого теста
|
||||
"""
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||
|
||||
class TestDescriptionModel(DescriptionMixin):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
app_label = 'test'
|
||||
db_table = 'test_slug_mixin'
|
||||
|
||||
self.TestModel = TestDescriptionModel
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(TestDescriptionModel)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Удаляем таблицу после каждого теста
|
||||
"""
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.delete_model(self.TestModel)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = ON;')
|
||||
|
||||
def test_description_field_exists(self):
|
||||
"""
|
||||
Проверяем, что поле description добавлено в модель
|
||||
"""
|
||||
|
||||
self.assertTrue(hasattr(self.TestModel, 'description'))
|
||||
@@ -1,332 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ExpiredAtMixinTests
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.db.models.fields import DateTimeField
|
||||
from django.test import TransactionTestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.models.mixins import ExpiresAtMixin
|
||||
|
||||
|
||||
class ExpiresAtMixinTests(TransactionTestCase):
|
||||
"""
|
||||
Тесты для миксина ExpiresAtMixin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Создаем тестовую модель и таблицу для каждого теста
|
||||
"""
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||
|
||||
class TestExpiredAtModel(ExpiresAtMixin):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
app_label = 'test'
|
||||
db_table = 'test_expires_at_mixin'
|
||||
|
||||
self.TestModel = TestExpiredAtModel
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(TestExpiredAtModel)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Удаляем таблицу после каждого теста
|
||||
"""
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.delete_model(self.TestModel)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = ON;')
|
||||
|
||||
def test_expires_at_not_required(self):
|
||||
"""
|
||||
Проверяем, что expires_at не обязательное поле
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('expires_at')
|
||||
self.assertTrue(field.null)
|
||||
self.assertIsInstance(field, DateTimeField)
|
||||
self.assertTrue(field.blank)
|
||||
|
||||
def test_expires_at_with_none_never_expires(self):
|
||||
"""
|
||||
Проверяем, что при expires_at == None срок действия не ограничен
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=None
|
||||
)
|
||||
self.assertFalse(instance.is_expired())
|
||||
|
||||
def test_renew_sets_expiration_time(self):
|
||||
"""
|
||||
Проверяем, что renew() устанавливает время истечения
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
before = timezone.now()
|
||||
instance.renew(seconds=3600, save=False)
|
||||
|
||||
self.assertIsNotNone(instance.expires_at)
|
||||
self.assertGreaterEqual(instance.expires_at, before + timedelta(seconds=3600))
|
||||
self.assertLessEqual(instance.expires_at, timezone.now() + timedelta(seconds=3600))
|
||||
|
||||
def test_renew_saves_to_db_when_save_true(self):
|
||||
"""
|
||||
Проверяем, что renew(save=True) сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
instance.renew(seconds=3600, save=True)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertGreaterEqual(instance.expires_at, timezone.now())
|
||||
|
||||
def test_renew_does_not_save_when_save_false(self):
|
||||
"""
|
||||
Проверяем, что renew(save=False) НЕ сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
old_expires_at = instance.expires_at
|
||||
|
||||
instance.renew(seconds=3600, save=False)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.expires_at, old_expires_at)
|
||||
|
||||
def test_renew_raises_error_for_negative_seconds(self):
|
||||
"""
|
||||
Проверяем, что renew() выбрасывает ошибку для отрицательного значения
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
instance.renew(seconds=-1)
|
||||
|
||||
self.assertIn('greater than 0', str(context.exception))
|
||||
|
||||
def test_renew_raises_error_for_zero_seconds(self):
|
||||
"""
|
||||
Проверяем, что renew() выбрасывает ошибку для нулевого значения
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
instance.renew(seconds=0)
|
||||
|
||||
self.assertIn('greater than 0', str(context.exception))
|
||||
|
||||
def test_expire_sets_time_to_now(self):
|
||||
"""
|
||||
Проверяем, что expire() устанавливает время истечения на сейчас
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
before = timezone.now()
|
||||
instance.expire(save=False)
|
||||
|
||||
self.assertIsNotNone(instance.expires_at)
|
||||
self.assertGreaterEqual(instance.expires_at, before)
|
||||
self.assertLessEqual(instance.expires_at, timezone.now())
|
||||
|
||||
def test_expire_saves_to_db_when_save_true(self):
|
||||
"""
|
||||
Проверяем, что expire(save=True) сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
instance.expire(save=True)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertLessEqual(instance.expires_at, timezone.now())
|
||||
|
||||
def test_expire_does_not_save_when_save_false(self):
|
||||
"""
|
||||
Проверяем, что expire(save=False) НЕ сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
old_expires_at = instance.expires_at
|
||||
|
||||
instance.expire(save=False)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.expires_at, old_expires_at)
|
||||
|
||||
def test_is_expired_returns_false_when_not_expired(self):
|
||||
"""
|
||||
Проверяем, что is_expired() возвращает False для активной записи
|
||||
"""
|
||||
|
||||
future_time = timezone.now() + timedelta(days=1)
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=future_time
|
||||
)
|
||||
|
||||
self.assertFalse(instance.is_expired())
|
||||
|
||||
def test_is_expired_returns_true_when_expired(self):
|
||||
"""
|
||||
Проверяем, что is_expired() возвращает True для просроченной записи
|
||||
"""
|
||||
|
||||
past_time = timezone.now() - timedelta(days=1)
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=past_time
|
||||
)
|
||||
|
||||
self.assertTrue(instance.is_expired())
|
||||
|
||||
def test_is_expired_after_renew(self):
|
||||
"""
|
||||
Проверяем, что is_expired() корректно работает после renew()
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() - timedelta(days=1)
|
||||
)
|
||||
self.assertTrue(instance.is_expired())
|
||||
|
||||
instance.renew(seconds=3600, save=False)
|
||||
self.assertFalse(instance.is_expired())
|
||||
|
||||
def test_is_expired_after_expire(self):
|
||||
"""
|
||||
Проверяем, что is_expired() корректно работает после expire()
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
self.assertFalse(instance.is_expired())
|
||||
|
||||
instance.expire(save=False)
|
||||
self.assertTrue(instance.is_expired())
|
||||
|
||||
def test_renew_with_large_seconds(self):
|
||||
"""
|
||||
Проверяем, что renew() работает с большими значениями
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
|
||||
seconds = 86400 * 365 # 1 год
|
||||
instance.renew(seconds=seconds, save=False)
|
||||
|
||||
expected = timezone.now() + timedelta(seconds=seconds)
|
||||
self.assertLessEqual(instance.expires_at, expected + timedelta(seconds=1))
|
||||
self.assertGreaterEqual(instance.expires_at, expected - timedelta(seconds=1))
|
||||
|
||||
def test_renew_saves_by_default_false(self):
|
||||
"""
|
||||
Проверяем, что по умолчанию renew() НЕ сохраняет в БД
|
||||
(save=False по умолчанию, в отличие от soft_delete)
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
old_expires_at = instance.expires_at
|
||||
|
||||
instance.renew(seconds=3600)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.expires_at, old_expires_at)
|
||||
|
||||
def test_expire_saves_by_default_false(self):
|
||||
"""
|
||||
Проверяем, что по умолчанию expire() НЕ сохраняет в БД
|
||||
(save=False по умолчанию, в отличие от soft_delete)
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=timezone.now() + timedelta(days=1)
|
||||
)
|
||||
old_expires_at = instance.expires_at
|
||||
|
||||
instance.expire()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.expires_at, old_expires_at)
|
||||
|
||||
def test_expires_at_editable_true(self):
|
||||
"""
|
||||
Проверяем, что поле expires_at редактируемое
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('expires_at')
|
||||
self.assertTrue(field.editable)
|
||||
|
||||
def test_can_set_expires_at_manually(self):
|
||||
"""
|
||||
Проверяем, что можно установить expires_at вручную
|
||||
"""
|
||||
|
||||
custom_time = timezone.now() + timedelta(days=5)
|
||||
|
||||
instance = self.TestModel.objects.create(
|
||||
name='Test Instance',
|
||||
expires_at=custom_time
|
||||
)
|
||||
instance.save()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertEqual(instance.expires_at, custom_time)
|
||||
@@ -1,59 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса SlugMixinTests
|
||||
"""
|
||||
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from net.xeaf.rack.models.mixins import SlugMixin
|
||||
|
||||
|
||||
class SlugMixinTests(TransactionTestCase):
|
||||
"""
|
||||
Тесты для миксина SlugMixin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Создаем тестовую модель и таблицу для каждого теста
|
||||
"""
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||
|
||||
class TestSlugModel(SlugMixin):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
app_label = 'test'
|
||||
db_table = 'test_slug_mixin'
|
||||
|
||||
self.TestModel = TestSlugModel
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(TestSlugModel)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Удаляем таблицу после каждого теста
|
||||
"""
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.delete_model(self.TestModel)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = ON;')
|
||||
|
||||
def test_slug_field_exists(self):
|
||||
"""
|
||||
Проверяем, что поле slug добавлено в модель
|
||||
"""
|
||||
|
||||
self.assertTrue(hasattr(self.TestModel, 'slug'))
|
||||
@@ -1,182 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса UpdatedAtMixinTests
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.test import TransactionTestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.models.mixins import UpdatedAtMixin
|
||||
|
||||
|
||||
class UpdatedAtMixinTests(TransactionTestCase):
|
||||
"""
|
||||
Тесты для миксина UpdatedAtMixin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Создаем тестовую модель и таблицу для каждого теста
|
||||
"""
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||
|
||||
class TestUpdatedAtModel(UpdatedAtMixin):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
app_label = 'test'
|
||||
db_table = 'test_updated_at_mixin'
|
||||
|
||||
self.TestModel = TestUpdatedAtModel
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(TestUpdatedAtModel)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Удаляем таблицу после каждого теста
|
||||
"""
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.delete_model(self.TestModel)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = ON;')
|
||||
|
||||
def test_updated_at_null_by_default(self):
|
||||
"""
|
||||
Проверяем, что при создании updated_at = None
|
||||
"""
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
self.assertIsNone(instance.updated_at)
|
||||
|
||||
def test_update_sets_current_time(self):
|
||||
"""
|
||||
Проверяем, что update() устанавливает текущее время
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
before = timezone.now()
|
||||
|
||||
instance.update(save=False)
|
||||
|
||||
self.assertIsNotNone(instance.updated_at)
|
||||
self.assertGreaterEqual(instance.updated_at, before)
|
||||
self.assertLessEqual(instance.updated_at, timezone.now())
|
||||
|
||||
def test_update_saves_to_db_when_save_true(self):
|
||||
"""
|
||||
Проверяем, что update(save=True) сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.update(save=True)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertIsNotNone(instance.updated_at)
|
||||
|
||||
def test_update_does_not_save_when_save_false(self):
|
||||
"""
|
||||
Проверяем, что update(save=False) НЕ сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.update(save=False)
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertIsNone(instance.updated_at)
|
||||
|
||||
def test_update_saves_by_default(self):
|
||||
"""
|
||||
Проверяем, что по умолчанию update() сохраняет в БД
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.update()
|
||||
instance.refresh_from_db()
|
||||
|
||||
self.assertIsNotNone(instance.updated_at)
|
||||
|
||||
def test_is_updated_returns_false_when_not_updated(self):
|
||||
"""
|
||||
Проверяем, что is_updated() возвращает False для новой записи
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
self.assertFalse(instance.is_updated())
|
||||
|
||||
def test_is_updated_returns_true_when_updated(self):
|
||||
"""
|
||||
Проверяем, что is_updated() возвращает True после update()
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.update(save=False)
|
||||
self.assertTrue(instance.is_updated())
|
||||
|
||||
def test_is_updated_since_returns_false_when_not_updated(self):
|
||||
"""
|
||||
Проверяем, что is_updated_since() возвращает False для новой записи
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
dt = timezone.now() - timedelta(minutes=5)
|
||||
|
||||
self.assertFalse(instance.is_updated_since(dt))
|
||||
|
||||
def test_is_updated_since_returns_true_after_update(self):
|
||||
"""
|
||||
Проверяем, что is_updated_since() возвращает True после update()
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
dt = timezone.now()
|
||||
|
||||
time.sleep(0.1)
|
||||
instance.update(save=False)
|
||||
|
||||
self.assertTrue(instance.is_updated_since(dt))
|
||||
|
||||
def test_is_updated_since_returns_false_if_updated_before_dt(self):
|
||||
"""
|
||||
Проверяем, что is_updated_since() возвращает False,
|
||||
если обновление было раньше указанного времени
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
instance.update(save=False)
|
||||
old_updated_at = instance.updated_at
|
||||
|
||||
# Проверяем, что updated_at был раньше, чем через секунду
|
||||
dt = old_updated_at + timedelta(seconds=1)
|
||||
self.assertFalse(instance.is_updated_since(dt))
|
||||
|
||||
def test_multiple_updates(self):
|
||||
"""
|
||||
Проверяем, что повторный вызов update() обновляет время
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
|
||||
instance.update(save=False)
|
||||
first_time = instance.updated_at
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
instance.update(save=False)
|
||||
second_time = instance.updated_at
|
||||
|
||||
self.assertNotEqual(first_time, second_time)
|
||||
self.assertGreater(second_time, first_time)
|
||||
@@ -1,100 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса UuidIdMixinTests
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from net.xeaf.rack.models.mixins import UuidIdMixin
|
||||
|
||||
|
||||
class UuidIdMixinTests(TransactionTestCase):
|
||||
"""
|
||||
Тесты для миксина UuidIdMixin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Создаем тестовую модель и таблицу для каждого теста
|
||||
"""
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = OFF;')
|
||||
|
||||
class TestUuidModel(UuidIdMixin):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
app_label = 'test'
|
||||
db_table = 'test_uuid_mixin'
|
||||
|
||||
self.TestModel = TestUuidModel
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(TestUuidModel)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Удаляем таблицу после каждого теста
|
||||
"""
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.delete_model(self.TestModel)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('PRAGMA foreign_keys = ON;')
|
||||
|
||||
def test_id_is_uuid_by_default(self):
|
||||
"""
|
||||
Проверяем, что при создании id автоматически генерируется как UUID
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
|
||||
self.assertIsNotNone(instance.id)
|
||||
self.assertIsInstance(instance.id, uuid.UUID)
|
||||
|
||||
def test_id_is_unique(self):
|
||||
"""
|
||||
Проверяем, что каждый экземпляр получает уникальный UUID
|
||||
"""
|
||||
|
||||
instance1 = self.TestModel.objects.create(name='First')
|
||||
instance2 = self.TestModel.objects.create(name='Second')
|
||||
|
||||
self.assertNotEqual(instance1.id, instance2.id)
|
||||
|
||||
def test_id_is_primary_key(self):
|
||||
"""
|
||||
Проверяем, что id является первичным ключом
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('id')
|
||||
self.assertTrue(field.primary_key)
|
||||
|
||||
def test_id_is_editable_false(self):
|
||||
"""
|
||||
Проверяем, что id нельзя редактировать
|
||||
"""
|
||||
|
||||
field = self.TestModel._meta.get_field('id')
|
||||
self.assertFalse(field.editable)
|
||||
|
||||
def test_id_can_be_used_in_queries(self):
|
||||
"""
|
||||
Проверяем, что можно искать по UUID
|
||||
"""
|
||||
|
||||
instance = self.TestModel.objects.create(name='Test Instance')
|
||||
|
||||
found = self.TestModel.objects.get(id=instance.id)
|
||||
self.assertEqual(found.id, instance.id)
|
||||
@@ -1,16 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов классов ответов по протоколу HTTP
|
||||
"""
|
||||
|
||||
from .bool_response_tests import BoolResponseTests
|
||||
from .data_response_tests import DataResponseTests
|
||||
from .empty_response_tests import EmptyResponseTests
|
||||
from .error_response_tests import ErrorResponseTests
|
||||
from .exception_response_tests import ExceptionResponseTests
|
||||
from .list_response_tests import ListResponseTests
|
||||
@@ -1,51 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса BoolResponseTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.models.responses import BoolResponse
|
||||
|
||||
|
||||
class BoolResponseTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса BoolResponse
|
||||
"""
|
||||
|
||||
def test_init_with_true_value(self):
|
||||
"""
|
||||
Проверяет корректное создание ответа со значением True
|
||||
"""
|
||||
|
||||
test_meta = {"action": "verify"}
|
||||
|
||||
response = BoolResponse(value=True, meta=test_meta)
|
||||
|
||||
# Проверяем заголовки и статус
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["status"], status.HTTP_200_OK)
|
||||
|
||||
# Булево значение должно лежать в ключе "data", так как код 200 (< 400)
|
||||
self.assertIs(response.data["data"], True)
|
||||
self.assertEqual(response.data["meta"], test_meta)
|
||||
|
||||
def test_init_with_false_value(self):
|
||||
"""
|
||||
Проверяет корректное создание ответа со значением False
|
||||
"""
|
||||
|
||||
response = BoolResponse(value=False)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["status"], status.HTTP_200_OK)
|
||||
|
||||
# Проверяем, что False не интерпретируется как отсутствие данных
|
||||
self.assertIs(response.data["data"], False)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса DataResponseTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.models.responses import DataResponse
|
||||
|
||||
|
||||
class DataResponseTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса DataResponse
|
||||
"""
|
||||
|
||||
def test_init_with_dict_data(self):
|
||||
"""
|
||||
Проверяет корректное создание ответа со словарем данных и дефолтными статус-кодами
|
||||
"""
|
||||
|
||||
test_dict = {"id": 42, "name": "Main Project", "is_active": True}
|
||||
test_meta = {"version": "1.0.0"}
|
||||
|
||||
response = DataResponse(data=test_dict, meta=test_meta)
|
||||
|
||||
# Проверяем честный HTTP-статус в заголовках
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Проверяем контракт тела JSON
|
||||
self.assertEqual(response.data["status"], status.HTTP_200_OK)
|
||||
# Так как код 200 (< 400), объект обязан лежать именно в "data"
|
||||
self.assertEqual(response.data["data"], test_dict)
|
||||
|
||||
# Проверяем проброс метаданных
|
||||
self.assertEqual(response.data["meta"], test_meta)
|
||||
@@ -1,39 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса EmptyResponseTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.models.responses import EmptyResponse
|
||||
|
||||
|
||||
class EmptyResponseTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса EmptyResponse
|
||||
"""
|
||||
|
||||
def test_init_sugar_configuration(self):
|
||||
"""
|
||||
Проверяет жестко зашитую конфигурацию пустого успешного ответа
|
||||
"""
|
||||
|
||||
response = EmptyResponse()
|
||||
|
||||
# Проверяем честный HTTP-статус в заголовках
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Проверяем контракт тела ответа в JSON
|
||||
self.assertEqual(response.data["status"], status.HTTP_200_OK)
|
||||
self.assertIn("timestamp", response.data)
|
||||
|
||||
# Гарантируем, что ключи данных и деталей не создались, так как data=None
|
||||
self.assertNotIn("data", response.data)
|
||||
self.assertNotIn("detail", response.data)
|
||||
self.assertNotIn("meta", response.data)
|
||||
@@ -1,65 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ErrorResponseTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.models.responses import ErrorResponse
|
||||
|
||||
|
||||
class ErrorResponseTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса ErrorResponse
|
||||
"""
|
||||
|
||||
def test_calc_external_status_for_client_error(self):
|
||||
"""
|
||||
Ошибки типа 4xx (меньше 500) должны маскироваться под 200 OK
|
||||
"""
|
||||
|
||||
# Вызываем protected метод класса напрямую
|
||||
result = ErrorResponse._calc_external_status(status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(result, status.HTTP_200_OK)
|
||||
|
||||
result_403 = ErrorResponse._calc_external_status(status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(result_403, status.HTTP_200_OK)
|
||||
|
||||
def test_calc_external_status_for_server_error(self):
|
||||
"""
|
||||
Ошибки типа 5xx (500 и выше) должны возвращать свой честный HTTP код
|
||||
"""
|
||||
|
||||
result = ErrorResponse._calc_external_status(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
self.assertEqual(result, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
result_503 = ErrorResponse._calc_external_status(status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
self.assertEqual(result_503, status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
|
||||
def test_init_integration(self):
|
||||
"""
|
||||
Проверяет базовый контракт сборки ответа с ошибкой
|
||||
"""
|
||||
|
||||
error_msg = "Доступ запрещен"
|
||||
internal_code = status.HTTP_403_FORBIDDEN
|
||||
|
||||
response = ErrorResponse(
|
||||
status_code=internal_code,
|
||||
detail=error_msg
|
||||
)
|
||||
|
||||
# Проверяем, что метод класса отработал внутри init и вернул 200 OK в заголовки
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Проверяем структуру тела ответа
|
||||
self.assertEqual(response.data["status"], internal_code)
|
||||
# Так как код ошибки >= 400, данные обязаны лежать в ключе "detail"
|
||||
self.assertEqual(response.data["detail"], error_msg)
|
||||
self.assertNotIn("data", response.data)
|
||||
@@ -1,64 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ExceptionResponseTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.core import CoreException
|
||||
from net.xeaf.rack.models.responses import ExceptionResponse
|
||||
|
||||
|
||||
class ExceptionResponseTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса ExceptionResponse
|
||||
"""
|
||||
|
||||
def test_init_with_standard_exception(self):
|
||||
"""
|
||||
Если у исключения is_pure_status() == False,
|
||||
внешний HTTP статус должен быть 200 OK, но внутренний — реальный код ошибки,
|
||||
а данные обязаны лежать в ключе 'detail'
|
||||
"""
|
||||
|
||||
# Симулируем обычную ошибку (is_pure_status по умолчанию выдает False)
|
||||
exc = CoreException(status_code=status.HTTP_400_BAD_REQUEST, detail="Неверный логин")
|
||||
|
||||
response = ExceptionResponse(exc=exc)
|
||||
|
||||
# Проверяем маскировку под 200 OK для фронтенда в заголовках
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Проверяем JSON контракт
|
||||
self.assertEqual(response.data["status"], status.HTTP_400_BAD_REQUEST)
|
||||
# Так как реальный статус ошибки >= 400, данные ДОЛЖНЫ быть в detail
|
||||
self.assertEqual(response.data["detail"], "Неверный логин")
|
||||
self.assertNotIn("data", response.data)
|
||||
|
||||
def test_init_with_pure_http_exception(self):
|
||||
"""
|
||||
Если у исключения is_pure_status() == True,
|
||||
и внешний HTTP статус, и внутренний код должны быть равны коду ошибки
|
||||
"""
|
||||
|
||||
# Создаем тестовый класс, переопределяющий метод на True (как твой CriticalException)
|
||||
class PureException(CoreException):
|
||||
def is_pure_status(self) -> bool:
|
||||
return True
|
||||
|
||||
exc = PureException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Крах БД")
|
||||
|
||||
response = ExceptionResponse(exc=exc)
|
||||
|
||||
# Проверяем, что маскировка отключилась и в заголовках честный HTTP статус
|
||||
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# Внутренний контракт тоже содержит реальный код ошибки
|
||||
self.assertEqual(response.data["status"], status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
self.assertEqual(response.data["detail"], "Крах БД")
|
||||
@@ -1,41 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ListResponseTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.models.responses import ListResponse
|
||||
|
||||
|
||||
class ListResponseTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для класса ListResponse
|
||||
"""
|
||||
|
||||
def test_init_with_list_data(self):
|
||||
"""
|
||||
Проверяет корректное создание ответа со списком и дефолтными статус-кодами
|
||||
"""
|
||||
|
||||
test_list = [{"id": 1, "name": "Project A"}, {"id": 2, "name": "Project B"}]
|
||||
test_meta = {"total_count": 2}
|
||||
|
||||
response = ListResponse(data=test_list, meta=test_meta)
|
||||
|
||||
# Проверяем честный HTTP-статус в заголовках
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Проверяем контракт тела JSON
|
||||
self.assertEqual(response.data["status"], status.HTTP_200_OK)
|
||||
# Так как код 200 (< 400), список обязан лежать именно в "data"
|
||||
self.assertEqual(response.data["data"], test_list)
|
||||
|
||||
# Проверяем, что метаданные тоже успешно долетели через super()
|
||||
self.assertEqual(response.data["meta"], test_meta)
|
||||
@@ -1,64 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса SessionModelTests
|
||||
"""
|
||||
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.management.userstory import AccountTestData
|
||||
from net.xeaf.rack.management.userstory import SessionTestData
|
||||
from net.xeaf.rack.models import SessionModel
|
||||
|
||||
|
||||
class SessionModelTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для класса SessionModel
|
||||
"""
|
||||
|
||||
def test_user_story_sessions_exist_and_linked(self):
|
||||
"""
|
||||
Проверяет, что сессии из User Story созданы и правильно связаны с аккаунтами
|
||||
"""
|
||||
|
||||
# Ищем сессию Ивана по фиксированному токену
|
||||
session = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
# Проверяем связи (включая твой крутой механизм имперсонации)
|
||||
self.assertEqual(session.account_id, AccountTestData.I_IVANOV_ID)
|
||||
self.assertEqual(session.actual_account_id, AccountTestData.I_IVANOV_ID)
|
||||
|
||||
# Проверяем дефолты текстовых полей, которые мы закладывали в модель
|
||||
self.assertEqual(session.device_id, "WEB")
|
||||
self.assertIsNone(session.fcm_token)
|
||||
|
||||
def test_session_expiration_logic(self):
|
||||
"""
|
||||
Проверяет, что метод renew() правильно выставил expires_at в будущем
|
||||
"""
|
||||
|
||||
session = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
# Сессия должна быть активной прямо сейчас
|
||||
self.assertGreater(session.expires_at, timezone.now())
|
||||
|
||||
def test_token_uniqueness_constraint(self):
|
||||
"""
|
||||
Проверяет жесткое ограничение уникальности токенов сессий
|
||||
"""
|
||||
|
||||
# Попытка создать новую сессию с токеном Петра должна упасть
|
||||
duplicate_session = SessionModel()
|
||||
duplicate_session.token = SessionTestData.P_PETROV_SESSION_TOKEN
|
||||
duplicate_session.account_id = AccountTestData.I_IVANOV_ID
|
||||
duplicate_session.actual_account_id = AccountTestData.I_IVANOV_ID
|
||||
duplicate_session.expire()
|
||||
|
||||
with self.assertRaises(IntegrityError):
|
||||
duplicate_session.save()
|
||||
@@ -1,10 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов для служебных классов и утилит
|
||||
"""
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов классов авторизации
|
||||
"""
|
||||
|
||||
from .expiring_token_authentication_tests import ExpiringTokenAuthenticationTests
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ExpiringTokenAuthenticationTests
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils import translation
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from net.xeaf.rack.core.tests import CoreTestCase
|
||||
from net.xeaf.rack.management.userstory import AccountTestData
|
||||
from net.xeaf.rack.management.userstory import SessionTestData
|
||||
from net.xeaf.rack.models import AccountModel
|
||||
from net.xeaf.rack.models import SessionModel
|
||||
from net.xeaf.rack.utils.auth import ExpiringTokenAuthentication
|
||||
|
||||
|
||||
class ExpiringTokenAuthenticationTests(CoreTestCase):
|
||||
"""
|
||||
Тесты для ExpiringTokenAuthentication
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Подготовка к тестированию
|
||||
"""
|
||||
|
||||
super().setUp()
|
||||
self.auth = ExpiringTokenAuthentication()
|
||||
self.factory = APIRequestFactory()
|
||||
self.old_language = translation.get_language()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
"""
|
||||
Восстанавливаем язык глобального контекста после тестов
|
||||
"""
|
||||
|
||||
super().tearDown()
|
||||
translation.activate(self.old_language)
|
||||
|
||||
@classmethod
|
||||
def _wrap_to_drf_request(cls, wsgi_request) -> Request:
|
||||
"""
|
||||
Вспомогательный метод для честного оборачивания Django WSGIRequest в DRF Request
|
||||
"""
|
||||
|
||||
return Request(wsgi_request, parsers=[JSONParser()])
|
||||
|
||||
def test_authenticate_adjusts_language_via_query_param(self) -> None:
|
||||
"""
|
||||
Должен переключить глобальную локаль Django, используя механизмы LocaleManager
|
||||
"""
|
||||
|
||||
# 1. Создаем нативный запрос Django
|
||||
wsgi_req = self.factory.get("/", {"x-ui-language": "en"})
|
||||
# 2. Оборачиваем его в честный DRF Request, у которого ЕСТЬ query_params
|
||||
drf_request = self._wrap_to_drf_request(wsgi_req)
|
||||
|
||||
self.auth.authenticate(drf_request)
|
||||
|
||||
# Проверяем, что LocaleManager отработал "вживую" и активировал перевод
|
||||
self.assertEqual(translation.get_language(), "en")
|
||||
self.assertEqual(getattr(drf_request, "LANGUAGE_CODE", None), "EN")
|
||||
|
||||
def test_authenticate_extracts_token_from_query_params_and_logs_in(self) -> None:
|
||||
"""
|
||||
Должен успешно авторизовать Ивана Иванова, если его токен передан в GET параметре
|
||||
"""
|
||||
|
||||
wsgi_req = self.factory.get("/", {ExpiringTokenAuthentication.AUTH_TOKEN_PARAM: SessionTestData.I_IVANOV_SESSION_TOKEN})
|
||||
drf_request = self._wrap_to_drf_request(wsgi_req)
|
||||
|
||||
result = self.auth.authenticate(drf_request)
|
||||
|
||||
# Линтер-безопасная проверка контейнера
|
||||
self.assertIsInstance(result, tuple)
|
||||
assert result is not None
|
||||
|
||||
account, session = result
|
||||
self.assertEqual(account.id, AccountTestData.I_IVANOV_ID)
|
||||
self.assertEqual(session.token, SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
def test_authenticate_fallback_to_super_headers_if_no_query_param(self) -> None:
|
||||
"""
|
||||
Должен отработать через стандартный заголовок 'Authorization: Token ...' родительского класса
|
||||
"""
|
||||
|
||||
wsgi_req = self.factory.get("/", HTTP_AUTHORIZATION=f"Token {SessionTestData.P_PETROV_SESSION_TOKEN}")
|
||||
drf_request = self._wrap_to_drf_request(wsgi_req)
|
||||
|
||||
result = self.auth.authenticate(drf_request)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
assert result is not None
|
||||
|
||||
account, session = result
|
||||
self.assertEqual(account.id, AccountTestData.P_PETROV_ID)
|
||||
|
||||
def test_authenticate_credentials_returns_none_tuple_if_token_not_found(self) -> None:
|
||||
"""
|
||||
Должен мягко вернуть (None, None), если токена вообще нет в базе сессий
|
||||
"""
|
||||
|
||||
result = self.auth.authenticate_credentials("completely_fake_token")
|
||||
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
def test_authenticate_credentials_returns_none_tuple_if_session_is_expired(self) -> None:
|
||||
"""
|
||||
Должен вернуть (None, None), если сессия Петра Петрова просрочена
|
||||
"""
|
||||
|
||||
session = SessionModel.objects.get(token=SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||
session.expires_at = timezone.now() - datetime.timedelta(seconds=1)
|
||||
session.save()
|
||||
|
||||
result = self.auth.authenticate_credentials(SessionTestData.P_PETROV_SESSION_TOKEN)
|
||||
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
def test_authenticate_credentials_returns_none_tuple_if_account_is_not_active(self) -> None:
|
||||
"""
|
||||
Должен вернуть (None, None), если AccountManager заблокировал вход (is_active = False)
|
||||
"""
|
||||
|
||||
account = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
|
||||
account.is_active = False
|
||||
account.save()
|
||||
|
||||
result = self.auth.authenticate_credentials(SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
def test_authenticate_credentials_returns_none_tuple_if_account_is_deleted(self) -> None:
|
||||
"""
|
||||
Должен вернуть (None, None), если пользователь помечен как удаленный
|
||||
"""
|
||||
|
||||
account = AccountModel.objects.get(id=AccountTestData.I_IVANOV_ID)
|
||||
account.soft_delete(save=True)
|
||||
|
||||
result = self.auth.authenticate_credentials(SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
def test_authenticate_credentials_renews_session_timestamp_on_success(self) -> None:
|
||||
"""
|
||||
Должен обновить время жизни сессии в БД и вернуть валидные объекты при успешном входе
|
||||
"""
|
||||
|
||||
session_before = SessionModel.objects.get(token=SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
result = self.auth.authenticate_credentials(SessionTestData.I_IVANOV_SESSION_TOKEN)
|
||||
|
||||
self.assertIsInstance(result, tuple)
|
||||
assert result is not None
|
||||
account, session = result
|
||||
|
||||
self.assertIsInstance(account, AccountModel)
|
||||
self.assertEqual(account.id, AccountTestData.I_IVANOV_ID)
|
||||
|
||||
self.assertIsInstance(session, SessionModel)
|
||||
session.refresh_from_db()
|
||||
self.assertGreater(session.expires_at, session_before.expires_at)
|
||||
@@ -1,99 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса DateUtilsTest
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.utils import DateUtils
|
||||
|
||||
|
||||
class DateUtilsTestCase(SimpleTestCase):
|
||||
"""
|
||||
Классы для DateUtils.
|
||||
"""
|
||||
|
||||
def test_now_returns_frozen_current_time(self) -> None:
|
||||
"""
|
||||
Должен вернуть точно зафиксированное текущее время через mock.patch
|
||||
"""
|
||||
|
||||
fixed_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=timezone.get_current_timezone())
|
||||
|
||||
with patch("django.utils.timezone.now", return_value=fixed_time):
|
||||
result = DateUtils.now()
|
||||
self.assertEqual(result, fixed_time)
|
||||
|
||||
def test_now_returns_timezone_aware_datetime(self) -> None:
|
||||
"""
|
||||
Должен возвращать объект datetime с информацией о часовом поясе (is_aware)
|
||||
"""
|
||||
|
||||
result = DateUtils.now()
|
||||
self.assertTrue(timezone.is_aware(result))
|
||||
|
||||
def test_shift_time_uses_now_if_date_time_is_none(self) -> None:
|
||||
"""
|
||||
Должен автоматически взять текущее время (now), если date_time передан как None
|
||||
"""
|
||||
|
||||
fixed_now = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2026, 7, 17, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
result = DateUtils.shift_time(date_time=None, days=3)
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_uses_now_by_omission(self) -> None:
|
||||
"""
|
||||
Должен автоматически взять текущее время (now), если параметр date_time вообще опущен
|
||||
"""
|
||||
|
||||
fixed_now = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2026, 7, 14, 14, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||
result = DateUtils.shift_time(hours=2)
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_adds_days_correctly_with_explicit_date(self) -> None:
|
||||
"""
|
||||
Должен корректно сдвигать время вперед, если передан явный объект datetime
|
||||
"""
|
||||
|
||||
base_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2026, 7, 19, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
result = DateUtils.shift_time(base_time, days=5)
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_subtracts_hours_correctly(self) -> None:
|
||||
"""
|
||||
Должен корректно сдвигать время назад при передаче отрицательных значений
|
||||
"""
|
||||
|
||||
base_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2026, 7, 14, 9, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
result = DateUtils.shift_time(base_time, hours=-3)
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_complex_modification(self) -> None:
|
||||
"""
|
||||
Должен атомарно применять комбинированный сдвиг по всем осям времени
|
||||
"""
|
||||
|
||||
base_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2026, 7, 15, 14, 30, 15, tzinfo=datetime.timezone.utc)
|
||||
|
||||
result = DateUtils.shift_time(base_time, days=1, hours=2, minutes=30, seconds=15)
|
||||
self.assertEqual(result, expected_time)
|
||||
@@ -1,16 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов для классов исключений
|
||||
"""
|
||||
|
||||
from .bad_request_exception_tests import BadRequestExceptionTests
|
||||
from .critical_exception_tests import CriticalExceptionTests
|
||||
from .file_not_found_exception_tests import FileNotFoundExceptionTests
|
||||
from .forbidden_exception_tests import ForbiddenExceptionTests
|
||||
from .not_found_exception_tests import NotFoundExceptionTests
|
||||
from .unauthorized_exception_tests import UnauthorizedExceptionTests
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса BadRequestExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import BadRequestException
|
||||
|
||||
|
||||
class BadRequestExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для BadRequestException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет инициализацию по умолчанию (без параметров)
|
||||
"""
|
||||
|
||||
exc = BadRequestException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.bad_request")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_false(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает False для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = BadRequestException()
|
||||
|
||||
self.assertFalse(exc.is_pure_status())
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CriticalExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import CriticalException
|
||||
|
||||
|
||||
class CriticalExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для CriticalException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет дефолтные параметры: статус и сообщение об ошибке
|
||||
"""
|
||||
|
||||
exc = CriticalException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.internal_server_error")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_true(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает True для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = CriticalException()
|
||||
|
||||
self.assertTrue(exc.is_pure_status())
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса FileNotFoundExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import FileNotFoundException
|
||||
|
||||
|
||||
class FileNotFoundExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для FileNotFoundException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет дефолтные параметры: статус и сообщение об ошибке
|
||||
"""
|
||||
|
||||
exc = FileNotFoundException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.not_found")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_true(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает True для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = FileNotFoundException()
|
||||
|
||||
self.assertTrue(exc.is_pure_status())
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ForbiddenExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import ForbiddenException
|
||||
|
||||
|
||||
class ForbiddenExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для BadRequestException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет инициализацию по умолчанию (без параметров)
|
||||
"""
|
||||
|
||||
exc = ForbiddenException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.forbidden")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_false(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает False для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = ForbiddenException()
|
||||
|
||||
self.assertFalse(exc.is_pure_status())
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса NotFoundExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import NotFoundException
|
||||
|
||||
|
||||
class NotFoundExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для NotFoundException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет дефолтные параметры: статус и сообщение об ошибке
|
||||
"""
|
||||
|
||||
exc = NotFoundException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.not_found")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_false(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает False для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = NotFoundException()
|
||||
|
||||
self.assertFalse(exc.is_pure_status())
|
||||
@@ -1,40 +0,0 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса UnauthorizedExceptionTests
|
||||
"""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework import status
|
||||
|
||||
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||
|
||||
|
||||
class UnauthorizedExceptionTests(SimpleTestCase):
|
||||
"""
|
||||
Тесты для UnauthorizedException
|
||||
"""
|
||||
|
||||
def test_init_with_defaults(self):
|
||||
"""
|
||||
Проверяет дефолтные параметры: статус и сообщение об ошибке
|
||||
"""
|
||||
|
||||
exc = UnauthorizedException()
|
||||
|
||||
self.assertEqual(exc.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
self.assertEqual(exc.detail, "rack.errors.http.unauthorized")
|
||||
self.assertIsNone(exc.meta)
|
||||
|
||||
def test_is_pure_status_returns_false(self):
|
||||
"""
|
||||
Гарантирует, что ошибка возвращает False для чистого HTTP статуса
|
||||
"""
|
||||
|
||||
exc = UnauthorizedException()
|
||||
|
||||
self.assertFalse(exc.is_pure_status())
|
||||
Reference in New Issue
Block a user