From 69db9d26726ed9220ccf9a8b5e74f7fe037a56fc Mon Sep 17 00:00:00 2001 From: coldsofttech Date: Mon, 25 Mar 2024 22:12:37 +0000 Subject: [PATCH] Initial release --- .github/workflows/pipeline.yml | 40 ++++ CHANGELOG.md | 3 + README.md | 53 +++++- conftest.py | 45 +++++ pyconfigurationmanager/__init__.py | 38 ++++ pyconfigurationmanager/__main__.py | 242 ++++++++++++++++++++++++ pytest.ini | 5 + requirements.txt | 3 + setup.py | 37 ++++ tests/test_configurationmanager.py | 156 +++++++++++++++ tests/test_configurationmanagererror.py | 35 ++++ tests/utilityclass.py | 87 +++++++++ 12 files changed, 743 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pipeline.yml create mode 100644 CHANGELOG.md create mode 100644 conftest.py create mode 100644 pyconfigurationmanager/__init__.py create mode 100644 pyconfigurationmanager/__main__.py create mode 100644 pytest.ini create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/test_configurationmanager.py create mode 100644 tests/test_configurationmanagererror.py create mode 100644 tests/utilityclass.py diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 0000000..a14ee8c --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,40 @@ +name: Test & Build + +on: + push: + branches: + - '*' + +jobs: + build_and_test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + + - name: Install Pytest + run: | + pip install pytest + + - name: Build Package + run: | + python setup.py sdist + + - name: Install Package + run: | + pip install dist/* + + - name: Run Tests + run: | + pytest tests -vv -rEPW -o pytest_collection_order=alphabetical --cache-clear --color=yes \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a5af8fd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# Version History + +- 0.1.0: Initial Release (latest) \ No newline at end of file diff --git a/README.md b/README.md index 77e4100..761b23f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,53 @@ -# pyconfigurationmanager +# `pyconfigurationmanager` The pyconfigurationmanager package provides a set of utilities for managing configuration settings in Python applications. It includes classes and functions for loading configuration data from files, auditing configuration changes, and ensuring secure access to configuration files. + +## Installation +Configuration Manager can be installed using pip: +```bash +pip install git+https://github.com/coldsofttech/pyconfigurationmanager.git +``` + +## Usage +````python +from pyconfigurationmanager import ConfigurationManager + +# Load the configuration file +ConfigurationManager.load_config(file_path='config.json') + +# Retrieve a string value +string_value = ConfigurationManager.get_config_value('sample_string') +print(string_value) # Output: 'string_of_sample' + +# Retrieve an integer value +integer_value = ConfigurationManager.get_config_value('sample_integer') +print(integer_value) # Output: 100 + +# Retrieve a dictionary value +dict_value = ConfigurationManager.get_config_value('sample_others') +print(dict_value) +# Output: +# {'sample_boolean': True, 'sample_list': ['list_1', 'list_2']} + +# Retrieve a boolean value +boolean_value = ConfigurationManager.get_config_value('sample_others.sample_boolean') +print(boolean_value) # Output: True + +# Retrieve a list value +list_value = ConfigurationManager.get_config_value('sample_others.sample_list') +print(list_value) # Output: ['list_1', 'list_2'] +```` + +# Documentation +## `pyconfigurationmanager` +### `ConfigurationManager` +The ConfigurationManager class is the heart of the package, offering a suite of functionalities for managing configuration settings. + +#### Methods +- `load_config(file_path: Optional[str] = 'config.json', secure: Optional[bool] = True, audit: Optional[bool] = False, audit_file_path: Optional[str] = 'audit.log')`: Loads configuration settings from a file into memory, with options to enable secure mode and auditing. By default, secure mode is enabled, ensuring that the configuration file has only readable permissions for the user. If auditing is enabled, all configuration accesses are logged for future audit purposes. +- `get_config_value(key: Optional[str] = None)`: Retrieves the value of a specific configuration setting or the entire configuration if no key is provided. Hierarchical keys can be accessed by separating them with '.'. + +### `ConfigurationManagerError` +This error class is employed to signal any issues or errors encountered during the execution of ConfigurationManager methods. + +#### Methods +- `__init__(message: Optional[str])` - Initialize a ConfigurationManagerError \ No newline at end of file diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..44e2306 --- /dev/null +++ b/conftest.py @@ -0,0 +1,45 @@ +# Copyright (c) 2024 coldsofttech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +def pytest_collection_modifyitems(config, items): + """ + Custom pytest hook to modify the collection of test items. + This function sorts test items to execute specific tests first. + """ + def test_order(test_name): + # Define the desired order of execution for specific test names + order_mapping = { + 'test_get_config_value_invalid_no_config_set': 1, + 'test_valid_no_params': 2, + 'test_invalid_exposed_permissions': 3, + 'test_valid_insecure': 4, + 'test_invalid_file_not_found': 5, + 'test_invalid_json': 6, + 'test_get_config_value_valid_string': 7, + 'test_get_config_value_valid_integer': 8, + 'test_get_config_value_valid_dict': 9, + 'test_get_config_value_valid_boolean': 10, + 'test_get_config_value_valid_list': 11, + 'test_get_config_value_valid_audit_all': 12, + 'test_get_config_value_valid_audit_specific': 13 + } + return order_mapping.get(test_name, float('inf')) # Default to infinity for tests not in the mapping + + items.sort(key=lambda item: (test_order(item.nodeid.split("::")[-1]), item.fspath, item.originalname)) diff --git a/pyconfigurationmanager/__init__.py b/pyconfigurationmanager/__init__.py new file mode 100644 index 0000000..9f32694 --- /dev/null +++ b/pyconfigurationmanager/__init__.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024 coldsofttech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +__all__ = [ + "ConfigurationManager", + "ConfigurationManagerError", + "__author__", + "__description__", + "__name__", + "__version__" +] +__author__ = "coldsofttech" +__description__ = """ +The pyconfigurationmanager package provides a set of utilities for managing configuration settings in +Python applications. It includes classes and functions for loading configuration data from files, +auditing configuration changes, and ensuring secure access to configuration files. +""" +__name__ = "pyconfigurationmanager" +__version__ = "0.1.0" + +from pyconfigurationmanager.__main__ import ConfigurationManager, ConfigurationManagerError diff --git a/pyconfigurationmanager/__main__.py b/pyconfigurationmanager/__main__.py new file mode 100644 index 0000000..e2d8d7d --- /dev/null +++ b/pyconfigurationmanager/__main__.py @@ -0,0 +1,242 @@ +# Copyright (c) 2024 coldsofttech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import getpass +import json +import os.path +import platform +import socket +import subprocess +from types import NoneType +from typing import Optional, Any, Union + + +class UtilityClass: + """ + Utility class providing methods to retrieve information about the local machine and current user. + """ + + @staticmethod + def get_ip_address() -> str: + """ + Get the IP address of the local machine. + + :return: The IP address of the local machine as a string. + :rtype: str + """ + hostname = socket.gethostname() + ip_address = socket.gethostbyname(hostname) + return ip_address + + @staticmethod + def get_username() -> str: + """ + Get the username of the current user. + + :return: The username of the current user as a string. + :rtype: str + """ + username = getpass.getuser() + return username + + +class ConfigurationManagerError(RuntimeError): + """ + Error class raised when there is an issue with ConfigurationManager. + """ + + def __init__( + self, + message: Optional[str] = 'ConfigurationManager load_config is not set.' + ): + """ + Initialize a ConfigurationManagerError. + + :param message: The error message to be associated with the exception. + Defaults to 'ConfigurationManager load_config is not set.' + :type message: Optional[str] + """ + if not isinstance(message, str): + raise TypeError('message should be a string.') + + self.message = message + super().__init__(self.message) + + +class ConfigurationManager: + """ + Class for managing configuration settings. + """ + + _audit = False + _audit_file_path = None + _audit_logger = None + _config = None + _file_full_path = '' + + @classmethod + def _configure_audit_logger(cls) -> None: + """ + Configure the audit logger. + """ + import pyloggermanager + + if cls._audit: + formatter = pyloggermanager.formatters.DefaultFormatter( + format_str='%(time)s :: %(message)s' + ) + handler = pyloggermanager.handlers.FileHandler( + file_name=cls._audit_file_path, + level=pyloggermanager.LogLevel.INFO, + formatter=formatter + ) + cls._audit_logger = pyloggermanager.Logger(name='audit_logger', level=pyloggermanager.LogLevel.INFO) + cls._audit_logger.add_handler(handler) + + @classmethod + def _configure_loggers(cls) -> None: + """ + Configure all loggers. + """ + cls._configure_audit_logger() + + @classmethod + def _check_file_permissions(cls, file_path: str) -> None: + """ + Check the permissions of the configuration file. + + :param file_path: The path of the configuration file. + :type file_path: str + """ + error = f'Configuration file "{file_path}" should be readable only by the user.' + if platform.system().lower() == "windows": + command = f'powershell.exe icacls "{file_path}"' + process = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE + ) + output, _ = process.communicate() + permissions = output.decode().strip().replace(file_path, '') + permissions_list = [p.strip() for p in permissions.split('\n') if p.strip()] + + if len(permissions_list) > 2: + raise OSError(error) + elif not UtilityClass.get_username() in permissions_list[0]: + raise OSError(error) + elif any(perm in permissions_list[0] for perm in ['(F)', '(M)']): + raise OSError(error) + elif platform.system().lower() == 'linux': + file_stat = os.stat(file_path) + if file_stat.st_mode & 0o777 != 0o400: + raise OSError(error) + else: + raise OSError("Unsupported operating system.") + + @classmethod + def _log_audit(cls, method: str, message: str) -> None: + """ + Log audit messages. + + :param method: The method name. + :type method: str + :param message: The message to be logged. + :type message: str + """ + if cls._audit: + source_ip = UtilityClass.get_ip_address() + username = UtilityClass.get_username() + formatted_message = f'{method} :: {source_ip} :: {username} :: {message}' + cls._audit_logger.info(formatted_message, ignore_display=True) + + @classmethod + def load_config( + cls, + file_path: Optional[str] = 'config.json', + secure: Optional[bool] = True, + audit: Optional[bool] = False, + audit_file_path: Optional[str] = 'audit.log' + ) -> None: + """ + Load configuration settings from a file. + + :param file_path: The path of the configuration file. Defaults to 'config.json'. + :type file_path: Optional[str] + :param secure: Flag indicating whether secure mode is enabled. Defaults to True. + :type secure: Optional[bool] + :param audit: Flag indicating whether auditing is enabled. Defaults to False. + :type audit: Optional[bool] + :param audit_file_path: The file path for storing audit logs. Defaults to 'audit.log'. + :type audit_file_path: Optional[str] + """ + if not isinstance(file_path, str): + raise TypeError('file_path should be a string.') + elif not isinstance(secure, bool): + raise TypeError('secure should be a boolean.') + elif not isinstance(audit, bool): + raise TypeError('audit should be a boolean.') + elif not isinstance(audit_file_path, str): + raise TypeError('audit_file_path should be a string.') + + try: + cls._file_full_path = os.path.abspath(file_path) + + if secure: + cls._check_file_permissions(cls._file_full_path) + + with open(cls._file_full_path, 'r') as config_file: + cls._config = json.load(config_file) + except FileNotFoundError: + raise ConfigurationManagerError(f'Configuration file "{file_path}" not found.') + except json.JSONDecodeError: + raise ConfigurationManagerError(f'Error decoding JSON in the configuration file "{file_path}".') + except OSError as e: + raise ConfigurationManagerError(str(e)) + + cls._audit = audit + cls._audit_file_path = audit_file_path + cls._configure_loggers() + + @classmethod + def get_config_value(cls, key: Optional[str] = None) -> Any: + """ + Get the value of a configuration setting. + + :param key: The key (separated by '.') to access a specific configuration value. + If None, returns the entire configuration. + :type key: Optional[str] + :return: The value of the configuration key + :rtype: Any + """ + if not isinstance(key, Union[str, NoneType]): + raise TypeError('key should be a string.') + + if cls._config is None: + raise ConfigurationManagerError() + + if key is None: + cls._log_audit(cls.__name__, 'Returning entire configuration.') + return cls._config + else: + keys = key.split('.') + value = cls._config + for k in keys: + value = value[k] + + cls._log_audit(cls.__name__, f'Accessed configuration value for {key}.') + return value diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..32f0665 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +markers = + sequential_order + +addopts = -o pytest_collection_order=alphabetical \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ed5e95a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pyloggermanager@ git+https://github.com/coldsofttech/pyloggermanager.git +pytest~=7.4.0 +setuptools~=69.2.0 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..25dac7a --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +# Copyright (c) 2024 coldsofttech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from setuptools import setup +import pyconfigurationmanager + +setup( + name=pyconfigurationmanager.__name__, + version=pyconfigurationmanager.__version__, + packages=[ + pyconfigurationmanager.__name__ + ], + url='https://github.com/coldsofttech/pyconfigurationmanager', + license='MIT', + author='coldsofttech', + description=pyconfigurationmanager.__description__, + install_requires=[ + 'pyloggermanager@ git+https://github.com/coldsofttech/pyloggermanager.git' + ] +) diff --git a/tests/test_configurationmanager.py b/tests/test_configurationmanager.py new file mode 100644 index 0000000..0e89353 --- /dev/null +++ b/tests/test_configurationmanager.py @@ -0,0 +1,156 @@ +# Copyright (c) 2024 coldsofttech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import pytest +import unittest +from pyconfigurationmanager import ConfigurationManager, ConfigurationManagerError +from utilityclass import UtilityClass + + +class TestConfigurationManager(unittest.TestCase): + """Unit test cases for ConfigurationManager""" + + def setUp(self) -> None: + self.file_name = UtilityClass.generate_name('json') + self.audit_file_name = UtilityClass.generate_name('log') + UtilityClass.generate_config(self.file_name) + + def tearDown(self) -> None: + UtilityClass.delete_config(self.file_name) + UtilityClass.delete_config(self.audit_file_name) + + def test_invalid_file_path(self): + """Test if load config raises TypeError""" + with self.assertRaises(TypeError): + ConfigurationManager.load_config(file_path=100) + + def test_invalid_secure(self): + """Test if load config raises TypeError""" + with self.assertRaises(TypeError): + ConfigurationManager.load_config(secure=100) + + def test_invalid_audit(self): + """Test if load config raises TypeError""" + with self.assertRaises(TypeError): + ConfigurationManager.load_config(audit=100) + + def test_invalid_audit_file_path(self): + """Test if load config raises TypeError""" + with self.assertRaises(TypeError): + ConfigurationManager.load_config(audit_file_path=100) + + @pytest.mark.sequential_order + def test_valid_no_params(self): + """Test if load config raises TypeError""" + self.assertEqual(None, ConfigurationManager._config) + ConfigurationManager.load_config(file_path=self.file_name) + self.assertIsInstance(ConfigurationManager._config, dict) + + @pytest.mark.sequential_order + def test_invalid_exposed_permissions(self): + """Test if load config raises ConfigurationManagerError""" + UtilityClass.delete_config(self.file_name) + self.file_name = UtilityClass.generate_name('json') + UtilityClass.generate_config(self.file_name, False) + with self.assertRaises(ConfigurationManagerError): + ConfigurationManager.load_config(file_path=self.file_name) + + @pytest.mark.sequential_order + def test_valid_insecure(self): + """Test if load config works as expected""" + UtilityClass.delete_config(self.file_name) + self.file_name = UtilityClass.generate_name('json') + UtilityClass.generate_config(self.file_name, False) + self.assertIsInstance(ConfigurationManager._config, dict) + + @pytest.mark.sequential_order + def test_invalid_file_not_found(self): + """Test if load config raises ConfigurationManagerError""" + with self.assertRaises(ConfigurationManagerError): + ConfigurationManager.load_config(file_path='sample.txt') + + @pytest.mark.sequential_order + def test_invalid_json(self): + """Test if load config works as expected""" + UtilityClass.delete_config(self.file_name) + self.file_name = UtilityClass.generate_name('json') + UtilityClass.generate_config(self.file_name, invalid_json=True) + ConfigurationManager.load_config(file_path=self.file_name) + + @pytest.mark.sequential_order + def test_get_config_value_invalid_no_config_set(self): + """Test if load config raises ConfigurationManagerError""" + with self.assertRaises(ConfigurationManagerError): + ConfigurationManager.get_config_value('sample') + + @pytest.mark.sequential_order + def test_get_config_value_valid_string(self): + """Test if load config works as expected""" + ConfigurationManager.load_config(self.file_name) + self.assertEqual('string_of_sample', ConfigurationManager.get_config_value('sample_string')) + + @pytest.mark.sequential_order + def test_get_config_value_valid_integer(self): + """Test if load config works as expected""" + ConfigurationManager.load_config(self.file_name) + self.assertEqual(100, ConfigurationManager.get_config_value('sample_integer')) + + @pytest.mark.sequential_order + def test_get_config_value_valid_dict(self): + """Test if load config works as expected""" + ConfigurationManager.load_config(self.file_name) + self.assertIsInstance(ConfigurationManager.get_config_value('sample_others'), dict) + + @pytest.mark.sequential_order + def test_get_config_value_valid_boolean(self): + """Test if load config works as expected""" + ConfigurationManager.load_config(self.file_name) + self.assertTrue(ConfigurationManager.get_config_value('sample_others.sample_boolean')) + + @pytest.mark.sequential_order + def test_get_config_value_valid_list(self): + """Test if load config works as expected""" + ConfigurationManager.load_config(self.file_name) + self.assertIsInstance(ConfigurationManager.get_config_value('sample_others.sample_list'), list) + assert 2 == len(ConfigurationManager.get_config_value('sample_others.sample_list')) + + @pytest.mark.sequential_order + def test_get_config_value_valid_audit_all(self): + """Test if load config works as expected""" + ConfigurationManager.load_config(self.file_name, audit=True, audit_file_path=self.audit_file_name) + ConfigurationManager.get_config_value() + expected_output = 'Returning entire configuration.' + with open(self.audit_file_name, 'r') as file: + file_content = file.read() + self.assertIn(expected_output, file_content) + + @pytest.mark.sequential_order + def test_get_config_value_valid_audit_specific(self): + """Test if load config works as expected""" + ConfigurationManager.load_config(self.file_name, audit=True, audit_file_path=self.audit_file_name) + ConfigurationManager.get_config_value('sample_integer') + expected_output = 'Accessed configuration value for sample_integer.' + with open(self.audit_file_name, 'r') as file: + file_content = file.read() + self.assertIn(expected_output, file_content) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_configurationmanagererror.py b/tests/test_configurationmanagererror.py new file mode 100644 index 0000000..3501a5a --- /dev/null +++ b/tests/test_configurationmanagererror.py @@ -0,0 +1,35 @@ +# Copyright (c) 2024 coldsofttech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import unittest +from pyconfigurationmanager import ConfigurationManagerError + + +class TestConfigurationManagerError(unittest.TestCase): + """Unit test cases for ConfigurationManagerError""" + + def test_init_valid(self): + """Test if init method works as expected""" + with self.assertRaises(ConfigurationManagerError): + raise ConfigurationManagerError('Test error') + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/utilityclass.py b/tests/utilityclass.py new file mode 100644 index 0000000..411a83a --- /dev/null +++ b/tests/utilityclass.py @@ -0,0 +1,87 @@ +# Copyright (c) 2024 coldsofttech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import json +import os +import platform +import random +import string +import subprocess + + +class UtilityClass: + """Utility class for unit test cases""" + + @staticmethod + def generate_name(extension: str, length: int = 10) -> str: + """Generates file name based on the extension provided""" + letters = string.ascii_lowercase + file_name = ''.join(random.choice(letters) for _ in range(length)) + return f'{file_name}.{extension}' + + @staticmethod + def delete_config(file_name: str) -> None: + """Delete provided file""" + if platform.system().lower() == 'windows': + command = f'icacls "{file_name}" /inheritance:r /grant:r "%USERNAME%:F"' + try: + subprocess.run(command, shell=True, check=True) + except subprocess.CalledProcessError as e: + pass + + try: + os.remove(file_name) + except (PermissionError, OSError, FileNotFoundError) as e: + pass + + @staticmethod + def generate_config(file_name: str, limited_permissions: bool = True, invalid_json: bool = False) -> None: + """Creates sample config json file""" + config_data = { + 'sample_string': 'string_of_sample', + 'sample_integer': 100, + 'sample_others': { + 'sample_boolean': True, + 'sample_list': [ + 'list_1', + 'list_2' + ] + } + } if not invalid_json else 100 + + with open(file_name, 'w') as config_file: + config_file.write(json.dumps(config_data, indent=4)) + + if limited_permissions: + if platform.system().lower() == 'windows': + commands = [ + f'icacls "{file_name}" /inheritance:r /grant:r "%USERNAME%:RX"', + f'icacls "{file_name}" /deny *S-1-1-0:(OI)(CI)(F)' + ] + try: + for command in commands: + subprocess.run(command, shell=True, check=True) + except subprocess.CalledProcessError as e: + pass + elif platform.system().lower() == 'linux': + try: + os.chmod(file_name, 0o400) + except (PermissionError, OSError) as e: + pass