145 lines
7.0 KiB
Python
145 lines
7.0 KiB
Python
"""Tests for how update_configs() applies a config's env/comment/uncomment keys to a .env file."""
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
from dotenv import dotenv_values
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from alakazam import format_env_value, set_extending_key, update_configs
|
|
|
|
|
|
class TestUnknownConfigKeys:
|
|
def test_an_unknown_key_never_reaches_the_env_file(self, tmp_path):
|
|
"""A promotion mechanism sets 'hold: <reason>' on a recipe, which must pass through untouched."""
|
|
env = tmp_path / "app.env"
|
|
env.write_text("#SMTP_HOST=mail.example.com\n")
|
|
update_configs(env, {"hold": "waiting for the upstream fix", "env": {"SMTP_HOST": "mail.a.org"}})
|
|
content = env.read_text()
|
|
assert "SMTP_HOST=mail.a.org" in content
|
|
assert "hold" not in content
|
|
assert "waiting for the upstream fix" not in content
|
|
|
|
def test_an_unknown_key_alone_changes_nothing(self, tmp_path):
|
|
env = tmp_path / "app.env"
|
|
env.write_text("#SMTP_HOST=mail.example.com\n")
|
|
update_configs(env, {"hold": "waiting for the upstream fix"})
|
|
assert env.read_text() == "#SMTP_HOST=mail.example.com\n"
|
|
|
|
|
|
class TestExtendingEnvKeys:
|
|
"""
|
|
Some env keys are repeated on multiple lines, relying on bash accumulation
|
|
("COMPOSE_FILE=\"$COMPOSE_FILE:compose.mariadb.yml\"") to build up a value.
|
|
A config that sets such a key should extend that accumulation, not replace
|
|
or uncomment every line sharing the key.
|
|
"""
|
|
|
|
ACCUMULATING_ENV = (
|
|
'COMPOSE_FILE="compose.yml"\n'
|
|
'COMPOSE_FILE="$COMPOSE_FILE:compose.mariadb.yml"\n'
|
|
'# COMPOSE_FILE="$COMPOSE_FILE:compose.onlyoffice.yml"\n'
|
|
)
|
|
|
|
def test_a_key_sharing_a_prefix_is_not_matched_by_substring(self, tmp_path):
|
|
"""COMPOSE must not be treated as matching lines whose key is COMPOSE_FILE."""
|
|
env = tmp_path / "app.env"
|
|
env.write_text(self.ACCUMULATING_ENV)
|
|
update_configs(env, {"env": {"COMPOSE": "compose.custom.yml"}}) # not self-referencing
|
|
lines = env.read_text().splitlines()
|
|
assert lines[:-1] == self.ACCUMULATING_ENV.splitlines() # every original line untouched
|
|
assert lines[-1] == 'COMPOSE=compose.custom.yml' # set as its own, unrelated key
|
|
|
|
def test_a_self_referencing_value_is_appended_without_touching_other_lines(self, tmp_path):
|
|
env = tmp_path / "app.env"
|
|
env.write_text(self.ACCUMULATING_ENV)
|
|
update_configs(env, {"env": {"COMPOSE_FILE": "$COMPOSE_FILE:../../customoverride.yml"}})
|
|
lines = env.read_text().splitlines()
|
|
assert lines[:-1] == self.ACCUMULATING_ENV.splitlines() # every original line untouched
|
|
assert lines[-1] == 'COMPOSE_FILE="$COMPOSE_FILE:../../customoverride.yml"' # appended
|
|
|
|
def test_setting_the_same_self_referencing_value_twice_is_idempotent(self, tmp_path):
|
|
env = tmp_path / "app.env"
|
|
env.write_text(self.ACCUMULATING_ENV)
|
|
config = {"env": {"COMPOSE_FILE": "$COMPOSE_FILE:../../customoverride.yml"}}
|
|
update_configs(env, config)
|
|
update_configs(env, config)
|
|
lines = env.read_text().splitlines()
|
|
assert lines.count('COMPOSE_FILE="$COMPOSE_FILE:../../customoverride.yml"') == 1
|
|
|
|
def test_a_self_referencing_value_matching_an_existing_commented_line_uncomments_it_in_place(self, tmp_path):
|
|
env = tmp_path / "app.env"
|
|
env.write_text(self.ACCUMULATING_ENV)
|
|
update_configs(env, {"env": {"COMPOSE_FILE": "$COMPOSE_FILE:compose.onlyoffice.yml"}})
|
|
lines = env.read_text().splitlines()
|
|
assert lines[:-1] == self.ACCUMULATING_ENV.splitlines()[:-1] # untouched, incl. the already-active line
|
|
assert lines[-1] == 'COMPOSE_FILE="$COMPOSE_FILE:compose.onlyoffice.yml"' # uncommented in place
|
|
assert len(lines) == len(self.ACCUMULATING_ENV.splitlines()) # no new line appended
|
|
|
|
|
|
class TestQuotedEnvValues:
|
|
"""
|
|
A value keeps the quotes of its YAML scalar, and jinja substitutes globals that carry quotes of
|
|
their own. Quotes of the same style then end the line early where it is read back, so which
|
|
style a configuration happens to use must not change the value that arrives in the .env file.
|
|
"""
|
|
|
|
def test_both_quoting_styles_produce_the_same_line(self, tmp_path):
|
|
env = tmp_path / "app.env"
|
|
env.write_text("")
|
|
update_configs(env, {"env": {
|
|
"DOUBLE": '"["turn:a.org:443?transport=tcp"]"',
|
|
"SINGLE": '\'["turn:a.org:443?transport=tcp"]\'',
|
|
}})
|
|
assert env.read_text().splitlines() == [
|
|
'DOUBLE=\'["turn:a.org:443?transport=tcp"]\'',
|
|
'SINGLE=\'["turn:a.org:443?transport=tcp"]\'',
|
|
]
|
|
|
|
def test_an_inner_quote_does_not_end_the_line(self, tmp_path):
|
|
"""Otherwise the parser stops after "[" and reads the rest of the line as further keys."""
|
|
env = tmp_path / "app.env"
|
|
env.write_text("")
|
|
update_configs(env, {"env": {"TURN_URIS": '"["turn:a.org:443"]"'}})
|
|
assert dotenv_values(env) == {"TURN_URIS": '["turn:a.org:443"]'}
|
|
|
|
def test_the_other_style_is_preferred_over_escaping(self):
|
|
"""It needs no escape at all, so it survives a reader that resolves none."""
|
|
assert format_env_value('"["a"]"') == '\'["a"]\''
|
|
assert format_env_value("'it's here'") == '"it\'s here"'
|
|
|
|
def test_a_value_that_expands_keeps_its_style_and_escapes(self):
|
|
"""Single quotes would stop abra from expanding the "$", which changes what the app gets."""
|
|
assert format_env_value('"a "b" $VAR"') == '"a \\"b\\" $VAR"'
|
|
|
|
def test_an_already_escaped_quote_is_not_escaped_again(self):
|
|
assert format_env_value(r'"a \" b"') == r'"a \" b"'
|
|
|
|
def test_a_value_holding_both_styles_is_reported(self, caplog):
|
|
"""Neither style can hold it, single quotes have no escape in abra's parser."""
|
|
with caplog.at_level(logging.WARNING):
|
|
format_env_value('\'it\'s "quoted"\'')
|
|
assert "single quote" in caplog.text
|
|
|
|
def test_a_value_without_quotes_is_written_unchanged(self):
|
|
assert format_env_value("plain value") == "plain value"
|
|
|
|
def test_a_non_string_passes_through(self):
|
|
assert format_env_value(8080) == 8080
|
|
|
|
def test_a_quoted_extending_value_is_not_wrapped_twice(self, tmp_path):
|
|
"""Two layers of quotes read back as an empty string, which drops every compose file."""
|
|
env = tmp_path / "app.env"
|
|
env.write_text("")
|
|
set_extending_key(env, "COMPOSE_FILE", '"$COMPOSE_FILE:compose.mariadb.yml"')
|
|
assert dotenv_values(env)["COMPOSE_FILE"] == "$COMPOSE_FILE:compose.mariadb.yml"
|
|
|
|
def test_an_unquoted_extending_value_is_still_quoted_for_expansion(self, tmp_path):
|
|
"""The quotes have to be double ones, abra expands the "$" only inside those."""
|
|
env = tmp_path / "app.env"
|
|
env.write_text("")
|
|
set_extending_key(env, "COMPOSE_FILE", "$COMPOSE_FILE:compose.mariadb.yml")
|
|
assert env.read_text() == 'COMPOSE_FILE="$COMPOSE_FILE:compose.mariadb.yml"\n'
|