Files
ninlight-w600/tools/station/tests/test_setup_pi.sh
T
Michael Sauer 74198f6d31 fix(station): stat-Reihenfolge, Phase nur im Migrationsmodus, eingerueckte Konfig-Werte, kein /tmp-Zugriff im Test
Fixrunde 2 zu Task 4:

- N1 (wichtig): file_mode() in test_cli.sh/test_setup_pi.sh probierte
  "stat -f '%Lp'" vor "stat -c '%a'" - auf GNU-coreutils (dem Pi) deutet
  "-f" dort "Dateisystemstatus" und liefert Muell statt der Rechte-Zahl,
  faellt aber trotzdem in den "||"-Zweig durch, der zusaetzlich "644"
  anhaengt. Reihenfolge umgedreht und mit einem per gstat erzwungenen
  GNU-Testlauf zusaetzlich abgesichert.
- N2 (wichtig): status zeigte nach einem Wechsel zurueck in den
  Rettungsmodus weiterhin die letzte Migrationsphase aus einer liegen
  gebliebenen control.json (moeglich seit RuntimeDirectoryPreserve=
  restart aus Fixrunde 1). Phase wird jetzt nur noch angezeigt, wenn
  Modus tatsaechlich "migration" ist.
- N3 (klein): die Schluesselerkennung in setup_pi.sh war rein
  zeilenverankert ("^KEY=") und haette einen eingerueckten Bestandswert
  als fehlend gewertet - beim naechsten Sourcen der Konfiguration haette
  die zusaetzlich angehaengte Vorlagenzeile das eigene Passwort
  ueberschrieben. Muster auf fuehrenden Leerraum/export gehaertet.
- N4 (klein): test_setup_pi.sh liess setup_pi.sh ueber den scp-Stub echte
  Dateien im /tmp des Entwicklungsrechners anlegen und wieder loeschen.
  Neue Variable NINLIGHT_STAGE_DIR biegt das auf eine Sandbox um.

Alle vier Punkte mit Regressionstests, N2 und N3 zusaetzlich per
Mutationsprobe (Fix zurueckgenommen, Test wird rot) verifiziert.
2026-09-13 19:28:42 +02:00

