Добавлен класс CoreViewSet и тесты к нему

This commit is contained in:
2026-07-16 11:52:51 +03:00
parent 4ce8a1ba58
commit b713a2124b
32 changed files with 2263 additions and 55 deletions

View File

@@ -16,4 +16,3 @@ from .core_model import CoreModel
from .core_persistent_model import CorePersistentModel
from .core_persistent_model import CorePersistentModel
from .core_response import CoreResponse
from .core_test_case import CoreTestCase

View File

@@ -1,25 +0,0 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreTestCase
"""
from django.core.management import call_command
from django.test import TestCase
class CoreTestCase(TestCase):
"""
Реализует инициализацию тестовой базы данных перед каждым тестом
"""
def setUp(self):
"""
Подготовка к тестированию
"""
call_command("rack_test_database")

View File

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

View File

@@ -0,0 +1,175 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreTestCase
"""
import json
from django.core.management import call_command
from django.test import TestCase
from rest_framework.parsers import JSONParser
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
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
class CoreTestCase(TestCase):
"""
Реализует инициализацию тестовой базы данных перед каждым тестом
"""
factory: APIRequestFactory
""" Фабрика запросов """
def setUp(self):
"""
Подготовка к тестированию
"""
call_command("rack_test_database")
self.factory = APIRequestFactory()
def create_request(self, method, path, data=None, query_params=None, auth=None):
"""
Создает DRF Request объект для тестирования
:param method: HTTP метод (GET, POST, PUT, PATCH, DELETE)
:param path: URL путь
:param data: Данные для тела запроса
:param query_params: Параметры запроса
:param auth: Объект аутентификации (сессия)
:return: DRF Request объект
"""
method = method.upper()
data = data or {}
query_params = query_params or {}
# Создаем WSGI request в зависимости от метода
if method == 'GET':
wsgi_request = self.factory.get(path, data=query_params)
elif method == 'POST':
wsgi_request = self.factory.post(path, data=data, format='json')
elif method == 'PUT':
wsgi_request = self.factory.put(path, data=data, format='json')
elif method == 'PATCH':
wsgi_request = self.factory.patch(path, data=data, format='json')
elif method == 'DELETE':
wsgi_request = self.factory.delete(path, data=data, format='json')
else:
wsgi_request = self.factory.generic(
method,
path,
content_type='application/json',
data=json.dumps(data) if data else ''
)
# Преобразуем в DRF Request
drf_request = Request(wsgi_request, parsers=[JSONParser()])
# Добавляем аутентификацию если передана
if auth:
drf_request.auth = auth
if hasattr(auth, 'account'):
drf_request.user = auth.account
# Добавляем query_params для не-GET запросов
if query_params and method != 'GET':
# noinspection PyTypeChecker
drf_request.query_params._mutable = True
for key, value in query_params.items():
drf_request.query_params[key] = value
drf_request.query_params._mutable = False
return drf_request
def create_authenticated_request(self, method, path, token, data=None, query_params=None):
"""
Создает аутентифицированный DRF Request
:param method: HTTP метод
:param path: URL путь
:param token: Токен сессии
:param data: Данные для тела запроса
:param query_params: Параметры запроса
:return: DRF Request с аутентификацией
"""
session = SessionModel.objects.get(token=token)
return self.create_request(method, path, data, query_params, auth=session)
@classmethod
def get_i_ivanov_account(cls) -> AccountModel:
"""
Возвращает аккаунт Ивана И. Иванова
:return: Аккаунт Ивана И. Иванова
"""
return AccountTestData.get_i_ivanov_account()
@classmethod
def get_i_ivanov_session(cls) -> SessionModel:
"""
Возвращает сессию Ивана И. Иванова
:return: Сессия Ивана И. Иванова
"""
return SessionTestData.get_i_ivanov_session()
def create_i_ivanov_request(self, method, path, data=None, query_params=None):
"""
Создает запрос от имени Ивана И. Иванова
"""
return self.create_authenticated_request(
method,
path,
SessionTestData.I_IVANOV_SESSION_TOKEN,
data,
query_params
)
@classmethod
def get_p_petrov_account(cls) -> AccountModel:
"""
Возвращает аккаунт Петра П. Петрова
:return: Аккаунт Петра П. Петрова
"""
return AccountTestData.get_p_petrov_account()
@classmethod
def get_p_petrov_session(cls) -> SessionModel:
"""
Возвращает сессию Петра П. Петрова
:return: Сессия Петра П. Петрова
"""
return SessionTestData.get_p_petrov_session()
def create_p_petrov_request(self, method, path, data=None, query_params=None):
"""
Создает запрос от имени Петра П. Петрова
"""
return self.create_authenticated_request(
method,
path,
SessionTestData.P_PETROV_SESSION_TOKEN,
data,
query_params
)

