"""Node credential repair must be targeted, idempotent and silent about secrets.""" import importlib.util from pathlib import Path import unittest from unittest.mock import patch, Mock spec = importlib.util.spec_from_file_location( "node_admin_access", Path(__file__).resolve().parents[2] / "scripts/node_admin_access.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) class NodeAccessTests(unittest.TestCase): def setUp(self): self.payload = {"hostname": "titan-test", "passwords": {"atlas": "synthetic-only"}} self.root = patch.object(module.os, "geteuid", return_value=0) self.host = patch.object(module.socket, "gethostname", return_value="titan-test") self.account = patch.object(module.pwd, "getpwnam") for item in [self.root, self.host, self.account]: item.start() self.addCleanup(item.stop) def test_wrong_host_never_changes_password(self): with patch.object(module.subprocess, "run") as run: self.payload["hostname"] = "wrong-node" with self.assertRaisesRegex(ValueError, "hostname_mismatch"): module.run(self.payload, True) run.assert_not_called() def test_validate_all_accounts_before_mutation(self): with patch.object(module.subprocess, "run") as run: self.payload["passwords"]["other"] = "synthetic" with self.assertRaisesRegex(ValueError, "account_not_allowed"): module.run(self.payload, True) run.assert_not_called() def test_password_record_injection_rejected(self): self.payload["passwords"]["atlas"] = "bad\nroot:injected" with self.assertRaisesRegex(ValueError, "invalid_password"): module.run(self.payload, True) def test_audit_is_read_only(self): with patch.object(module, "password_status", return_value={"vault_password_matches": False}), \ patch.object(module.subprocess, "run") as run: result = module.run(self.payload) run.assert_not_called() self.assertEqual(result["changed_accounts"], []) def test_matching_password_is_unchanged(self): with patch.object(module, "password_status", return_value={"vault_password_matches": True}), \ patch.object(module.subprocess, "run") as run: module.run(self.payload, True) run.assert_not_called() def test_restore_uses_stdin_and_returns_no_secret(self): with patch.object(module, "password_status", side_effect=[ {"vault_password_matches": False, "locked": True}, {"vault_password_matches": True, "locked": False}]), \ patch.object(module.subprocess, "run", return_value=Mock(returncode=0)) as run: result = module.run(self.payload, True) self.assertEqual(run.call_args.args[0], ["/usr/sbin/chpasswd"]) self.assertEqual(run.call_args.kwargs["input"], "atlas:synthetic-only\n") self.assertNotIn("synthetic-only", str(result)) self.assertEqual(result["changed_accounts"], ["atlas"]) self.assertTrue(result["after"]["atlas"]["vault_password_matches"]) def test_failed_update_does_not_echo_subprocess(self): with patch.object(module, "password_status", return_value={"vault_password_matches": False}), \ patch.object(module.subprocess, "run", return_value=Mock(returncode=1, stderr="SECRET")): with self.assertRaisesRegex(ValueError, "^password_update_failed$"): module.run(self.payload, True) if __name__ == "__main__": unittest.main()