257 lines
9.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Stub-Tests fuer setup_pi.sh - ohne Pi ausfuehrbar.
#
# ssh/scp werden im PATH durch Stubs ersetzt (Muster wie tests/test_deploy.sh):
# ssh fuehrt das per Heredoc uebergebene Remote-Skript lokal aus, scp uebersetzt
# "host:pfad"-Ziele auf den blossen lokalen Pfad und ruft dann "cp" auf. Die
# System-Zielpfade (/etc/ninlight-station.conf, /opt/ninlight-station, ...) sind
# per Umgebungsvariable auf ein Sandbox-Verzeichnis umgebogen (dasselbe Prinzip
# wie NINLIGHT_STATION_ROOT/_BIN/_UNIT in deploy.sh) - setup_pi.sh fasst damit
# nichts auf dem Testrechner selbst an ausser dem transienten /tmp, das schon
# deploy.sh fuer denselben Zweck benutzt.
#
# Schwerpunkt: Fixrunde 1, I4 - eine bestehende Konfiguration muss um fehlende
# Schluessel ergaenzt werden, ohne vorhandene Werte anzutasten.
#
# Aufruf: tools/station/tests/test_setup_pi.sh
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
STATION_DIR=$(cd "$HERE/.." && pwd)
TMPROOT=$(mktemp -d)
trap 'rm -rf "$TMPROOT"' EXIT
FAILED=0
pass() { echo "ok - $1"; }
fail() { echo "FEHLER - $1" >&2; FAILED=1; }
file_mode() {
# $1 = Pfad. GNU-stat (Linux/Pi) muss zuerst probiert werden, nicht BSD-stat
# (macOS): "stat -f '%Lp' datei" deutet auf GNU-coreutils "-f" als
# "Dateisystemstatus" und '%Lp' als zweites Dateiargument, scheitert dabei
# zwar (rc != 0), schreibt aber vorher schon mehrere Zeilen Dateisystem-Infos
# auf stdout - der "||"-Zweig haengt danach zusaetzlich "600"/"644" an, und
# der Aufrufer sieht Muell statt der reinen Zahl (Fixrunde 2, N1). BSD-"stat
# -c" bricht dagegen sofort mit einem Fehler auf stderr ab, ganz ohne
# Ausgabe auf stdout.
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1" 2>/dev/null
}
setup_stubbin() {
local bindir=$1 log=$2
mkdir -p "$bindir"
cat >"$bindir/ssh" <<EOF
#!/usr/bin/env bash
echo "STUB ssh \$*" >>"$log"
host=\$1; shift
"\$@"
EOF
cat >"$bindir/scp" <<EOF
#!/usr/bin/env bash
echo "STUB scp \$*" >>"$log"
args=()
for a in "\$@"; do
case "\$a" in
-*) continue ;;
*:*) args+=("\${a#*:}") ;;
*) args+=("\$a") ;;
esac
done
exec cp "\${args[@]}"
EOF
# "sudo install -o root -g root ..." (im NetworkManager-Keyfile-Schritt) braucht
# echtes Root, um den Eigentuemer zu setzen - hat der Testrechner (anders als der
# Pi) nicht. Der Stub streicht -o/-g nur fuer "install", der Rest laeuft wie auf
# dem Pi ueber ein exec des ungestubbten Kommandos.
cat >"$bindir/sudo" <<EOF
#!/usr/bin/env bash
echo "STUB sudo \$*" >>"$log"
if [ "\$1" = "install" ]; then
args=()
skip=0
for a in "\$@"; do
if [ "\$skip" -eq 1 ]; then skip=0; continue; fi
case "\$a" in
-o|-g) skip=1 ;;
*) args+=("\$a") ;;
esac
done
exec "\${args[@]}"
fi
exec "\$@"
EOF
cat >"$bindir/systemctl" <<EOF
#!/usr/bin/env bash
echo "STUB systemctl \$*" >>"$log"
case "\$1" in
is-active) exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}" ;;
*) exit 0 ;;
esac
EOF
cat >"$bindir/nmcli" <<EOF
#!/usr/bin/env bash
echo "STUB nmcli \$*" >>"$log"
if [ "\$1" = "-t" ] && [ "\$2" = "-f" ] && [ "\$3" = "DEVICE,STATE" ]; then
echo "wlan0:connected"
exit 0
fi
exit 0
EOF
chmod +x "$bindir"/ssh "$bindir"/scp "$bindir"/sudo "$bindir"/systemctl "$bindir"/nmcli
}
# $1 = Sandbox-Wurzel fuer die "Systempfade"
setup_state() {
local state=$1
rm -rf "$state"
mkdir -p "$state/etc" "$state/opt" "$state/usr/local/bin" "$state/etc/systemd/system" \
"$state/etc/NetworkManager/system-connections" "$state/tmp"
}
# Fuehrt setup_pi.sh mit gestubbtem PATH und auf die Sandbox umgebogenen
# Systempfaden aus. $1 = state-Verzeichnis, $2 = stub-bin-Verzeichnis.
#
# NINLIGHT_STAGE_DIR biegt auch die per scp "uebertragenen" Zwischendateien auf
# die Sandbox um (Fixrunde 2, N4): ohne das wuerden scp/ssh-Stub sie unter dem
# echten /tmp des Entwicklungsrechners ablegen und am Ende per "rm -f" wieder
# loeschen - mit denselben, leicht kollidierenden Namen wie
# "ninlight-station.conf.example" oder "server.py".
run_setup() {
local state=$1 stubbin=$2
PATH="$stubbin:$PATH" \
NINLIGHT_SSH_BIN="$stubbin/ssh" \
NINLIGHT_SCP_BIN="$stubbin/scp" \
NINLIGHT_STAGE_DIR="$state/tmp" \
NINLIGHT_STATION_CONF="$state/etc/ninlight-station.conf" \
NINLIGHT_STATION_ROOT="$state/opt/ninlight-station" \
NINLIGHT_STATION_BIN="$state/usr/local/bin/ninlight-station" \
NINLIGHT_STATION_UNIT="$state/etc/systemd/system/ninlight-station.service" \
NINLIGHT_NM_DIR="$state/etc/NetworkManager/system-connections" \
"$STATION_DIR/setup_pi.sh" pi-test
}
echo "=== Szenario 1: frischer Pi ohne Konfiguration -> Vorlage wird komplett angelegt ==="
STATE="$TMPROOT/s1-state"; STUBBIN="$TMPROOT/bin1"; LOG="$TMPROOT/s1.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
if diff -q "$STATION_DIR/station.conf.example" "$STATE/etc/ninlight-station.conf" >/dev/null 2>&1; then
pass "Konfiguration wurde 1:1 aus der Vorlage angelegt"
else
fail "Konfiguration weicht von der Vorlage ab oder fehlt"
fi
[ -x "$STATE/opt/ninlight-station/server.py" ] && pass "server.py installiert" || fail "server.py fehlt"
[ -f "$STATE/opt/ninlight-station/products.py" ] && pass "products.py installiert" || fail "products.py fehlt (arm braucht es)"
[ -x "$STATE/usr/local/bin/ninlight-station" ] && pass "CLI installiert" || fail "ninlight-station CLI fehlt"
[ -f "$STATE/etc/systemd/system/ninlight-station.service" ] && pass "Unit installiert" || fail "Unit fehlt"
[ -d "$STATE/opt/ninlight-station/images" ] && pass "images/-Verzeichnis angelegt" || fail "images/-Verzeichnis fehlt"
echo
echo "=== Szenario 2 (Fixrunde 1, I4): bestehende Konfiguration ohne die neuen Schluessel wird ergaenzt, nicht ersetzt ==="
STATE="$TMPROOT/s2-state"; STUBBIN="$TMPROOT/bin2"; LOG="$TMPROOT/s2.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
cat >"$STATE/etc/ninlight-station.conf" <<'CONF'
SSID='eigene-ssid'
PSK='eigenes-passwort'
IMAGE_DIR='/opt/ninlight-station/images'
HOME_CONNECTION='preconfigured'
AP_CONNECTION='ninlight-recovery'
CONF
chmod 0600 "$STATE/etc/ninlight-station.conf"
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
echo "$OUT" | grep -q "ergaenzt" && pass "Ausgabe kuendigt das Ergaenzen fehlender Schluessel an" || fail "Ausgabe erwaehnt 'ergaenzt' nicht: $OUT"
CONF_OUT="$STATE/etc/ninlight-station.conf"
if grep -Fqx "SSID='eigene-ssid'" "$CONF_OUT" && grep -Fqx "PSK='eigenes-passwort'" "$CONF_OUT"; then
pass "vorhandene Werte (SSID/PSK) blieben unveraendert"
else
fail "vorhandene Werte wurden ueberschrieben: $(cat "$CONF_OUT")"
fi
for KEY in MIGRATE_AP_CONNECTION MIGRATE_SSID MIGRATE_PSK EPD_REVISION; do
if grep -Eq "^${KEY}=" "$CONF_OUT"; then
pass "fehlender Schluessel $KEY wurde ergaenzt"
else
fail "Schluessel $KEY fehlt weiterhin in der Konfiguration"
fi
done
MODE=$(file_mode "$CONF_OUT")
[ "$MODE" = "600" ] && pass "Konfigurationsdatei bleibt 0600" || fail "Konfigurationsdatei hat falsche Rechte: $MODE"
echo
echo "=== Szenario 3: erneuter Lauf auf einer bereits vollstaendigen Konfiguration ist ein echtes No-Op ==="
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
echo "$OUT" | grep -q "alle Schluessel vorhanden" && pass "meldet 'alle Schluessel vorhanden' statt erneut zu ergaenzen" || fail "Ausgabe: $OUT"
DUP=$(grep -c '^EPD_REVISION=' "$CONF_OUT")
[ "$DUP" -eq 1 ] && pass "EPD_REVISION steht nur einmal in der Datei (kein doppeltes Anhaengen)" || fail "EPD_REVISION steht $DUP mal in der Datei"
echo
echo "=== Szenario 4 (Fixrunde 2, N3): ein eingerueckter Bestandswert gilt nicht als fehlend ==="
STATE="$TMPROOT/s4-state"; STUBBIN="$TMPROOT/bin4"; LOG="$TMPROOT/s4.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
# Fuehrendes Leerzeichen vor PSK= - gueltiges ". datei"-Shell, aber eine rein
# zeilenverankerte Erkennung ("^PSK=") wuerde das als "fehlt" werten und die
# Vorlagenzeile anhaengen. Da das CLI die Datei per ". $CONF" sourct, gewinnt
# dann die LETZTE Zuweisung - das eigene Passwort waere durch den oeffentlichen
# Default aus der Vorlage ersetzt, ohne dass die Datei das irgendwo anzeigt.
cat >"$STATE/etc/ninlight-station.conf" <<'CONF'
SSID='eigene-ssid'
PSK='eigenes-eingeruecktes-passwort'
IMAGE_DIR='/opt/ninlight-station/images'
HOME_CONNECTION='preconfigured'
AP_CONNECTION='ninlight-recovery'
CONF
chmod 0600 "$STATE/etc/ninlight-station.conf"
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
CONF_OUT4="$STATE/etc/ninlight-station.conf"
PSK_LINES=$(grep -cE '^[[:space:]]*PSK=' "$CONF_OUT4")
[ "$PSK_LINES" -eq 1 ] && pass "PSK steht weiterhin nur einmal in der Datei (nicht dupliziert)" || fail "PSK steht $PSK_LINES mal in der Datei: $(cat "$CONF_OUT4")"
if grep -Fq "eigenes-eingeruecktes-passwort" "$CONF_OUT4"; then
pass "der eingerueckte Bestandswert von PSK blieb erhalten"
else
fail "der eingerueckte Bestandswert von PSK wurde ersetzt: $(cat "$CONF_OUT4")"
fi
if grep -Fqx "PSK='ninlight-recovery'" "$CONF_OUT4"; then
fail "der oeffentliche Vorlagen-Default fuer PSK wurde zusaetzlich angehaengt - eigenes Passwort waere beim Sourcen ueberschrieben worden"
else
pass "der oeffentliche Vorlagen-Default fuer PSK wurde NICHT angehaengt"
fi
for KEY in MIGRATE_AP_CONNECTION MIGRATE_SSID MIGRATE_PSK EPD_REVISION; do
grep -Eq "^${KEY}=" "$CONF_OUT4" && pass "fehlender Schluessel $KEY wurde trotzdem ergaenzt" || fail "Schluessel $KEY fehlt weiterhin"
done
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."
else
echo "Mindestens ein Test ist fehlgeschlagen." >&2
fi
exit "$FAILED"