"""End-to-end integration tests for ``cli.administration.inventory.credentials.main``. Per-module unit tests for the helper concerns live next to their module under ``tests/unit/python/cli/administration/inventory/credentials/`` (``test_vault.py``, ``test_overrides.py``, ``test_emit.py``, ``test_prompts.py``).""" import subprocess import sys import tempfile import unittest import unittest.mock from pathlib import Path from cli.administration.inventory.credentials import main from utils.cache.yaml import dump_yaml, load_yaml_any from utils.roles.mapping import ( ROLE_FILE_META_SECRETS, ROLE_FILE_META_SERVICES, ROLE_FILE_VARS_MAIN, ) class TestCreateCredentials(unittest.TestCase): def test_main_overrides_and_file_writing(self): with tempfile.TemporaryDirectory() as tmpdir: role_path = str(Path(tmpdir) / "role") Path(str(Path(role_path) / "meta")).mkdir(parents=True) Path(str(Path(role_path) / "vars")).mkdir(parents=True) main_vars = {"application_id": "app_test"} dump_yaml(str(Path(role_path) / ROLE_FILE_VARS_MAIN), main_vars) config = {"features": {"central_database": False}} dump_yaml(str(Path(role_path) / ROLE_FILE_META_SERVICES), config) schema = { "credentials": { "api_key": { "description": "API key", "algorithm": "plain", "validation": {}, } } } dump_yaml(str(Path(role_path) / ROLE_FILE_META_SECRETS), schema) inventory_file = str(Path(tmpdir) / "inventory.yml") dump_yaml(inventory_file, {}) vault_pw_file = str(Path(tmpdir) / "pw.txt") with Path(vault_pw_file).open("w") as f: f.write("pw") fake_snippet = "!vault |\n $ANSIBLE_VAULT;1.1;AES256\n ENCRYPTEDVALUE" completed = subprocess.CompletedProcess( args=["ansible-vault"], returncode=0, stdout=fake_snippet, stderr="" ) with unittest.mock.patch("subprocess.run", return_value=completed): sys.argv = [ "create/credentials.py", "--role-path", role_path, "--inventory-file", inventory_file, "--vault-password-file", vault_pw_file, "--set", "applications.app_test.secrets.credentials.api_key=SECRET", "--force", ] main() data = load_yaml_any(inventory_file) creds = data["applications"]["app_test"]["secrets"]["credentials"] self.assertIn("api_key", creds) self.assertIsInstance(creds["api_key"], str) self.assertTrue(creds["api_key"].lstrip().startswith("$ANSIBLE_VAULT")) def test_main_plain_algorithm_allow_empty_plain_sets_empty_string_without_vault( self, ): """ When --allow-empty-plain is used, a 'plain' credential without override should be set to "" and *not* encrypted (no ansible-vault calls). """ with tempfile.TemporaryDirectory() as tmpdir: role_path = str(Path(tmpdir) / "role") Path(str(Path(role_path) / "meta")).mkdir(parents=True) Path(str(Path(role_path) / "vars")).mkdir(parents=True) main_vars = {"application_id": "app_empty_plain"} dump_yaml(str(Path(role_path) / ROLE_FILE_VARS_MAIN), main_vars) config = {"features": {"central_database": False}} dump_yaml(str(Path(role_path) / ROLE_FILE_META_SERVICES), config) schema = { "credentials": { "api_key": { "description": "API key", "algorithm": "plain", "validation": {}, } } } dump_yaml(str(Path(role_path) / ROLE_FILE_META_SECRETS), schema) inventory_file = str(Path(tmpdir) / "inventory.yml") dump_yaml(inventory_file, {}) vault_pw_file = str(Path(tmpdir) / "pw.txt") with Path(vault_pw_file).open("w") as f: f.write("pw") # Ensure ansible-vault is *not* called at all in this scenario def fail_run(*_args, **_kwargs): raise AssertionError( "ansible-vault must not be called for allow_empty_plain + empty plain" ) with unittest.mock.patch("subprocess.run", side_effect=fail_run): sys.argv = [ "create/credentials.py", "--role-path", role_path, "--inventory-file", inventory_file, "--vault-password-file", vault_pw_file, "--allow-empty-plain", ] main() data = load_yaml_any(inventory_file) creds = data["applications"]["app_empty_plain"]["secrets"]["credentials"] self.assertIn("api_key", creds) self.assertEqual(creds["api_key"], "") def test_main_forwards_variant_to_inventory_manager(self): """The `--variant` CLI flag must reach `InventoryManager(variant=)` so shared-provider discovery uses the variants.yml overlay.""" with tempfile.TemporaryDirectory() as tmpdir: role_path = str(Path(tmpdir) / "role") Path(str(Path(role_path) / "meta")).mkdir(parents=True) Path(str(Path(role_path) / "vars")).mkdir(parents=True) dump_yaml( str(Path(role_path) / ROLE_FILE_VARS_MAIN), {"application_id": "app_test"}, ) dump_yaml(str(Path(role_path) / ROLE_FILE_META_SERVICES), {}) dump_yaml(str(Path(role_path) / ROLE_FILE_META_SECRETS), {}) inventory_file = str(Path(tmpdir) / "inventory.yml") dump_yaml(inventory_file, {}) vault_pw_file = str(Path(tmpdir) / "pw.txt") with Path(vault_pw_file).open("w") as f: f.write("pw") captured: dict = {} class FakeManager: def __init__(self, **kwargs): captured.update(kwargs) self.app_id = kwargs.get("overrides", {}).get( "_fake_app_id", "app_test" ) self.vault_handler = None self.schema = {} self.inventory = {} def apply_schema(self): return {"applications": {}} with unittest.mock.patch( "cli.administration.inventory.credentials.__main__.InventoryManager", FakeManager, ): sys.argv = [ "credentials", "--role-path", role_path, "--inventory-file", inventory_file, "--vault-password-file", vault_pw_file, "--snippet", "--variant", "2", ] main() self.assertEqual(captured.get("variant"), 2) def test_main_variant_defaults_to_none(self): with tempfile.TemporaryDirectory() as tmpdir: role_path = str(Path(tmpdir) / "role") Path(str(Path(role_path) / "meta")).mkdir(parents=True) Path(str(Path(role_path) / "vars")).mkdir(parents=True) dump_yaml( str(Path(role_path) / ROLE_FILE_VARS_MAIN), {"application_id": "app_test"}, ) dump_yaml(str(Path(role_path) / ROLE_FILE_META_SERVICES), {}) dump_yaml(str(Path(role_path) / ROLE_FILE_META_SECRETS), {}) inventory_file = str(Path(tmpdir) / "inventory.yml") dump_yaml(inventory_file, {}) vault_pw_file = str(Path(tmpdir) / "pw.txt") with Path(vault_pw_file).open("w") as f: f.write("pw") captured: dict = {} class FakeManager: def __init__(self, **kwargs): captured.update(kwargs) self.app_id = "app_test" self.vault_handler = None self.schema = {} self.inventory = {} def apply_schema(self): return {"applications": {}} with unittest.mock.patch( "cli.administration.inventory.credentials.__main__.InventoryManager", FakeManager, ): sys.argv = [ "credentials", "--role-path", role_path, "--inventory-file", inventory_file, "--vault-password-file", vault_pw_file, "--snippet", ] main() self.assertIsNone(captured.get("variant")) def test_nested_credentials_dict_recurses_instead_of_stringifying(self): """Regression for run 26428080957 jobs 77797371397 / 77797371442. A `meta/secrets.yml` `credentials..{leaf,...}` nested block must materialise as a nested CommentedMap with one vault block per leaf — NOT collapsed via ``str(dict)`` into the Python-repr blob ``"{'key': '...', 'secret': '...'}"`` that the runtime `config` lookup choked on. """ with tempfile.TemporaryDirectory() as tmpdir: role_path = str(Path(tmpdir) / "role") Path(str(Path(role_path) / "meta")).mkdir(parents=True) Path(str(Path(role_path) / "vars")).mkdir(parents=True) dump_yaml( str(Path(role_path) / ROLE_FILE_VARS_MAIN), {"application_id": "app_nested"}, ) dump_yaml(str(Path(role_path) / ROLE_FILE_META_SERVICES), {}) schema = { "credentials": { "recaptcha": { "key": { "description": "key", "algorithm": "plain", "default": "{{ CAPTCHA.RECAPTCHA.KEY }}", }, "secret": { "description": "secret", "algorithm": "plain", "default": "{{ CAPTCHA.RECAPTCHA.SECRET }}", }, } } } dump_yaml(str(Path(role_path) / ROLE_FILE_META_SECRETS), schema) inventory_file = str(Path(tmpdir) / "inventory.yml") dump_yaml(inventory_file, {}) vault_pw_file = str(Path(tmpdir) / "pw.txt") with Path(vault_pw_file).open("w") as f: f.write("pw") fake_snippet = "!vault |\n $ANSIBLE_VAULT;1.1;AES256\n ENCRYPTEDVALUE" completed = subprocess.CompletedProcess( args=["ansible-vault"], returncode=0, stdout=fake_snippet, stderr="" ) with unittest.mock.patch("subprocess.run", return_value=completed): sys.argv = [ "credentials", "--role-path", role_path, "--inventory-file", inventory_file, "--vault-password-file", vault_pw_file, ] main() data = load_yaml_any(inventory_file) creds = data["applications"]["app_nested"]["secrets"]["credentials"] self.assertIn("recaptcha", creds) self.assertIsInstance( creds["recaptcha"], dict, f"`credentials.recaptcha` must be a nested mapping, " f"got {type(creds['recaptcha']).__name__}: {creds['recaptcha']!r}", ) for leaf in ("key", "secret"): self.assertIn(leaf, creds["recaptcha"]) self.assertIsInstance(creds["recaptcha"][leaf], str) self.assertTrue( creds["recaptcha"][leaf].lstrip().startswith("$ANSIBLE_VAULT") )