from __future__ import annotations import os import subprocess from pathlib import Path from fastapi.testclient import TestClient from karldns import DdnsSettings, make_app ROOT = Path(__file__).parents[1] def run_script(*args: str, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]: merged_env = os.environ.copy() if env: merged_env.update(env) return subprocess.run( args, check=True, cwd=ROOT, env=merged_env, text=True, capture_output=True, ) def test_shell_scripts_parse() -> None: for script in ("install-linux.sh", "karldns-update.sh", "uninstall-linux.sh"): run_script("sh", "-n", str(ROOT / "scripts" / script)) def test_updater_strips_fritzbox_placeholders_and_caches_ip(tmp_path: Path) -> None: app = make_app( DdnsSettings( database_path=tmp_path / "ddns.sqlite3", admin_password="admin", public_base_url="https://karldns.de", hostname_suffix="karldns.de", ) ) client = TestClient(app) account = client.post("/api/v1/hostnames").json() update_url = account["update_url"] script = tmp_path / "curl" log_file = tmp_path / "curl.log" cache_file = tmp_path / "last-ip" lock_file = tmp_path / "updater.lock" script.write_text( f"""#!/bin/sh printf '%s\\n' "$*" >> {log_file} case "$*" in *''*|*''*) printf 'bad placeholder leaked\\n'; exit 1 ;; *'myip=203.0.113.25'*) printf 'good 203.0.113.25\\n' ;; *) printf 'bad request\\n'; exit 1 ;; esac """, encoding="utf-8", ) script.chmod(0o700) result = run_script( "sh", str(ROOT / "scripts" / "karldns-update.sh"), env={ "KARLDNS_UPDATE_URL": update_url, "KARLDNS_CURL": str(script), "KARLDNS_CACHE_FILE": str(cache_file), "KARLDNS_LOCK_FILE": str(lock_file), "KARLDNS_IPV4": "203.0.113.25", "KARLDNS_IPV6": "off", }, ) assert "good 203.0.113.25" in result.stdout assert cache_file.read_text(encoding="utf-8").strip() == "203.0.113.25|" assert "" not in log_file.read_text(encoding="utf-8") def test_installer_registers_generated_hostname_before_use(tmp_path: Path) -> None: bin_dir = tmp_path / "bin" home_dir = tmp_path / "home" log_file = tmp_path / "curl.log" bin_dir.mkdir() home_dir.mkdir() curl = bin_dir / "curl" curl.write_text( f"""#!/bin/sh printf '%s\\n' "$*" >> {log_file} output_file="" previous="" for argument in "$@"; do if [ "$previous" = "-o" ]; then output_file="$argument" fi previous="$argument" done case "$*" in *'/karldns-update.sh'*) cat > "$output_file" <<'EOS' #!/bin/sh printf 'updater placeholder\\n' EOS ;; *'/api/v1/hostnames'*) printf '%s\\n' '{{"hostname":"abc.karldns.test","status":"pending","update_url":"https://karldns.test/api/v1/router/update/karl_update_secret_12345678901234567890?myip=&myipv6=","dashboard_url":"https://karldns.test/m/karl_manage_secret_12345678901234567890","registration_url":"https://karldns.test/api/v1/router/register/karl_manage_secret_12345678901234567890"}}' ;; *'/api/v1/router/register/karl_manage_secret_12345678901234567890'*) printf '%s\\n' 'good registered' ;; *) printf 'unexpected curl call: %s\\n' "$*" >&2 exit 1 ;; esac """, encoding="utf-8", ) curl.chmod(0o700) result = run_script( "sh", str(ROOT / "scripts" / "install-linux.sh"), "--mode", "user", "--no-cron", "--no-run", env={ "HOME": str(home_dir), "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", "KARLDNS_BASE_URL": "https://karldns.test", "KARLDNS_RAW_BASE_URL": "https://raw.karldns.test", }, ) log = log_file.read_text(encoding="utf-8") assert "/api/v1/hostnames" in log assert "/api/v1/router/register/karl_manage_secret_12345678901234567890" in log assert "KarlDNS: registering hostname" in result.stdout config = home_dir / ".config" / "karldns" / "updater.env" config_text = config.read_text(encoding="utf-8") assert ( "KARLDNS_UPDATE_URL='https://karldns.test/api/v1/router/update/karl_update_secret_12345678901234567890'" in config_text ) assert "KARLDNS_HOSTNAME='abc.karldns.test'" in config_text def test_user_installer_and_uninstaller_manage_cron_idempotently(tmp_path: Path) -> None: bin_dir = tmp_path / "bin" home_dir = tmp_path / "home" log_file = tmp_path / "curl.log" crontab_file = tmp_path / "crontab.txt" bin_dir.mkdir() home_dir.mkdir() curl = bin_dir / "curl" crontab = bin_dir / "crontab" curl.write_text( f"""#!/bin/sh printf '%s\\n' "$*" >> {log_file} output_file="" previous="" for argument in "$@"; do if [ "$previous" = "-o" ]; then output_file="$argument" fi previous="$argument" done case "$*" in *'/karldns-update.sh'*) cat > "$output_file" <<'EOS' #!/bin/sh printf 'updater placeholder\\n' EOS ;; *'/api/v1/hostnames'*) printf '%s\\n' '{{"hostname":"abc.karldns.test","status":"pending","update_url":"https://karldns.test/api/v1/router/update/karl_update_secret_12345678901234567890?myip=&myipv6=","dashboard_url":"https://karldns.test/m/karl_manage_secret_12345678901234567890","registration_url":"https://karldns.test/api/v1/router/register/karl_manage_secret_12345678901234567890"}}' ;; *'/api/v1/router/register/karl_manage_secret_12345678901234567890'*) printf '%s\\n' 'good registered' ;; *) printf 'unexpected curl call: %s\\n' "$*" >&2 exit 1 ;; esac """, encoding="utf-8", ) crontab.write_text( f"""#!/bin/sh if [ "$1" = "-l" ]; then cat {crontab_file} 2>/dev/null || exit 1 exit 0 fi cp "$1" {crontab_file} """, encoding="utf-8", ) curl.chmod(0o700) crontab.chmod(0o700) env = { "HOME": str(home_dir), "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", "KARLDNS_BASE_URL": "https://karldns.test", "KARLDNS_RAW_BASE_URL": "https://raw.karldns.test", } for _ in range(2): run_script( "sh", str(ROOT / "scripts" / "install-linux.sh"), "--mode", "user", "--no-run", env=env, ) cron_text = crontab_file.read_text(encoding="utf-8") log = log_file.read_text(encoding="utf-8") assert cron_text.count("# karldns-update start") == 1 assert "KARLDNS_CONFIG=" in cron_text assert "karldns-update" in cron_text assert log.count("/api/v1/hostnames") == 1 assert log.count("/api/v1/router/register/karl_manage_secret_12345678901234567890") == 1 assert (home_dir / ".local" / "bin" / "karldns-update").exists() run_script("sh", str(ROOT / "scripts" / "uninstall-linux.sh"), "--mode", "user", env=env) assert "# karldns-update start" not in crontab_file.read_text(encoding="utf-8") assert not (home_dir / ".local" / "bin" / "karldns-update").exists() assert (home_dir / ".config" / "karldns" / "updater.env").exists() run_script("sh", str(ROOT / "scripts" / "uninstall-linux.sh"), "--mode", "user", "--delete-config", env=env) assert not (home_dir / ".config" / "karldns").exists() assert not (home_dir / ".cache" / "karldns").exists() def test_hosted_script_routes_redirect_to_github(tmp_path: Path) -> None: app = make_app(DdnsSettings(database_path=tmp_path / "ddns.sqlite3", admin_password="admin")) client = TestClient(app) for path, target in { "/install.sh": "install-linux.sh", "/uninstall.sh": "uninstall-linux.sh", "/karldns-update.sh": "karldns-update.sh", }.items(): response = client.get(path, follow_redirects=False) assert response.status_code == 302 assert response.headers["location"].endswith(f"/scripts/{target}")