"""Unit tests for devx.utils.vault.""" from __future__ import annotations from pathlib import Path from unittest.mock import MagicMock, patch from devx.utils.vault import ( decrypt_file, encrypt_file, is_encrypted, load_vault_yaml, save_vault_yaml, ) class TestIsEncrypted: def test_encrypted_file(self, tmp_path: Path) -> None: f = tmp_path / "secret.yml" f.write_text("$ANSIBLE_VAULT;1.1;AES256\n9382928...\n") assert is_encrypted(f) is True def test_plain_file(self, tmp_path: Path) -> None: f = tmp_path / "plain.yml" f.write_text("key: value\n") assert is_encrypted(f) is False class TestLoadVaultYaml: def test_plain_yaml_no_vault_pass(self, tmp_path: Path) -> None: f = tmp_path / "data.yml" f.write_text("key: value\nlist:\n - a\n - b\n") data = load_vault_yaml(f) assert data == {"key": "value", "list": ["a", "b"]} def test_empty_file(self, tmp_path: Path) -> None: f = tmp_path / "empty.yml" f.write_text("") data = load_vault_yaml(f) assert data == {} def test_vault_pass_not_exists(self, tmp_path: Path) -> None: f = tmp_path / "data.yml" f.write_text("key: value\n") data = load_vault_yaml(f, vault_pass=tmp_path / "nonexistent") assert data == {"key": "value"} @patch("devx.utils.vault.subprocess.run") def test_encrypted_file_success(self, mock_run: MagicMock, tmp_path: Path) -> None: f = tmp_path / "secret.yml" f.write_text("$ANSIBLE_VAULT\n...") vp = tmp_path / "vault-password" vp.write_text("secret") mock_run.return_value = MagicMock(returncode=0, stdout="key: decrypted\n", stderr="") data = load_vault_yaml(f, vault_pass=vp) assert data == {"key": "decrypted"} @patch("devx.utils.vault.subprocess.run") def test_not_vault_encrypted_fallback(self, mock_run: MagicMock, tmp_path: Path) -> None: f = tmp_path / "plain.yml" f.write_text("key: value\n") vp = tmp_path / "vault-password" vp.write_text("secret") mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="is not vault encrypted") data = load_vault_yaml(f, vault_pass=vp) assert data == {"key": "value"} class TestSaveVaultYaml: def test_save_plain(self, tmp_path: Path) -> None: f = tmp_path / "output.yml" save_vault_yaml(f, {"key": "value"}) content = f.read_text() assert "key: value" in content def test_save_with_vault_pass_not_exists(self, tmp_path: Path) -> None: f = tmp_path / "output.yml" vp = tmp_path / "nonexistent" save_vault_yaml(f, {"key": "value"}, vault_pass=vp) # Should save as plain YAML content = f.read_text() assert "key: value" in content assert "$ANSIBLE_VAULT" not in content @patch("devx.utils.vault.subprocess.run") def test_save_and_encrypt(self, mock_run: MagicMock, tmp_path: Path) -> None: f = tmp_path / "output.yml" vp = tmp_path / "vault-password" vp.write_text("secret") save_vault_yaml(f, {"key": "value"}, vault_pass=vp) # File should be written assert f.exists() # ansible-vault encrypt should be called mock_run.assert_called_once() cmd = mock_run.call_args[0][0] assert "ansible-vault" in cmd assert "encrypt" in cmd class TestEncryptFile: @patch("devx.utils.vault.subprocess.run") def test_calls_ansible_vault(self, mock_run: MagicMock, tmp_path: Path) -> None: f = tmp_path / "file.yml" f.write_text("key: value") vp = tmp_path / "vault-password" vp.write_text("secret") encrypt_file(f, vp) mock_run.assert_called_once() cmd = mock_run.call_args[0][0] assert "ansible-vault" in cmd assert "encrypt" in cmd assert str(f) in cmd assert str(vp) in cmd class TestDecryptFile: @patch("devx.utils.vault.subprocess.run") def test_calls_ansible_vault(self, mock_run: MagicMock, tmp_path: Path) -> None: f = tmp_path / "file.yml" f.write_text("$ANSIBLE_VAULT\n...") vp = tmp_path / "vault-password" vp.write_text("secret") decrypt_file(f, vp) mock_run.assert_called_once() cmd = mock_run.call_args[0][0] assert "ansible-vault" in cmd assert "decrypt" in cmd assert str(f) in cmd assert str(vp) in cmd