diff --git a/Justfile b/Justfile index 4b3f49a..4e01783 100644 --- a/Justfile +++ b/Justfile @@ -159,6 +159,9 @@ test: @echo "[test] running Python unit tests..." @python3 -m unittest discover -t . -s tests -p 'test_*.py' -v @echo "" + @echo "[test] running operator env audit unit tests..." + @python3 -m unittest tests.test_operator_env -v + @echo "" @echo "[test] running shell integration tests..." @bash tests/test_forge_auth_integration.sh @echo "" diff --git a/tests/test_operator_env.py b/tests/test_operator_env.py new file mode 100644 index 0000000..5a91c98 --- /dev/null +++ b/tests/test_operator_env.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import os +import stat +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +sys.path.insert(0, str(ROOT / "forge-stack-orchestrator" / "scripts")) + +import operator_env as oe # noqa: E402 + + +class UpdateEnvFileTests(unittest.TestCase): + def test_replaces_commented_placeholders_and_preserves_mode(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + env_path = Path(tmpdir) / ".env" + env_path.write_text( + "FORGE_STACK_DEVPI_PASSWORD=change-me\n" + "# FORGE_STACK_DEVPI_GITHUB_TOKEN=\n" + "FORGE_STACK_DEVPI_PYARMOR_DEVICE_ID=1\n", + encoding="utf-8", + ) + os.chmod(env_path, 0o600) + + oe.update_env_file( + env_path, + { + "FORGE_STACK_DEVPI_PASSWORD": "secret-pass", + "FORGE_STACK_DEVPI_GITHUB_TOKEN": "ghp_test", + }, + ) + + rendered = env_path.read_text(encoding="utf-8") + self.assertIn("FORGE_STACK_DEVPI_PASSWORD=secret-pass\n", rendered) + self.assertIn("FORGE_STACK_DEVPI_GITHUB_TOKEN=ghp_test\n", rendered) + self.assertEqual(stat.S_IMODE(env_path.stat().st_mode), 0o600) + + +class CollectUpdatesTests(unittest.TestCase): + def test_collects_password_github_and_pyarmor_when_missing(self) -> None: + env_values = { + oe.DEVPI_PASSWORD_KEY: "change-me", + oe.GITHUB_TOKEN_KEY: "", + oe.PYARMOR_ACTIVATION_KEY: "", + oe.PYARMOR_REGFILE_KEY: "", + oe.PYARMOR_DEVICE_KEY: "1", + } + with mock.patch.object(oe, "interactive", return_value=True), mock.patch.object( + oe, + "prompt_value", + side_effect=[ + "backend-pass", + "ghp_123", + "/secure/license-email.txt", + "/secure/pyarmor-reg.zip", + "7", + ], + ), mock.patch.object(oe, "prompt_bool", return_value=True): + updates = oe.collect_updates(env_values, created_env=False) + + self.assertEqual(updates[oe.DEVPI_PASSWORD_KEY], "backend-pass") + self.assertEqual(updates[oe.GITHUB_TOKEN_KEY], "ghp_123") + self.assertEqual(updates[oe.PYARMOR_ACTIVATION_KEY], "/secure/license-email.txt") + self.assertEqual(updates[oe.PYARMOR_REGFILE_KEY], "/secure/pyarmor-reg.zip") + self.assertEqual(updates[oe.PYARMOR_DEVICE_KEY], "7") + + def test_skips_pyarmor_when_operator_declines_release_host_mode(self) -> None: + env_values = { + oe.DEVPI_PASSWORD_KEY: "backend-pass", + oe.GITHUB_TOKEN_KEY: "", + oe.PYARMOR_ACTIVATION_KEY: "", + oe.PYARMOR_REGFILE_KEY: "", + oe.PYARMOR_DEVICE_KEY: "1", + } + with mock.patch.object(oe, "interactive", return_value=True), mock.patch.object( + oe, + "prompt_value", + side_effect=[""], + ), mock.patch.object(oe, "prompt_bool", return_value=False): + updates = oe.collect_updates(env_values, created_env=False) + + self.assertNotIn(oe.GITHUB_TOKEN_KEY, updates) + self.assertNotIn(oe.PYARMOR_ACTIVATION_KEY, updates) + self.assertNotIn(oe.PYARMOR_REGFILE_KEY, updates) + self.assertNotIn(oe.PYARMOR_DEVICE_KEY, updates) + + +if __name__ == "__main__": + unittest.main()