Files
computer-playbook/tests/unit/utils/env/test_writer.py
T
kevinveenbirkenbachandClaude Opus 4.7 a3782bf8d3 fix(env): break NIX_CONFIG bootstrap-loop
Two independent bugs were compounding into a corrupted NIX_CONFIG that
broke every local image rebuild:

1. scripts/tests/code/run.sh non-idempotently appended
   "accept-flake-config = true" to whatever NIX_CONFIG it inherited.
   On the next source of .env (which carried the previous value back),
   the line accumulated -- "true\naccept-flake-config = true\n..." --
   until Nix rejected the Boolean. Fix: pipe through `awk '\!seen[$0]++'`
   to dedup lines while preserving order.

2. utils/env/writer.py silently re-encoded literal newlines as `\n`
   escape sequences inside double-quoted values. docker-compose's
   --env-file loader does not decode them and neither does bash
   `source`, so any multi-line value round-tripped into garbage on the
   next read. Fix: raise ValueError instead -- producers must collapse
   to a single line before handing off to the env-builder.

Test fixtures for the writer adjusted to assert the new raise
semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 03:10:14 +02:00

166 lines
6.1 KiB
Python

"""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:
# Backslash escaping must precede other escapes to avoid double-coding.
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))
# Each key block is preceded by a blank line.
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))
# Only the 3 header comment lines + trailing newline.
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 has no comment block above it.
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()