Files
TallyNote/scripts/test-uninstaller.sh
T
Qiufeng 344985f514
TallyNote release / linux-x64 (push) Successful in 6m26s
release: 1.1.12
2026-09-02 20:23:46 +08:00

196 lines
8.4 KiB
Bash
Executable File

#!/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'