View File

@@ -0,0 +1,16 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет базовых классов представлений
"""
from .core_view_set import CoreViewSet
from .view_set_create_mixin import ViewSetCreateMixin
from .view_set_destroy_mixin import ViewSetDestroyMixin
from .view_set_list_mixin import ViewSetListMixin
from .view_set_retrieve_mixin import ViewSetRetrieveMixin
from .view_set_update_mixin import ViewSetUpdateMixin

View File

@@ -0,0 +1,283 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreViewSet
"""
from typing import Any
from typing import Type
from uuid import UUID
from django.core.exceptions import ImproperlyConfigured
from django.urls import path
from django.urls import URLPattern
from rest_framework.permissions import IsAuthenticated
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.enums import ViewSetAction
from net.xeaf.rack.models import AccountModel
from net.xeaf.rack.models import SessionModel
from net.xeaf.rack.utils.exceptions import BadRequestException
from net.xeaf.rack.utils.exceptions import UnauthorizedException
class CoreViewSet(ViewSet):
"""
Реализует базовый функционал для наборов представлений API
"""
permission_classes = (IsAuthenticated,)
""" Классы проверки прав доступа """
item_list_pattern: str | None = None
""" Путь для работы со списком объектов """
item_detail_pattern: str | None = None
""" Путь для работы с объектом """
response_serializer: Type[Serializer] | None = None
""" Общий класс сериализатора для ответа """
def _get_serializer(self, serializer_class: Type[Serializer] | None, default: Type[Serializer] | None) -> Type[Serializer]:
"""
Возвращает класс сериализатора
:param serializer_class: Класс сериализатора
:param default: Класс сериализатора по умолчанию
:return: Класс сериализатора
"""
if serializer_class is not None:
return serializer_class
if default is not None:
return default
raise AttributeError(f"{self.__class__.__name__} must define 'serializer' attribute")
@classmethod
def _process_pk_param(cls, **kwargs: Any) -> UUID | None:
"""
Обработка параметра идентификатора объекта
:param kwargs: Именованные параметры
:return: Идентификатор объекта или None
"""
result = kwargs.get("pk", None)
if not result:
raise BadRequestException()
return UUID(str(result))
def _process_query_params(self, serializer: Type[Serializer] | None, **kwargs: Any) -> dict[str, Any]:
"""
Обрабатывает параметры запроса
:param serializer: Сериализатор параметров
:param kwargs: Именованные аргументы
:return: Параметры запроса
"""
# Строим набор параметров
result = {}
params = list(self.request.query_params)
for param in params:
if param != 'request':
result[param] = self.request.query_params.get(param)
# Добавляем именованные параметры
result.update(kwargs)
# Если был задан сериализатор для параметров
if serializer is not None:
srz = serializer(data=self.request.query_params, context=self._get_request_context())
srz.is_valid(raise_exception=True)
return srz.validated_data
return result
def _process_data_params(self, serializer: Type[Serializer] | None) -> dict[str, Any]:
"""
Обрабатывает параметры данных запроса
:param serializer: Сериализатор параметров
:return: Параметры запроса
"""
data = self.request.data
# Если был задан сериализатор для параметров
if serializer is not None:
partial = self._is_partial_update()
srz = serializer(data=data, context=self._get_request_context(), partial=partial)
srz.is_valid(raise_exception=True)
return srz.validated_data
return data
def _is_partial_update(self) -> bool:
"""
Проверяет, является ли текущий запрос частичным обновлением
:return: True, если запрос является частичным обновлением
"""
return self._get_request_method() == HttpMethod.PATCH
def _get_request_method(self) -> str:
"""
Возвращает имя метода HTTP протокола запроса
:return: Имя метода HTTP протокола запроса
"""
method = self.request.method
if not method:
raise ImproperlyConfigured()
return method.upper()
def _get_request_session[T: SessionModel](self) -> T | None:
"""
Возвращает сессию, связанную с текущим запросом
:return: Сессия, связанная с текущим запросом
"""
# noinspection PyTypeChecker
session: SessionModel = self.request.auth
return session
def _get_request_account[T: AccountModel](self) -> T:
"""
Возвращает аккаунт, связанный с текущим запросом
:return: Аккаунт, связанный с текущим запросом
"""
session = self._get_request_session()
if session is not None:
# noinspection PyTypeChecker
account: T | None = session.account
if account is not None:
return account
raise UnauthorizedException()
def _get_request_context(self) -> dict[str, Any]:
"""
Возвращает контекст запроса
:return: Объект контекста запроса
"""
try:
account = self._get_request_account()
except UnauthorizedException:
account = None
return {
"account": account,
"request": self.request
}
@classmethod
def get_paths(cls) -> list[URLPattern]:
"""
Возвращает полный список путей набора представлений
:return: Список путей
"""
return cls._item_list_paths() + cls._item_detail_paths()
@classmethod
def _path_method(cls, action: ViewSetAction) -> str:
"""
Возвращает имя метода для реализации действия представления
:param action: Действие
:return: Имя метода
"""
return ViewSetAction.get_method(action)
@classmethod
def _path_method_exists(cls, action: ViewSetAction) -> bool:
"""
Проверяет наличие метода для реализации действия представления
:param action: Действие
:return: True, если метод найден, иначе False
"""
method_name = cls._path_method(action)
if hasattr(cls, method_name):
attr = getattr(cls, method_name)
if callable(attr):
return True
return False
@classmethod
def _item_list_paths(cls) -> list[URLPattern]:
"""
Возвращает список путей для работы со списком объектов
:return: Список путей
"""
if cls.item_list_pattern is None:
return []
args = {}
if cls._path_method_exists(ViewSetAction.LIST):
args["get"] = cls._path_method(ViewSetAction.LIST)
if cls._path_method_exists(ViewSetAction.CREATE):
args["post"] = cls._path_method(ViewSetAction.CREATE)
# Если есть путь, но нет методов
if not args:
raise ImproperlyConfigured("No methods defined for item_list_pattern.")
return [path(cls.item_list_pattern, cls.as_view(args))]
@classmethod
def _item_detail_paths(cls) -> list[URLPattern]:
"""
Возвращает список путей для работы с объектом
:return: Список путей
"""
if cls.item_detail_pattern is None:
return []
args = {}
if cls._path_method_exists(ViewSetAction.RETRIEVE):
args["get"] = cls._path_method(ViewSetAction.RETRIEVE)
if cls._path_method_exists(ViewSetAction.UPDATE):
args["put"] = cls._path_method(ViewSetAction.UPDATE)
if cls._path_method_exists(ViewSetAction.PARTIAL_UPDATE):
args["patch"] = cls._path_method(ViewSetAction.PARTIAL_UPDATE)
if cls._path_method_exists(ViewSetAction.DESTROY):
args["delete"] = cls._path_method(ViewSetAction.DESTROY)
# Если есть путь, но нет методов
if not args:
raise ImproperlyConfigured("No methods defined for item_detail_pattern.")
return [path(cls.item_detail_pattern, cls.as_view(args))]

