"""Unit tests for :mod:`utils.env.writer`. Covers value quoting (bare-safe vs quoted-with-escapes) and the end-to-end ``write_dotenv`` shape (header, sorted keys, comment-above pattern, multi-line escape). """ from __future__ import annotations import unittest from pathlib import Path from tempfile import TemporaryDirectory from utils.cache.files import read_text from utils.env.builder import EnvBuilder from utils.env.writer import quote_for_dotenv, write_dotenv class TestQuoteForDotenv(unittest.TestCase): def test_safe_value_emitted_bare(self) -> None: self.assertEqual(quote_for_dotenv("plain"), "plain") self.assertEqual(quote_for_dotenv("a/b.c"), "a/b.c") self.assertEqual(quote_for_dotenv("v1.2.3"), "v1.2.3") self.assertEqual(quote_for_dotenv("user@host"), "user@host") self.assertEqual(quote_for_dotenv("a-b_c+d=e"), "a-b_c+d=e") self.assertEqual(quote_for_dotenv("a:b%c"), "a:b%c") def test_empty_string_is_quoted(self) -> None: self.assertEqual(quote_for_dotenv(""), '""') def test_value_with_space_is_quoted(self) -> None: self.assertEqual(quote_for_dotenv("hello world"), '"hello world"') def test_double_quote_is_escaped(self) -> None: self.assertEqual(quote_for_dotenv('a"b'), '"a\\"b"') def test_backslash_is_escaped_first(self) -> None: self.assertEqual(quote_for_dotenv("a\\b"), '"a\\\\b"') def test_backslash_then_quote(self) -> None: self.assertEqual(quote_for_dotenv('a\\"b'), '"a\\\\\\"b"') def test_newline_raises(self) -> None: with self.assertRaises(ValueError): quote_for_dotenv("line1\nline2") def test_carriage_return_raises(self) -> None: with self.assertRaises(ValueError): quote_for_dotenv("a\rb") def test_unicode_passes_through_quoted(self) -> None: self.assertEqual(quote_for_dotenv("naïve"), '"naïve"') class TestWriteDotenv(unittest.TestCase): def _build( self, values: dict[str, str], comments: dict[str, str] | None = None ) -> EnvBuilder: eb = EnvBuilder() eb.values = values eb.comments = comments or {} return eb def test_writes_header(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" write_dotenv(self._build({}), dest) text = read_text(str(dest)) self.assertIn("Generated by `make dotenv`", text) def test_sorts_keys_alphabetically(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" write_dotenv(self._build({"Z": "1", "A": "1", "M": "1"}), dest) text = read_text(str(dest)) self.assertLess(text.index("A=1"), text.index("M=1")) self.assertLess(text.index("M=1"), text.index("Z=1")) def test_comment_emitted_above_key(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" write_dotenv( self._build({"K": "v"}, {"K": "my doc"}), dest, ) text = read_text(str(dest)) i = text.index("K=") self.assertIn("# my doc", text[:i]) def test_no_comment_emitted_when_blank(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" write_dotenv(self._build({"K": "v"}, {"K": " "}), dest) text = read_text(str(dest)) self.assertNotIn("# ", text.split("K=")[0].split("\n")[-2]) def test_blank_line_between_entries(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" write_dotenv(self._build({"A": "1", "B": "2"}), dest) text = read_text(str(dest)) self.assertRegex(text, r"\nA=1\n") self.assertRegex(text, r"\nB=2\n") def test_safe_value_emitted_bare(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" write_dotenv(self._build({"K": "plain"}), dest) text = read_text(str(dest)) self.assertIn("K=plain", text) self.assertNotIn('K="plain"', text) def test_unsafe_value_quoted(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" write_dotenv(self._build({"K": "with space"}), dest) text = read_text(str(dest)) self.assertIn('K="with space"', text) def test_multiline_value_raises(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" with self.assertRaises(ValueError): write_dotenv(self._build({"K": "a\nb"}), dest) def test_trailing_newline(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" write_dotenv(self._build({"K": "v"}), dest) text = read_text(str(dest)) self.assertTrue(text.endswith("\n")) def test_empty_builder_writes_header_only(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" write_dotenv(self._build({}), dest) text = read_text(str(dest)) self.assertEqual(text.count("\n"), 3) for line in text.splitlines(): self.assertTrue(line.startswith("#")) def test_keys_with_and_without_comment_mixed(self) -> None: with TemporaryDirectory() as td: dest = Path(td) / ".env" write_dotenv( self._build( {"A": "1", "B": "2"}, {"A": "doc-a"}, ), dest, ) text = read_text(str(dest)) self.assertIn("# doc-a\nA=1", text) b_block = text[text.index("B=") - 60 : text.index("B=")] self.assertNotIn( "# ", b_block.rsplit("\n", 2)[-2] if "\n" in b_block else "" ) if __name__ == "__main__": unittest.main()