182 lines
9.7 KiB
Python
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")
|