View File

@@ -0,0 +1,92 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ViewSetCreateMixin
"""
from typing import Any
from typing import Type
from django.db import transaction
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.request import Request
from rest_framework.serializers import Serializer
from net.xeaf.rack.core import CoreModel
from net.xeaf.rack.core import CoreResponse
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.models.responses import DataResponse
from .core_view_set import CoreViewSet
class ViewSetCreateMixin(CoreViewSet):
"""
Реализует базовый функционал для создания объекта
"""
create_param_serializer: Type[Serializer] | None = None
""" Класс сериализатора для параметров запроса на создание объекта """
create_data_serializer: Type[Serializer] | None = None
""" Класс сериализатора для данных запроса на создание объекта """
create_response_serializer: Type[Serializer] | None = None
""" Класс сериализатора для ответа на запрос о создании объекта """
def item_create(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
"""
Обрабатывает HTTP запрос и возвращает ответ с новым объектом
:param request: Запрос
:param kwargs: Именованные аргументы
:return: Ответ
"""
params = self._process_create_params(**kwargs)
with transaction.atomic():
result = self._process_create(params)
return self._process_create_response(result)
def _process_create(self, params: dict[str, Any]) -> dict[str, Any] | CoreModel:
"""
Обрабатывает создание объекта для формирования ответа
:param params: Параметры запроса
:return: Выбранный объект
"""
raise MethodNotAllowed(method=HttpMethod.POST)
def _process_create_params(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
"""
Разбирает и проверяет параметры запроса списка объектов
:param kwargs: Именованные аргументы
:return: Список параметров
"""
query_params = self._process_query_params(self.create_param_serializer, **kwargs)
data_params = self._process_data_params(self.create_data_serializer)
return {**query_params, **data_params}
def _process_create_response(self, data: dict[str, Any] | CoreModel) -> CoreResponse:
"""
Обрабатывает список объектов для формирования ответа
:param data: Список объектов
:return: Ответ для отправки
"""
srz_class = self._get_serializer(self.create_response_serializer, self.response_serializer)
srz = srz_class(data, many=False, context=self._get_request_context())
return DataResponse(data=srz.data)

View File

@@ -0,0 +1,95 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ViewSetDestroyMixin
"""
import uuid
from typing import Any
from typing import Type
from django.db import transaction
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.request import Request
from rest_framework.serializers import Serializer
from net.xeaf.rack.core import CoreModel
from net.xeaf.rack.core import CoreResponse
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.models.responses import DataResponse
from net.xeaf.rack.models.responses import EmptyResponse
from .core_view_set import CoreViewSet
class ViewSetDestroyMixin(CoreViewSet):
"""
Реализует базовый функционал представления для удаления объекта
"""
destroy_param_serializer: Type[Serializer] | None = None
""" Класс сериализатора для параметров запроса объекта """
destroy_response_serializer: Type[Serializer] | None = None
""" Класс сериализатора для ответа на запрос объекта """
def item_destroy(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
"""
Обрабатывает HTTP запрос и возвращает ответ с объектом
:param request: Запрос
:param kwargs: Именованные аргументы
:return: Ответ
"""
pk = self._process_pk_param(**kwargs)
params = self._process_destroy_params(**kwargs)
with transaction.atomic():
result = self._process_destroy(pk, params)
return self._process_destroy_response(result)
def _process_destroy(self, pk: uuid.UUID | None, params: dict[str, Any]) -> dict[str, Any] | CoreModel | None:
"""
Обрабатывает удаление объекта
:param pk: Идентификатор объекта
:param params: Параметры запроса
:return: Выбранный объект
"""
raise MethodNotAllowed(method=HttpMethod.DELETE)
def _process_destroy_params(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
"""
Разбирает и проверяет параметры запроса объекта
:param kwargs: Именованные аргументы
:return: Список параметров
"""
return self._process_query_params(self.destroy_param_serializer, **kwargs)
def _process_destroy_response(self, data: dict[str, Any] | CoreModel | None) -> CoreResponse:
"""
Обрабатывает объект для формирования ответа
:param data: Список объектов
:return: Ответ для отправки
"""
# Частный случай пустого ответа
if data is None:
return EmptyResponse()
srz_class = self._get_serializer(self.destroy_response_serializer, self.response_serializer)
srz = srz_class(data, many=False, context=self._get_request_context())
return DataResponse(data=srz.data)

View File

@@ -0,0 +1,85 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ViewSetListMixin
"""
from typing import Any
from typing import Type
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.request import Request
from rest_framework.serializers import Serializer
from net.xeaf.rack.core import CoreModel
from net.xeaf.rack.core import CoreResponse
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.models.responses import ListResponse
from .core_view_set import CoreViewSet
class ViewSetListMixin(CoreViewSet):
"""
Реализует базовый функционал для выбора списков
"""
list_param_serializer: Type[Serializer] | None = None
""" Класс сериализатора для параметров запроса списка объектов """
list_response_serializer: Type[Serializer] | None = None
""" Класс сериализатора для ответа на запрос списка объектов """
def item_list(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
"""
Обрабатывает HTTP запрос и возвращает ответ со списком объектов
:param request: Запрос
:param kwargs: Именованные аргументы
:return: Ответ
"""
params = self._process_list_params(**kwargs)
result = self._process_list(params)
return self._process_list_response(result)
def _process_list(self, params: dict[str, Any]) -> list[dict[str, Any] | CoreModel]:
"""
Обрабатывает выбор списка объектов для формирования ответа
:param params: Параметры запроса
:return: Выбранный объект
"""
raise MethodNotAllowed(method=HttpMethod.GET)
def _process_list_params(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
"""
Разбирает и проверяет параметры запроса списка объектов
:param kwargs: Именованные аргументы
:return: Список параметров
"""
return self._process_query_params(self.list_param_serializer, **kwargs)
def _process_list_response(self, data: list[dict[str, Any] | CoreModel]) -> CoreResponse:
"""
Обрабатывает список объектов для формирования ответа
:param data: Список объектов
:return: Ответ для отправки
"""
srz_class = self._get_serializer(self.list_response_serializer, self.response_serializer)
srz = srz_class(data, many=True, context=self._get_request_context())
return ListResponse(data=list(srz.data))

View File

@@ -0,0 +1,88 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ViewSetUpdateMixin
"""
import uuid
from typing import Any
from typing import Type
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.request import Request
from rest_framework.serializers import Serializer
from net.xeaf.rack.core import CoreModel
from net.xeaf.rack.core import CoreResponse
from net.xeaf.rack.enums import HttpMethod
from net.xeaf.rack.models.responses import DataResponse
from .core_view_set import CoreViewSet
class ViewSetRetrieveMixin(CoreViewSet):
"""
Реализует базовый функционал представления для выбора объекта
"""
retrieve_param_serializer: Type[Serializer] | None = None
""" Класс сериализатора для параметров запроса объекта """
retrieve_response_serializer: Type[Serializer] | None = None
""" Класс сериализатора для ответа на запрос объекта """
def item_retrieve(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
"""
Обрабатывает HTTP запрос и возвращает ответ с объектом
:param request: Запрос
:param kwargs: Именованные аргументы
:return: Ответ
"""
pk = self._process_pk_param(**kwargs)
params = self._process_retrieve_params(**kwargs)
result = self._process_retrieve(pk, params)
return self._process_retrieve_response(result)
def _process_retrieve(self, pk: uuid.UUID | None, params: dict[str, Any]) -> dict[str, Any] | CoreModel:
"""
Обрабатывает выбор объекта для формирования ответа
:param pk: Идентификатор объекта
:param params: Параметры запроса
:return: Выбранный объект
"""
raise MethodNotAllowed(method=HttpMethod.GET)
def _process_retrieve_params(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
"""
Разбирает и проверяет параметры запроса объекта
:param kwargs: Именованные аргументы
:return: Список параметров
"""
return self._process_query_params(self.retrieve_param_serializer, **kwargs)
def _process_retrieve_response(self, data: dict[str, Any] | CoreModel) -> CoreResponse:
"""
Обрабатывает объект для формирования ответа
:param data: Список объектов
:return: Ответ для отправки
"""
srz_class = self._get_serializer(self.retrieve_response_serializer, self.response_serializer)
srz = srz_class(data, many=False, context=self._get_request_context())
return DataResponse(data=srz.data)

View File

@@ -0,0 +1,107 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ViewSetUpdateMixin
"""
import uuid
from typing import Any
from typing import Type
from django.db import transaction
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.request import Request
from rest_framework.serializers import Serializer
from net.xeaf.rack.core import CoreModel
from net.xeaf.rack.core import CoreResponse
from net.xeaf.rack.models.responses import DataResponse
from .core_view_set import CoreViewSet
class ViewSetUpdateMixin(CoreViewSet):
"""
Реализует базовый функционал представления для изменения объекта
"""
update_param_serializer: Type[Serializer] | None = None
""" Класс сериализатора для параметров запроса на изменение объекта """
update_data_serializer: Type[Serializer] | None = None
""" Класс сериализатора для данных запроса на изменение объекта """
update_response_serializer: Type[Serializer] | None = None
""" Класс сериализатора для ответа на запрос на изменение объекта """
def item_update(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
"""
Обрабатывает HTTP запрос и возвращает ответ с объектом
:param request: Запрос
:param kwargs: Именованные аргументы
:return: Ответ
"""
pk = self._process_pk_param(**kwargs)
params = self._process_update_params(**kwargs)
with transaction.atomic():
result = self._process_update(pk, params)
return self._process_update_response(result)
def item_partial_update(self, request: Request, **kwargs: dict[str, Any]) -> CoreResponse:
"""
Обрабатывает HTTP запрос и возвращает ответ с измененным объектом
:param request: Запрос
:param kwargs: Именованные аргументы
:return: Ответ
"""
return self.item_update(request, **kwargs)
def _process_update(self, pk: uuid.UUID | None, params: dict[str, Any]) -> dict[str, Any] | CoreModel:
"""
Обрабатывает выбор и изменение объекта для формирования ответа
:param pk: Идентификатор объекта
:param params: Параметры запроса
:return: Выбранный объект
"""
raise MethodNotAllowed(method=self._get_request_method())
def _process_update_params(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
"""
Разбирает и проверяет параметры запроса объекта
:param kwargs: Именованные аргументы
:return: Список параметров
"""
query_params = self._process_query_params(self.update_param_serializer, **kwargs)
data_params = self._process_data_params(self.update_data_serializer)
return {**query_params, **data_params}
def _process_update_response(self, data: dict[str, Any] | CoreModel) -> CoreResponse:
"""
Обрабатывает объект для формирования ответа
:param data: Список объектов
:return: Ответ для отправки
"""
srz_class = self._get_serializer(self.update_response_serializer, self.response_serializer)
srz = srz_class(data, many=False, context=self._get_request_context())
return DataResponse(data=srz.data)

View File

@@ -8,7 +8,7 @@
Пакет описания классов перечислений
"""
from .ui_language import UILanguage
from .auth_reject_reson import AuthRejectReason
from .http_method import HttpMethod
from .ui_language import UILanguage
from .view_set_action import ViewSetAction

View File

@@ -0,0 +1,42 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса HttpMethod
"""
from enum import unique
from net.xeaf.rack.core import CoreEnum
@unique
class HttpMethod(CoreEnum):
"""
Перечисление методов протокола HTTP
"""
GET = 'GET'
""" Получение данных """
POST = 'POST'
""" Создание данных """
PUT = 'PUT'
""" Изменение данных """
PATCH = 'PATCH'
""" Частичное изменение данных """
DELETE = 'DELETE'
""" Удаление данных """
HEAD = 'HEAD'
""" Получение заголовков """
OPTIONS = 'OPTIONS'
""" Получение возможных действий """

View File

@@ -19,45 +19,31 @@ class ViewSetAction(CoreEnum):
Перечисление действий набора представлений
"""
LIST = "list"
LIST = "LIST"
""" Список объектов """
RETRIEVE = "retrieve"
RETRIEVE = "RETRIEVE"
""" Получение объекта """
CREATE = "create"
CREATE = "CREATE"
""" Создание объекта """
UPDATE = "update"
UPDATE = "UPDATE"
""" Изменение объекта """
PARTIAL_UPDATE = "partial_update"
PARTIAL_UPDATE = "PARTIAL_UPDATE"
""" Частичное изменение объекта """
DESTROY = "destroy"
DESTROY = "DESTROY"
""" Удаление объекта """
@classmethod
def get_method(cls, action: ViewSetAction) -> str:
"""
Возвращает HTTP метод для указанного действия
Возвращает идентификатор метода класса для указанного действия
:param action: Действие
:return: HTTP метод
:return: Идентификатор метода класса
"""
match action:
case cls.LIST:
return "item_list"
case cls.CREATE:
return "item_create"
case cls.RETRIEVE:
return "item_retrieve"
case cls.UPDATE:
return "item_update"
case cls.PARTIAL_UPDATE:
return "item_partial_update"
case cls.DESTROY:
return "item_destroy"
case _:
raise ValueError(f"Invalid action: {action}")
return f"item_{action.value.lower()}"

View File

@@ -22,9 +22,15 @@ class AccountTestData:
I_IVANOV_ID = uuid.UUID("bd428358-6944-4657-8044-a3dc1c4dff8a")
""" Идентификатор пользователя Иван И. Иванов """
I_IVANOV_USERNAME = 'i.ivanov@xeaf.net'
""" Имя пользователя Иван И. Иванов """
P_PETROV_ID = uuid.UUID("9c5010cc-f3be-42ae-bc05-d142d68a610a")
""" Идентификатор пользователя Петр П. Петров """
P_PETROV_USERNAME = 'p.petrov@xeaf.net'
""" Имя пользователя Петр П. Петров """
DEFAULT_PASSWORD = "manager123!"
""" Пароль по умолчанию для тестов """

View File

@@ -60,3 +60,23 @@ class SessionTestData:
session.actual_account_id = AccountTestData.P_PETROV_ID
session.renew(cls.TEST_SESSION_TTL)
session.save()
@classmethod
def get_i_ivanov_session(cls) -> SessionModel:
"""
Возвращает сессию пользователя Иван И. Иванов
:return: Сессия пользователя Иван И. Иванов
"""
return SessionModel.objects.get(token=cls.I_IVANOV_SESSION_TOKEN)
@classmethod
def get_p_petrov_session(cls) -> SessionModel:
"""
Возвращает сессию пользователя Петр П. Петров
:return: Сессия пользователя Петр П. Петров
"""
return SessionModel.objects.get(token=cls.P_PETROV_SESSION_TOKEN)

View File

@@ -11,3 +11,5 @@
from .core_enum_tests import CoreEnumTests
from .core_exception_tests import CoreExceptionTests
from .core_response_tests import CoreResponseTests
from .views import *

View File

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

View File

@@ -0,0 +1,70 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса CoreViewSetGetSerializerTest
"""
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(AttributeError) as context:
view_set._get_serializer(None, None)
self.assertIn("must define 'serializer' attribute", str(context.exception))

View File

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

View File

@@ -0,0 +1,163 @@
# 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 CoreViewSetPathGenerationTest(SimpleTestCase):
"""
Тесты генерации URL паттернов
"""
class FullCRUDViewSet(CoreViewSet):
"""
ViewSet с полным CRUD
"""
item_list_pattern = "test/"
item_detail_pattern = "test/<uuid:pk>/"
@classmethod
def item_list(cls, request):
pass
@classmethod
def item_create(cls, request):
pass
@classmethod
def item_retrieve(cls, request, pk):
pass
@classmethod
def item_update(cls, request, pk):
pass
@classmethod
def item_partial_update(cls, request, pk):
pass
@classmethod
def item_destroy(cls, request, pk):
pass
class ListOnlyViewSet(CoreViewSet):
"""
ViewSet только для списка
"""
item_list_pattern = "test/"
@classmethod
def item_list(cls, request):
pass
class DetailOnlyViewSet(CoreViewSet):
"""
ViewSet только для деталей
"""
item_detail_pattern = "test/<uuid:pk>/"
@classmethod
def item_retrieve(cls, request, pk):
pass
def test_get_paths_full_crud(self):
"""
Полный CRUD - все пути сгенерированы
"""
paths = self.FullCRUDViewSet.get_paths()
self.assertEqual(len(paths), 2)
list_path = paths[0]
detail_path = paths[1]
self.assertEqual(list_path.pattern._route, "test/")
self.assertEqual(detail_path.pattern._route, "test/<uuid:pk>/")
def test_get_paths_list_only(self):
"""
Только список - только list path
"""
paths = self.ListOnlyViewSet.get_paths()
self.assertEqual(len(paths), 1)
list_path = paths[0]
self.assertEqual(list_path.pattern._route, "test/")
def test_get_paths_detail_only(self):
"""
Только детали - только detail path
"""
paths = self.DetailOnlyViewSet.get_paths()
self.assertEqual(len(paths), 1)
detail_path = paths[0]
self.assertEqual(detail_path.pattern._route, "test/<uuid:pk>/")
def test_get_paths_no_patterns(self):
"""
Нет паттернов - пустой список
"""
class EmptyViewSet(CoreViewSet):
pass
paths = EmptyViewSet.get_paths()
self.assertEqual(paths, [])
def test_get_paths_list_without_methods(self):
"""
Есть паттерн, но нет методов - пустой список
"""
class NoMethodsViewSet(CoreViewSet):
item_list_pattern = "test/"
with self.assertRaises(ImproperlyConfigured):
NoMethodsViewSet.get_paths()
def test_path_method_valid_action(self):
"""
Получение метода для валидного действия
"""
result = CoreViewSet._path_method(ViewSetAction.LIST)
self.assertEqual(result, "item_list")
def test_path_method_exists_true(self):
"""
Проверка существования метода - True
"""
result = self.FullCRUDViewSet._path_method_exists(ViewSetAction.LIST)
self.assertTrue(result)
def test_path_method_exists_false(self):
"""
Проверка существования метода - False
"""
class EmptyViewSet(CoreViewSet):
pass
result = EmptyViewSet._path_method_exists(ViewSetAction.LIST)
self.assertFalse(result)

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings
from django.utils import timezone
from net.xeaf.rack.core import CoreTestCase
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

View File

@@ -10,7 +10,7 @@
from django.db.utils import IntegrityError
from net.xeaf.rack.core import CoreTestCase
from net.xeaf.rack.core.tests import CoreTestCase
from net.xeaf.rack.management.userstory import AccountTestData
from net.xeaf.rack.models import AccountModel

View File

@@ -11,7 +11,7 @@
from django.db.utils import IntegrityError
from django.utils import timezone
from net.xeaf.rack.core import CoreTestCase
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

View File

@@ -16,7 +16,7 @@ from rest_framework.parsers import JSONParser
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
from net.xeaf.rack.core import CoreTestCase
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

View File

@@ -9,3 +9,4 @@
"""
from .date_utils import DateUtils
from .model_utils import ModelUtils

View File

@@ -0,0 +1,53 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса ModelUtils
"""
from typing import Any
from django.db.models import Model
from django.forms import model_to_dict
class ModelUtils:
"""
Реализует служебные функции работы с моделями данных
"""
@classmethod
def concat_data(cls, data: dict[str, Any] | Model, target: dict[str, Any], ) -> None:
"""
Объединяет данные
:param data: Источник данных
:param target: Приемник
"""
# Получаем объект данных
if isinstance(data, Model):
source = model_to_dict(data)
else:
source = data
# noinspection PyUnresolvedReferences
for key, value in source.items():
if key not in target:
target[key] = value
@classmethod
def update_data(cls, instance: dict[str, Any] | Model, validated_data: dict[str, Any]) -> None:
"""
Обновляет данные объекта
:param instance: Объект
:param validated_data: Данные для обновления
"""
for attr, value in validated_data.items():
if hasattr(instance, attr):
setattr(instance, attr, value)

View File

@@ -37,6 +37,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'net.xeaf.rack',
]
@@ -117,3 +118,11 @@ USE_TZ = True
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser',
],
}