#!/usr/bin/env bash # DC-122 full-cycle functional test: backup → deploy → rollback on a CUSTOM # frontend target, proving (a) rollback restores the custom target, (b) an # unrelated sibling directory is never touched, (c) invalid targets are # refused. Sources the real functions from the real script. set -euo pipefail # Updater script path: $1 overrides; default = adjacent to this test file, # falling back to the installed location (works in repo, worktree, and prod). SCRIPT="${1:-}" if [[ -z "$SCRIPT" ]]; then # Worktree layouts prefix files with numbers (2_dashcaddy-update.sh); # repo layout is unprefixed. Resolve whichever exists, adjacent first. local_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" for cand in "$local_dir/dashcaddy-update.sh" \ "$local_dir"/*dashcaddy-update.sh \ /opt/dashcaddy/scripts/dashcaddy-update.sh; do [[ -f "$cand" ]] && SCRIPT="$cand" && break done fi [[ -f "$SCRIPT" ]] || { echo "updater script not found"; exit 1; } # Trap-covered workspace for ALL temp artifacts (collision-safe). WORK=$(mktemp -d) || { echo "mktemp failed"; exit 1; } trap 'rm -rf "$WORK"' EXIT # Extract just the functions + constants we need (no main execution). sed -n '/^FRONTEND_SUBDIRS=/p;/^json_escape()/,/^}/p;/^backup_frontend_dir()/,/^}/p;/^restore_frontend_dir()/,/^}/p;/^validate_frontend_target()/,/^}/p' "$SCRIPT" > "$WORK/dcfuncs.sh" log() { echo "[t] $*"; } source "$WORK/dcfuncs.sh" T="$WORK/tree" mkdir -p "$T" CUSTOM_TARGET="$T/custom-webroot" VICTIM="$T/custom-webroot-sibling" mkdir -p "$CUSTOM_TARGET/dist" "$CUSTOM_TARGET/js" "$VICTIM" # Pre-update state echo v1 > "$CUSTOM_TARGET/index.html" echo v1 > "$CUSTOM_TARGET/sw.js" echo v1 > "$CUSTOM_TARGET/dist/core.js" echo v1 > "$CUSTOM_TARGET/js/app.js" echo secret > "$VICTIM/precious.txt" BACKUP="$T/backup/v1.15.0" mkdir -p "$BACKUP" # 1. backup (as the update flow does, with the custom target) backup_frontend_dir "$BACKUP" "$CUSTOM_TARGET" [[ -f "$BACKUP/frontend.meta" ]] && [[ -f "$BACKUP/frontend.manifest" ]] || { echo "FAIL: snapshot incomplete"; exit 1; } grep -q "$CUSTOM_TARGET" "$BACKUP/frontend.meta" || { echo "FAIL: meta lost target"; exit 1; } echo "PASS backup: meta + manifest written, target recorded" # 2. "deploy" a new version (simulates what the update does) echo v2 > "$CUSTOM_TARGET/index.html" echo v2 > "$CUSTOM_TARGET/dist/core.js" echo new > "$CUSTOM_TARGET/dist/newfile.js" # update-introduced file mkdir -p "$CUSTOM_TARGET/dist/newdir" && echo x > "$CUSTOM_TARGET/dist/newdir/f.js" printf '{"version":"v2"}' > "$CUSTOM_TARGET/update-stamp.json" # 3. rollback restore_frontend_dir "$BACKUP" || { echo "FAIL: restore errored"; exit 1; } [[ "$(cat "$CUSTOM_TARGET/index.html")" == v1 ]] && echo "PASS: index.html rolled back" || { echo "FAIL: index.html"; exit 1; } [[ "$(cat "$CUSTOM_TARGET/dist/core.js")" == v1 ]] && echo "PASS: dist/core.js rolled back" || { echo "FAIL: core.js"; exit 1; } [[ ! -e "$CUSTOM_TARGET/dist/newfile.js" ]] && echo "PASS: update-introduced file removed" || { echo "FAIL: newfile.js survived"; exit 1; } [[ ! -e "$CUSTOM_TARGET/dist/newdir" ]] && echo "PASS: update-introduced dir removed" || { echo "FAIL: newdir survived"; exit 1; } [[ ! -f "$CUSTOM_TARGET/update-stamp.json" ]] && echo "PASS: stamp cleared on rollback" || { echo "FAIL: stamp survived"; exit 1; } [[ "$(cat "$VICTIM/precious.txt")" == secret ]] && echo "PASS: sibling directory untouched" || { echo "FAIL: VICTIM MODIFIED"; exit 1; } # 4. invalid targets are refused outright (relative path = traversal risk class) BACKUP2="$T/backup2"; mkdir -p "$BACKUP2/frontend" printf '{"target":"relative/not-absolute","indexExisted":false,"swExisted":false}\n' > "$BACKUP2/frontend.meta" ( cd "$BACKUP2/frontend" && find . -type f -printf '%P\n' | sort ) > "$BACKUP2/frontend.manifest" if restore_frontend_dir "$BACKUP2" 2>/dev/null; then echo "FAIL: relative target was accepted"; exit 1 fi echo "PASS: invalid (relative) target refused" # 5. a validated custom target round-trips: snapshot→destroy→restore CUSTOM2="$T/custom2-webroot"; mkdir -p "$CUSTOM2/js" echo orig > "$CUSTOM2/js/only.js" BACKUP3="$T/backup3"; mkdir -p "$BACKUP3" backup_frontend_dir "$BACKUP3" "$CUSTOM2" rm -rf "$CUSTOM2/js" && mkdir -p "$CUSTOM2/js" && echo clobbered > "$CUSTOM2/js/only.js" restore_frontend_dir "$BACKUP3" >/dev/null 2>&1 || { echo "FAIL: custom-target restore errored"; exit 1; } [[ "$(cat "$CUSTOM2/js/only.js")" == orig ]] && echo "PASS: validated custom target restored" || { echo "FAIL: custom restore"; exit 1; } # 6. update-introduced OWNED subtree (css/ absent at backup, created by # "deploy") must be REMOVED by rollback — not left behind. CUSTOM3="$T/custom3-webroot"; mkdir -p "$CUSTOM3/dist" # note: NO css/ yet echo v1 > "$CUSTOM3/dist/core.js" BACKUP4="$T/backup4"; mkdir -p "$BACKUP4" backup_frontend_dir "$BACKUP4" "$CUSTOM3" mkdir -p "$CUSTOM3/css" && echo new > "$CUSTOM3/css/introduced.css" # update creates css/ restore_frontend_dir "$BACKUP4" >/dev/null 2>&1 || { echo "FAIL: introduced-subtree restore errored"; exit 1; } [[ ! -e "$CUSTOM3/css" ]] && echo "PASS: update-introduced owned subtree removed" || { echo "FAIL: css/ survived rollback"; exit 1; } [[ "$(cat "$CUSTOM3/dist/core.js")" == v1 ]] && echo "PASS: pre-existing subtree intact" || { echo "FAIL: dist broken"; exit 1; } echo "=== ALL FULL-CYCLE TESTS PASSED ==="