178 lines
8.0 KiB
Python
178 lines
8.0 KiB
Python
import importlib.util
|
|
import io
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
|
|
MODULE_PATH = Path(__file__).resolve().parents[1] / "scripts" / "env_config.py"
|
|
SPEC = importlib.util.spec_from_file_location("env_config", MODULE_PATH)
|
|
env_config = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(env_config)
|
|
|
|
|
|
class EnvironmentConfigTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.directory = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.directory.cleanup)
|
|
self.path = Path(self.directory.name) / ".env"
|
|
|
|
def load(self, contents, current=None):
|
|
self.path.write_text(contents, encoding="utf-8")
|
|
return env_config.load_environment(self.path, current or {})
|
|
|
|
def test_current_environment_wins_including_empty_values(self):
|
|
current = {"VALUE": "from-environment", "EMPTY": "", "EXTRA": "kept"}
|
|
result = self.load("VALUE=from-file\nEMPTY=from-file\nFILE_ONLY=yes", current)
|
|
self.assertEqual(
|
|
result,
|
|
{"VALUE": "from-environment", "EMPTY": "", "EXTRA": "kept", "FILE_ONLY": "yes"},
|
|
)
|
|
self.assertEqual(current, {"VALUE": "from-environment", "EMPTY": "", "EXTRA": "kept"})
|
|
self.assertIsNot(result, current)
|
|
|
|
def test_missing_file_returns_a_copy_of_current_environment(self):
|
|
current = {"VALUE": "present", "EMPTY": ""}
|
|
result = env_config.load_environment(self.path, current)
|
|
self.assertEqual(result, current)
|
|
self.assertIsNot(result, current)
|
|
|
|
def test_blank_lines_comments_export_and_quotes(self):
|
|
result = self.load(
|
|
"\n \t\n # comment\n FIRST=one\n\texport SECOND='two words'\n"
|
|
'THIRD="three words" \nEMPTY=\nEMPTY_QUOTED=\'\'\n'
|
|
)
|
|
self.assertEqual(
|
|
result,
|
|
{"FIRST": "one", "SECOND": "two words", "THIRD": "three words", "EMPTY": "", "EMPTY_QUOTED": ""},
|
|
)
|
|
|
|
def test_special_characters_remain_literal(self):
|
|
value = r"$NAME:${NAME}:#part=a=b:C:\folder\file\n"
|
|
self.assertEqual(self.load(f"VALUE={value}\n"), {"VALUE": value})
|
|
self.assertEqual(self.load(f"VALUE='{value}'\n"), {"VALUE": value})
|
|
self.assertEqual(self.load(f'VALUE="{value}"\n'), {"VALUE": value})
|
|
|
|
def test_unquoted_whitespace_and_inline_hash_are_preserved(self):
|
|
value = " value # still part of the value \t "
|
|
self.assertEqual(self.load(f"VALUE={value}\n"), {"VALUE": value})
|
|
|
|
def test_commands_are_not_executed(self):
|
|
marker = Path(self.directory.name) / "should-not-exist"
|
|
value = f"$(touch {marker}) `touch {marker}` $HOME ${{HOME}}"
|
|
self.assertEqual(self.load(f"VALUE={value}\n"), {"VALUE": value})
|
|
self.assertFalse(marker.exists())
|
|
|
|
def test_last_file_assignment_wins(self):
|
|
self.assertEqual(self.load("VALUE=first\nVALUE=last\n"), {"VALUE": "last"})
|
|
|
|
def test_crlf_line_endings(self):
|
|
self.path.write_bytes(b"FIRST=one\r\nSECOND=two\r\n")
|
|
self.assertEqual(env_config.load_environment(self.path, {}), {"FIRST": "one", "SECOND": "two"})
|
|
|
|
def test_invalid_syntax_only_reports_line_number(self):
|
|
for invalid in (
|
|
"secret-without-an-assignment",
|
|
"1INVALID=private-value",
|
|
"INVALID-KEY=private-value",
|
|
"INVALID KEY=private-value",
|
|
"=private-value",
|
|
"export VALUE",
|
|
"VALUE='private-value",
|
|
'VALUE="private-value',
|
|
"VALUE='",
|
|
'VALUE="',
|
|
"VALUE='private-value' trailing-text",
|
|
"VALUE=private\x00value",
|
|
"# private\x00value",
|
|
"VALUE=private\rvalue",
|
|
):
|
|
with self.subTest(invalid=invalid):
|
|
with self.assertRaises(ValueError) as caught:
|
|
self.load(f"VALID=ok\n{invalid}\n")
|
|
self.assertEqual(str(caught.exception), "Invalid .env syntax at line 2")
|
|
|
|
def test_invalid_encoding_only_reports_line_number(self):
|
|
self.path.write_bytes(b"VALID=ok\nVALUE=private\xffvalue\n")
|
|
with self.assertRaises(ValueError) as caught:
|
|
env_config.load_environment(self.path, {})
|
|
self.assertEqual(str(caught.exception), "Invalid .env syntax at line 2")
|
|
self.assertTrue(caught.exception.__suppress_context__)
|
|
|
|
def test_invalid_file_is_validated_even_when_current_has_the_key(self):
|
|
with self.assertRaisesRegex(ValueError, r"^Invalid \.env syntax at line 1$"):
|
|
self.load("VALUE='unterminated", {"VALUE": "current"})
|
|
|
|
|
|
class EnvironmentCommandTests(unittest.TestCase):
|
|
def test_command_executes_with_environment_and_literal_arguments(self):
|
|
current = {"EXISTING": "present", "EMPTY": ""}
|
|
environment = {**current, "FROM_FILE": "loaded"}
|
|
command = ["./mvnw", "test", "literal $HOME ; $(command) `command`"]
|
|
original_directory = Path.cwd()
|
|
with (
|
|
patch.dict(os.environ, current, clear=True),
|
|
patch.object(env_config, "load_environment", return_value=environment) as load,
|
|
patch.object(env_config.os, "execvpe") as execute,
|
|
):
|
|
self.assertEqual(env_config.main(command), 0)
|
|
load.assert_called_once_with(MODULE_PATH.parents[1] / ".env", current)
|
|
execute.assert_called_once_with(command[0], command, environment)
|
|
self.assertEqual(Path.cwd(), original_directory)
|
|
|
|
def test_help_and_missing_command_do_not_load_or_execute(self):
|
|
for arguments, status, stream in (([], 2, "stderr"), (["--help"], 0, "stdout"), (["-h"], 0, "stdout")):
|
|
with (
|
|
self.subTest(arguments=arguments),
|
|
patch.object(env_config, "load_environment") as load,
|
|
patch.object(env_config.os, "execvpe") as execute,
|
|
patch.object(env_config.sys, stream, new_callable=io.StringIO) as output,
|
|
):
|
|
self.assertEqual(env_config.main(arguments), status)
|
|
self.assertIn("COMMAND [ARGS...]", output.getvalue())
|
|
load.assert_not_called()
|
|
execute.assert_not_called()
|
|
|
|
def test_invalid_file_reports_only_safe_line_number(self):
|
|
with (
|
|
patch.object(env_config, "load_environment", side_effect=ValueError("Invalid .env syntax at line 7")),
|
|
patch.object(env_config.os, "execvpe") as execute,
|
|
patch.object(env_config.sys, "stderr", new_callable=io.StringIO) as output,
|
|
):
|
|
self.assertEqual(env_config.main(["command"]), 1)
|
|
self.assertEqual(output.getvalue(), "Invalid .env syntax at line 7\n")
|
|
execute.assert_not_called()
|
|
|
|
def test_unexpected_value_error_is_sanitized(self):
|
|
with (
|
|
patch.object(env_config, "load_environment", side_effect=ValueError("private-value")),
|
|
patch.object(env_config.sys, "stderr", new_callable=io.StringIO) as output,
|
|
):
|
|
self.assertEqual(env_config.main(["command"]), 1)
|
|
self.assertEqual(output.getvalue(), "Invalid .env syntax.\n")
|
|
|
|
def test_read_error_is_sanitized(self):
|
|
with (
|
|
patch.object(env_config, "load_environment", side_effect=OSError("private-value")),
|
|
patch.object(env_config.os, "execvpe") as execute,
|
|
patch.object(env_config.sys, "stderr", new_callable=io.StringIO) as output,
|
|
):
|
|
self.assertEqual(env_config.main(["command"]), 1)
|
|
self.assertEqual(output.getvalue(), "Unable to read .env.\n")
|
|
execute.assert_not_called()
|
|
|
|
def test_execution_error_is_sanitized(self):
|
|
with (
|
|
patch.object(env_config, "load_environment", return_value={}),
|
|
patch.object(env_config.os, "execvpe", side_effect=OSError("private-value")),
|
|
patch.object(env_config.sys, "stderr", new_callable=io.StringIO) as output,
|
|
):
|
|
self.assertEqual(env_config.main(["private-command", "private-value"]), 1)
|
|
self.assertEqual(output.getvalue(), "Unable to start command.\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|