mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-29 15:06:26 +02:00
Fail safed more parts of the code
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import unittest
|
||||
import yaml
|
||||
|
||||
from filter_plugins.get_all_application_ids import get_all_application_ids
|
||||
|
||||
class TestApplicationIDsInPorts(unittest.TestCase):
|
||||
def test_all_ports_application_ids_are_valid(self):
|
||||
# Path to the ports definition file
|
||||
ports_file = os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__), '..', '..', 'group_vars', 'all', '09_ports.yml'
|
||||
)
|
||||
)
|
||||
with open(ports_file, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# Collect all referenced application IDs under ports.hosttype.porttype
|
||||
refs = set()
|
||||
ports = data.get('ports', {}) or {}
|
||||
for hosttype, porttypes in ports.items():
|
||||
if not isinstance(porttypes, dict):
|
||||
continue
|
||||
for porttype, apps in porttypes.items():
|
||||
if not isinstance(apps, dict):
|
||||
continue
|
||||
for app_id in apps.keys():
|
||||
refs.add(app_id)
|
||||
|
||||
# Retrieve valid application IDs from Ansible roles
|
||||
valid_ids = set(get_all_application_ids())
|
||||
|
||||
# Identify IDs that are neither valid nor have a valid prefix before the first underscore
|
||||
missing = []
|
||||
for app_id in refs:
|
||||
if app_id in valid_ids:
|
||||
continue
|
||||
prefix = app_id.split('_', 1)[0]
|
||||
if prefix in valid_ids:
|
||||
continue
|
||||
missing.append(app_id)
|
||||
|
||||
if missing:
|
||||
self.fail(
|
||||
f"Undefined application IDs in ports definition: {', '.join(sorted(missing))}"
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@@ -9,10 +9,15 @@ class TestAnsibleRolesMetadata(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not os.path.isdir(cls.ROLES_DIR):
|
||||
raise unittest.SkipTest(f"Roles directory not found at {cls.ROLES_DIR}")
|
||||
cls.roles = [d for d in os.listdir(cls.ROLES_DIR)
|
||||
if os.path.isdir(os.path.join(cls.ROLES_DIR, d))]
|
||||
all_dirs = os.listdir(cls.ROLES_DIR)
|
||||
cls.roles = [
|
||||
d for d in all_dirs
|
||||
if (
|
||||
os.path.isdir(os.path.join(cls.ROLES_DIR, d))
|
||||
and d != '__pycache__'
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_each_role_has_valid_meta(self):
|
||||
"""
|
||||
|
@@ -1,51 +1,74 @@
|
||||
import os
|
||||
import unittest
|
||||
import yaml
|
||||
import glob
|
||||
import yaml
|
||||
import warnings
|
||||
import unittest
|
||||
|
||||
class TestApplicationIdMatchesRoleName(unittest.TestCase):
|
||||
def test_application_id_matches_role_directory(self):
|
||||
"""
|
||||
Warn if application_id in vars/main.yml does not match
|
||||
the role directory basename, to avoid confusion.
|
||||
If vars/main.yml is missing, do nothing.
|
||||
"""
|
||||
# locate the 'roles' directory (two levels up)
|
||||
# import your filters
|
||||
from filter_plugins.invokable_paths import get_invokable_paths, get_non_invokable_paths
|
||||
|
||||
class TestApplicationIdAndInvocability(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# locate roles dir (two levels up)
|
||||
base_dir = os.path.dirname(__file__)
|
||||
roles_dir = os.path.abspath(os.path.join(base_dir, '..', '..', 'roles'))
|
||||
cls.roles_dir = os.path.abspath(os.path.join(base_dir, '..', '..', 'roles'))
|
||||
|
||||
# iterate over each role folder
|
||||
for role_path in glob.glob(os.path.join(roles_dir, '*')):
|
||||
# get lists of invokable and non-invokable role *names*
|
||||
# filters return dash-joined paths; for top-level roles names are just the basename
|
||||
cls.invokable = {
|
||||
p.split('-', 1)[0]
|
||||
for p in get_invokable_paths()
|
||||
}
|
||||
cls.non_invokable = {
|
||||
p.split('-', 1)[0]
|
||||
for p in get_non_invokable_paths()
|
||||
}
|
||||
|
||||
def test_application_id_presence_and_match(self):
|
||||
"""
|
||||
- Invokable roles must have application_id defined (else fail).
|
||||
- Non-invokable roles must NOT have application_id (else fail).
|
||||
- If application_id exists but != folder name, warn and recommend aligning.
|
||||
"""
|
||||
for role_path in glob.glob(os.path.join(self.roles_dir, '*')):
|
||||
if not os.path.isdir(role_path):
|
||||
continue
|
||||
|
||||
role_name = os.path.basename(role_path)
|
||||
vars_main = os.path.join(role_path, 'vars', 'main.yml')
|
||||
|
||||
# skip roles without vars/main.yml
|
||||
if not os.path.exists(vars_main):
|
||||
continue
|
||||
|
||||
with open(vars_main, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
# load vars/main.yml if it exists
|
||||
data = {}
|
||||
if os.path.exists(vars_main):
|
||||
with open(vars_main, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
|
||||
app_id = data.get('application_id')
|
||||
if app_id is None:
|
||||
|
||||
if role_name in self.invokable:
|
||||
# must have application_id
|
||||
if app_id is None:
|
||||
self.fail(f"{role_name}: invokable role is missing 'application_id' in vars/main.yml")
|
||||
elif role_name in self.non_invokable:
|
||||
# must NOT have application_id
|
||||
if app_id is not None:
|
||||
self.fail(f"{role_name}: non-invokable role should not define 'application_id' in vars/main.yml")
|
||||
else:
|
||||
# roles not mentioned in categories.yml? we'll skip them
|
||||
continue
|
||||
|
||||
# if present but mismatched, warn
|
||||
if app_id is not None and app_id != role_name:
|
||||
warnings.warn(
|
||||
f"{role_name}: 'application_id' is missing in vars/main.yml. "
|
||||
f"Consider setting it to '{role_name}' to avoid confusion."
|
||||
)
|
||||
elif app_id != role_name:
|
||||
warnings.warn(
|
||||
f"{role_name}: 'application_id' is '{app_id}', "
|
||||
f"but the folder name is '{role_name}'. "
|
||||
"This can lead to confusion—using the directory name "
|
||||
"as the application_id is recommended."
|
||||
f"{role_name}: 'application_id' is '{app_id}',"
|
||||
f" but the folder name is '{role_name}'."
|
||||
" Consider setting application_id to exactly the role folder name to avoid confusion."
|
||||
)
|
||||
|
||||
# always pass
|
||||
# if we get here, all presence/absence checks passed
|
||||
self.assertTrue(True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@@ -20,7 +20,8 @@ class TestDependencyApplicationId(unittest.TestCase):
|
||||
vars_file = os.path.join(role_path, 'vars', 'main.yml')
|
||||
if not os.path.isfile(vars_file):
|
||||
return None
|
||||
data = yaml.safe_load(open(vars_file, encoding='utf-8')) or {}
|
||||
with open(vars_file, encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
return data.get('application_id')
|
||||
|
||||
# Iterate all roles
|
||||
@@ -33,7 +34,9 @@ class TestDependencyApplicationId(unittest.TestCase):
|
||||
if not os.path.isfile(meta_file):
|
||||
continue
|
||||
|
||||
meta = yaml.safe_load(open(meta_file, encoding='utf-8')) or {}
|
||||
with open(meta_file, encoding='utf-8') as f:
|
||||
meta = yaml.safe_load(f) or {}
|
||||
|
||||
deps = meta.get('dependencies', [])
|
||||
if not isinstance(deps, list):
|
||||
continue
|
||||
|
46
tests/integration/test_get_domain_application_ids.py
Normal file
46
tests/integration/test_get_domain_application_ids.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
# Ensure filter_plugins is on the path
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
from filter_plugins.get_all_application_ids import get_all_application_ids
|
||||
|
||||
class TestGetDomainApplicationIds(unittest.TestCase):
|
||||
"""
|
||||
Integration test to verify that all string literals passed to get_domain()
|
||||
correspond to valid application_id values defined in roles/*/vars/main.yml.
|
||||
"""
|
||||
|
||||
GET_DOMAIN_PATTERN = re.compile(r"get_domain\(\s*['\"]([^'\"]+)['\"]\s*\)")
|
||||
|
||||
def test_get_domain_literals_are_valid_ids(self):
|
||||
# Collect all application IDs from roles
|
||||
valid_ids = set(get_all_application_ids())
|
||||
|
||||
# Walk through project files
|
||||
invalid_usages = []
|
||||
for root, dirs, files in os.walk(PROJECT_ROOT):
|
||||
# Skip tests directory to avoid matching in test code
|
||||
if 'tests' in root.split(os.sep):
|
||||
continue
|
||||
for fname in files:
|
||||
if not fname.endswith('.py'):
|
||||
continue
|
||||
path = os.path.join(root, fname)
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
for match in self.GET_DOMAIN_PATTERN.finditer(content):
|
||||
literal = match.group(1)
|
||||
if literal not in valid_ids:
|
||||
invalid_usages.append((path, literal))
|
||||
|
||||
if invalid_usages:
|
||||
msgs = [f"{path}: '{lit}' is not a valid application_id" for path, lit in invalid_usages]
|
||||
self.fail("Found invalid get_domain() usages:\n" + "\n".join(msgs))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
42
tests/integration/test_roles_folder_names.py
Normal file
42
tests/integration/test_roles_folder_names.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
class TestRolesFolderNames(unittest.TestCase):
|
||||
def test_no_underscore_in_role_folder_names(self):
|
||||
"""
|
||||
Integration test that verifies none of the folders under 'roles' contain an underscore in their name,
|
||||
ignoring the '__pycache__' folder.
|
||||
"""
|
||||
# Determine the absolute path to the 'roles' directory
|
||||
roles_dir = os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__), '..', '..', 'roles'
|
||||
)
|
||||
)
|
||||
|
||||
# List all entries in the roles directory
|
||||
try:
|
||||
entries = os.listdir(roles_dir)
|
||||
except FileNotFoundError:
|
||||
self.fail(f"Roles directory not found at expected location: {roles_dir}")
|
||||
|
||||
# Identify any role folders containing underscores, excluding '__pycache__'
|
||||
invalid = []
|
||||
for name in entries:
|
||||
# Skip the '__pycache__' directory
|
||||
if name == '__pycache__':
|
||||
continue
|
||||
path = os.path.join(roles_dir, name)
|
||||
if os.path.isdir(path) and '_' in name:
|
||||
invalid.append(name)
|
||||
|
||||
# Fail the test if any invalid folder names are found
|
||||
if invalid:
|
||||
self.fail(
|
||||
f"Role folder names must not contain underscores: {', '.join(sorted(invalid))}"
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
55
tests/unit/filter_plugins/test_get_all_application_ids.py
Normal file
55
tests/unit/filter_plugins/test_get_all_application_ids.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import unittest
|
||||
import tempfile
|
||||
import os
|
||||
import yaml
|
||||
|
||||
from filter_plugins.get_all_application_ids import get_all_application_ids
|
||||
|
||||
class TestGetAllApplicationIds(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Create a temporary directory to act as the roles base
|
||||
self.tmpdir = tempfile.TemporaryDirectory()
|
||||
self.roles_dir = os.path.join(self.tmpdir.name, 'roles')
|
||||
os.makedirs(self.roles_dir)
|
||||
|
||||
def tearDown(self):
|
||||
# Clean up temporary directory
|
||||
self.tmpdir.cleanup()
|
||||
|
||||
def create_role(self, role_name, data):
|
||||
# Helper to create roles/<role_name>/vars/main.yml with given dict
|
||||
path = os.path.join(self.roles_dir, role_name, 'vars')
|
||||
os.makedirs(path, exist_ok=True)
|
||||
with open(os.path.join(path, 'main.yml'), 'w', encoding='utf-8') as f:
|
||||
yaml.safe_dump(data, f)
|
||||
|
||||
def test_single_application_id(self):
|
||||
self.create_role('role1', {'application_id': 'app1'})
|
||||
result = get_all_application_ids(self.roles_dir)
|
||||
self.assertEqual(result, ['app1'])
|
||||
|
||||
def test_multiple_application_ids(self):
|
||||
self.create_role('role1', {'application_id': 'app1'})
|
||||
self.create_role('role2', {'application_id': 'app2'})
|
||||
result = get_all_application_ids(self.roles_dir)
|
||||
self.assertEqual(sorted(result), ['app1', 'app2'])
|
||||
|
||||
def test_duplicate_application_ids(self):
|
||||
self.create_role('role1', {'application_id': 'app1'})
|
||||
self.create_role('role2', {'application_id': 'app1'})
|
||||
result = get_all_application_ids(self.roles_dir)
|
||||
self.assertEqual(result, ['app1'])
|
||||
|
||||
def test_missing_application_id(self):
|
||||
self.create_role('role1', {'other_key': 'value'})
|
||||
result = get_all_application_ids(self.roles_dir)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_no_roles_directory(self):
|
||||
# Point to a non-existent directory
|
||||
empty_dir = os.path.join(self.tmpdir.name, 'no_roles_here')
|
||||
result = get_all_application_ids(empty_dir)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
63
tests/unit/filter_plugins/test_get_application_id.py
Normal file
63
tests/unit/filter_plugins/test_get_application_id.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# 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()
|
@@ -5,7 +5,7 @@ import unittest
|
||||
import yaml
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
from filter_plugins.get_role_folder import get_role_folder
|
||||
from filter_plugins.get_role import get_role
|
||||
|
||||
class TestGetRoleFolder(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -35,20 +35,20 @@ class TestGetRoleFolder(unittest.TestCase):
|
||||
|
||||
def test_find_existing_role(self):
|
||||
# Should find role1 for application_id 'app-123'
|
||||
result = get_role_folder('app-123', roles_path=self.roles_dir)
|
||||
result = get_role('app-123', roles_path=self.roles_dir)
|
||||
self.assertEqual(result, 'role1')
|
||||
|
||||
def test_no_match_raises(self):
|
||||
# No role has application_id 'nonexistent'
|
||||
with self.assertRaises(AnsibleFilterError) as cm:
|
||||
get_role_folder('nonexistent', roles_path=self.roles_dir)
|
||||
get_role('nonexistent', roles_path=self.roles_dir)
|
||||
self.assertIn("No role found with application_id 'nonexistent'", str(cm.exception))
|
||||
|
||||
def test_missing_roles_path(self):
|
||||
# Path does not exist
|
||||
invalid_path = os.path.join(self.tempdir, 'invalid')
|
||||
with self.assertRaises(AnsibleFilterError) as cm:
|
||||
get_role_folder('any', roles_path=invalid_path)
|
||||
get_role('any', roles_path=invalid_path)
|
||||
self.assertIn(f"Roles path not found: {invalid_path}", str(cm.exception))
|
||||
|
||||
def test_invalid_yaml_raises(self):
|
||||
@@ -59,7 +59,7 @@ class TestGetRoleFolder(unittest.TestCase):
|
||||
f.write("::: invalid yaml :::")
|
||||
|
||||
with self.assertRaises(AnsibleFilterError) as cm:
|
||||
get_role_folder('app-123', roles_path=self.roles_dir)
|
||||
get_role('app-123', roles_path=self.roles_dir)
|
||||
self.assertIn('Failed to load', str(cm.exception))
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# tests/unit/lookup_plugins/test_docker_cards.py
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -9,14 +11,22 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../roles/web-a
|
||||
|
||||
from docker_cards import LookupModule
|
||||
|
||||
|
||||
class TestDockerCardsLookup(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Create a temporary directory to simulate the roles directory.
|
||||
self.test_roles_dir = tempfile.mkdtemp(prefix="test_roles_")
|
||||
# Create a sample role "web-app-port-ui".
|
||||
|
||||
# Create a sample role "web-app-port-ui" under that directory.
|
||||
self.role_name = "web-app-port-ui"
|
||||
self.role_dir = os.path.join(self.test_roles_dir, self.role_name)
|
||||
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")
|
||||
|
||||
# Create a sample README.md with a H1 line for the title.
|
||||
readme_path = os.path.join(self.role_dir, "README.md")
|
||||
@@ -54,11 +64,11 @@ galaxy_info:
|
||||
"group_names": ["portfolio"]
|
||||
}
|
||||
result = lookup_module.run([self.test_roles_dir], variables=fake_variables)
|
||||
|
||||
|
||||
# The result is a list containing one list of card dictionaries.
|
||||
self.assertIsInstance(result, list)
|
||||
self.assertEqual(len(result), 1)
|
||||
|
||||
|
||||
cards = result[0]
|
||||
self.assertIsInstance(cards, list)
|
||||
# Since "portfolio" is in group_names, one card should be present.
|
||||
@@ -80,21 +90,22 @@ galaxy_info:
|
||||
"applications": {
|
||||
"portfolio": {
|
||||
"features": {
|
||||
"iframe": True
|
||||
"portfolio_iframe": True
|
||||
}
|
||||
}
|
||||
},
|
||||
"group_names": [] # Not including "portfolio"
|
||||
}
|
||||
result = lookup_module.run([self.test_roles_dir], variables=fake_variables)
|
||||
|
||||
|
||||
# Since the application_id is not in group_names, no card should be added.
|
||||
self.assertIsInstance(result, list)
|
||||
self.assertEqual(len(result), 1)
|
||||
|
||||
|
||||
cards = result[0]
|
||||
self.assertIsInstance(cards, list)
|
||||
self.assertEqual(len(cards), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
Reference in New Issue
Block a user