Files
computer-playbook/tests/unit/utils/manager/test_inventory_manager.py
T
kevinveenbirkenbachandClaude Opus 4.7 c55ad69c41 refactor: drop requirement-file references from code, add doc rule + lint
Strip every `req <NNN>`, `req-<NNN>`, `Requirement <NNN>`,
`requirement-<NNN>`, and `docs/requirements/<NNN>-...md` mention from
non-Markdown source files. Comments anchored to requirement numbers
rot when requirements are renumbered or rewritten and conflate
present-state behaviour (which the comment should describe) with the
audit trail of how the code got there (which belongs in commit
messages, PR descriptions, and the requirement file itself).

* Document the rule in `docs/contributing/documentation.md` under
  "Comments". Code MUST describe the *why* in present-state language;
  contract enforcement lives in tests, not folklore.
* Clean 254 source files across `roles/`, `tests/`, `plugins/`,
  `cli/`, `scripts/`, `utils/`, `group_vars/`, and the repo-root
  `eslint.config.js`. Pure comment / docstring / log-message edits;
  no behavioural change.
* Add `tests/lint/repository/test_no_req_references_in_code.py` to
  enforce the rule going forward across `.js`, `.py`, `.yml`,
  `.yaml`, `.j2`, and `.sh` files. The legacy
  `test_no_stale_requirement_refs.py` is exempt as the only file
  whose purpose is to detect these very tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:22 +02:00

576 lines
24 KiB
Python

