#!/usr/bin/env bash # K-114: Unit-Tests für die Input-Validierung in lib/build.func. # Läuft ohne PVE (sourct nur die Helfer). Aufruf: bash tests/test_validation.sh set -u REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=/dev/null source "$REPO_ROOT/lib/build.func" PASS=0 FAIL=0 ok() { PASS=$((PASS + 1)); echo "ok - $1"; } nok() { FAIL=$((FAIL + 1)); echo "NOT OK - $1"; } assert_true() { # assert_true "desc" cmd args... local desc="$1"; shift if "$@"; then ok "$desc"; else nok "$desc"; fi } assert_false() { local desc="$1"; shift if "$@"; then nok "$desc"; else ok "$desc"; fi } # ── Repro des Vorfalls vom 2026-06-11: VLAN-Tag mit unsichtbarem Byte ──────── JUNK_VLAN="$(printf '2\x800')" # "20" mit eingebettetem Non-UTF-8-Byte assert_false "VLAN-Repro: '2<0x80>0' wird abgelehnt (kein silent strip)" is_vlan_tag "$JUNK_VLAN" assert_false "is_clean_ascii erkennt eingebettetes Junk-Byte" is_clean_ascii "$JUNK_VLAN" # Re-Prompt-Verhalten: erste Eingabe ist der Junk-Wert, zweite ist sauber — # prompt_validated muss die zweite liefern (AC: Re-Prompt statt pct-Fehler). reprompt_result="$( printf '%s\n20\n' "$JUNK_VLAN" | { VLAN="" prompt_validated VLAN "VLAN tag (empty for none): " is_vlan_tag "" yes >/dev/null 2>&1 printf '%s' "$VLAN" } )" if [[ "$reprompt_result" == "20" ]]; then ok "prompt_validated re-promptet bei Junk und akzeptiert dann '20'" else nok "prompt_validated re-promptet bei Junk (got: '$reprompt_result')" fi # ── sanitize_input: CR + Rand-Whitespace weg, Inhalt unangetastet ───────────── [[ "$(sanitize_input $' 42\r\n')" == "42" ]] && ok "sanitize_input trimmt CR/Whitespace" \ || nok "sanitize_input trimmt CR/Whitespace" [[ "$(sanitize_input "$JUNK_VLAN")" == "$JUNK_VLAN" ]] && ok "sanitize_input strippt KEIN eingebettetes Junk (Validator soll es sehen)" \ || nok "sanitize_input lässt eingebettetes Junk unangetastet" # ── numerische Validatoren ──────────────────────────────────────────────────── assert_true "is_uint akzeptiert 8" is_uint "8" assert_false "is_uint lehnt '8 ' mit Junk ab" is_uint "$(printf '8\x01')" assert_false "is_uint lehnt 'abc' ab" is_uint "abc" assert_false "is_uint lehnt leeren Wert ab" is_uint "" assert_true "is_vlan_tag akzeptiert 20" is_vlan_tag "20" assert_false "is_vlan_tag lehnt 0 ab" is_vlan_tag "0" assert_false "is_vlan_tag lehnt 5000 ab" is_vlan_tag "5000" # ── Netz-Validatoren ────────────────────────────────────────────────────────── assert_true "is_ipv4 akzeptiert 10.11.20.66" is_ipv4 "10.11.20.66" assert_false "is_ipv4 lehnt 10.11.20.666 ab" is_ipv4 "10.11.20.666" assert_false "is_ipv4 lehnt 10.11.20 ab" is_ipv4 "10.11.20" assert_true "is_cidr akzeptiert 10.11.20.5/24" is_cidr "10.11.20.5/24" assert_false "is_cidr lehnt 10.11.20.5/33 ab" is_cidr "10.11.20.5/33" assert_false "is_cidr lehnt 10.11.20.5 ohne Maske" is_cidr "10.11.20.5" assert_true "is_ipcfg akzeptiert dhcp" is_ipcfg "dhcp" assert_true "is_ipcfg akzeptiert CIDR" is_ipcfg "192.168.0.7/24" assert_false "is_ipcfg lehnt 'static' ab" is_ipcfg "static" assert_true "is_ipv4_list akzeptiert Liste" is_ipv4_list "1.1.1.1, 8.8.8.8" assert_false "is_ipv4_list lehnt Hostnamen ab" is_ipv4_list "dns.local" # ── Namen/Token ─────────────────────────────────────────────────────────────── assert_true "is_hostname akzeptiert nexus-db" is_hostname "nexus-db" assert_false "is_hostname lehnt '-bad' ab" is_hostname "-bad" assert_false "is_hostname lehnt 'a b' ab" is_hostname "a b" assert_true "is_token akzeptiert local-lvm" is_token "local-lvm" assert_false "is_token lehnt 'a;b' ab" is_token "a;b" # ── require_valid: env-Werte werden sanitisiert + geprüft ──────────────────── CHECKVAL=$' 7\r' require_valid CHECKVAL is_uint "Testwert" && [[ "$CHECKVAL" == "7" ]] \ && ok "require_valid sanitisiert env-Wert (CR weg)" \ || nok "require_valid sanitisiert env-Wert" ( CHECKBAD="$JUNK_VLAN"; require_valid CHECKBAD is_uint "Testwert" ) >/dev/null 2>&1 \ && nok "require_valid bricht bei Junk-env-Wert ab" \ || ok "require_valid bricht bei Junk-env-Wert ab (exit != 0)" # ── Negativ-Beweis: build.func-Source-Check schlägt bei Präparat an ────────── TMPDIR_CT="$(mktemp -d)" cat >"$TMPDIR_CT/broken.sh" <<'EOF' #!/usr/bin/env bash # absichtlich ohne source build.func (Repro des zweimal aufgetretenen Bugs) APP="broken" prompt_lxc_config EOF if bash "$REPO_ROOT/tests/check_ct_source.sh" "$TMPDIR_CT" >/dev/null 2>&1; then nok "check_ct_source.sh erkennt fehlendes 'source build.func'" else ok "check_ct_source.sh erkennt fehlendes 'source build.func'" fi rm -rf "$TMPDIR_CT" # Positiv: das echte ct/-Verzeichnis ist sauber. if bash "$REPO_ROOT/tests/check_ct_source.sh" "$REPO_ROOT/ct" >/dev/null 2>&1; then ok "alle ct/*.sh sourcen build.func" else nok "alle ct/*.sh sourcen build.func" fi # ── Review-Findings K-114: Oktal, Newline, EOF, Nameref-Guard ──────────────── assert_true "is_vlan_tag akzeptiert '08' (kein Oktal-Fehler)" is_vlan_tag "08" assert_true "is_cidr akzeptiert /08 (kein Oktal-Fehler)" is_cidr "10.0.0.1/08" assert_false "is_clean_ascii lehnt eingebettetes Newline ab" is_clean_ascii $'1.1.1.1\n8.8.8.8' assert_false "is_ipv4_list lehnt Newline-Liste ab" is_ipv4_list $'1.1.1.1\n8.8.8.8' # EOF statt Eingabe: prompt_validated darf nicht endlos loopen. ( printf '' | { V=""; prompt_validated V "Wert: " is_uint; } ) >/dev/null 2>&1 rc=$? [[ "$rc" -ne 0 ]] && ok "prompt_validated bricht bei EOF ab (rc=$rc)" \ || nok "prompt_validated bricht bei EOF ab" # Reservierte Namen → Guard statt zirkulärem nameref. ( _pv_ref=""; prompt_validated _pv_ref "x: " is_uint ) >/dev/null 2>&1 [[ $? -eq 2 ]] && ok "prompt_validated weist reservierte Variablennamen ab" \ || nok "prompt_validated weist reservierte Variablennamen ab" # CTID-Re-Prompt: ungültig → erneut fragen, leer wäre auto (hier: gültige Zahl). ctid_out="$( printf 'abc\n123\n' | env -u CTID bash -c " source '$REPO_ROOT/lib/build.func' CTID='' while true; do read -rp 'Container ID [auto]: ' CTID || exit 1 CTID=\"\$(sanitize_input \"\$CTID\")\" [[ -z \"\$CTID\" ]] && exit 1 is_uint \"\$CTID\" && break done printf '%s' \"\$CTID\"" )" [[ "$ctid_out" == "123" ]] && ok "CTID-Loop re-promptet bei 'abc' und nimmt '123'" \ || nok "CTID-Loop re-promptet (got: '$ctid_out')" # ── Dry-Run: prompt_lxc_config komplett aus env, ohne PVE/TTY ──────────────── smoke_out="$( env CTID=999 CT_HOSTNAME=smoke DISK_SIZE=8 CORES=2 RAM=1024 BRIDGE=vmbr0 \ VLAN_TAG=20 TEMPLATE_STORAGE=local ROOTFS_STORAGE=local-lvm \ IPCFG=10.11.20.99/24 GATEWAY=10.11.20.1 NAMESERVER="" \ NET_PROFILES_FILE="$REPO_ROOT/lib/networks.conf" \ bash -c "source '$REPO_ROOT/lib/build.func' && prompt_lxc_config >/dev/null && echo SMOKE-OK" )" || true [[ "$smoke_out" == *SMOKE-OK* ]] \ && ok "prompt_lxc_config Dry-Run mit validen env-Werten läuft durch" \ || nok "prompt_lxc_config Dry-Run (got: '$smoke_out')" smoke_bad="$( env CTID="$(printf '9\x809')" CT_HOSTNAME=smoke DISK_SIZE=8 CORES=2 RAM=1024 BRIDGE=vmbr0 \ VLAN_TAG=20 TEMPLATE_STORAGE=local ROOTFS_STORAGE=local-lvm IPCFG=dhcp NAMESERVER="" \ NET_PROFILES_FILE="$REPO_ROOT/lib/networks.conf" \ bash -c "source '$REPO_ROOT/lib/build.func' && prompt_lxc_config >/dev/null && echo SMOKE-OK" 2>/dev/null )" || true [[ "$smoke_bad" == *SMOKE-OK* ]] \ && nok "prompt_lxc_config bricht bei Junk-CTID aus env ab" \ || ok "prompt_lxc_config bricht bei Junk-CTID aus env ab" echo echo "passed=$PASS failed=$FAIL" [[ "$FAIL" -eq 0 ]]