#!/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")