Files
lemana-vpn/tests/configuration.py
T

182 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""Exercise installer rendering in temporary directories; never run installer main."""
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import tomllib
ROOT = Path(__file__).resolve().parents[1]
INSTALLER = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "install.sh"
SOURCE = INSTALLER.read_text().rsplit('\nmain "$@"', 1)[0]
KEYS = ("LEMANA_VPN_MODE", "LEMANA_VPN_USERNAME", "LEMANA_VPN_BW_ITEM", "LEMANA_VPN_USE_TOUCHID")
def setup(path):
env = {key: value for key, value in os.environ.items()
if not key.startswith(("LEMANA_VPN_", "OPENCONNECT_LITE_"))}
env.update(HOME=str(path), LEMANA_VPN_CONFIG_DIR=str(path / "settings"),
OPENCONNECT_LITE_CONFIG_DIR=str(path / "openconnect"),
TEST_STAGE=str(path / "stage"))
for directory in ("settings", "openconnect", "stage"):
(path / directory).mkdir()
return env
def render(env, *args):
command = SOURCE + '''
# Only configuration writes are exercised, with repository-local templates.
download_file() { cp "$TEST_ROOT/$1" "$2"; }
preserve_existing_username
install_config "$TEST_STAGE"
set -a
. "$CONFIG_DIR/env"
python3 - <<'PYRESULT'
import json, os
print("RESULT=" + json.dumps({key: os.environ[key] for key in (
"LEMANA_VPN_MODE", "LEMANA_VPN_USERNAME", "LEMANA_VPN_BW_ITEM", "LEMANA_VPN_USE_TOUCHID")}))
PYRESULT
'''
result = subprocess.run(["/bin/sh", "-c", command, str(ROOT / "install.sh"), *args],
env=dict(env, TEST_ROOT=str(ROOT)), capture_output=True, text=True, timeout=10)
assert result.returncode == 0, result.stderr or result.stdout
return json.loads(next(line[7:] for line in result.stdout.splitlines() if line.startswith("RESULT=")))
def mode_from_environment(path, env):
settings = render(dict(env, LEMANA_VPN_MODE="bitwarden-auto"))
assert settings["LEMANA_VPN_MODE"] == "bitwarden-auto", settings
def literal_settings(path, env):
sentinel = path / "must-not-exist"
special = 'Sales/VPN & "quotes" \\ $HOME `touch ' + str(sentinel) + '` $(touch ' + str(sentinel) + ') {{USERNAME}}'
settings = render(env, "--mode", "bitwarden-auto", "--username", special, "--bw-item", special)
assert settings["LEMANA_VPN_USERNAME"] == special, settings
assert settings["LEMANA_VPN_BW_ITEM"] == special, settings
assert not sentinel.exists(), "configuration executed a command substitution"
text = (path / "openconnect/config.toml").read_text()
username = next(line.split("=", 1)[1].strip() for line in text.splitlines() if line.startswith("username ="))
assert json.loads(username) == special, text
again = render(env)
assert again == settings, (settings, again)
assert not sentinel.exists()
def update_preserves_settings(path, env):
config = path / "settings/env"
custom = '# User runtime options\nLEMANA_VPN_LOG_MAX_BYTES="12345"\nLEMANA_VPN_CONNECT_WAIT_SECONDS="37"\n'
config.write_text('LEMANA_VPN_MODE="bitwarden-auto"\nLEMANA_VPN_USERNAME="saved-user"\nLEMANA_VPN_BW_ITEM="Saved/VPN & more" # custom item\nLEMANA_VPN_USE_TOUCHID="0"\n' + custom)
original_toml = ('# User SSO profile\n[default_profile]\naddress = "custom.example"\n'
'username = "leave-profile-field"\n\n[credentials] # saved login\nusername = "old-user"\n'
'\n[[auto_fill_rules."https://custom.example/*"]]\nselector = "#custom"\nfill = "username"\n')
(path / "openconnect/config.toml").write_text(original_toml)
settings = render(env)
assert settings == dict(zip(KEYS, ("bitwarden-auto", "saved-user", "Saved/VPN & more", "0"))), settings
assert custom.rstrip() in config.read_text()
assert (path / "openconnect/config.toml").read_text() == original_toml.replace('username = "old-user"', 'username = "saved-user"')
first = config.read_text()
assert render(env) == settings
assert config.read_text() == first, "a second update changed unowned settings"
assert config.stat().st_mode & 0o777 == 0o600
assert (path / "openconnect/config.toml").stat().st_mode & 0o777 == 0o600
def local_auto_migration(path, env):
config = path / "settings/env"
legacy = 'LEMANA_VPN_MODE="manual-keychain"\nLEMANA_VPN_LOCAL_OTP="1"\nLEMANA_VPN_USERNAME="saved"\n'
config.write_text(legacy)
assert render(env)["LEMANA_VPN_MODE"] == "keychain-auto"
assert 'LEMANA_VPN_AUTO_SOURCE="keychain"' in config.read_text()
assert render(env)["LEMANA_VPN_MODE"] == "keychain-auto"
assert render(env, "--mode", "manual-keychain")["LEMANA_VPN_MODE"] == "manual-keychain"
assert 'LEMANA_VPN_LOCAL_OTP="0"' in config.read_text()
config.write_text(legacy)
assert render(dict(env, LEMANA_VPN_MODE="manual-keychain"))["LEMANA_VPN_MODE"] == "manual-keychain"
def precedence(path, env):
(path / "settings/env").write_text('LEMANA_VPN_MODE="manual-keychain"\nLEMANA_VPN_USERNAME="saved"\nLEMANA_VPN_BW_ITEM="saved"\nLEMANA_VPN_USE_TOUCHID="0"\n')
invocation = dict(env, LEMANA_VPN_MODE="bitwarden-auto", LEMANA_VPN_USERNAME="environment",
LEMANA_VPN_BW_ITEM="environment", LEMANA_VPN_USE_TOUCHID="1")
settings = render(invocation)
assert settings == dict(zip(KEYS, ("bitwarden-auto", "environment", "environment", "1"))), settings
settings = render(invocation, "--mode", "manual-keychain", "--username", "flag", "--bw-item", "flag", "--without-touchid")
assert settings == dict(zip(KEYS, ("manual-keychain", "flag", "flag", "0"))), settings
def legacy_environment_precedence(path, env):
for saved_auto in (False, True):
saved = ("bitwarden-auto", "auto", "bitwarden", "1") if saved_auto else ("manual-keychain", "saved-login", "keychain", "0")
overrides = (("CREDENTIAL_SOURCE", "keychain"), ("AUTH_PROFILE", "manual"), ("USE_BITWARDEN", "0")) if saved_auto else (("CREDENTIAL_SOURCE", "bitwarden"), ("AUTH_PROFILE", "auto"), ("USE_BITWARDEN", "1"))
for key, value in overrides:
(path / "settings/env").write_text("".join("LEMANA_VPN_" + name + "=" + json.dumps(setting) + "\n" for name, setting in zip(("MODE", "AUTH_PROFILE", "CREDENTIAL_SOURCE", "USE_BITWARDEN"), saved)))
settings = render(dict(env, **{"LEMANA_VPN_" + key: value}))
expected = "manual-keychain" if saved_auto else "bitwarden-auto"
assert settings["LEMANA_VPN_MODE"] == expected, (key, value, settings)
def safe_saved_read(path, env):
sentinel = path / "must-not-exist"
(path / "settings/env").write_text('LEMANA_VPN_MODE="manual-keychain"\nLEMANA_VPN_USERNAME="$(touch ' + str(sentinel) + ')"\n')
settings = render(env)
assert not sentinel.exists(), "reading saved settings executed shell code"
assert settings["LEMANA_VPN_USERNAME"] == '$(touch ' + str(sentinel) + ')'
def missing_credentials_section(path, env):
(path / "openconnect/config.toml").write_text('[default_profile]\nusername = "profile-user"\n')
render(env, "--username", "new-user")
text = (path / "openconnect/config.toml").read_text()
assert text == '[default_profile]\nusername = "profile-user"\n\n[credentials]\nusername = "new-user"\n', text
def empty_saved_username(path, env):
(path / "settings/env").write_text('LEMANA_VPN_USERNAME=""\n')
(path / "openconnect/config.toml").write_text('[credentials]\nusername = "toml-user"\n')
assert render(env)["LEMANA_VPN_USERNAME"] == "toml-user"
assert render(dict(env, LEMANA_VPN_USERNAME=""))["LEMANA_VPN_USERNAME"] == ""
def toml_scalar_forms(path, env):
for header, key in (("[ credentials ]", "username"), ('["credentials"]', '"username"'), ("[ 'credentials' ]", "'username'")):
(path / "settings/env").write_text('LEMANA_VPN_USERNAME=""\n')
original = header + ' # custom login\n' + key + ' = "toml-user"\n[default_profile]\naddress = "custom.example"\n'
(path / "openconnect/config.toml").write_text(original)
assert render(env)["LEMANA_VPN_USERNAME"] == "toml-user"
parsed = tomllib.loads((path / "openconnect/config.toml").read_text())
assert parsed == {"credentials": {"username": "toml-user"}, "default_profile": {"address": "custom.example"}}, parsed
render(env, "--username", "new-user")
parsed = tomllib.loads((path / "openconnect/config.toml").read_text())
assert parsed["credentials"]["username"] == "new-user", parsed
(path / "openconnect/config.toml").write_text('[ "credentials" ]')
render(env, "--username", "end-of-file")
assert tomllib.loads((path / "openconnect/config.toml").read_text()) == {"credentials": {"username": "end-of-file"}}
def fresh_help_without_python(path, env):
# Keep only the help command available; Python must not be needed yet.
(path / "stage/cat").symlink_to("/bin/cat")
result = subprocess.run(["/bin/sh", "-c", SOURCE, str(ROOT / "install.sh"), "--help"],
env=dict(env, PATH=str(path / "stage")), text=True, capture_output=True, timeout=10)
assert result.returncode == 0 and "Usage:" in result.stdout, result.stderr
failures = []
for check in (mode_from_environment, literal_settings, update_preserves_settings, local_auto_migration, precedence,
legacy_environment_precedence, safe_saved_read, missing_credentials_section,
empty_saved_username, toml_scalar_forms, fresh_help_without_python):
with tempfile.TemporaryDirectory(prefix="lemana-config-") as temporary:
path = Path(temporary)
try:
check(path, setup(path))
print("PASS", check.__name__)
except (AssertionError, subprocess.TimeoutExpired) as error:
failures.append(check.__name__)
print("FAIL", check.__name__, str(error))
if failures:
raise SystemExit(1)
print("configuration checks passed")