82 lines
2.7 KiB
Python
82 lines
2.7 KiB
Python
"""Secrets may come from a variable or a mounted file, never ambiguously."""
|
|||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from dsc.errors import ConfigError
|
||
|
|
from dsc.secrets import read_secret_file, resolve_secret
|
||
|
|
|
||
|
|
|
||
|
|
def write(tmp_path, content, name="secret"):
|
||
|
|
path = tmp_path / name
|
||
|
|
path.write_text(content)
|
||
|
|
return str(path)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("content, expected", [
|
||
|
|
("s3cret", "s3cret"),
|
||
|
|
# Secret files are usually written with a trailing newline; a password
|
||
|
|
# that silently includes it fails to authenticate with no useful message.
|
||
|
|
("s3cret\n", "s3cret"),
|
||
|
|
("s3cret\r\n", "s3cret"),
|
||
|
|
("s3cret\n\n", "s3cret"),
|
||
|
|
# Everything else is part of the secret, including inner and edge spaces.
|
||
|
|
(" s3cret ", " s3cret "),
|
||
|
|
("pa ss", "pa ss"),
|
||
|
|
('pa"ss$(id)`x`', 'pa"ss$(id)`x`'),
|
||
|
|
("паssшörд\n", "паssшörд"),
|
||
|
|
])
|
||
|
|
def test_read_secret_file_strips_only_trailing_newlines(tmp_path, content, expected):
|
||
|
|
assert read_secret_file(write(tmp_path, content)) == expected
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("content", ["", "\n", "\r\n", "\n\n"])
|
||
|
|
def test_read_secret_file_rejects_an_empty_secret(tmp_path, content):
|
||
|
|
with pytest.raises(ConfigError):
|
||
|
|
read_secret_file(write(tmp_path, content))
|
||
|
|
|
||
|
|
|
||
|
|
def test_read_secret_file_reports_a_missing_file(tmp_path):
|
||
|
|
missing = str(tmp_path / "absent")
|
||
|
|
with pytest.raises(ConfigError) as err:
|
||
|
|
read_secret_file(missing)
|
||
|
|
assert missing in str(err.value)
|
||
|
|
|
||
|
|
|
||
|
|
def test_read_secret_file_never_puts_the_secret_in_the_error(tmp_path):
|
||
|
|
path = write(tmp_path, "")
|
||
|
|
with pytest.raises(ConfigError) as err:
|
||
|
|
read_secret_file(path)
|
||
|
|
assert "empty" in str(err.value).lower()
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_secret_prefers_nothing_when_neither_is_set():
|
||
|
|
assert resolve_secret("PASSWORD", None, None) is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_secret_reads_the_variable():
|
||
|
|
assert resolve_secret("PASSWORD", "s3cret", None) == "s3cret"
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_secret_reads_the_file(tmp_path):
|
||
|
|
path = write(tmp_path, "s3cret\n")
|
||
|
|
assert resolve_secret("PASSWORD", None, path) == "s3cret"
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_secret_rejects_two_sources_for_one_secret(tmp_path):
|
||
|
|
path = write(tmp_path, "s3cret")
|
||
|
|
with pytest.raises(ConfigError) as err:
|
||
|
|
resolve_secret("PASSWORD", "other", path)
|
||
|
|
assert "PASSWORD" in str(err.value)
|
||
|
|
assert "PASSWORD_FILE" in str(err.value)
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_secret_conflict_message_hides_both_values(tmp_path):
|
||
|
|
path = write(tmp_path, "file-secret")
|
||
|
|
with pytest.raises(ConfigError) as err:
|
||
|
|
resolve_secret("PASSWORD", "env-secret", path)
|
||
|
|
assert "env-secret" not in str(err.value)
|
||
|
|
assert "file-secret" not in str(err.value)
|
||
|
|
|
||
|
|
|
||
|
|
def test_resolve_secret_treats_an_empty_variable_as_unset():
|
||
|
|
assert resolve_secret("PASSWORD", "", None) is None
|