#!/usr/bin/env bash set -Eeuo pipefail root=$(cd -- "$(dirname -- "$0")/.." && pwd -P) bash -n "$root/uninstall.sh" tmp=$(cd "$(mktemp -d)" && pwd -P) cleanup() { rm -rf -- "$tmp" 2>/dev/null || true; } trap cleanup EXIT make_fixture() { local fixture=$1 mkdir -p "$fixture/opt/tallynote/releases/1.1.1/dist" \ "$fixture/opt/tallynote/.update-work" \ "$fixture/var/lib/tallynote/files" \ "$fixture/var/lib/tallynote/staging" \ "$fixture/var/lib/tallynote/exports" \ "$fixture/var/lib/tallynote-backups" \ "$fixture/etc/tallynote" \ "$fixture/etc/systemd/system" \ "$fixture/usr/local/sbin" \ "$fixture/usr/local/libexec" printf '%s\n' 'release' > "$fixture/opt/tallynote/releases/1.1.1/dist/index.js" ln -s "$fixture/opt/tallynote/releases/1.1.1" "$fixture/opt/tallynote/current" printf '%s\n' \ "TALLYNOTE_INSTALL_PREFIX=$fixture/opt/tallynote" \ "TALLYNOTE_DATA_DIR=$fixture/var/lib/tallynote" \ "TALLYNOTE_UPDATE_PUBLIC_KEY_FILE=$fixture/etc/tallynote/update-signing-key.pub" \ > "$fixture/etc/tallynote/tallynote.env" chmod 600 "$fixture/etc/tallynote/tallynote.env" printf '%s\n' 'fake public key' > "$fixture/etc/tallynote/update-signing-key.pub" for unit in tallynote.service tallynote-update.service tallynote-update.path; do printf '%s\n' "Description=TallyNote $unit" "WorkingDirectory=$fixture/opt/tallynote/current" "PathExists=$fixture/var/lib/tallynote/update-request.json" > "$fixture/etc/systemd/system/$unit" done printf '%s\n' '#!/usr/bin/env bash' 'PREFIX=${TALLYNOTE_INSTALL_PREFIX:-/opt/tallynote}' 'echo TallyNote' > "$fixture/usr/local/sbin/tallynote-update" printf '%s\n' '#!/usr/bin/env bash' 'PREFIX=${TALLYNOTE_INSTALL_PREFIX:-/opt/tallynote}' 'echo TallyNote' > "$fixture/usr/local/libexec/tallynote-update-runner" printf '%s\n' '#!/usr/bin/env bash' 'exec /opt/tallynote/current/runtime/bin/node /opt/tallynote/current/dist/server/cli/admin-init.js' > "$fixture/usr/local/sbin/tallynote-admin-init" cp "$root/uninstall.sh" "$fixture/usr/local/sbin/tallynote-uninstall" chmod 755 "$fixture/usr/local/sbin/tallynote-update" "$fixture/usr/local/libexec/tallynote-update-runner" "$fixture/usr/local/sbin/tallynote-admin-init" "$fixture/usr/local/sbin/tallynote-uninstall" printf '%s\n' 'sqlite' > "$fixture/var/lib/tallynote/tallynote.db" printf '%s\n' 'backup' > "$fixture/var/lib/tallynote-backups/backup.db" } make_systemctl() { local fixture=$1 cat > "$fixture/systemctl" <<'EOF' #!/usr/bin/env bash set -u printf '%s\n' "$*" >> "$TALLYNOTE_TEST_SYSTEMCTL_LOG" case "${1:-}" in is-active) exit 0 ;; stop|disable|daemon-reload) exit 0 ;; *) exit 0 ;; esac EOF chmod 755 "$fixture/systemctl" } run_uninstall() { local fixture=$1 TALLYNOTE_UNINSTALL_TEST_MODE=true \ TALLYNOTE_UNINSTALL_ROOT="$fixture" \ TALLYNOTE_SYSTEMCTL_BIN="$fixture/systemctl" \ TALLYNOTE_TEST_SYSTEMCTL_LOG="$fixture/systemctl.log" \ bash "$root/uninstall.sh" "${@:2}" } fixture="$tmp/normal" make_fixture "$fixture" make_systemctl "$fixture" run_uninstall "$fixture" [[ -d "$fixture/var/lib/tallynote" && -f "$fixture/var/lib/tallynote/tallynote.db" ]] [[ -d "$fixture/var/lib/tallynote-backups" && -f "$fixture/var/lib/tallynote-backups/backup.db" ]] [[ ! -e "$fixture/opt/tallynote" || -z "$(find "$fixture/opt/tallynote" -mindepth 1 -print -quit 2>/dev/null)" ]] [[ ! -e "$fixture/etc/systemd/system/tallynote.service" ]] [[ ! -e "$fixture/usr/local/sbin/tallynote-update" ]] [[ ! -e "$fixture/usr/local/sbin/tallynote-admin-init" ]] grep -n '^is-active.*tallynote-update.path' "$fixture/systemctl.log" >/dev/null grep -n '^stop tallynote-update.path' "$fixture/systemctl.log" >/dev/null path_stop=$(grep -n '^stop tallynote-update.path' "$fixture/systemctl.log" | head -n1 | cut -d: -f1) update_stop=$(grep -n '^stop tallynote-update.service' "$fixture/systemctl.log" | head -n1 | cut -d: -f1) main_stop=$(grep -n '^stop tallynote.service' "$fixture/systemctl.log" | head -n1 | cut -d: -f1) (( path_stop < update_stop && update_stop < main_stop )) # Re-running after the first uninstall is harmless and does not touch data. run_uninstall "$fixture" [[ -f "$fixture/var/lib/tallynote/tallynote.db" ]] # Purge requires the explicit acknowledgement flag and must fail before any # application files are removed. fixture="$tmp/purge" make_fixture "$fixture" make_systemctl "$fixture" if run_uninstall "$fixture" --purge-data >/dev/null 2>&1; then echo 'expected --purge-data without --yes to fail' >&2 exit 1 fi [[ -e "$fixture/opt/tallynote/current" && -f "$fixture/var/lib/tallynote/tallynote.db" ]] run_uninstall "$fixture" --purge-data --yes --purge-config [[ ! -e "$fixture/var/lib/tallynote" && ! -e "$fixture/var/lib/tallynote-backups" ]] [[ ! -e "$fixture/etc/tallynote" ]] # In production the service user owns the data directory. The target itself # must be accepted while its parent directories remain root-owned. This test # is meaningful only when the suite runs as root on a host with that account; # ordinary developer runs continue with the portable fixture coverage above. tallynote_uid=$(id -u tallynote 2>/dev/null || true) if [[ "$EUID" == 0 && -n "$tallynote_uid" && "$tallynote_uid" != "$(id -u)" ]]; then fixture="$tmp/service-user-data" make_fixture "$fixture" make_systemctl "$fixture" chown -R "$tallynote_uid" "$fixture/var/lib/tallynote" TALLYNOTE_UNINSTALL_TEST_DATA_OWNER_UID="$tallynote_uid" run_uninstall "$fixture" --purge-data --yes [[ ! -e "$fixture/var/lib/tallynote" ]] fi # A custom data path must not overlap the release prefix; otherwise removing # releases could destroy data that the default uninstall promises to keep. fixture="$tmp/overlap" make_fixture "$fixture" make_systemctl "$fixture" printf '%s\n' \ "TALLYNOTE_INSTALL_PREFIX=$fixture/opt/tallynote" \ "TALLYNOTE_DATA_DIR=$fixture/opt/tallynote/releases/data" \ > "$fixture/etc/tallynote/tallynote.env" mkdir -p "$fixture/opt/tallynote/releases/data" printf '%s\n' protected > "$fixture/opt/tallynote/releases/data/keep.db" if run_uninstall "$fixture" >/dev/null 2>&1; then echo 'expected overlapping data path to fail' >&2 exit 1 fi [[ -f "$fixture/opt/tallynote/releases/data/keep.db" ]] # Trailing-slash aliases are rejected before the lexical overlap guard can be # bypassed. fixture="$tmp/trailing" make_fixture "$fixture" make_systemctl "$fixture" printf '%s\n' \ "TALLYNOTE_INSTALL_PREFIX=$fixture/opt/tallynote/" \ "TALLYNOTE_DATA_DIR=$fixture/var/lib/tallynote" \ > "$fixture/etc/tallynote/tallynote.env" if run_uninstall "$fixture" >/dev/null 2>&1; then echo 'expected trailing slash path to fail' >&2 exit 1 fi [[ -e "$fixture/opt/tallynote/current" && -f "$fixture/var/lib/tallynote/tallynote.db" ]] # Dot-component aliases are rejected as well; textual paths must be canonical # before the managed-directory containment checks run. fixture="$tmp/dot" make_fixture "$fixture" make_systemctl "$fixture" printf '%s\n' \ "TALLYNOTE_INSTALL_PREFIX=$fixture/opt/tallynote/." \ "TALLYNOTE_DATA_DIR=$fixture/var/lib/tallynote" \ > "$fixture/etc/tallynote/tallynote.env" if run_uninstall "$fixture" >/dev/null 2>&1; then echo 'expected dot path component to fail' >&2 exit 1 fi [[ -e "$fixture/opt/tallynote/current" && -f "$fixture/var/lib/tallynote/tallynote.db" ]] # Pending update state blocks destructive work until an operator overrides it. fixture="$tmp/pending" make_fixture "$fixture" make_systemctl "$fixture" printf '%s\n' pending > "$fixture/opt/tallynote/.update-state" if run_uninstall "$fixture" >/dev/null 2>&1; then echo 'expected pending update state to block uninstall' >&2 exit 1 fi [[ -e "$fixture/opt/tallynote/current" && -f "$fixture/var/lib/tallynote/tallynote.db" ]] # A current link escaping the release tree is rejected without deleting data. fixture="$tmp/link" make_fixture "$fixture" make_systemctl "$fixture" rm -f "$fixture/opt/tallynote/current" ln -s "$fixture/outside" "$fixture/opt/tallynote/current" if run_uninstall "$fixture" >/dev/null 2>&1; then echo 'expected unsafe current symlink to fail' >&2 exit 1 fi [[ -L "$fixture/opt/tallynote/current" && -f "$fixture/var/lib/tallynote/tallynote.db" ]] # dry-run must not call systemctl or remove files. fixture="$tmp/dry-run" make_fixture "$fixture" make_systemctl "$fixture" run_uninstall "$fixture" --dry-run >/dev/null [[ -e "$fixture/opt/tallynote/current" && -f "$fixture/var/lib/tallynote/tallynote.db" ]] [[ ! -e "$fixture/systemctl.log" ]] printf '%s\n' 'uninstaller shell tests passed'