mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-12-17 22:32:52 +00:00
Fixed Unit Tests https://chatgpt.com/share/6941cd96-81b4-800f-9017-2e4bcdc4bd52
This commit is contained in:
30
Makefile
30
Makefile
@@ -1,7 +1,7 @@
|
||||
SHELL := /usr/bin/env bash
|
||||
VENV ?= .venv
|
||||
PYTHON := python3
|
||||
PIP := python3 -m pip
|
||||
PYTHON ?= python3
|
||||
PIP ?= $(PYTHON) -m pip
|
||||
|
||||
ROLES_DIR := ./roles
|
||||
APPLICATIONS_OUT := ./group_vars/all/04_applications.yml
|
||||
@@ -19,6 +19,9 @@ LINT_TESTS_DIR ?= tests/lint
|
||||
UNIT_TESTS_DIR ?= tests/unit
|
||||
INTEGRATION_TESTS_DIR ?= tests/integration
|
||||
|
||||
# Ensure repo root is importable (so module_utils/, filter_plugins/ etc. work)
|
||||
PYTHONPATH ?= .
|
||||
|
||||
# Compute extra users as before
|
||||
RESERVED_USERNAMES := $(shell \
|
||||
find $(ROLES_DIR) -maxdepth 1 -type d -printf '%f\n' \
|
||||
@@ -92,7 +95,10 @@ test-lint:
|
||||
exit 0; \
|
||||
fi
|
||||
@echo "🔎 Running lint tests (dir: $(LINT_TESTS_DIR), pattern: $(TEST_PATTERN))…"
|
||||
PYTHONPATH=. $(PYTHON) -m unittest discover -s $(LINT_TESTS_DIR) -p "$(TEST_PATTERN)"
|
||||
@PYTHONPATH="$(PYTHONPATH)" $(PYTHON) -m unittest discover \
|
||||
-s "$(LINT_TESTS_DIR)" \
|
||||
-p "$(TEST_PATTERN)" \
|
||||
-t "$(PYTHONPATH)"
|
||||
|
||||
test-unit:
|
||||
@if [ ! -d "$(UNIT_TESTS_DIR)" ]; then \
|
||||
@@ -100,7 +106,10 @@ test-unit:
|
||||
exit 0; \
|
||||
fi
|
||||
@echo "🧪 Running unit tests (dir: $(UNIT_TESTS_DIR), pattern: $(TEST_PATTERN))…"
|
||||
PYTHONPATH=. $(PYTHON) -m unittest discover -s $(UNIT_TESTS_DIR) -p "$(TEST_PATTERN)"
|
||||
@PYTHONPATH="$(PYTHONPATH)" $(PYTHON) -m unittest discover \
|
||||
-s "$(UNIT_TESTS_DIR)" \
|
||||
-p "$(TEST_PATTERN)" \
|
||||
-t "$(PYTHONPATH)"
|
||||
|
||||
test-integration:
|
||||
@if [ ! -d "$(INTEGRATION_TESTS_DIR)" ]; then \
|
||||
@@ -108,20 +117,23 @@ test-integration:
|
||||
exit 0; \
|
||||
fi
|
||||
@echo "🧪 Running integration tests (dir: $(INTEGRATION_TESTS_DIR), pattern: $(TEST_PATTERN))…"
|
||||
PYTHONPATH=. $(PYTHON) -m unittest discover -s $(INTEGRATION_TESTS_DIR) -p "$(TEST_PATTERN)"
|
||||
@PYTHONPATH="$(PYTHONPATH)" $(PYTHON) -m unittest discover \
|
||||
-s "$(INTEGRATION_TESTS_DIR)" \
|
||||
-p "$(TEST_PATTERN)" \
|
||||
-t "$(PYTHONPATH)"
|
||||
|
||||
# Backwards compatible target (kept)
|
||||
test-messy: test-lint test-unit test-integration
|
||||
@echo "📑 Checking Ansible syntax…"
|
||||
ansible-playbook -i localhost, -c local $(foreach f,$(wildcard group_vars/all/*.yml),-e @$(f)) playbook.yml --syntax-check
|
||||
|
||||
test: setup-clean test-messy
|
||||
@echo "Full test with setup-clean before was executed."
|
||||
test: clean setup test-messy
|
||||
@echo "✅ Full test (setup + tests) executed."
|
||||
|
||||
deps:
|
||||
@if [ ! -x "$(PYTHON)" ]; then \
|
||||
@if [ ! -d "$(VENV)" ]; then \
|
||||
echo "🐍 Creating virtualenv $(VENV)"; \
|
||||
python3 -m venv $(VENV); \
|
||||
python3 -m venv "$(VENV)"; \
|
||||
fi
|
||||
@echo "📦 Installing Python dependencies"
|
||||
@$(PIP) install --upgrade pip setuptools wheel
|
||||
|
||||
@@ -32,7 +32,7 @@ class TestGenerateDefaultApplications(unittest.TestCase):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
def test_script_generates_expected_yaml(self):
|
||||
script_path = Path(__file__).resolve().parent.parent.parent.parent.parent.parent / "cli/setup/applications.py"
|
||||
script_path = Path(__file__).resolve().parent.parent.parent.parent.parent / "cli/setup/applications.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
|
||||
@@ -1,59 +1,56 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
import os
|
||||
import yaml
|
||||
from collections import OrderedDict
|
||||
|
||||
# Add cli/ to import path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..", "cli/setup/")))
|
||||
from cli.setup import users
|
||||
|
||||
import users
|
||||
|
||||
class TestGenerateUsers(unittest.TestCase):
|
||||
def test_build_users_auto_increment_and_overrides(self):
|
||||
defs = {
|
||||
'alice': {},
|
||||
'bob': {'uid': 2000, 'email': 'bob@custom.com', 'description': 'Custom user'},
|
||||
'carol': {}
|
||||
"alice": {},
|
||||
"bob": {"uid": 2000, "email": "bob@custom.com", "description": "Custom user"},
|
||||
"carol": {},
|
||||
}
|
||||
build = users.build_users(
|
||||
defs=defs,
|
||||
primary_domain='example.com',
|
||||
primary_domain="example.com",
|
||||
start_id=1001,
|
||||
become_pwd='pw'
|
||||
become_pwd="pw",
|
||||
)
|
||||
# alice should get uid/gid 1001
|
||||
self.assertEqual(build['alice']['uid'], 1001)
|
||||
self.assertEqual(build['alice']['gid'], 1001)
|
||||
self.assertEqual(build['alice']['email'], 'alice@example.com')
|
||||
self.assertEqual(build["alice"]["uid"], 1001)
|
||||
self.assertEqual(build["alice"]["gid"], 1001)
|
||||
self.assertEqual(build["alice"]["email"], "alice@example.com")
|
||||
# bob overrides
|
||||
self.assertEqual(build['bob']['uid'], 2000)
|
||||
self.assertEqual(build['bob']['gid'], 2000)
|
||||
self.assertEqual(build['bob']['email'], 'bob@custom.com')
|
||||
self.assertIn('description', build['bob'])
|
||||
self.assertEqual(build["bob"]["uid"], 2000)
|
||||
self.assertEqual(build["bob"]["gid"], 2000)
|
||||
self.assertEqual(build["bob"]["email"], "bob@custom.com")
|
||||
self.assertIn("description", build["bob"])
|
||||
# carol should get next free id = 1002
|
||||
self.assertEqual(build['carol']['uid'], 1002)
|
||||
self.assertEqual(build['carol']['gid'], 1002)
|
||||
self.assertEqual(build["carol"]["uid"], 1002)
|
||||
self.assertEqual(build["carol"]["gid"], 1002)
|
||||
|
||||
def test_build_users_default_lookup_password(self):
|
||||
"""
|
||||
When no 'password' override is provided,
|
||||
the become_pwd lookup template string must be used as the password.
|
||||
"""
|
||||
defs = {'frank': {}}
|
||||
defs = {"frank": {}}
|
||||
lookup_template = '{{ lookup("password", "/dev/null length=42 chars=ascii_letters,digits") }}'
|
||||
build = users.build_users(
|
||||
defs=defs,
|
||||
primary_domain='example.com',
|
||||
primary_domain="example.com",
|
||||
start_id=1001,
|
||||
become_pwd=lookup_template
|
||||
become_pwd=lookup_template,
|
||||
)
|
||||
self.assertEqual(
|
||||
build['frank']['password'],
|
||||
build["frank"]["password"],
|
||||
lookup_template,
|
||||
"The lookup template string was not correctly applied as the default password"
|
||||
"The lookup template string was not correctly applied as the default password",
|
||||
)
|
||||
|
||||
def test_build_users_override_password(self):
|
||||
@@ -61,72 +58,71 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
When a 'password' override is provided,
|
||||
that custom password must be used instead of become_pwd.
|
||||
"""
|
||||
defs = {'eva': {'password': 'custompw'}}
|
||||
defs = {"eva": {"password": "custompw"}}
|
||||
lookup_template = '{{ lookup("password", "/dev/null length=42 chars=ascii_letters,digits") }}'
|
||||
build = users.build_users(
|
||||
defs=defs,
|
||||
primary_domain='example.com',
|
||||
primary_domain="example.com",
|
||||
start_id=1001,
|
||||
become_pwd=lookup_template
|
||||
become_pwd=lookup_template,
|
||||
)
|
||||
self.assertEqual(
|
||||
build['eva']['password'],
|
||||
'custompw',
|
||||
"The override password was not correctly applied"
|
||||
build["eva"]["password"],
|
||||
"custompw",
|
||||
"The override password was not correctly applied",
|
||||
)
|
||||
|
||||
|
||||
def test_build_users_duplicate_override_uid(self):
|
||||
defs = {
|
||||
'u1': {'uid': 1001},
|
||||
'u2': {'uid': 1001}
|
||||
"u1": {"uid": 1001},
|
||||
"u2": {"uid": 1001},
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
users.build_users(defs, 'ex.com', 1001, 'pw')
|
||||
users.build_users(defs, "ex.com", 1001, "pw")
|
||||
|
||||
def test_build_users_shared_gid_allowed(self):
|
||||
# Allow two users to share the same GID when one overrides gid and the other uses that as uid
|
||||
defs = {
|
||||
'a': {'uid': 1500},
|
||||
'b': {'gid': 1500}
|
||||
"a": {"uid": 1500},
|
||||
"b": {"gid": 1500},
|
||||
}
|
||||
build = users.build_users(defs, 'ex.com', 1500, 'pw')
|
||||
build = users.build_users(defs, "ex.com", 1500, "pw")
|
||||
# Both should have gid 1500
|
||||
self.assertEqual(build['a']['gid'], 1500)
|
||||
self.assertEqual(build['b']['gid'], 1500)
|
||||
self.assertEqual(build["a"]["gid"], 1500)
|
||||
self.assertEqual(build["b"]["gid"], 1500)
|
||||
|
||||
def test_build_users_duplicate_username_email(self):
|
||||
defs = {
|
||||
'u1': {'username': 'same', 'email': 'same@ex.com'},
|
||||
'u2': {'username': 'same'}
|
||||
"u1": {"username": "same", "email": "same@ex.com"},
|
||||
"u2": {"username": "same"},
|
||||
}
|
||||
# second user with same username should raise
|
||||
with self.assertRaises(ValueError):
|
||||
users.build_users(defs, 'ex.com', 1001, 'pw')
|
||||
users.build_users(defs, "ex.com", 1001, "pw")
|
||||
|
||||
def test_dictify_converts_ordereddict(self):
|
||||
od = users.OrderedDict([('a', 1), ('b', {'c': 2})])
|
||||
od = users.OrderedDict([("a", 1), ("b", {"c": 2})])
|
||||
result = users.dictify(OrderedDict(od))
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(result, {'a': 1, 'b': {'c': 2}})
|
||||
self.assertEqual(result, {"a": 1, "b": {"c": 2}})
|
||||
|
||||
def test_load_user_defs_and_conflict(self):
|
||||
# create temp roles structure
|
||||
tmp = tempfile.mkdtemp()
|
||||
try:
|
||||
os.makedirs(os.path.join(tmp, 'role1/users'))
|
||||
os.makedirs(os.path.join(tmp, 'role2/users'))
|
||||
os.makedirs(os.path.join(tmp, "role1/users"))
|
||||
os.makedirs(os.path.join(tmp, "role2/users"))
|
||||
# role1 defines user x
|
||||
with open(os.path.join(tmp, 'role1/users/main.yml'), 'w') as f:
|
||||
yaml.safe_dump({'users': {'x': {'email': 'x@a'}}}, f)
|
||||
with open(os.path.join(tmp, "role1/users/main.yml"), "w") as f:
|
||||
yaml.safe_dump({"users": {"x": {"email": "x@a"}}}, f)
|
||||
# role2 defines same user x with same value
|
||||
with open(os.path.join(tmp, 'role2/users/main.yml'), 'w') as f:
|
||||
yaml.safe_dump({'users': {'x': {'email': 'x@a'}}}, f)
|
||||
with open(os.path.join(tmp, "role2/users/main.yml"), "w") as f:
|
||||
yaml.safe_dump({"users": {"x": {"email": "x@a"}}}, f)
|
||||
defs = users.load_user_defs(tmp)
|
||||
self.assertIn('x', defs)
|
||||
self.assertIn("x", defs)
|
||||
# now conflict definition
|
||||
with open(os.path.join(tmp, 'role2/users/main.yml'), 'w') as f:
|
||||
yaml.safe_dump({'users': {'x': {'email': 'x@b'}}}, f)
|
||||
with open(os.path.join(tmp, "role2/users/main.yml"), "w") as f:
|
||||
yaml.safe_dump({"users": {"x": {"email": "x@b"}}}, f)
|
||||
with self.assertRaises(ValueError):
|
||||
users.load_user_defs(tmp)
|
||||
finally:
|
||||
@@ -136,7 +132,6 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
"""
|
||||
Ensure that default_users keys are written in alphabetical order.
|
||||
"""
|
||||
import tempfile
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
@@ -147,10 +142,10 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
|
||||
# Create multiple roles with users in unsorted order
|
||||
for role, users_map in [
|
||||
("role-zeta", {"zeta": {"email": "z@ex"}}),
|
||||
("role-zeta", {"zeta": {"email": "z@ex"}}),
|
||||
("role-alpha", {"alpha": {"email": "a@ex"}}),
|
||||
("role-mu", {"mu": {"email": "m@ex"}}),
|
||||
("role-beta", {"beta": {"email": "b@ex"}}),
|
||||
("role-mu", {"mu": {"email": "m@ex"}}),
|
||||
("role-beta", {"beta": {"email": "b@ex"}}),
|
||||
]:
|
||||
(roles_dir / role / "users").mkdir(parents=True, exist_ok=True)
|
||||
with open(roles_dir / role / "users" / "main.yml", "w") as f:
|
||||
@@ -158,15 +153,20 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
|
||||
out_file = tmpdir / "users.yml"
|
||||
|
||||
# Resolve script path like in other tests (relative to repo root)
|
||||
script_path = Path(__file__).resolve().parents[4] / "cli" / "setup" / "users.py"
|
||||
# Always resolve the real script path from the imported module
|
||||
script_path = Path(users.__file__).resolve()
|
||||
|
||||
# Run generator
|
||||
result = subprocess.run(
|
||||
["python3", str(script_path),
|
||||
"--roles-dir", str(roles_dir),
|
||||
"--output", str(out_file)],
|
||||
capture_output=True, text=True
|
||||
[
|
||||
"python3",
|
||||
str(script_path),
|
||||
"--roles-dir",
|
||||
str(roles_dir),
|
||||
"--output",
|
||||
str(out_file),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
self.assertTrue(out_file.exists(), "Output file was not created.")
|
||||
@@ -176,24 +176,21 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
users_map = data["default_users"]
|
||||
keys_in_file = list(users_map.keys())
|
||||
|
||||
# Expect alphabetical order
|
||||
self.assertEqual(
|
||||
keys_in_file, sorted(keys_in_file),
|
||||
msg=f"Users are not sorted alphabetically: {keys_in_file}"
|
||||
keys_in_file,
|
||||
sorted(keys_in_file),
|
||||
msg=f"Users are not sorted alphabetically: {keys_in_file}",
|
||||
)
|
||||
# Sanity: all expected keys present
|
||||
for k in ["alpha", "beta", "mu", "zeta"]:
|
||||
self.assertIn(k, users_map)
|
||||
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def test_cli_users_sorting_stable_across_runs(self):
|
||||
"""
|
||||
Running the generator multiple times yields identical content (stable sort).
|
||||
"""
|
||||
import tempfile
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
@@ -202,7 +199,6 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
roles_dir = tmpdir / "roles"
|
||||
roles_dir.mkdir()
|
||||
|
||||
# Unsorted creation order on purpose
|
||||
cases = [
|
||||
("role-d", {"duser": {"email": "d@ex"}}),
|
||||
("role-a", {"auser": {"email": "a@ex"}}),
|
||||
@@ -215,35 +211,45 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
yaml.safe_dump({"users": users_map}, f)
|
||||
|
||||
out_file = tmpdir / "users.yml"
|
||||
script_path = Path(__file__).resolve().parents[5] / "cli" / "setup" / "users.py"
|
||||
script_path = Path(users.__file__).resolve()
|
||||
|
||||
# First run
|
||||
r1 = subprocess.run(
|
||||
["python3", str(script_path),
|
||||
"--roles-dir", str(roles_dir),
|
||||
"--output", str(out_file)],
|
||||
capture_output=True, text=True
|
||||
[
|
||||
"python3",
|
||||
str(script_path),
|
||||
"--roles-dir",
|
||||
str(roles_dir),
|
||||
"--output",
|
||||
str(out_file),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(r1.returncode, 0, msg=r1.stderr)
|
||||
content1 = out_file.read_text()
|
||||
|
||||
# Touch dirs to shuffle filesystem mtimes
|
||||
for p in roles_dir.iterdir():
|
||||
os.utime(p, None)
|
||||
|
||||
# Second run
|
||||
r2 = subprocess.run(
|
||||
["python3", str(script_path),
|
||||
"--roles-dir", str(roles_dir),
|
||||
"--output", str(out_file)],
|
||||
capture_output=True, text=True
|
||||
[
|
||||
"python3",
|
||||
str(script_path),
|
||||
"--roles-dir",
|
||||
str(roles_dir),
|
||||
"--output",
|
||||
str(out_file),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(r2.returncode, 0, msg=r2.stderr)
|
||||
content2 = out_file.read_text()
|
||||
|
||||
self.assertEqual(
|
||||
content1, content2,
|
||||
msg="Output differs between runs; user sorting should be stable."
|
||||
content1,
|
||||
content2,
|
||||
msg="Output differs between runs; user sorting should be stable.",
|
||||
)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
@@ -265,11 +271,8 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
become_pwd="pw",
|
||||
)
|
||||
|
||||
# Reserved user should carry the flag
|
||||
self.assertIn("reserved", build["admin"])
|
||||
self.assertTrue(build["admin"]["reserved"])
|
||||
|
||||
# Non-reserved user should not have the flag at all
|
||||
self.assertNotIn("reserved", build["bob"])
|
||||
|
||||
def test_cli_reserved_usernames_flag_sets_reserved_field(self):
|
||||
@@ -278,7 +281,6 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
in the generated YAML, and that existing definitions are preserved
|
||||
(only 'reserved' is added).
|
||||
"""
|
||||
import tempfile
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
@@ -287,7 +289,6 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
roles_dir = tmpdir / "roles"
|
||||
roles_dir.mkdir()
|
||||
|
||||
# Role with an existing user definition "admin"
|
||||
(roles_dir / "role-base" / "users").mkdir(parents=True, exist_ok=True)
|
||||
with open(roles_dir / "role-base" / "users" / "main.yml", "w") as f:
|
||||
yaml.safe_dump(
|
||||
@@ -303,7 +304,7 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
)
|
||||
|
||||
out_file = tmpdir / "users.yml"
|
||||
script_path = Path(__file__).resolve().parents[5] / "cli" / "setup" / "users.py"
|
||||
script_path = Path(users.__file__).resolve()
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
@@ -326,12 +327,9 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
self.assertIn("default_users", data)
|
||||
users_map = data["default_users"]
|
||||
|
||||
# "service" was created from the reserved list and must be reserved
|
||||
self.assertIn("service", users_map)
|
||||
self.assertTrue(users_map["service"].get("reserved", False))
|
||||
|
||||
# "admin" existed before; its fields must remain unchanged,
|
||||
# but it must now be marked as reserved
|
||||
self.assertIn("admin", users_map)
|
||||
self.assertEqual(users_map["admin"]["email"], "admin@ex")
|
||||
self.assertEqual(users_map["admin"]["description"], "Admin from role")
|
||||
@@ -340,5 +338,6 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -74,7 +74,7 @@ class TestMainHelpers(unittest.TestCase):
|
||||
|
||||
main.show_full_help_for_all("/fake/cli", available)
|
||||
|
||||
expected_modules = {"cli.deploy", "cli.build.defaults.users"}
|
||||
expected_modules = {"cli.deploy", "cli.setup.users"}
|
||||
invoked_modules = set()
|
||||
|
||||
for call in mock_run.call_args_list:
|
||||
|
||||
Reference in New Issue
Block a user