#!/bin/bash # Self-contained unit test for the first-run restore preflight # (scripts/restore/restore_preflight.sh) and the snapshot path resolver it # leans on (scripts/storage/storage_restore_path.sh). Runs against a throwaway # tree in $TMPDIR — never touches a real install. Exits non-zero on failure. # # scripts/dev/lp-preflight-test # # It exists because this preflight has twice shipped checks that could not fail: # # * the manifest is pretty-printed, so `"size_bytes": 123` carries a space # that a `"key":[0-9]*` pattern misses — size came back empty, and the fit # check was skipped for every app # * storageSnapshotSourcePath asked engineSnapshotsJson for a snapshot id, # but that argument is an app TAG filter — so it matched nothing, returned # 1 every time, and the manifest was never read at all # # Both failed silently and *looked* like a clean report: thirteen green ticks. # So the cases worth keeping are the ones that must say NO. REPO="$(cd "$(dirname "$0")/../.." && pwd)" BASE="$(mktemp -d "${TMPDIR:-/tmp}/lp-preflight-test-XXXXXX")" trap 'rm -rf "$BASE"' EXIT fail=0 chk(){ if [[ "$2" == "$3" ]]; then echo " ok $1"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; } has(){ if [[ "$2" == *"$3"* ]]; then echo " ok $1"; else echo " FAIL $1: '$2' lacks '$3'"; fail=1; fi; } # --- a machine to check against ---------------------------------------------- install_containers_dir="$BASE/templates/" mkdir -p "$BASE/templates/bookstack" "$BASE/templates/nextcloud" "$BASE/templates/jellyfin" "$BASE/data" : > "$BASE/templates/bookstack/bookstack.config" : > "$BASE/templates/nextcloud/nextcloud.config" : > "$BASE/templates/jellyfin/jellyfin.config" # note: no template for 'obsolete' — that is case 2 isNotice(){ :; }; isError(){ :; }; isSuccessful(){ :; }; isQuestion(){ :; } appDir(){ printf '%s/data/%s' "$BASE" "$1"; } storageLocationName(){ printf 'primary'; } storageLocationPath(){ [[ "$1" == "here" ]] && { printf '%s/data' "$BASE"; return 0; }; return 1; } migrateDiscoverApps(){ :; } engineSnapshotLatestId(){ printf 'abc123'; } source "$REPO/scripts/restore/restore_preflight.sh" # Stubbed AFTER the source, or the real definition wins. Manifests are # pretty-printed exactly as the real ones are — size_bytes at the top level, # location under "storage" — because the spaces after those colons are the # point of this fixture, not an accident. _manifest(){ printf '{\n "app": "%s",\n "size_bytes": %s,\n "storage": {\n "location": "%s"\n }\n}\n' \ "$1" "$2" "$3" } restorePreflightManifest(){ _manifest "$2" "$MANIFEST_SIZE" "$MANIFEST_LOC"; } echo "--- an app that restores as-is ---" MANIFEST_LOC="default"; MANIFEST_SIZE=147483648 rec=$(restorePreflightApp 1 bookstack somehost) chk "verdict" "$(cut -f1 <<< "$rec")" "ok" chk "size parsed" "$(cut -f3 <<< "$rec")" "140M" echo "--- an app this version no longer ships ---" rec=$(restorePreflightApp 1 obsolete somehost) chk "verdict" "$(cut -f1 <<< "$rec")" "skip" has "reason" "$rec" "no longer ships" echo "--- an app too big for the disk (the check that could not fail) ---" MANIFEST_LOC="default"; MANIFEST_SIZE=9007199254740992 # 8 PiB rec=$(restorePreflightApp 1 jellyfin somehost) chk "verdict" "$(cut -f1 <<< "$rec")" "skip" has "reason" "$rec" "needs 8192T" echo "--- an app whose old storage location is gone ---" MANIFEST_LOC="ssd"; MANIFEST_SIZE=1048576 rec=$(restorePreflightApp 1 nextcloud somehost) chk "verdict" "$(cut -f1 <<< "$rec")" "move" has "reason" "$rec" "not on this machine" echo "--- a location this machine still has is NOT a move ---" MANIFEST_LOC="here"; MANIFEST_SIZE=1048576 rec=$(restorePreflightApp 1 nextcloud somehost) chk "verdict" "$(cut -f1 <<< "$rec")" "ok" echo "--- storageSnapshotSourcePath picks the app's path from the snapshot ---" # If this ever calls engineSnapshotsJson again it is passing a snapshot id into # an app-tag filter, which is the bug. Make that loud rather than silent. engineSnapshotsJson(){ echo " FAIL resolver used the app-tag filter again" >&2; fail=1; return 1; } engineSnapshotPaths(){ printf '/mnt/ssd/apps/nextcloud\n/libreportal-containers/bookstack\n'; } source "$REPO/scripts/storage/storage_restore_path.sh" chk "matching app" "$(storageSnapshotSourcePath 1 abc123 nextcloud)" "/mnt/ssd/apps/nextcloud" chk "other app" "$(storageSnapshotSourcePath 1 abc123 bookstack)" "/libreportal-containers/bookstack" storageSnapshotSourcePath 1 abc123 grafana >/dev/null 2>&1 \ && { echo " FAIL absent app should not resolve"; fail=1; } \ || echo " ok absent app does not resolve" # An engine with no adapter (borg) must fall back quietly, not error. engineSnapshotPaths(){ return 1; } storageSnapshotSourcePath 1 abc123 bookstack >/dev/null 2>&1 \ && { echo " FAIL engine without paths should not resolve"; fail=1; } \ || echo " ok engine without paths falls back" echo "" if (( fail )); then echo "FAILED"; exit 1; fi echo "All preflight checks passed."