From b3d16f00ea06e2fad1faa219f7fac846cf98d392 Mon Sep 17 00:00:00 2001 From: Jonathan Green Date: Thu, 14 Sep 2023 13:41:41 -0300 Subject: [PATCH] Remove configuration storage classes. --- api/s3_analytics_provider.py | 9 +- core/model/configuration.py | 551 +----------------------- tests/core/models/test_configuration.py | 447 ------------------- 3 files changed, 2 insertions(+), 1005 deletions(-) diff --git a/api/s3_analytics_provider.py b/api/s3_analytics_provider.py index 8ddff535cc..4a954cc736 100644 --- a/api/s3_analytics_provider.py +++ b/api/s3_analytics_provider.py @@ -10,24 +10,17 @@ from core.config import CannotLoadConfiguration from core.local_analytics_provider import LocalAnalyticsProvider from core.model import Library, LicensePool, MediaTypes -from core.model.configuration import ConfigurationGrouping from core.service.container import Services from core.service.storage.s3 import S3Service -class S3AnalyticsProviderConfiguration(ConfigurationGrouping): - """Contains configuration settings of the S3 Analytics provider.""" - - class S3AnalyticsProvider(LocalAnalyticsProvider): """Analytics provider storing data in a S3 bucket.""" NAME = _("S3 Analytics") DESCRIPTION = _("Store analytics events in a S3 bucket.") - SETTINGS = ( - LocalAnalyticsProvider.SETTINGS + S3AnalyticsProviderConfiguration.to_settings() - ) + SETTINGS = LocalAnalyticsProvider.SETTINGS def __init__( self, diff --git a/core/model/configuration.py b/core/model/configuration.py index 696368053e..c98e822845 100644 --- a/core/model/configuration.py +++ b/core/model/configuration.py @@ -1,20 +1,17 @@ from __future__ import annotations # ExternalIntegration, ExternalIntegrationLink, ConfigurationSetting -import inspect import json import logging from abc import ABCMeta, abstractmethod -from contextlib import contextmanager from enum import Enum -from typing import TYPE_CHECKING, Any, Iterable, Iterator, List, Optional, TypeVar +from typing import TYPE_CHECKING, List, Optional from sqlalchemy import Column, ForeignKey, Index, Integer, Unicode from sqlalchemy.orm import Mapped, relationship from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import and_ -from core.configuration.ignored_identifier import IgnoredIdentifierSettings from core.model.hybrid import hybrid_property from ..config import CannotLoadConfiguration, Configuration @@ -787,554 +784,8 @@ def external_integration(self, db: Session) -> Optional[ExternalIntegration]: raise NotImplementedError() -class BaseConfigurationStorage(metaclass=ABCMeta): - """Serializes and deserializes values as configuration settings""" - - @abstractmethod - def save(self, db: Session, setting_name: str, value: Any): - """Save the value as as a new configuration setting - - :param db: Database session - :param setting_name: Name of the configuration setting - :param value: Value to be saved - """ - raise NotImplementedError() - - @abstractmethod - def load(self, db: Session, setting_name: str) -> Any: - """Loads and returns the library's configuration setting - - :param db: Database session - :param setting_name: Name of the configuration setting - """ - raise NotImplementedError() - - -class ConfigurationStorage(BaseConfigurationStorage): - """Serializes and deserializes values as configuration settings""" - - def __init__(self, integration_association: HasExternalIntegration): - """Initializes a new instance of ConfigurationStorage class - - :param integration_association: Association with an external integration - """ - self._integration_association = integration_association - - def save(self, db: Session, setting_name: str, value: Any): - """Save the value as as a new configuration setting - - :param db: Database session - :param setting_name: Name of the configuration setting - :param value: Value to be saved - """ - integration = self._integration_association.external_integration(db) - ConfigurationSetting.for_externalintegration( - setting_name, integration - ).value = value - - def load(self, db: Session, setting_name: str) -> Any: - """Loads and returns the library's configuration setting - - :param db: Database session - :param setting_name: Name of the library's configuration setting - """ - integration = self._integration_association.external_integration(db) - value = ConfigurationSetting.for_externalintegration( - setting_name, integration - ).value - - return value - - -class ConfigurationAttributeType(Enum): - """Enumeration of configuration setting types""" - - TEXT = "text" - TEXTAREA = "textarea" - SELECT = "select" - NUMBER = "number" - LIST = "list" - MENU = "menu" - - def to_control_type(self) -> str | None: - """Converts the value to a attribute type understandable by circulation-admin - - :return: String representation of attribute's type - """ - # NOTE: For some reason, circulation-admin converts "text" into so we have to turn it into None - # In this case circulation-admin will use - # TODO: To be fixed in https://jira.nypl.org/browse/SIMPLY-3008 - if self.value == ConfigurationAttributeType.TEXT.value: - return None - else: - return self.value - - -class ConfigurationAttribute(Enum): - """Enumeration of configuration setting attributes""" - - KEY = "key" - LABEL = "label" - DESCRIPTION = "description" - TYPE = "type" - REQUIRED = "required" - DEFAULT = "default" - OPTIONS = "options" - CATEGORY = "category" - FORMAT = "format" - - class ConfigurationAttributeValue(Enum): """Enumeration of common configuration attribute values""" YESVALUE = "yes" NOVALUE = "no" - - -class ConfigurationOption: - """Key-value pair containing information about configuration attribute option""" - - def __init__(self, key: str, label: str) -> None: - """Initializes a new instance of ConfigurationOption class - - :param key: Key - :param label: Label - """ - self._key = key - self._label = label - - def __eq__(self, other: object) -> bool: - """Compares two ConfigurationOption objects - - :param other: ConfigurationOption object - - :return: Boolean value indicating whether two items are equal - """ - if not isinstance(other, ConfigurationOption): - return False - - return self.key == other.key and self.label == other.label - - @property - def key(self) -> str: - """Returns option's key - - :return: Option's key - """ - return self._key - - @property - def label(self) -> str: - """Returns option's label - - :return: Option's label - """ - return self._label - - def to_settings(self) -> dict[str, str]: - """Returns a dictionary containing option metadata in the SETTINGS format - - :return: Dictionary containing option metadata in the SETTINGS format - """ - return {"key": self.key, "label": self.label} - - @staticmethod - def from_enum(cls: type[Enum]) -> list[ConfigurationOption]: - """Convers Enum to a list of options in the SETTINGS format - - :param cls: Enum type - - :return: List of options in the SETTINGS format - """ - if not issubclass(cls, Enum): - raise ValueError("Class should be descendant of Enum") - - return [ConfigurationOption(element.value, element.name) for element in cls] - - -class HasConfigurationSettings(metaclass=ABCMeta): - """Interface representing class containing ConfigurationMetadata properties""" - - @abstractmethod - def get_setting_value(self, setting_name: str) -> Any: - """Returns a settings'value - - :param setting_name: Name of the setting - - :return: Setting's value - """ - raise NotImplementedError() - - @abstractmethod - def set_setting_value(self, setting_name: str, setting_value: Any): - """Sets setting's value - - :param setting_name: Name of the setting - - :param setting_value: New value of the setting - """ - raise NotImplementedError() - - -class ConfigurationMetadata: - """Contains configuration metadata""" - - _counter = 0 - - def __init__( - self, - key: str, - label: str, - description: str, - type: ConfigurationAttributeType, - required: bool = False, - default: Any | None = None, - options: list[ConfigurationOption] | None = None, - category: str | None = None, - format=None, - index=None, - ): - """Initializes a new instance of ConfigurationMetadata class - - :param key: Setting's key - :param label: Setting's label - :param description: Setting's description - :param type: Setting's type - :param required: Boolean value indicating whether the setting is required or not - :param default: Setting's default value - :param options: Setting's options (used in the case of select) - :param category: Setting's category - """ - self._key = key - self._label = label - self._description = description - self._type = type - self._required = required - self._default = default - self._options = options - self._category = category - self._format = format - - if index is not None: - self._index = index - else: - ConfigurationMetadata._counter += 1 - self._index = ConfigurationMetadata._counter - - def __get__( - self, - owner_instance: HasConfigurationSettings | IgnoredIdentifierSettings | None, - owner_type: type | None, - ) -> Any: - """Returns a value of the setting - - :param owner_instance: Instance of the owner, class having instance of ConfigurationMetadata as an attribute - :param owner_type: Owner's class - - :return: ConfigurationMetadata instance (when called via a static method) or - the setting's value (when called via an instance method) - """ - # If owner_instance is empty, it means that this method was called - # via a static method of ConfigurationMetadataOwner (for example, ConfigurationBucket.to_settings). - # In this case we need to return the metadata instance itself - if owner_instance is None: - return self - - if not isinstance(owner_instance, HasConfigurationSettings): - raise Exception( - "owner must be an instance of HasConfigurationSettings type" - ) - - setting_value = owner_instance.get_setting_value(self._key) - - if setting_value is None: - setting_value = self.default - elif self.type == ConfigurationAttributeType.NUMBER: - try: - setting_value = float(setting_value) - except ValueError: - if setting_value != "": - # A non-empty value is a "bad" value, and should raise an exception - raise CannotLoadConfiguration( - f"Could not covert {self.label}'s value '{setting_value}'." - ) - setting_value = self.default - else: - # LIST and MENU configuration settings are stored as JSON-serialized lists in the database. - # We need to deserialize them to get actual values. - if self.type in ( - ConfigurationAttributeType.LIST, - ConfigurationAttributeType.MENU, - ): - if isinstance(setting_value, str): - setting_value = json.loads(setting_value) - else: - # We assume that LIST and MENU values can be either JSON or empty. - if setting_value is not None: - raise ValueError( - f"{self._type} configuration setting '{self._key}' has an incorrect format. " - f"Expected JSON-serialized list but got {setting_value}." - ) - - setting_value = [] - - return setting_value - - def __set__( - self, - owner_instance: HasConfigurationSettings | IgnoredIdentifierSettings | None, - value: Any, - ) -> Any: - """Updates the setting's value - - :param owner_instance: Instance of the owner, class having instance of ConfigurationMetadata as an attribute - - :param value: New setting's value - """ - if not isinstance(owner_instance, HasConfigurationSettings): - raise Exception( - "owner must be an instance of HasConfigurationSettings type" - ) - - return owner_instance.set_setting_value(self._key, value) - - @property - def key(self) -> str: - """Returns the setting's key - - :return: Setting's key - """ - return self._key - - @property - def label(self) -> str: - """Returns the setting's label - - :return: Setting's label - """ - return self._label - - @property - def description(self) -> str: - """Returns the setting's description - - :return: Setting's description - """ - return self._description - - @property - def type(self) -> ConfigurationAttributeType: - """Returns the setting's type - - :return: Setting's type - """ - return self._type - - @property - def required(self) -> bool: - """Returns the boolean value indicating whether the setting is required or not - - :return: Boolean value indicating whether the setting is required or not - """ - return self._required - - @property - def default(self) -> Any | None: - """Returns the setting's default value - - :return: Setting's default value - """ - return self._default - - @property - def options(self) -> list[ConfigurationOption] | None: - """Returns the setting's options (used in the case of select) - - :return: Setting's options (used in the case of select) - """ - return self._options - - @property - def category(self) -> str | None: - """Returns the setting's category - - :return: Setting's category - """ - return self._category - - @property - def format(self) -> str: - """Returns the setting's format - - :return: Setting's format - """ - return self._format - - @property - def index(self): - return self._index - - @staticmethod - def get_configuration_metadata(cls) -> list[tuple[str, ConfigurationMetadata]]: - """Returns a list of 2-tuples containing information ConfigurationMetadata properties in the specified class - - :param cls: Class - :return: List of 2-tuples containing information ConfigurationMetadata properties in the specified class - """ - members = inspect.getmembers(cls) - configuration_metadata = [] - - for name, member in members: - if isinstance(member, ConfigurationMetadata): - configuration_metadata.append((name, member)) - - configuration_metadata.sort(key=lambda pair: pair[1].index) - - return configuration_metadata - - def to_settings(self): - return { - ConfigurationAttribute.KEY.value: self.key, - ConfigurationAttribute.LABEL.value: self.label, - ConfigurationAttribute.DESCRIPTION.value: self.description, - ConfigurationAttribute.TYPE.value: self.type.to_control_type(), - ConfigurationAttribute.REQUIRED.value: self.required, - ConfigurationAttribute.DEFAULT.value: self.default, - ConfigurationAttribute.OPTIONS.value: [ - option.to_settings() for option in self.options - ] - if self.options - else None, - ConfigurationAttribute.CATEGORY.value: self.category, - ConfigurationAttribute.FORMAT.value: self.format, - } - - @staticmethod - def to_bool(metadata: ConfigurationMetadata) -> bool: - """Return a boolean scalar indicating whether the configuration setting - contains a value that can be treated as True (see ConfigurationSetting.MEANS_YES). - - :param metadata: ConfigurationMetadata object - :return: Boolean scalar indicating - whether this configuration setting contains a value that can be treated as True - """ - return str(metadata).lower() in ConfigurationSetting.MEANS_YES - - -class ConfigurationGrouping(HasConfigurationSettings): - """Base class for all classes containing configuration settings - - NOTE: Be aware that it's valid only while a database session is valid and must not be stored between requests - """ - - def __init__( - self, configuration_storage: BaseConfigurationStorage, db: Session - ) -> None: - """Initializes a new instance of ConfigurationGrouping - - :param configuration_storage: ConfigurationStorage object - :param db: Database session - """ - self._logger = logging.getLogger() - self._configuration_storage = configuration_storage - self._db = db - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self._db = None - - def get_setting_value(self, setting_name: str) -> Any: - """Returns a settings'value - - :param setting_name: Name of the setting - :return: Setting's value - """ - return self._configuration_storage.load(self._db, setting_name) - - def set_setting_value(self, setting_name: str, setting_value: Any) -> Any: - """Sets setting's value - - :param setting_name: Name of the setting - :param setting_value: New value of the setting - """ - self._configuration_storage.save(self._db, setting_name, setting_value) - - @classmethod - def to_settings_generator(cls) -> Iterable[dict]: - """Return a generator object returning settings in a format understandable by circulation-admin. - - :return: list of settings in a format understandable by circulation-admin. - """ - for name, member in ConfigurationMetadata.get_configuration_metadata(cls): - key_attribute = getattr(member, ConfigurationAttribute.KEY.value, None) - label_attribute = getattr(member, ConfigurationAttribute.LABEL.value, None) - description_attribute = getattr( - member, ConfigurationAttribute.DESCRIPTION.value, None - ) - type_attribute = getattr(member, ConfigurationAttribute.TYPE.value, None) - control_type = ( - type_attribute.to_control_type() if type_attribute is not None else None - ) - required_attribute = getattr( - member, ConfigurationAttribute.REQUIRED.value, None - ) - default_attribute = getattr( - member, ConfigurationAttribute.DEFAULT.value, None - ) - options_attribute = getattr( - member, ConfigurationAttribute.OPTIONS.value, None - ) - category_attribute = getattr( - member, ConfigurationAttribute.CATEGORY.value, None - ) - - yield { - ConfigurationAttribute.KEY.value: key_attribute, - ConfigurationAttribute.LABEL.value: label_attribute, - ConfigurationAttribute.DESCRIPTION.value: description_attribute, - ConfigurationAttribute.TYPE.value: control_type, - ConfigurationAttribute.REQUIRED.value: required_attribute, - ConfigurationAttribute.DEFAULT.value: default_attribute, - ConfigurationAttribute.OPTIONS.value: [ - option.to_settings() for option in options_attribute - ] - if options_attribute - else None, - ConfigurationAttribute.CATEGORY.value: category_attribute, - } - - @classmethod - def to_settings(cls) -> list[dict[str, Any]]: - """Return a list of settings in a format understandable by circulation-admin. - - :return: list of settings in a format understandable by circulation-admin. - """ - return list(cls.to_settings_generator()) - - -C = TypeVar("C", bound="ConfigurationGrouping") - - -class ConfigurationFactory: - """Factory creating new instances of ConfigurationGrouping class descendants.""" - - @contextmanager - def create( - self, - configuration_storage: ConfigurationStorage, - db: Session, - configuration_grouping_class: type[C], - ) -> Iterator[C]: - """Create a new instance of ConfigurationGrouping. - - :param configuration_storage: ConfigurationStorage object - :param db: Database session - :param configuration_grouping_class: Configuration bucket's class - :return: ConfigurationGrouping instance - """ - with configuration_grouping_class( - configuration_storage, db - ) as configuration_bucket: - yield configuration_bucket diff --git a/tests/core/models/test_configuration.py b/tests/core/models/test_configuration.py index e7f7fe8ee6..4d53c70b0d 100644 --- a/tests/core/models/test_configuration.py +++ b/tests/core/models/test_configuration.py @@ -1,25 +1,13 @@ -import json -from enum import Enum -from unittest.mock import MagicMock, create_autospec - import pytest -import sqlalchemy from sqlalchemy.exc import IntegrityError from core.config import CannotLoadConfiguration, Configuration from core.model import create, get_one from core.model.collection import Collection from core.model.configuration import ( - ConfigurationAttribute, - ConfigurationAttributeType, - ConfigurationGrouping, - ConfigurationMetadata, - ConfigurationOption, ConfigurationSetting, - ConfigurationStorage, ExternalIntegration, ExternalIntegrationLink, - HasExternalIntegration, ) from core.model.datasource import DataSource from tests.fixtures.database import DatabaseTransactionFixture @@ -757,438 +745,3 @@ def test_delete( external_integrations = session.query(ExternalIntegration).all() assert integration1 not in external_integrations assert integration2 in external_integrations - - -SETTING1_KEY = "setting1" -SETTING1_LABEL = "Setting 1's label" -SETTING1_DESCRIPTION = "Setting 1's description" -SETTING1_TYPE = ConfigurationAttributeType.TEXT -SETTING1_REQUIRED = False -SETTING1_DEFAULT = "12345" -SETTING1_CATEGORY = "Settings" - -SETTING2_KEY = "setting2" -SETTING2_LABEL = "Setting 2's label" -SETTING2_DESCRIPTION = "Setting 2's description" -SETTING2_TYPE = ConfigurationAttributeType.SELECT -SETTING2_REQUIRED = False -SETTING2_DEFAULT = "value1" -SETTING2_OPTIONS = [ - ConfigurationOption("key1", "value1"), - ConfigurationOption("key2", "value2"), - ConfigurationOption("key3", "value3"), -] -SETTING2_CATEGORY = "Settings" - -SETTING3_KEY = "setting3" -SETTING3_LABEL = "Setting 3's label" -SETTING3_DESCRIPTION = "Setting 3's description" -SETTING3_TYPE = ConfigurationAttributeType.MENU -SETTING3_REQUIRED = False -SETTING3_OPTIONS = [ - ConfigurationOption("key1", "value1"), - ConfigurationOption("key2", "value2"), - ConfigurationOption("key3", "value3"), -] -SETTING3_DEFAULT = [SETTING3_OPTIONS[0].key, SETTING3_OPTIONS[1].key] -SETTING3_CATEGORY = "Settings" - - -SETTING4_KEY = "setting4" -SETTING4_LABEL = "Setting 4's label" -SETTING4_DESCRIPTION = "Setting 4's description" -SETTING4_TYPE = ConfigurationAttributeType.LIST -SETTING4_REQUIRED = False -SETTING4_OPTIONS = None -SETTING4_DEFAULT = None -SETTING4_CATEGORY = "Settings" - -SETTING5_KEY = "setting5" -SETTING5_LABEL = "Setting 5's label" -SETTING5_DESCRIPTION = "Setting 5's description" -SETTING5_TYPE = ConfigurationAttributeType.NUMBER -SETTING5_REQUIRED = False -SETTING5_DEFAULT = 12345 -SETTING5_CATEGORY = "Settings" - - -class MockConfiguration(ConfigurationGrouping): - setting1 = ConfigurationMetadata( - key=SETTING1_KEY, - label=SETTING1_LABEL, - description=SETTING1_DESCRIPTION, - type=SETTING1_TYPE, - required=SETTING1_REQUIRED, - default=SETTING1_DEFAULT, - category=SETTING1_CATEGORY, - ) - - setting2 = ConfigurationMetadata( - key=SETTING2_KEY, - label=SETTING2_LABEL, - description=SETTING2_DESCRIPTION, - type=SETTING2_TYPE, - required=SETTING2_REQUIRED, - default=SETTING2_DEFAULT, - options=SETTING2_OPTIONS, - category=SETTING2_CATEGORY, - ) - - setting3 = ConfigurationMetadata( - key=SETTING3_KEY, - label=SETTING3_LABEL, - description=SETTING3_DESCRIPTION, - type=SETTING3_TYPE, - required=SETTING3_REQUIRED, - default=SETTING3_DEFAULT, - options=SETTING3_OPTIONS, - category=SETTING3_CATEGORY, - ) - - setting4 = ConfigurationMetadata( - key=SETTING4_KEY, - label=SETTING4_LABEL, - description=SETTING4_DESCRIPTION, - type=SETTING4_TYPE, - required=SETTING4_REQUIRED, - default=SETTING4_DEFAULT, - options=SETTING4_OPTIONS, - category=SETTING4_CATEGORY, - ) - - setting5 = ConfigurationMetadata( - key=SETTING5_KEY, - label=SETTING5_LABEL, - description=SETTING5_DESCRIPTION, - type=SETTING5_TYPE, - required=SETTING5_REQUIRED, - default=SETTING5_DEFAULT, - category=SETTING5_CATEGORY, - ) - - -class ConfigurationWithBooleanProperty(ConfigurationGrouping): - boolean_setting = ConfigurationMetadata( - key="boolean_setting", - label="Boolean Setting", - description="Boolean Setting", - type=ConfigurationAttributeType.SELECT, - required=True, - default="true", - options=[ - ConfigurationOption("true", "True"), - ConfigurationOption("false", "False"), - ], - ) - - -class MockConfiguration2(ConfigurationGrouping): - setting1 = ConfigurationMetadata( - key="setting1", - label=SETTING1_LABEL, - description=SETTING1_DESCRIPTION, - type=SETTING1_TYPE, - required=SETTING1_REQUIRED, - default=SETTING1_DEFAULT, - category=SETTING1_CATEGORY, - index=1, - ) - - setting2 = ConfigurationMetadata( - key="setting2", - label=SETTING2_LABEL, - description=SETTING2_DESCRIPTION, - type=SETTING2_TYPE, - required=SETTING2_REQUIRED, - default=SETTING2_DEFAULT, - options=SETTING2_OPTIONS, - category=SETTING2_CATEGORY, - index=0, - ) - - -class TestConfigurationOption: - def test_to_settings(self): - # Arrange - option = ConfigurationOption("key1", "value1") - expected_result = {"key": "key1", "label": "value1"} - - # Act - result = option.to_settings() - - # Assert - assert result == expected_result - - def test_from_enum(self): - # Arrange - class TestEnum(Enum): - LABEL1 = "KEY1" - LABEL2 = "KEY2" - - expected_result = [ - ConfigurationOption("KEY1", "LABEL1"), - ConfigurationOption("KEY2", "LABEL2"), - ] - - # Act - result = ConfigurationOption.from_enum(TestEnum) - - # Assert - assert result == expected_result - - -class TestConfigurationGrouping: - @pytest.mark.parametrize( - "_,setting_name,expected_value", - [("setting1", "setting1", 12345), ("setting2", "setting2", "12345")], - ) - def test_getters(self, _, setting_name, expected_value): - # Arrange - configuration_storage = create_autospec(spec=ConfigurationStorage) - configuration_storage.load = MagicMock(return_value=expected_value) - db = create_autospec(spec=sqlalchemy.orm.session.Session) - configuration = MockConfiguration(configuration_storage, db) - - # Act - setting_value = getattr(configuration, setting_name) - - # Assert - assert setting_value == expected_value - configuration_storage.load.assert_called_once_with(db, setting_name) - - @pytest.mark.parametrize( - "_,setting_name,db_value,expected_value", - [ - ( - "default_menu_value", - MockConfiguration.setting3.key, - None, - MockConfiguration.setting3.default, - ), - ( - "menu_value", - MockConfiguration.setting3.key, - json.dumps( - [ - MockConfiguration.setting3.options[0].key, - MockConfiguration.setting3.options[1].key, - ] - ), - [ - MockConfiguration.setting3.options[0].key, - MockConfiguration.setting3.options[1].key, - ], - ), - ( - "default_list_value", - MockConfiguration.setting4.key, - None, - MockConfiguration.setting4.default, - ), - ( - "menu_value", - MockConfiguration.setting4.key, - json.dumps(["value1", "value2"]), - ["value1", "value2"], - ), - ], - ) - def test_menu_and_list_getters(self, _, setting_name, db_value, expected_value): - # Arrange - configuration_storage = create_autospec(spec=ConfigurationStorage) - configuration_storage.load = MagicMock(return_value=db_value) - db = create_autospec(spec=sqlalchemy.orm.session.Session) - configuration = MockConfiguration(configuration_storage, db) - - # Act - setting_value = getattr(configuration, setting_name) - - # Assert - assert setting_value == expected_value - configuration_storage.load.assert_called_once_with(db, setting_name) - - def test_getter_return_default_value(self): - # Arrange - configuration_storage = create_autospec(spec=ConfigurationStorage) - configuration_storage.load = MagicMock(return_value=None) - db = create_autospec(spec=sqlalchemy.orm.session.Session) - configuration = MockConfiguration(configuration_storage, db) - - # Act - setting1_value = configuration.setting1 - setting5_value = configuration.setting5 - - # Assert - assert SETTING1_DEFAULT == setting1_value - assert SETTING5_DEFAULT == setting5_value - - @pytest.mark.parametrize( - "_,setting_name,expected_value", - [("setting1", "setting1", 12345), ("setting2", "setting2", "12345")], - ) - def test_setters(self, _, setting_name, expected_value): - # Arrange - configuration_storage = create_autospec(spec=ConfigurationStorage) - configuration_storage.save = MagicMock(return_value=expected_value) - db = create_autospec(spec=sqlalchemy.orm.session.Session) - configuration = MockConfiguration(configuration_storage, db) - - # Act - setattr(configuration, setting_name, expected_value) - - # Assert - configuration_storage.save.assert_called_once_with( - db, setting_name, expected_value - ) - - def test_to_settings_considers_default_indices(self): - # Act - settings = MockConfiguration.to_settings() - - # Assert - assert len(settings) == 5 - - assert settings[0][ConfigurationAttribute.KEY.value] == SETTING1_KEY - assert settings[0][ConfigurationAttribute.LABEL.value] == SETTING1_LABEL - assert ( - settings[0][ConfigurationAttribute.DESCRIPTION.value] - == SETTING1_DESCRIPTION - ) - assert settings[0][ConfigurationAttribute.TYPE.value] == None - assert settings[0][ConfigurationAttribute.REQUIRED.value] == SETTING1_REQUIRED - assert settings[0][ConfigurationAttribute.DEFAULT.value] == SETTING1_DEFAULT - assert settings[0][ConfigurationAttribute.CATEGORY.value] == SETTING1_CATEGORY - - assert settings[1][ConfigurationAttribute.KEY.value] == SETTING2_KEY - assert settings[1][ConfigurationAttribute.LABEL.value] == SETTING2_LABEL - assert ( - settings[1][ConfigurationAttribute.DESCRIPTION.value] - == SETTING2_DESCRIPTION - ) - assert settings[1][ConfigurationAttribute.TYPE.value] == SETTING2_TYPE.value - assert settings[1][ConfigurationAttribute.REQUIRED.value] == SETTING2_REQUIRED - assert settings[1][ConfigurationAttribute.DEFAULT.value] == SETTING2_DEFAULT - assert settings[1][ConfigurationAttribute.OPTIONS.value] == [ - option.to_settings() for option in SETTING2_OPTIONS - ] - assert settings[1][ConfigurationAttribute.CATEGORY.value] == SETTING2_CATEGORY - - assert settings[2][ConfigurationAttribute.KEY.value] == SETTING3_KEY - assert settings[2][ConfigurationAttribute.LABEL.value] == SETTING3_LABEL - assert ( - settings[2][ConfigurationAttribute.DESCRIPTION.value] - == SETTING3_DESCRIPTION - ) - assert settings[2][ConfigurationAttribute.TYPE.value] == SETTING3_TYPE.value - assert settings[2][ConfigurationAttribute.REQUIRED.value] == SETTING3_REQUIRED - assert settings[2][ConfigurationAttribute.DEFAULT.value] == SETTING3_DEFAULT - assert settings[2][ConfigurationAttribute.OPTIONS.value] == [ - option.to_settings() for option in SETTING3_OPTIONS - ] - assert settings[2][ConfigurationAttribute.CATEGORY.value] == SETTING3_CATEGORY - - assert settings[3][ConfigurationAttribute.KEY.value] == SETTING4_KEY - assert settings[3][ConfigurationAttribute.LABEL.value] == SETTING4_LABEL - assert ( - settings[3][ConfigurationAttribute.DESCRIPTION.value] - == SETTING4_DESCRIPTION - ) - assert settings[3][ConfigurationAttribute.TYPE.value] == SETTING4_TYPE.value - assert settings[3][ConfigurationAttribute.REQUIRED.value] == SETTING4_REQUIRED - assert settings[3][ConfigurationAttribute.DEFAULT.value] == SETTING4_DEFAULT - assert settings[3][ConfigurationAttribute.CATEGORY.value] == SETTING4_CATEGORY - - def test_to_settings_considers_explicit_indices(self): - # Act - settings = MockConfiguration2.to_settings() - - # Assert - assert len(settings) == 2 - - assert settings[0][ConfigurationAttribute.KEY.value] == SETTING2_KEY - assert settings[0][ConfigurationAttribute.LABEL.value] == SETTING2_LABEL - assert ( - settings[0][ConfigurationAttribute.DESCRIPTION.value] - == SETTING2_DESCRIPTION - ) - assert settings[0][ConfigurationAttribute.TYPE.value] == SETTING2_TYPE.value - assert settings[0][ConfigurationAttribute.REQUIRED.value] == SETTING2_REQUIRED - assert settings[0][ConfigurationAttribute.DEFAULT.value] == SETTING2_DEFAULT - assert settings[0][ConfigurationAttribute.OPTIONS.value] == [ - option.to_settings() for option in SETTING2_OPTIONS - ] - assert settings[0][ConfigurationAttribute.CATEGORY.value] == SETTING2_CATEGORY - - assert settings[1][ConfigurationAttribute.KEY.value] == SETTING1_KEY - assert settings[1][ConfigurationAttribute.LABEL.value] == SETTING1_LABEL - assert ( - settings[1][ConfigurationAttribute.DESCRIPTION.value] - == SETTING1_DESCRIPTION - ) - assert settings[1][ConfigurationAttribute.TYPE.value] == None - assert settings[1][ConfigurationAttribute.REQUIRED.value] == SETTING1_REQUIRED - assert settings[1][ConfigurationAttribute.DEFAULT.value] == SETTING1_DEFAULT - assert settings[1][ConfigurationAttribute.CATEGORY.value] == SETTING1_CATEGORY - - -class TestNumberConfigurationMetadata: - def test_number_type_getter(self, db: DatabaseTransactionFixture): - # Arrange - external_integration = db.external_integration("test") - external_integration_association = create_autospec(spec=HasExternalIntegration) - external_integration_association.external_integration = MagicMock( - return_value=external_integration - ) - configuration_storage = ConfigurationStorage(external_integration_association) - configuration = MockConfiguration(configuration_storage, db.session) - - configuration.setting5 = "abc" - with pytest.raises(CannotLoadConfiguration): - configuration.setting5 - - configuration.setting5 = "123" - assert configuration.setting5 == 123.0 - - configuration.setting5 = "" - assert configuration.setting5 == SETTING5_DEFAULT - - -class TestBooleanConfigurationMetadata: - @pytest.mark.parametrize( - "provided,expected", - [ - ("true", True), - ("t", True), - ("yes", True), - ("y", True), - (1, False), - ("false", False), - ], - ) - def test_configuration_metadata_correctly_cast_bool_values( - self, db: DatabaseTransactionFixture, provided, expected - ): - """Ensure that ConfigurationMetadata.to_bool correctly translates different values into boolean (True/False).""" - # Arrange - external_integration = db.external_integration("test") - - external_integration_association = create_autospec(spec=HasExternalIntegration) - external_integration_association.external_integration = MagicMock( - return_value=external_integration - ) - - configuration_storage = ConfigurationStorage(external_integration_association) - - configuration = ConfigurationWithBooleanProperty( - configuration_storage, db.session - ) - - # We set a new value using ConfigurationMetadata.__set__ - configuration.boolean_setting = provided - - # Act - # We read the existing value using ConfigurationMetadata.__get__ - result = ConfigurationMetadata.to_bool(configuration.boolean_setting) - - # Assert - assert expected == result