import tempfile
from pathlib import Path
from unittest import TestCase, main, mock
from utils.handler.vault import VaultScalar
from utils.manager.inventory import InventoryManager
from utils.manager.value_generator import ValueGenerator
from utils.roles.mapping import (
ROLE_FILE_META_SCHEMA,
ROLE_FILE_META_SERVICES,
ROLE_FILE_VARS_MAIN,
)
class TestInventoryManager(TestCase):
def test_load_application_id_missing_exits(self):
"""
If vars/main.yml does not contain application_id, InventoryManager
must print an error and exit with code 1.
"""
with tempfile.TemporaryDirectory() as tmpdir:
role_path = Path(tmpdir) / "role"
inv_path = Path(tmpdir) / "inventory.yml"
role_path.mkdir(parents=True, exist_ok=True)
(role_path / "meta").mkdir(parents=True, exist_ok=True)
(role_path / "vars").mkdir(parents=True, exist_ok=True)
(role_path / "config").mkdir(parents=True, exist_ok=True)
# IMPORTANT: ensure files exist for .exists() checks
(role_path / ROLE_FILE_META_SCHEMA).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_VARS_MAIN).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SERVICES).write_text("{}", encoding="utf-8")
inv_path.write_text("{}", encoding="utf-8")
inventory_path = inv_path
def fake_load_yaml(path):
p = Path(path)
if p == inventory_path:
return {}
if p == role_path / ROLE_FILE_META_SCHEMA:
return {}
if p == role_path / ROLE_FILE_VARS_MAIN:
return {} # missing application_id on purpose
if p == role_path / ROLE_FILE_META_SERVICES:
# Per: meta/services.yml file root IS the
# services map (no `compose.services` envelope).
return {}
return {}
with (
mock.patch(
"utils.manager.inventory.YamlHandler.load_yaml",
side_effect=fake_load_yaml,
),
mock.patch("utils.manager.inventory.VaultHandler"),
):
with self.assertRaises(SystemExit) as ctx:
InventoryManager(
role_path=role_path,
inventory_path=inventory_path,
vault_pw="dummy",
overrides={},
)
self.assertEqual(ctx.exception.code, 1)
def test_plain_without_override_and_allow_empty_plain_exits(self):
"""
For a `plain` algorithm credential, if no override is provided and
allow_empty_plain=False, apply_schema must exit.
"""
with tempfile.TemporaryDirectory() as tmpdir:
role_path = Path(tmpdir) / "role"
inv_path = Path(tmpdir) / "inventory.yml"
role_path.mkdir(parents=True, exist_ok=True)
(role_path / "meta").mkdir(parents=True, exist_ok=True)
(role_path / "vars").mkdir(parents=True, exist_ok=True)
(role_path / "config").mkdir(parents=True, exist_ok=True)
inv_path.write_text("{}", encoding="utf-8")
# IMPORTANT: ensure files exist for .exists() checks
(role_path / ROLE_FILE_META_SCHEMA).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_VARS_MAIN).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SERVICES).write_text("{}", encoding="utf-8")
inventory_path = inv_path
schema_data = {
"credentials": {
"api_key": {
"description": "API key",
"algorithm": "plain",
"validation": {},
}
}
}
def fake_load_yaml(path):
p = Path(path)
if p == inventory_path:
return {"applications": {}}
if p == role_path / ROLE_FILE_META_SCHEMA:
return schema_data
if p == role_path / ROLE_FILE_VARS_MAIN:
return {"application_id": "app_test"}
if p == role_path / ROLE_FILE_META_SERVICES:
# Per: meta/services.yml file root IS the
# services map (no `compose.services` envelope).
return {}
return {}
with (
mock.patch(
"utils.manager.inventory.YamlHandler.load_yaml",
side_effect=fake_load_yaml,
),
mock.patch("utils.manager.inventory.VaultHandler"),
):
mgr = InventoryManager(
role_path=role_path,
inventory_path=inventory_path,
vault_pw="dummy",
overrides={}, # no plain override
allow_empty_plain=False,
)
with self.assertRaises(SystemExit) as ctx:
mgr.apply_schema()
self.assertEqual(ctx.exception.code, 1)
def test_plain_with_allow_empty_plain_sets_empty_string_unencrypted(self):
"""
For a `plain` algorithm credential, if no override is provided and
allow_empty_plain=True, the credential should be set to "" and must NOT be encrypted.
"""
with tempfile.TemporaryDirectory() as tmpdir:
role_path = Path(tmpdir) / "role"
inv_path = Path(tmpdir) / "inventory.yml"
role_path.mkdir(parents=True, exist_ok=True)
(role_path / "meta").mkdir(parents=True, exist_ok=True)
(role_path / "vars").mkdir(parents=True, exist_ok=True)
(role_path / "config").mkdir(parents=True, exist_ok=True)
inv_path.write_text("{}", encoding="utf-8")
# IMPORTANT: ensure files exist for .exists() checks
(role_path / ROLE_FILE_META_SCHEMA).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_VARS_MAIN).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SERVICES).write_text("{}", encoding="utf-8")
inventory_path = inv_path
schema_data = {
"credentials": {
"api_key": {
"description": "API key",
"algorithm": "plain",
"validation": {},
}
}
}
def fake_load_yaml(path):
p = Path(path)
if p == inventory_path:
return {"applications": {}}
if p == role_path / ROLE_FILE_META_SCHEMA:
return schema_data
if p == role_path / ROLE_FILE_VARS_MAIN:
return {"application_id": "app_test"}
if p == role_path / ROLE_FILE_META_SERVICES:
# Per: meta/services.yml file root IS the
# services map (no `compose.services` envelope).
return {}
return {}
with (
mock.patch(
"utils.manager.inventory.YamlHandler.load_yaml",
side_effect=fake_load_yaml,
),
mock.patch("utils.manager.inventory.VaultHandler") as mock_vault_cls,
):
mock_vault = mock_vault_cls.return_value
mock_vault.encrypt_string.return_value = (
"!vault |\n $ANSIBLE_VAULT;1.1;AES256\n ENCRYPTED"
)
mgr = InventoryManager(
role_path=role_path,
inventory_path=inventory_path,
vault_pw="dummy",
overrides={}, # no override for plain
allow_empty_plain=True,
)
inv = mgr.apply_schema()
creds = inv["applications"]["app_test"]["credentials"]
self.assertIn("api_key", creds)
self.assertEqual(creds["api_key"], "")
mock_vault.encrypt_string.assert_not_called()
def test_plain_preserves_existing_generated_value(self):
"""
If a plain credential was already populated by special-role logic,
allow_empty_plain must not overwrite it with an empty string.
"""
with tempfile.TemporaryDirectory() as tmpdir:
role_path = Path(tmpdir) / "role"
inv_path = Path(tmpdir) / "inventory.yml"
role_path.mkdir(parents=True, exist_ok=True)
(role_path / "meta").mkdir(parents=True, exist_ok=True)
(role_path / "vars").mkdir(parents=True, exist_ok=True)
(role_path / "config").mkdir(parents=True, exist_ok=True)
inv_path.write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SCHEMA).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_VARS_MAIN).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SERVICES).write_text("{}", encoding="utf-8")
inventory_path = inv_path
schema_data = {
"credentials": {
"oauth2_proxy_cookie_secret": {
"description": "Cookie secret",
"algorithm": "plain",
"validation": {},
}
}
}
def fake_load_yaml(path):
p = Path(path)
if p == inventory_path:
return {"applications": {}}
if p == role_path / ROLE_FILE_META_SCHEMA:
return schema_data
if p == role_path / ROLE_FILE_VARS_MAIN:
return {"application_id": "app_test"}
if p == role_path / ROLE_FILE_META_SERVICES:
# Per the file root IS the services map
# (no `compose.services` envelope).
return {"oauth2": {"enabled": True}}
return {}
with (
mock.patch(
"utils.manager.inventory.YamlHandler.load_yaml",
side_effect=fake_load_yaml,
),
mock.patch("utils.manager.inventory.VaultHandler") as mock_vault_cls,
mock.patch.object(
ValueGenerator, "generate_value", return_value="generated-secret"
),
):
mock_vault = mock_vault_cls.return_value
mgr = InventoryManager(
role_path=role_path,
inventory_path=inventory_path,
vault_pw="dummy",
overrides={},
allow_empty_plain=True,
)
inv = mgr.apply_schema()
creds = inv["applications"]["app_test"]["credentials"]
self.assertEqual(
creds["oauth2_proxy_cookie_secret"], "generated-secret"
)
mock_vault.encrypt_string.assert_not_called()
def test_oauth2_dynamic_flag_seeds_cookie_secret(self):
with tempfile.TemporaryDirectory() as tmpdir:
role_path = Path(tmpdir) / "role"
inv_path = Path(tmpdir) / "inventory.yml"
role_path.mkdir(parents=True, exist_ok=True)
(role_path / "meta").mkdir(parents=True, exist_ok=True)
(role_path / "vars").mkdir(parents=True, exist_ok=True)
(role_path / "config").mkdir(parents=True, exist_ok=True)
inv_path.write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SCHEMA).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_VARS_MAIN).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SERVICES).write_text("{}", encoding="utf-8")
schema_data = {
"credentials": {
"oauth2_proxy_cookie_secret": {
"description": "Cookie secret",
"algorithm": "plain",
"validation": {},
}
}
}
def fake_load_yaml(path):
p = Path(path)
if p == inv_path:
return {"applications": {}}
if p == role_path / ROLE_FILE_META_SCHEMA:
return schema_data
if p == role_path / ROLE_FILE_VARS_MAIN:
return {"application_id": "app_test"}
if p == role_path / ROLE_FILE_META_SERVICES:
return {
"oauth2": {
"enabled": "{{ 'web-app-keycloak' in group_names }}",
"shared": "{{ 'web-app-keycloak' in group_names }}",
}
}
return {}
with (
mock.patch(
"utils.manager.inventory.YamlHandler.load_yaml",
side_effect=fake_load_yaml,
),
mock.patch("utils.manager.inventory.VaultHandler"),
mock.patch.object(
ValueGenerator, "generate_value", return_value="dynamic-secret"
),
):
mgr = InventoryManager(
role_path=role_path,
inventory_path=inv_path,
vault_pw="dummy",
overrides={},
allow_empty_plain=True,
)
inv = mgr.apply_schema()
creds = inv["applications"]["app_test"]["credentials"]
self.assertEqual(creds["oauth2_proxy_cookie_secret"], "dynamic-secret")
def test_oauth2_disabled_skips_cookie_secret(self):
with tempfile.TemporaryDirectory() as tmpdir:
role_path = Path(tmpdir) / "role"
inv_path = Path(tmpdir) / "inventory.yml"
role_path.mkdir(parents=True, exist_ok=True)
(role_path / "meta").mkdir(parents=True, exist_ok=True)
(role_path / "vars").mkdir(parents=True, exist_ok=True)
(role_path / "config").mkdir(parents=True, exist_ok=True)
inv_path.write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SCHEMA).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_VARS_MAIN).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SERVICES).write_text("{}", encoding="utf-8")
def fake_load_yaml(path):
p = Path(path)
if p == inv_path:
return {"applications": {}}
if p == role_path / ROLE_FILE_META_SCHEMA:
return {"credentials": {}}
if p == role_path / ROLE_FILE_VARS_MAIN:
return {"application_id": "app_test"}
if p == role_path / ROLE_FILE_META_SERVICES:
return {"oauth2": {"enabled": False}, "oidc": {"enabled": False}}
return {}
with (
mock.patch(
"utils.manager.inventory.YamlHandler.load_yaml",
side_effect=fake_load_yaml,
),
mock.patch("utils.manager.inventory.VaultHandler"),
mock.patch.object(
ValueGenerator, "generate_value", return_value="should-not-fire"
),
):
mgr = InventoryManager(
role_path=role_path,
inventory_path=inv_path,
vault_pw="dummy",
overrides={},
allow_empty_plain=True,
)
inv = mgr.apply_schema()
creds = (
inv.get("applications", {})
.get("app_test", {})
.get("credentials", {})
)
self.assertNotIn("oauth2_proxy_cookie_secret", creds)
def test_non_plain_algorithm_encrypts_and_sets_vaultscalar(self):
"""
For non-plain algorithms, apply_schema must generate a value (via ValueGenerator)
and encrypt it into a VaultScalar.
"""
with tempfile.TemporaryDirectory() as tmpdir:
role_path = Path(tmpdir) / "role"
inv_path = Path(tmpdir) / "inventory.yml"
role_path.mkdir(parents=True, exist_ok=True)
(role_path / "meta").mkdir(parents=True, exist_ok=True)
(role_path / "vars").mkdir(parents=True, exist_ok=True)
(role_path / "config").mkdir(parents=True, exist_ok=True)
inv_path.write_text("{}", encoding="utf-8")
# IMPORTANT: ensure files exist for .exists() checks
(role_path / ROLE_FILE_META_SCHEMA).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_VARS_MAIN).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SERVICES).write_text("{}", encoding="utf-8")
inventory_path = inv_path
schema_data = {
"credentials": {
"api_key": {
"description": "API key",
"algorithm": "random_hex_16",
"validation": {},
}
}
}
def fake_load_yaml(path):
p = Path(path)
if p == inventory_path:
return {"applications": {}}
if p == role_path / ROLE_FILE_META_SCHEMA:
return schema_data
if p == role_path / ROLE_FILE_VARS_MAIN:
return {"application_id": "app_test"}
if p == role_path / ROLE_FILE_META_SERVICES:
# Per: meta/services.yml file root IS the
# services map (no `compose.services` envelope).
return {}
return {}
fake_snippet = "!vault |\n $ANSIBLE_VAULT;1.1;AES256\n ENCRYPTEDVALUE"
with (
mock.patch(
"utils.manager.inventory.YamlHandler.load_yaml",
side_effect=fake_load_yaml,
),
mock.patch("utils.manager.inventory.VaultHandler") as mock_vault_cls,
mock.patch.object(
ValueGenerator, "generate_value", return_value="PLAINVAL"
),
):
mock_vault = mock_vault_cls.return_value
mock_vault.encrypt_string.return_value = fake_snippet
mgr = InventoryManager(
role_path=role_path,
inventory_path=inventory_path,
vault_pw="dummy",
overrides={},
allow_empty_plain=False,
)
inv = mgr.apply_schema()
creds = inv["applications"]["app_test"]["credentials"]
self.assertIn("api_key", creds)
value = creds["api_key"]
self.assertIsInstance(value, VaultScalar)
self.assertIn("$ANSIBLE_VAULT", str(value))
mock_vault.encrypt_string.assert_called_once_with("PLAINVAL", "api_key")
def test_recurse_skips_existing_dict_and_vaultscalar(self):
"""
If the destination already contains:
- a dict for a credential key, or
- a VaultScalar for a credential key,
recurse_credentials must skip re-encryption and leave existing values untouched.
NOTE:
InventoryManager now checks schema/config file existence on disk before loading,
so we must create those files in the temp role directory.
"""
with tempfile.TemporaryDirectory() as tmpdir:
role_path = Path(tmpdir) / "role"
inv_path = Path(tmpdir) / "inventory.yml"
role_path.mkdir(parents=True, exist_ok=True)
(role_path / "meta").mkdir(parents=True, exist_ok=True)
(role_path / "vars").mkdir(parents=True, exist_ok=True)
(role_path / "config").mkdir(parents=True, exist_ok=True)
inv_path.write_text("{}", encoding="utf-8")
# IMPORTANT: ensure files exist for .exists() checks
(role_path / ROLE_FILE_META_SCHEMA).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_VARS_MAIN).write_text("{}", encoding="utf-8")
(role_path / ROLE_FILE_META_SERVICES).write_text("{}", encoding="utf-8")
inventory_path = inv_path
existing_vault = VaultScalar("EXISTING_BODY")
existing_dict = {"nested": "value"}
inventory_data = {
"applications": {
"app_test": {
"credentials": {
"already_vaulted": existing_vault,
"complex": existing_dict,
}
}
}
}
schema_data = {
"credentials": {
"already_vaulted": {
"description": "Vaulted",
"algorithm": "random_hex_16",
"validation": {},
},
"complex": {
"description": "Complex dict",
"algorithm": "random_hex_16",
"validation": {},
},
}
}
def fake_load_yaml(path):
p = Path(path)
if p == inventory_path:
return inventory_data
if p == role_path / ROLE_FILE_META_SCHEMA:
return schema_data
if p == role_path / ROLE_FILE_VARS_MAIN:
return {"application_id": "app_test"}
if p == role_path / ROLE_FILE_META_SERVICES:
# No provider resolution / no special rules
# Per: meta/services.yml file root IS the
# services map (no `compose.services` envelope).
return {}
return {}
with (
mock.patch(
"utils.manager.inventory.YamlHandler.load_yaml",
side_effect=fake_load_yaml,
),
mock.patch("utils.manager.inventory.VaultHandler") as mock_vault_cls,
# Even though encryption is skipped, the current implementation
# may still call ValueGenerator.generate_value() before checking
# existing destination values. Keep it deterministic.
mock.patch.object(
ValueGenerator, "generate_value", return_value="IGNORED"
),
):
mock_vault = mock_vault_cls.return_value
mock_vault.encrypt_string.side_effect = AssertionError(
"encrypt_string should not be called for existing VaultScalar/dict"
)
mgr = InventoryManager(
role_path=role_path,
inventory_path=inventory_path,
vault_pw="dummy",
overrides={},
allow_empty_plain=False,
)
inv = mgr.apply_schema()
creds = inv["applications"]["app_test"]["credentials"]
self.assertIn("already_vaulted", creds)
self.assertIn("complex", creds)
self.assertIs(creds["already_vaulted"], existing_vault)
self.assertIs(creds["complex"], existing_dict)
if __name__ == "__main__":
main()