diff --git a/net/xeaf/rack/managers/__init__.py b/net/xeaf/rack/managers/__init__.py index 3d4c502..23187f7 100644 --- a/net/xeaf/rack/managers/__init__.py +++ b/net/xeaf/rack/managers/__init__.py @@ -8,5 +8,6 @@ Пакет классов менеджеров """ +from .configuration_manager import ConfigurationManager from .crypto_manager import CryptoManager from .locale_manager import LocaleManager diff --git a/net/xeaf/rack/managers/configuration_manager.py b/net/xeaf/rack/managers/configuration_manager.py new file mode 100644 index 0000000..1e36fed --- /dev/null +++ b/net/xeaf/rack/managers/configuration_manager.py @@ -0,0 +1,66 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: MIT + +""" +Описание класса ConfigurationManager +""" + +from typing import Any + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + + +class ConfigurationManager: + """ + Прокси-менеджер для безопасного извлечения настроек из django.conf.settings + """ + + @classmethod + def get_string(cls, key: str, default: str) -> str: + """ + Извлекает строковое значение, параметр default обязателен + """ + + value = getattr(settings, key, default) + cls._assert_type(key, value, str) + return value + + @classmethod + def get_int(cls, key: str, default: int) -> int: + """ + Извлекает целочисленное значение, параметр default обязателен + """ + + value = getattr(settings, key, default) + cls._assert_type(key, value, int) + return value + + @classmethod + def get_bool(cls, key: str, default: bool) -> bool: + """ + Извлекает логическое значение, параметр default обязателен. + """ + + value = getattr(settings, key, default) + cls._assert_type(key, value, bool) + return value + + @classmethod + def _assert_type(cls, key: str, value: Any, expected_type: type) -> None: + """ + Проверяет типы, защищая от bool/int полиморфизма + """ + + if expected_type is int and isinstance(value, bool): + actual_type = bool + else: + actual_type = type(value) + + if actual_type is not expected_type: + raise ImproperlyConfigured( + f"Configuration error: '{key}' must be of type {expected_type.__name__}, got {actual_type.__name__}." + ) diff --git a/net/xeaf/rack/models/mixins/expired_at_mixin.py b/net/xeaf/rack/models/mixins/expired_at_mixin.py index 1afbe40..1f61717 100644 --- a/net/xeaf/rack/models/mixins/expired_at_mixin.py +++ b/net/xeaf/rack/models/mixins/expired_at_mixin.py @@ -37,7 +37,7 @@ class ExpiresAtMixin(models.Model): """ # Проверка значений - if seconds <=0: + if seconds <= 0: raise ValueError(f"The seconds parameter must be greater than 0") self.expires_at = timezone.now() + timedelta(seconds=seconds) diff --git a/net/xeaf/rack/tests/managers/__init__.py b/net/xeaf/rack/tests/managers/__init__.py index d770c19..bec8ead 100644 --- a/net/xeaf/rack/tests/managers/__init__.py +++ b/net/xeaf/rack/tests/managers/__init__.py @@ -8,5 +8,6 @@ Пакет описания тестов для классов менеджеров """ +from .configuration_manager_tests import ConfigurationManagerTests from .crypto_manager_tests import CryptoManagerTests from .locale_manager_tests import LocaleManagerTests diff --git a/net/xeaf/rack/tests/managers/configuration_manager_tests.py b/net/xeaf/rack/tests/managers/configuration_manager_tests.py new file mode 100644 index 0000000..6b00807 --- /dev/null +++ b/net/xeaf/rack/tests/managers/configuration_manager_tests.py @@ -0,0 +1,116 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: MIT + +""" +Описание класса CryptoTests +""" + +from django.core.exceptions import ImproperlyConfigured +from django.test import override_settings +from django.test import SimpleTestCase + +from net.xeaf.rack.managers import ConfigurationManager + + +class ConfigurationManagerTests(SimpleTestCase): + """ + Тесты для класса ConfigurationManager + """ + + def test_get_string_returns_default_if_setting_missing(self) -> None: + """ + Должен вернуть дефолтное значение, если настройки нет в settings.py + """ + + result = ConfigurationManager.get_string("NON_EXISTENT_STR_KEY", default="default_val") + self.assertEqual(result, "default_val") + + @override_settings(EXISTING_STR_KEY="custom_val") + def test_get_string_returns_custom_value(self) -> None: + """ + Должен успешно прочитать кастомную строку из настроек проекта + """ + + result = ConfigurationManager.get_string("EXISTING_STR_KEY", default="default_val") + self.assertEqual(result, "custom_val") + + @override_settings(INVALID_STR_KEY=123) + def test_get_string_raises_exception_on_type_mismatch(self) -> None: + """ + Должен взорваться исключением ImproperlyConfigured, если вместо строки пришел int + """ + + with self.assertRaises(ImproperlyConfigured) as context: + ConfigurationManager.get_string("INVALID_STR_KEY", default="default_val") + + self.assertIn("must be of type str, got int", str(context.exception)) + + def test_get_int_returns_default_if_setting_missing(self) -> None: + """ + Должен вернуть дефолтное число, если настройки нет в файле конфигурации + """ + + result = ConfigurationManager.get_int("NON_EXISTENT_INT_KEY", default=42) + self.assertEqual(result, 42) + + @override_settings(EXISTING_INT_KEY=100) + def test_get_int_returns_custom_value(self) -> None: + """ + Должен успешно вернуть настроенный int + """ + + result = ConfigurationManager.get_int("EXISTING_INT_KEY", default=42) + self.assertEqual(result, 100) + + @override_settings(INVALID_INT_KEY="not_an_int") + def test_get_int_raises_exception_on_type_mismatch(self) -> None: + """ + Должен упасть, если в числовую настройку хост-проект прокинул строку + """ + + with self.assertRaises(ImproperlyConfigured) as context: + ConfigurationManager.get_int("INVALID_INT_KEY", default=42) + + self.assertIn("must be of type int, got str", str(context.exception)) + + @override_settings(BOOLEAN_INSTEAD_OF_INT=True) + def test_get_int_raises_exception_on_boolean_value(self) -> None: + """ + Хирургический тест: должен упасть, если вместо int передан bool (True/False) + """ + + with self.assertRaises(ImproperlyConfigured) as context: + ConfigurationManager.get_int("BOOLEAN_INSTEAD_OF_INT", default=42) + + self.assertIn("must be of type int, got bool", str(context.exception)) + + def test_get_bool_returns_default_if_setting_missing(self) -> None: + """ + Должен вернуть дефолтный булевый флаг при отсутствии настройки + """ + + result = ConfigurationManager.get_bool("NON_EXISTENT_BOOL_KEY", default=True) + self.assertTrue(result) + + @override_settings(EXISTING_BOOL_KEY=False) + def test_get_bool_returns_custom_value(self) -> None: + """ + Должен успешно прочитать переопределенный булевый флаг + """ + + result = ConfigurationManager.get_bool("EXISTING_BOOL_KEY", default=True) + self.assertFalse(result) + + @override_settings(INT_INSTEAD_OF_BOOL=1) + def test_get_bool_raises_exception_on_int_value(self) -> None: + """ + Должен выбросить исключение, если вместо честного bool передан int + """ + + with self.assertRaises(ImproperlyConfigured) as context: + ConfigurationManager.get_bool("INT_INSTEAD_OF_BOOL", default=True) + + self.assertIn("must be of type bool, got int", str(context.exception))