Добавлены классы ответов
This commit is contained in:
@@ -10,3 +10,4 @@
|
|||||||
|
|
||||||
from .core_enum import CoreEnum
|
from .core_enum import CoreEnum
|
||||||
from .core_exception import CoreException
|
from .core_exception import CoreException
|
||||||
|
from .core_response import CoreResponse
|
||||||
|
|||||||
51
net/xeaf/rack/core/core_response.py
Normal file
51
net/xeaf/rack/core/core_response.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса CoreResponse
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
class CoreResponse(Response):
|
||||||
|
"""
|
||||||
|
Базовый класс для всех классов ответов API проекта
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, internal_status: int, external_status: int, data: list | dict | str | bool | None, meta: dict | None = None):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
|
||||||
|
:param internal_status: Внутренний код состояния
|
||||||
|
:param external_status: Внешний код состояния
|
||||||
|
:param data: Набор возвращаемых данных
|
||||||
|
:param meta: Дополнительные данные
|
||||||
|
"""
|
||||||
|
|
||||||
|
result: dict[str, list | dict | str | float | int | bool] = {
|
||||||
|
"status": internal_status,
|
||||||
|
"timestamp": timezone.now().timestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Формируем данные ответа
|
||||||
|
if data is not None:
|
||||||
|
if internal_status < status.HTTP_400_BAD_REQUEST:
|
||||||
|
result["data"] = data
|
||||||
|
else:
|
||||||
|
result["detail"] = data
|
||||||
|
|
||||||
|
# Набор дополнительных данных
|
||||||
|
if meta:
|
||||||
|
result["meta"] = meta
|
||||||
|
|
||||||
|
# Формируем ответ
|
||||||
|
super().__init__(
|
||||||
|
data=result,
|
||||||
|
status=external_status
|
||||||
|
)
|
||||||
16
net/xeaf/rack/models/responses/__init__.py
Normal file
16
net/xeaf/rack/models/responses/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Пакет описания классов моделей ответов по протоколу HTTP
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .bool_response import BoolResponse
|
||||||
|
from .data_response import DataResponse
|
||||||
|
from .empty_response import EmptyResponse
|
||||||
|
from .error_response import ErrorResponse
|
||||||
|
from .exception_response import ExceptionResponse
|
||||||
|
from .list_response import ListResponse
|
||||||
33
net/xeaf/rack/models/responses/bool_response.py
Normal file
33
net/xeaf/rack/models/responses/bool_response.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса BoolResponse
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
|
||||||
|
|
||||||
|
class BoolResponse(CoreResponse):
|
||||||
|
"""
|
||||||
|
Ответ с логическим значением
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, value: bool, meta: dict | None = None):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
|
||||||
|
:param value: Логическое значение
|
||||||
|
:param meta: Дополнительная информация
|
||||||
|
"""
|
||||||
|
|
||||||
|
super(BoolResponse, self).__init__(
|
||||||
|
internal_status=status.HTTP_200_OK,
|
||||||
|
external_status=status.HTTP_200_OK,
|
||||||
|
data=value,
|
||||||
|
meta=meta)
|
||||||
33
net/xeaf/rack/models/responses/data_response.py
Normal file
33
net/xeaf/rack/models/responses/data_response.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса DataResponse
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
|
||||||
|
|
||||||
|
class DataResponse(CoreResponse):
|
||||||
|
"""
|
||||||
|
Ответ с одним объектом данных
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data: dict, meta: dict | None = None):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
|
||||||
|
:param data: Объект данных
|
||||||
|
:param meta: Дополнительная информация
|
||||||
|
"""
|
||||||
|
|
||||||
|
super(DataResponse, self).__init__(
|
||||||
|
internal_status=status.HTTP_200_OK,
|
||||||
|
external_status=status.HTTP_200_OK,
|
||||||
|
data=data,
|
||||||
|
meta=meta)
|
||||||
30
net/xeaf/rack/models/responses/empty_response.py
Normal file
30
net/xeaf/rack/models/responses/empty_response.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса EmptyResponse
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyResponse(CoreResponse):
|
||||||
|
"""
|
||||||
|
Пустой положительный ответ
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
"""
|
||||||
|
|
||||||
|
super(EmptyResponse, self).__init__(
|
||||||
|
internal_status=status.HTTP_200_OK,
|
||||||
|
external_status=status.HTTP_200_OK,
|
||||||
|
data=None,
|
||||||
|
meta=None)
|
||||||
49
net/xeaf/rack/models/responses/error_response.py
Normal file
49
net/xeaf/rack/models/responses/error_response.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ErrorResponse
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorResponse(CoreResponse):
|
||||||
|
"""
|
||||||
|
Ответ с информацией об ошибке
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, status_code: int, detail: list | dict | str | bool | None, meta: dict | None = None):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
|
||||||
|
:param status_code: Код состояния
|
||||||
|
:param detail: Информация об ошибке
|
||||||
|
:param meta: Дополнительные данные
|
||||||
|
"""
|
||||||
|
|
||||||
|
super(ErrorResponse, self).__init__(
|
||||||
|
internal_status=status_code,
|
||||||
|
external_status=self._calc_external_status(status_code),
|
||||||
|
data=detail,
|
||||||
|
meta=meta)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _calc_external_status(cls, status_code: int) -> int:
|
||||||
|
"""
|
||||||
|
Вычисляет значение внешнего возвращаемого статуса
|
||||||
|
|
||||||
|
:param status_code: Код состояния
|
||||||
|
|
||||||
|
:return: Внешний код состояния
|
||||||
|
"""
|
||||||
|
|
||||||
|
if status_code < status.HTTP_500_INTERNAL_SERVER_ERROR:
|
||||||
|
return status.HTTP_200_OK
|
||||||
|
|
||||||
|
return status_code
|
||||||
40
net/xeaf/rack/models/responses/exception_response.py
Normal file
40
net/xeaf/rack/models/responses/exception_response.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ExceptionResponse
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreException
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionResponse(CoreResponse):
|
||||||
|
"""
|
||||||
|
Ответ с информацией об исключении
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, exc: CoreException, meta: dict | None = None):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
|
||||||
|
:param exc: Исключение
|
||||||
|
:param meta: Дополнительная информация
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Проверяем требование передать реальный статус
|
||||||
|
if exc.is_pure_status():
|
||||||
|
external_status = exc.status_code
|
||||||
|
else:
|
||||||
|
external_status = status.HTTP_200_OK
|
||||||
|
|
||||||
|
super(ExceptionResponse, self).__init__(
|
||||||
|
internal_status=exc.status_code,
|
||||||
|
external_status=external_status,
|
||||||
|
data=exc.detail,
|
||||||
|
meta=meta)
|
||||||
33
net/xeaf/rack/models/responses/list_response.py
Normal file
33
net/xeaf/rack/models/responses/list_response.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ListResponse
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
from net.xeaf.rack.core import CoreResponse
|
||||||
|
|
||||||
|
|
||||||
|
class ListResponse(CoreResponse):
|
||||||
|
"""
|
||||||
|
Ответ со списком объектов данных
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data: list, meta: dict | None = None):
|
||||||
|
"""
|
||||||
|
Инициализация
|
||||||
|
|
||||||
|
:param data: Список объектов данных
|
||||||
|
:param meta: Дополнительная информация
|
||||||
|
"""
|
||||||
|
|
||||||
|
super(ListResponse, self).__init__(
|
||||||
|
internal_status=status.HTTP_200_OK,
|
||||||
|
external_status=status.HTTP_200_OK,
|
||||||
|
data=data,
|
||||||
|
meta=meta)
|
||||||
@@ -10,3 +10,4 @@
|
|||||||
|
|
||||||
from .core_enum_tests import CoreEnumTests
|
from .core_enum_tests import CoreEnumTests
|
||||||
from .core_exception_tests import CoreExceptionTests
|
from .core_exception_tests import CoreExceptionTests
|
||||||
|
from .core_response_tests import CoreResponseTests
|
||||||
|
|||||||
98
net/xeaf/rack/tests/core/core_response_tests.py
Normal file
98
net/xeaf/rack/tests/core/core_response_tests.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# 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)
|
||||||
16
net/xeaf/rack/tests/models/responses/__init__.py
Normal file
16
net/xeaf/rack/tests/models/responses/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 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
|
||||||
51
net/xeaf/rack/tests/models/responses/bool_response_tests.py
Normal file
51
net/xeaf/rack/tests/models/responses/bool_response_tests.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
41
net/xeaf/rack/tests/models/responses/data_response_tests.py
Normal file
41
net/xeaf/rack/tests/models/responses/data_response_tests.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 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)
|
||||||
39
net/xeaf/rack/tests/models/responses/empty_response_tests.py
Normal file
39
net/xeaf/rack/tests/models/responses/empty_response_tests.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 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)
|
||||||
65
net/xeaf/rack/tests/models/responses/error_response_tests.py
Normal file
65
net/xeaf/rack/tests/models/responses/error_response_tests.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 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"], "Крах БД")
|
||||||
41
net/xeaf/rack/tests/models/responses/list_response_tests.py
Normal file
41
net/xeaf/rack/tests/models/responses/list_response_tests.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 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)
|
||||||
Reference in New Issue
Block a user