mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-10-31 18:29:21 +00:00 
			
		
		
		
	Implement filter checks: ensure all defined filters are used and remove dead code
Integration tests added/updated:
- tests/integration/test_filters_usage.py: AST-based detection of filter definitions (FilterModule.filters), robust Jinja detection ({{ ... }}, {% ... %}, {% filter ... %}), plus Python call tracking; fails if a filter is used only under tests/.
- tests/integration/test_filters_are_defined.py: inverse check — every filter used in .yml/.yaml/.j2/.jinja2/.tmpl must be defined locally. Scans only inside Jinja blocks and ignores pipes inside strings (e.g., lookup('pipe', "... | grep ... | awk ...")) to avoid false positives like trusted_hosts, woff/woff2, etc.
Bug fixes & robustness:
- Build regexes without %-string formatting to avoid ValueError from literal '%' in Jinja tags.
- Strip quoted strings in usage analysis so sed/grep/awk pipes are not miscounted as filters.
- Prevent self-matches in the defining file.
Cleanup / removal of dead code:
- Removed unused filter plugins and related unit tests:
  * filter_plugins/alias_domains_map.py
  * filter_plugins/get_application_id.py
  * filter_plugins/load_configuration.py
  * filter_plugins/safe.py
  * filter_plugins/safe_join.py
  * roles/svc-db-openldap/filter_plugins/build_ldap_nested_group_entries.py
  * roles/sys-ctl-bkp-docker-2-loc/filter_plugins/dict_to_cli_args.py
  * corresponding tests under tests/unit/*
- roles/svc-db-postgres/filter_plugins/split_postgres_connections.py: dropped no-longer-needed list_postgres_roles API; adjusted tests.
Misc:
- sys-stk-front-proxy/defaults/main.yml: clarified valid vhost_flavour values (comma-separated).
Ref: https://chatgpt.com/share/68b56bac-c4f8-800f-aeef-6708dbb44199
			
			
This commit is contained in:
		| @@ -1,111 +0,0 @@ | ||||
| import os | ||||
| import sys | ||||
| import unittest | ||||
|  | ||||
| # Add the filter_plugins directory to the import path | ||||
| dir_path = os.path.abspath( | ||||
|     os.path.join(os.path.dirname(__file__), '../../../filter_plugins') | ||||
| ) | ||||
| sys.path.insert(0, dir_path) | ||||
|  | ||||
| from ansible.errors import AnsibleFilterError | ||||
| from alias_domains_map import FilterModule | ||||
|  | ||||
| class TestDomainFilters(unittest.TestCase): | ||||
|     def setUp(self): | ||||
|         self.filter_module = FilterModule() | ||||
|         # Sample primary domain | ||||
|         self.primary = 'example.com' | ||||
|  | ||||
|     def test_alias_empty_apps(self): | ||||
|         apps = {} | ||||
|         expected = {} | ||||
|         result = self.filter_module.alias_domains_map(apps, self.primary) | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
|     def test_alias_without_aliases_and_no_canonical(self): | ||||
|         apps = {'app1': {}} | ||||
|         # canonical defaults to ['app1.example.com'], so alias should be [] | ||||
|         expected = {'app1': []} | ||||
|         result = self.filter_module.alias_domains_map(apps, self.primary) | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
|     def test_alias_with_explicit_aliases(self): | ||||
|         apps = { | ||||
|             'app1': { | ||||
|                 'server':{ | ||||
|                     'domains': {'aliases': ['alias.com']} | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         # canonical defaults to ['app1.example.com'], so alias should include alias.com and default | ||||
|         expected = {'app1': ['alias.com', 'app1.example.com']} | ||||
|         result = self.filter_module.alias_domains_map(apps, self.primary) | ||||
|         self.assertCountEqual(result['app1'], expected['app1']) | ||||
|  | ||||
|     def test_alias_with_canonical_not_default(self): | ||||
|         apps = { | ||||
|             'app1': { | ||||
|                 'server':{'domains': {'canonical': ['foo.com']}} | ||||
|             } | ||||
|         } | ||||
|         # foo.com is canonical, default not in canonical so added as alias | ||||
|         expected = {'app1': ['app1.example.com']} | ||||
|         result = self.filter_module.alias_domains_map(apps, self.primary) | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
|     def test_alias_with_existing_default(self): | ||||
|         apps = { | ||||
|             'app1': { | ||||
|                 'server':{ | ||||
|                     'domains': { | ||||
|                         'canonical': ['foo.com'], | ||||
|                         'aliases': ['app1.example.com'] | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         # default present in aliases, should not be duplicated | ||||
|         expected = {'app1': ['app1.example.com']} | ||||
|         result = self.filter_module.alias_domains_map(apps, self.primary) | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
|     def test_invalid_aliases_type(self): | ||||
|         apps = { | ||||
|             'app1': {'server':{'domains': {'aliases': 123}}} | ||||
|         } | ||||
|         with self.assertRaises(AnsibleFilterError): | ||||
|             self.filter_module.alias_domains_map(apps, self.primary) | ||||
|  | ||||
|     def test_alias_with_empty_domains_cfg(self): | ||||
|         apps = { | ||||
|             'app1': { | ||||
|                 'server':{ | ||||
|                     'domains': {} | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         expected = apps | ||||
|         result = self.filter_module.alias_domains_map(apps, self.primary) | ||||
|         self.assertEqual(result, expected) | ||||
|          | ||||
|     def test_alias_with_canonical_dict_not_default(self): | ||||
|         apps = { | ||||
|             'app1': { | ||||
|                 'server':{ | ||||
|                     'domains': { | ||||
|                         'canonical': { | ||||
|                             'one': 'one.com', | ||||
|                             'two': 'two.com' | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         expected = {'app1': ['app1.example.com']} | ||||
|         result = self.filter_module.alias_domains_map(apps, self.primary) | ||||
|         self.assertEqual(result, expected) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     unittest.main() | ||||
| @@ -1,63 +0,0 @@ | ||||
| # tests/unit/filter_plugins/test_get_application_id.py | ||||
| import unittest | ||||
| import os | ||||
| import tempfile | ||||
| import shutil | ||||
| import yaml | ||||
| from ansible.errors import AnsibleFilterError | ||||
| from filter_plugins.get_application_id import get_application_id | ||||
|  | ||||
| class TestGetApplicationIdFilter(unittest.TestCase): | ||||
|     def setUp(self): | ||||
|         # Create a temporary project directory and switch to it | ||||
|         self.tmpdir = tempfile.mkdtemp() | ||||
|         self.original_cwd = os.getcwd() | ||||
|         os.chdir(self.tmpdir) | ||||
|  | ||||
|         # Create the roles/testrole/vars directory structure | ||||
|         self.role_name = 'testrole' | ||||
|         self.vars_dir = os.path.join('roles', self.role_name, 'vars') | ||||
|         os.makedirs(self.vars_dir) | ||||
|         self.vars_file = os.path.join(self.vars_dir, 'main.yml') | ||||
|  | ||||
|     def tearDown(self): | ||||
|         # Return to original cwd and remove temp directory | ||||
|         os.chdir(self.original_cwd) | ||||
|         shutil.rmtree(self.tmpdir) | ||||
|  | ||||
|     def write_vars_file(self, content): | ||||
|         with open(self.vars_file, 'w') as f: | ||||
|             yaml.dump(content, f) | ||||
|  | ||||
|     def test_returns_application_id(self): | ||||
|         # Given a valid vars file with application_id | ||||
|         expected_id = '12345' | ||||
|         self.write_vars_file({'application_id': expected_id}) | ||||
|         # When | ||||
|         result = get_application_id(self.role_name) | ||||
|         # Then | ||||
|         self.assertEqual(result, expected_id) | ||||
|  | ||||
|     def test_file_not_found_raises_error(self): | ||||
|         # Given no vars file for a nonexistent role | ||||
|         with self.assertRaises(AnsibleFilterError) as cm: | ||||
|             get_application_id('nonexistent_role') | ||||
|         self.assertIn("Vars file not found", str(cm.exception)) | ||||
|  | ||||
|     def test_missing_key_raises_error(self): | ||||
|         # Given a vars file without application_id | ||||
|         self.write_vars_file({'other_key': 'value'}) | ||||
|         with self.assertRaises(AnsibleFilterError) as cm: | ||||
|             get_application_id(self.role_name) | ||||
|         self.assertIn("Key 'application_id' not found", str(cm.exception)) | ||||
|  | ||||
|     def test_invalid_yaml_raises_error(self): | ||||
|         # Write invalid YAML content | ||||
|         with open(self.vars_file, 'w') as f: | ||||
|             f.write(":::not a yaml:::") | ||||
|         with self.assertRaises(AnsibleFilterError) as cm: | ||||
|             get_application_id(self.role_name) | ||||
|         self.assertIn("Error reading YAML", str(cm.exception)) | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     unittest.main() | ||||
| @@ -1,122 +0,0 @@ | ||||
| import os | ||||
| import sys | ||||
| import unittest | ||||
| from unittest.mock import patch, mock_open | ||||
| from ansible.errors import AnsibleFilterError | ||||
|  | ||||
| # make sure our plugin is on PYTHONPATH | ||||
| root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../filter_plugins')) | ||||
| sys.path.insert(0, root) | ||||
|  | ||||
| import load_configuration | ||||
| from load_configuration import FilterModule, _cfg_cache | ||||
|  | ||||
| class TestLoadConfigurationFilter(unittest.TestCase): | ||||
|     def setUp(self): | ||||
|         _cfg_cache.clear() | ||||
|         self.f = FilterModule().filters()['load_configuration'] | ||||
|         self.app = 'html' | ||||
|         self.nested_cfg = { | ||||
|             'html': { | ||||
|                 'features': {'matomo': True}, | ||||
|                 'server': { | ||||
|                     'domains':{'canonical': ['html.example.com']} | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         self.flat_cfg = { | ||||
|             'features': {'matomo': False}, | ||||
|             'server': {'domains':{'canonical': ['flat.example.com']}} | ||||
|         } | ||||
|  | ||||
|     def test_invalid_key(self): | ||||
|         with self.assertRaises(AnsibleFilterError): | ||||
|             self.f(self.app, None) | ||||
|  | ||||
|     @patch('load_configuration.os.path.isdir', return_value=False) | ||||
|     def test_no_roles_dir(self, _): | ||||
|         with self.assertRaises(AnsibleFilterError): | ||||
|             self.f(self.app, 'features.matomo') | ||||
|  | ||||
|     @patch('load_configuration.os.listdir', return_value=['r1']) | ||||
|     @patch('load_configuration.os.path.isdir', return_value=True) | ||||
|     @patch('load_configuration.os.path.exists', return_value=False) | ||||
|     def test_no_matching_role(self, *_): | ||||
|         self.assertIsNone(self.f(self.app, 'features.matomo')) | ||||
|  | ||||
|     @patch('load_configuration.os.listdir', return_value=['r1']) | ||||
|     @patch('load_configuration.os.path.isdir', return_value=True) | ||||
|     @patch('load_configuration.os.path.exists') | ||||
|     @patch('load_configuration.open', new_callable=mock_open) | ||||
|     @patch('load_configuration.yaml.safe_load') | ||||
|     def test_primary_missing_conf(self, mock_yaml, mock_file, mock_exists, *_): | ||||
|         mock_exists.side_effect = lambda p: p.endswith('vars/main.yml') | ||||
|         mock_yaml.return_value = {'application_id': self.app} | ||||
|         with self.assertRaises(AnsibleFilterError): | ||||
|             self.f(self.app, 'features.matomo') | ||||
|  | ||||
|     @patch('load_configuration.os.listdir', return_value=['r1']) | ||||
|     @patch('load_configuration.os.path.isdir', return_value=True) | ||||
|     @patch('load_configuration.os.path.exists') | ||||
|     @patch('load_configuration.open', new_callable=mock_open) | ||||
|     @patch('load_configuration.yaml.safe_load') | ||||
|     def test_primary_and_cache(self, mock_yaml, mock_file, mock_exists, *_): | ||||
|         mock_exists.side_effect = lambda p: p.endswith('vars/main.yml') or p.endswith('config/main.yml') | ||||
|         mock_yaml.side_effect = [ | ||||
|             {'application_id': self.app},  # main.yml | ||||
|             self.nested_cfg              # config/main.yml | ||||
|         ] | ||||
|         # first load | ||||
|         self.assertTrue(self.f(self.app, 'features.matomo')) | ||||
|         self.assertIn(self.app, _cfg_cache) | ||||
|         mock_yaml.reset_mock() | ||||
|         # from cache | ||||
|         self.assertEqual(self.f(self.app, 'server.domains.canonical'), | ||||
|                          ['html.example.com']) | ||||
|         mock_yaml.assert_not_called() | ||||
|  | ||||
|     @patch('load_configuration.os.listdir', return_value=['r1']) | ||||
|     @patch('load_configuration.os.path.isdir', return_value=True) | ||||
|     @patch('load_configuration.os.path.exists', return_value=True) | ||||
|     @patch('load_configuration.open', mock_open(read_data="html: {}")) | ||||
|     @patch('load_configuration.yaml.safe_load', return_value={'html': {}}) | ||||
|     def test_key_not_found_after_load(self, *_): | ||||
|         with self.assertRaises(AnsibleFilterError): | ||||
|             self.f(self.app, 'does.not.exist') | ||||
|  | ||||
|     @patch('load_configuration.os.listdir', return_value=['r2']) | ||||
|     @patch('load_configuration.os.path.isdir', return_value=True) | ||||
|     @patch('load_configuration.os.path.exists') | ||||
|     @patch('load_configuration.open', new_callable=mock_open) | ||||
|     @patch('load_configuration.yaml.safe_load') | ||||
|     def test_fallback_nested(self, mock_yaml, mock_file, mock_exists, *_): | ||||
|         mock_exists.side_effect = lambda p: p.endswith('config/main.yml') | ||||
|         mock_yaml.return_value = self.nested_cfg | ||||
|         # nested fallback must work | ||||
|         self.assertTrue(self.f(self.app, 'features.matomo')) | ||||
|         self.assertEqual(self.f(self.app, 'server.domains.canonical'), | ||||
|                          ['html.example.com']) | ||||
|  | ||||
|     @patch('load_configuration.os.listdir', return_value=['r4']) | ||||
|     @patch('load_configuration.os.path.isdir', return_value=True) | ||||
|     @patch('load_configuration.os.path.exists') | ||||
|     @patch('load_configuration.open', new_callable=mock_open) | ||||
|     @patch('load_configuration.yaml.safe_load') | ||||
|     def test_fallback_with_indexed_key(self, mock_yaml, mock_file, mock_exists, *_): | ||||
|         # Testing with an indexed key like domains.canonical[0] | ||||
|         mock_exists.side_effect = lambda p: p.endswith('config/main.yml') | ||||
|         mock_yaml.return_value = { | ||||
|             'file': { | ||||
|                 'server': { | ||||
|                     'domains':{ | ||||
|                         'canonical': ['files.example.com', 'extra.example.com'] | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         # should get the first element of the canonical domains list | ||||
|         self.assertEqual(self.f('file', 'server.domains.canonical[0]'), | ||||
|                          'files.example.com') | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     unittest.main() | ||||
| @@ -1,56 +0,0 @@ | ||||
| import unittest | ||||
| import sys | ||||
| import os | ||||
|  | ||||
| # Ensure filter_plugins directory is on the path | ||||
| sys.path.insert( | ||||
|     0, | ||||
|     os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../filter_plugins')) | ||||
| ) | ||||
|  | ||||
| from safe_join import safe_join | ||||
|  | ||||
| class TestSafeJoinFilter(unittest.TestCase): | ||||
|     def test_join_with_trailing_slashes(self): | ||||
|         self.assertEqual( | ||||
|             safe_join('http://example.com/', '/path/to'), | ||||
|             'http://example.com/path/to' | ||||
|         ) | ||||
|  | ||||
|     def test_join_without_slashes(self): | ||||
|         self.assertEqual( | ||||
|             safe_join('http://example.com', 'path/to'), | ||||
|             'http://example.com/path/to' | ||||
|         ) | ||||
|  | ||||
|     def test_base_none(self): | ||||
|         with self.assertRaises(ValueError): | ||||
|             safe_join(None, 'path') | ||||
|  | ||||
|     def test_tail_none(self): | ||||
|         with self.assertRaises(ValueError): | ||||
|             safe_join('http://example.com', None) | ||||
|  | ||||
|     def test_base_empty(self): | ||||
|         self.assertEqual(safe_join('', 'path'), '/path') | ||||
|  | ||||
|     def test_tail_empty(self): | ||||
|         # joining with empty tail should yield base with trailing slash | ||||
|         self.assertEqual( | ||||
|             safe_join('http://example.com', ''), | ||||
|             'http://example.com/' | ||||
|         ) | ||||
|  | ||||
|     def test_numeric_base(self): | ||||
|         # numeric base is cast to string | ||||
|         self.assertEqual(safe_join(123, 'path'), '123/path') | ||||
|  | ||||
|     def test_exception_in_str(self): | ||||
|         class Bad: | ||||
|             def __str__(self): | ||||
|                 raise ValueError('bad') | ||||
|         # on exception, safe_join returns '' | ||||
|         self.assertEqual(safe_join(Bad(), 'x'), '') | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     unittest.main() | ||||
| @@ -1,59 +0,0 @@ | ||||
| import unittest | ||||
| import sys | ||||
| import os | ||||
|  | ||||
| # Ensure filter_plugins directory is on the path | ||||
| sys.path.insert( | ||||
|     0, | ||||
|     os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../filter_plugins')) | ||||
| ) | ||||
|  | ||||
| from safe import safe_placeholders | ||||
|  | ||||
| class TestSafePlaceholdersFilter(unittest.TestCase): | ||||
|     def test_simple_replacement(self): | ||||
|         template = "Hello, {user}!" | ||||
|         mapping = {'user': 'Alice'} | ||||
|         self.assertEqual(safe_placeholders(template, mapping), "Hello, Alice!") | ||||
|  | ||||
|     def test_missing_placeholder(self): | ||||
|         template = "Hello, {user}!" | ||||
|         # Missing placeholder should raise KeyError | ||||
|         with self.assertRaises(KeyError): | ||||
|             safe_placeholders(template, {}) | ||||
|  | ||||
|     def test_none_template(self): | ||||
|         self.assertEqual(safe_placeholders(None, {'user': 'Alice'}), "") | ||||
|  | ||||
|     def test_no_placeholders(self): | ||||
|         template = "Just a plain string" | ||||
|         mapping = {'any': 'value'} | ||||
|         self.assertEqual(safe_placeholders(template, mapping), "Just a plain string") | ||||
|  | ||||
|     def test_multiple_placeholders(self): | ||||
|         template = "{greet}, {user}!" | ||||
|         mapping = {'greet': 'Hi', 'user': 'Bob'} | ||||
|         self.assertEqual(safe_placeholders(template, mapping), "Hi, Bob!") | ||||
|  | ||||
|     def test_numeric_values(self): | ||||
|         template = "Count: {n}" | ||||
|         mapping = {'n': 0} | ||||
|         self.assertEqual(safe_placeholders(template, mapping), "Count: 0") | ||||
|  | ||||
|     def test_extra_mapping_keys(self): | ||||
|         template = "Value: {a}" | ||||
|         mapping = {'a': '1', 'b': '2'} | ||||
|         self.assertEqual(safe_placeholders(template, mapping), "Value: 1") | ||||
|  | ||||
|     def test_malformed_template(self): | ||||
|         # Unclosed placeholder should be caught and return empty string | ||||
|         template = "Unclosed {key" | ||||
|         mapping = {'key': 'value'} | ||||
|         self.assertEqual(safe_placeholders(template, mapping), "") | ||||
|  | ||||
|     def test_mapping_none(self): | ||||
|         template = "Test {x}" | ||||
|         self.assertEqual(safe_placeholders(template, None), "") | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     unittest.main() | ||||
| @@ -1,44 +0,0 @@ | ||||
| import unittest | ||||
| import sys | ||||
| import os | ||||
| from jinja2 import Undefined | ||||
|  | ||||
| # Ensure filter_plugins directory is on the path | ||||
| sys.path.insert( | ||||
|     0, | ||||
|     os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../filter_plugins')) | ||||
| ) | ||||
|  | ||||
| from safe import FilterModule | ||||
|  | ||||
| class TestSafeVarFilter(unittest.TestCase): | ||||
|     def setUp(self): | ||||
|         # Retrieve the safe_var filter function | ||||
|         self.filter = FilterModule().filters()['safe_var'] | ||||
|  | ||||
|     def test_returns_non_empty_string(self): | ||||
|         self.assertEqual(self.filter('hello'), 'hello') | ||||
|  | ||||
|     def test_returns_empty_string(self): | ||||
|         self.assertEqual(self.filter(''), '') | ||||
|  | ||||
|     def test_returns_empty_for_none(self): | ||||
|         self.assertEqual(self.filter(None), '') | ||||
|  | ||||
|     def test_returns_empty_for_jinja_undefined(self): | ||||
|         # Instantiate an Undefined without arguments | ||||
|         undef = Undefined() | ||||
|         self.assertEqual(self.filter(undef), '') | ||||
|  | ||||
|     def test_returns_zero_for_zero(self): | ||||
|         # 0 is falsey but not None or Undefined, so safe_var returns it | ||||
|         self.assertEqual(self.filter(0), 0) | ||||
|  | ||||
|     def test_returns_list_and_dict_unchanged(self): | ||||
|         data = {'key': 'value'} | ||||
|         self.assertEqual(self.filter(data), data) | ||||
|         lst = [1, 2, 3] | ||||
|         self.assertEqual(self.filter(lst), lst) | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     unittest.main() | ||||
| @@ -23,7 +23,6 @@ class TestDockerCardsLookup(unittest.TestCase): | ||||
|         os.makedirs(os.path.join(self.role_dir, "meta")) | ||||
|         os.makedirs(os.path.join(self.role_dir, "vars")) | ||||
|  | ||||
|         # Create vars/main.yml so get_application_id() can find the application_id. | ||||
|         vars_main = os.path.join(self.role_dir, "vars", "main.yml") | ||||
|         with open(vars_main, "w", encoding="utf-8") as f: | ||||
|             f.write("application_id: portfolio\n") | ||||
|   | ||||
| @@ -77,12 +77,6 @@ class SplitPostgresConnectionsTests(unittest.TestCase): | ||||
|     def test_registry_contains_filters(self): | ||||
|         registry = self.mod.FilterModule().filters() | ||||
|         self.assertIn("split_postgres_connections", registry) | ||||
|         self.assertIn("list_postgres_roles", registry) | ||||
|  | ||||
|     def test_list_postgres_roles(self): | ||||
|         roles = self.mod.list_postgres_roles(self.roles_dir) | ||||
|         self.assertIsInstance(roles, list) | ||||
|         self.assertSetEqual(set(roles), {"app_a", "app_b"}) | ||||
|  | ||||
|     def test_split_postgres_connections_division(self): | ||||
|         # There are 2 postgres roles -> 200 / 2 = 100 | ||||
|   | ||||
| @@ -1,61 +0,0 @@ | ||||
| import unittest | ||||
| import os | ||||
| import sys | ||||
|  | ||||
| # Add the path to roles/sys-ctl-bkp-docker-2-loc/filter_plugins | ||||
| CURRENT_DIR = os.path.dirname(__file__) | ||||
| FILTER_PLUGIN_DIR = os.path.abspath( | ||||
|     os.path.join(CURRENT_DIR, '../../../../../roles/sys-ctl-bkp-docker-2-loc/filter_plugins') | ||||
| ) | ||||
| sys.path.insert(0, FILTER_PLUGIN_DIR) | ||||
|  | ||||
| from dict_to_cli_args import dict_to_cli_args | ||||
|  | ||||
|  | ||||
| class TestDictToCliArgs(unittest.TestCase): | ||||
|     def test_simple_string_args(self): | ||||
|         data = {"backup-dir": "/mnt/backups", "version-suffix": "-nightly"} | ||||
|         expected = "--backup-dir=/mnt/backups --version-suffix=-nightly" | ||||
|         self.assertEqual(dict_to_cli_args(data), expected) | ||||
|  | ||||
|     def test_boolean_true(self): | ||||
|         data = {"shutdown": True, "everything": True} | ||||
|         expected = "--shutdown --everything" | ||||
|         self.assertEqual(dict_to_cli_args(data), expected) | ||||
|  | ||||
|     def test_boolean_false(self): | ||||
|         data = {"shutdown": False, "everything": True} | ||||
|         expected = "--everything" | ||||
|         self.assertEqual(dict_to_cli_args(data), expected) | ||||
|  | ||||
|     def test_list_argument(self): | ||||
|         data = {"ignore-volumes": ["redis", "memcached"]} | ||||
|         expected = '--ignore-volumes="redis memcached"' | ||||
|         self.assertEqual(dict_to_cli_args(data), expected) | ||||
|  | ||||
|     def test_mixed_arguments(self): | ||||
|         data = { | ||||
|             "backup-dir": "/mnt/backups", | ||||
|             "shutdown": True, | ||||
|             "ignore-volumes": ["redis", "memcached"] | ||||
|         } | ||||
|         result = dict_to_cli_args(data) | ||||
|         self.assertIn("--backup-dir=/mnt/backups", result) | ||||
|         self.assertIn("--shutdown", result) | ||||
|         self.assertIn('--ignore-volumes="redis memcached"', result) | ||||
|  | ||||
|     def test_empty_dict(self): | ||||
|         self.assertEqual(dict_to_cli_args({}), "") | ||||
|  | ||||
|     def test_none_value(self): | ||||
|         data = {"some-value": None, "other": "yes"} | ||||
|         expected = "--other=yes" | ||||
|         self.assertEqual(dict_to_cli_args(data), expected) | ||||
|  | ||||
|     def test_invalid_type(self): | ||||
|         with self.assertRaises(TypeError): | ||||
|             dict_to_cli_args(["not", "a", "dict"]) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     unittest.main() | ||||
		Reference in New Issue
	
	Block a user