382 lines
16 KiB
Bash
382 lines
16 KiB
Bash
#!/usr/bin/env bats
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
# Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
# E2E: skeptical-updater against the REAL package manager of the container
|
|
# (apt, pacman, dnf or zypper), on the su-dummy-{a,b,c} fixture: 1.0 installed,
|
|
# 2.0 available, su-dummy-a depends on su-dummy-b. Every test starts from that
|
|
# state (e2e_reset) with no statefile and no holds/locks.
|
|
|
|
load helpers
|
|
|
|
setup_file() { e2e_setup_file; }
|
|
setup() { e2e_reset; }
|
|
|
|
# shellcheck source=/dev/null
|
|
zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbleweed | opensuse-slowroll) echo dist-upgrade ;; *) echo update ;; esac; }
|
|
|
|
@test "banner shows the detected backend and the distro, refresh succeeds" {
|
|
run skeptical-updater list
|
|
assert_success
|
|
assert_line --index 0 "Backend: $BE ($(pretty_name))"
|
|
assert_line --index 1 "Refreshing package lists... done."
|
|
}
|
|
|
|
@test "list shows exactly the three dummies 1.0 -> 2.0" {
|
|
run skeptical-updater list --no-refresh
|
|
assert_success
|
|
assert_line "3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:"
|
|
assert_line --regexp '^ \[1\] su-dummy-a +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$'
|
|
assert_line --regexp '^ \[2\] su-dummy-b +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$'
|
|
assert_line --regexp '^ \[3\] su-dummy-c +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$'
|
|
refute_line --regexp '^.\[4\]'
|
|
}
|
|
|
|
@test "N? prints the description; su-dummy-b is required by su-dummy-a" {
|
|
run skeptical-updater --no-refresh <<<$'1?\n2?\nq'
|
|
assert_success
|
|
assert_line --regexp '^su-dummy-a: .*([Dd]ummy|skeptical-updater).*$'
|
|
assert_line --regexp '^su-dummy-b: .*([Dd]ummy|skeptical-updater).*$'
|
|
# [1] su-dummy-a has no installed reverse dependency, [2] su-dummy-b is needed by su-dummy-a
|
|
assert_line ' required by (installed): nothing'
|
|
assert_line ' required by (installed): su-dummy-a'
|
|
assert_line 'Quit: nothing was upgraded.'
|
|
}
|
|
|
|
@test "N?? prints the native full info" {
|
|
run skeptical-updater --no-refresh <<<$'1??\nq'
|
|
assert_success
|
|
assert_line --regexp '^(Package: su-dummy-a|Name +: su-dummy-a)$'
|
|
}
|
|
|
|
@test "N+ writes the user row and the dep row; list hides both and counts them" {
|
|
run skeptical-updater --no-refresh <<<$'1+\nq'
|
|
assert_success
|
|
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
|
[[ -f $STATE_FILE ]]
|
|
[[ $(stat -c %a "$STATE_DIR") == 755 ]]
|
|
run cat "$STATE_FILE"
|
|
assert_line --index 0 '# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
|
|
assert_line --index 1 --regexp "^$BE"$'\t'"su-dummy-a"$'\t'"user"$'\t''[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:]{8}Z$'
|
|
assert_line --index 2 --regexp "^$BE"$'\t'"su-dummy-b"$'\t'"dep:su-dummy-a"$'\t''[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:]{8}Z$'
|
|
run skeptical-updater list --no-refresh
|
|
assert_success
|
|
assert_line "3 upgradable, 2 trusted (hidden, use --all to show), 1 to review:"
|
|
refute_output --partial 'su-dummy-a'
|
|
refute_output --partial 'su-dummy-b'
|
|
assert_line --regexp '^ \[1\] su-dummy-c '
|
|
run skeptical-updater list --no-refresh --all
|
|
assert_success
|
|
assert_line "3 upgradable, 2 trusted (marked with =), 1 to review:"
|
|
assert_line --regexp '^=\[1\] su-dummy-a '
|
|
assert_line --regexp '^=\[2\] su-dummy-b '
|
|
assert_line --regexp '^ \[3\] su-dummy-c '
|
|
}
|
|
|
|
@test "trust / trusted / refresh / untrust from the command line" {
|
|
run skeptical-updater trust su-dummy-a
|
|
assert_success
|
|
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
|
run skeptical-updater trusted
|
|
assert_success
|
|
assert_line --partial "$BE"$'\t'"su-dummy-a"$'\t'"user"$'\t'
|
|
assert_line --partial "$BE"$'\t'"su-dummy-b"$'\t'"dep:su-dummy-a"$'\t'
|
|
# refresh recomputes the closure: a removed dep row comes back
|
|
sed -i '/dep:su-dummy-a/d' "$STATE_FILE"
|
|
run skeptical-updater refresh
|
|
assert_success
|
|
assert_output 'refreshed the dependency closures of 1 trusted root: 1 added, 0 removed'
|
|
run grep -c $'\tdep:su-dummy-a\t' "$STATE_FILE"
|
|
assert_output 1
|
|
run skeptical-updater untrust su-dummy-a
|
|
assert_success
|
|
assert_output 'untrusted su-dummy-a (2 rows removed)'
|
|
run skeptical-updater trusted
|
|
assert_success
|
|
assert_output '# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
|
|
}
|
|
|
|
@test "excluding su-dummy-b (2- then enter) upgrades the others" {
|
|
before=$(holds_snapshot)
|
|
run skeptical-updater <<<$'2-\n'
|
|
assert_success
|
|
assert_line 'excluded from this run: su-dummy-b'
|
|
assert_line 'Excluded from this run: su-dummy-b'
|
|
assert_line 'Upgrade finished.'
|
|
[[ $(versions) == "a=2.0 b=1.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
|
[[ $(holds_snapshot) == "$before" ]] || fail "hold/lock state changed: $(holds_snapshot)"
|
|
run skeptical-updater list --no-refresh
|
|
assert_line --regexp '^ \[1\] su-dummy-b +1\.0'
|
|
}
|
|
|
|
@test "a pre-existing hold/lock/IgnorePkg on su-dummy-c survives a run that excludes su-dummy-b" {
|
|
hold_c
|
|
before=$(holds_snapshot)
|
|
[[ -n $before ]]
|
|
run skeptical-updater list --no-refresh
|
|
assert_line --regexp '^ \[2\] su-dummy-b '
|
|
run skeptical-updater <<<$'2-\n'
|
|
assert_success
|
|
assert_line 'Excluded from this run: su-dummy-b'
|
|
[[ $(versions) == "a=2.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
|
after=$(holds_snapshot)
|
|
[[ $after == "$before" ]] || fail $'hold/lock state changed.\nbefore:\n'"$before"$'\nafter:\n'"$after"
|
|
if [[ $BE == apt ]]; then [[ ! -e $STATE_DIR/holds.added ]]; fi
|
|
}
|
|
|
|
@test "trusting a package re-includes its excluded dependency" {
|
|
run skeptical-updater <<<$'2-\n1+\n'
|
|
assert_success
|
|
assert_line ' (su-dummy-b is trusted now: no longer excluded from this run)'
|
|
assert_line 'Excluded from this run: none'
|
|
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
|
}
|
|
|
|
@test "enter upgrades everything" {
|
|
run skeptical-updater <<<''
|
|
assert_success
|
|
assert_line 'Excluded from this run: none'
|
|
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
|
run skeptical-updater list --no-refresh
|
|
assert_success
|
|
assert_output --partial 'Nothing to upgrade.'
|
|
}
|
|
|
|
@test "Nx purges su-dummy-c after a y confirmation" {
|
|
run skeptical-updater --no-refresh <<<$'3x\ny\nl\nq'
|
|
assert_success
|
|
assert_line 'Purge su-dummy-c now? [y/N] y'
|
|
assert_line 'purged: su-dummy-c'
|
|
assert_line '2 upgradable, 0 trusted (hidden, use --all to show), 2 to review:'
|
|
[[ $(versions) == "a=1.0 b=1.0 c=absent" ]] || fail "versions after the run: $(versions)"
|
|
[[ $(pkg_ver su-dummy-orphan) == 1.0 ]] || fail "the unrelated orphan was removed"
|
|
}
|
|
|
|
@test "Nx on su-dummy-b is refused: su-dummy-a depends on it; both stay, the row stays listed" {
|
|
run skeptical-updater --no-refresh <<<$'2x\nl\nq'
|
|
assert_success
|
|
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
|
refute_output --partial '[y/N]'
|
|
[[ $(grep -c '^ \[2\] su-dummy-b ' <<<"$output") -eq 2 ]] || fail "row 2 left the list"
|
|
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
|
}
|
|
|
|
@test "Nx on su-dummy-a removes su-dummy-b with it (its dependency, nothing else needs it); the orphan survives" {
|
|
run skeptical-updater --no-refresh <<<$'1x\ny\nl\nq'
|
|
assert_success
|
|
assert_line 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N] y'
|
|
assert_line 'purged: su-dummy-a su-dummy-b'
|
|
assert_line '1 upgradable, 0 trusted (hidden, use --all to show), 1 to review:'
|
|
[[ $(versions) == "a=absent b=absent c=1.0" ]] || fail "versions after the run: $(versions)"
|
|
[[ $(pkg_ver su-dummy-orphan) == 1.0 ]] || fail "the unrelated orphan was removed"
|
|
}
|
|
|
|
@test "purging every package ends the session cleanly" {
|
|
run skeptical-updater --no-refresh <<<$'1x\ny\n3x\ny'
|
|
assert_success
|
|
assert_line 'Nothing left to upgrade.'
|
|
[[ $(versions) == "a=absent b=absent c=absent" ]] || fail "versions after the run: $(versions)"
|
|
[[ $(pkg_ver su-dummy-orphan) == 1.0 ]] || fail "the unrelated orphan was removed"
|
|
}
|
|
|
|
@test "Nx answered with anything but y keeps the package" {
|
|
run skeptical-updater --no-refresh <<<$'3x\nn\nq'
|
|
assert_success
|
|
assert_line 'not purged: su-dummy-c'
|
|
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
|
}
|
|
|
|
@test "q leaves everything untouched" {
|
|
before=$(holds_snapshot)
|
|
run skeptical-updater <<<$'2-\n1-\nq'
|
|
assert_success
|
|
assert_line 'Quit: nothing was upgraded.'
|
|
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
|
[[ $(holds_snapshot) == "$before" ]]
|
|
[[ ! -e $STATE_FILE ]]
|
|
}
|
|
|
|
@test "EOF aborts with exit 2 and changes nothing" {
|
|
before=$(holds_snapshot)
|
|
run skeptical-updater <<<'2-'
|
|
assert_failure 2
|
|
assert_output --partial 'end of input: aborted, nothing was upgraded'
|
|
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
|
[[ $(holds_snapshot) == "$before" ]]
|
|
[[ ! -e $STATE_FILE ]]
|
|
}
|
|
|
|
@test "--dry-run: enter prints the native upgrade command and changes nothing" {
|
|
before=$(holds_snapshot)
|
|
run skeptical-updater --dry-run <<<''
|
|
assert_success
|
|
assert_line --regexp "$(native_upgrade_regex)"
|
|
assert_line 'Dry run: nothing was changed.'
|
|
# (3+: trusting su-dummy-a would hide su-dummy-b and so re-include it)
|
|
run skeptical-updater --dry-run <<<$'2-\n3+\n'
|
|
assert_success
|
|
case $BE in
|
|
apt)
|
|
assert_line '+ apt-mark hold su-dummy-b'
|
|
assert_line '+ apt-mark unhold su-dummy-b'
|
|
;;
|
|
pacman)
|
|
assert_line --regexp '^\+ cp -p -- /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+/sync/su-dummy\.db /var/lib/pacman/sync/$'
|
|
assert_line '+ pacman -Su --noconfirm --ignore su-dummy-b'
|
|
;;
|
|
dnf) assert_line --regexp '^\+ (dnf|yum) -y --setopt=skip_if_unavailable=0 --setopt=\\\*\.metadata_expire=-1 upgrade --exclude=su-dummy-b$' ;;
|
|
zypper)
|
|
assert_line --regexp '^\+ env ZYPP_CONF=.*/zypp\.conf zypper --non-interactive --quiet addlock su-dummy-b$'
|
|
assert_line --regexp "^\+ env ZYPP_CONF=.*/zypp\.conf zypper --non-interactive --no-refresh $(zypper_verb)\$"
|
|
;;
|
|
esac
|
|
assert_line --partial "+ $STATE_FILE add: $BE su-dummy-c user"
|
|
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
|
[[ $(holds_snapshot) == "$before" ]]
|
|
[[ ! -e $STATE_DIR ]]
|
|
}
|
|
|
|
@test "read-only commands work unprivileged" {
|
|
if command -v setpriv >/dev/null 2>&1; then
|
|
asuser() { setpriv --reuid=65534 --regid=65534 --clear-groups -- env TMPDIR=/tmp "$@"; }
|
|
elif command -v python3 >/dev/null 2>&1; then # almalinux:10 has neither setpriv nor runuser/su
|
|
asuser() { python3 -c 'import os, sys; os.setgroups([]); os.setgid(65534); os.setuid(65534); os.execvp(sys.argv[1], sys.argv[1:])' env TMPDIR=/tmp "$@"; }
|
|
else
|
|
skip "neither setpriv nor python3 available to drop privileges"
|
|
fi
|
|
skeptical-updater trust su-dummy-c >/dev/null
|
|
run asuser skeptical-updater trusted
|
|
assert_success
|
|
assert_line --partial "$BE"$'\t'"su-dummy-c"$'\t'"user"
|
|
run asuser skeptical-updater info su-dummy-b
|
|
assert_success
|
|
assert_line ' required by (installed): su-dummy-a'
|
|
run asuser skeptical-updater --dry-run --no-refresh <<<''
|
|
assert_success
|
|
assert_line --regexp "$(native_upgrade_regex)"
|
|
run asuser skeptical-updater upgrade-me-please
|
|
assert_failure 1
|
|
# a real upgrade without root and without sudo is refused
|
|
if ! command -v sudo >/dev/null 2>&1; then
|
|
run asuser skeptical-updater <<<''
|
|
assert_failure 1
|
|
assert_output --partial 'upgrading needs root and sudo is not available'
|
|
fi
|
|
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
|
# a state directory an unprivileged user cannot enter is an error, not "no trusted packages"
|
|
chmod 0000 /var/lib/skeptical-updater
|
|
run asuser skeptical-updater trusted
|
|
chmod 0755 /var/lib/skeptical-updater
|
|
assert_failure 1
|
|
assert_output --partial 'cannot access the state directory /var/lib/skeptical-updater (permission denied)'
|
|
}
|
|
|
|
@test "pacman: list, q, end of input and --dry-run never touch the real sync database" {
|
|
[[ $BE == pacman ]] || skip "pacman only"
|
|
before=$(pacman_sync_snapshot)
|
|
run skeptical-updater list
|
|
assert_success
|
|
run skeptical-updater <<<'q'
|
|
assert_success
|
|
run skeptical-updater <<<'2-'
|
|
assert_failure 2
|
|
run skeptical-updater --dry-run <<<''
|
|
assert_success
|
|
assert_line 'Checking the reviewed transaction (pacman -Sup)... ok.'
|
|
assert_line '+ pacman -Su --noconfirm'
|
|
after=$(pacman_sync_snapshot)
|
|
[[ $after == "$before" ]] || fail $'the real sync database changed.\nbefore:\n'"$before"$'\nafter:\n'"$after"
|
|
run ls /tmp
|
|
refute_output --partial 'skeptical-updater-db.'
|
|
}
|
|
|
|
@test "apt: temporary holds wait for a busy dpkg lock instead of failing" {
|
|
[[ $BE == apt ]] || skip "apt only"
|
|
command -v perl >/dev/null 2>&1 || skip "perl not available"
|
|
# hold dpkg's frontend lock for 4 s (fcntl, as dpkg does), starting now
|
|
perl -e 'use Fcntl; open(my $f, ">>", "/var/lib/dpkg/lock-frontend") or die; my $l = pack("s s l l i", F_WRLCK, 0, 0, 0, 0); fcntl($f, F_SETLKW, $l) or die; sleep 4;' &
|
|
locker=$!
|
|
sleep 0.5
|
|
run skeptical-updater --no-refresh <<<$'2-\n'
|
|
wait "$locker" || true
|
|
assert_success
|
|
assert_line 'Excluded from this run: su-dummy-b'
|
|
[[ $(versions) == "a=2.0 b=1.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
|
[[ -z $(apt-mark showhold) ]]
|
|
}
|
|
|
|
@test "zypper: a lock whose locksfile.path comes from a zypp.conf.d drop-in is honoured" {
|
|
[[ $BE == zypper ]] || skip "zypper only"
|
|
mkdir -p /etc/zypp/zypp.conf.d
|
|
printf '[main]\nlocksfile.path = /etc/zypp/locks-su-e2e\n' >/etc/zypp/zypp.conf.d/50-su-e2e.conf
|
|
zypper --non-interactive --quiet addlock su-dummy-c >/dev/null
|
|
grep -q 'solvable_name: su-dummy-c' /etc/zypp/locks-su-e2e
|
|
before=$(cat /etc/zypp/locks-su-e2e)
|
|
run skeptical-updater <<<$'2-\n'
|
|
assert_success
|
|
assert_line 'Excluded from this run: su-dummy-b'
|
|
[[ $(versions) == "a=2.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
|
[[ $(cat /etc/zypp/locks-su-e2e) == "$before" ]]
|
|
}
|
|
|
|
@test "zypper: the reviewed metadata is applied (no refresh between review and upgrade)" {
|
|
[[ $BE == zypper ]] || skip "zypper only (libdnf reads file:// repositories live; apt never refreshes on upgrade)"
|
|
mkfifo "$BATS_TEST_TMPDIR/in"
|
|
skeptical-updater <"$BATS_TEST_TMPDIR/in" >"$BATS_TEST_TMPDIR/out" 2>&1 &
|
|
pid=$!
|
|
exec {w}>"$BATS_TEST_TMPDIR/in" # (fd 3 belongs to bats)
|
|
for _ in $(seq 1 600); do
|
|
grep -q '^<enter> upgrade' "$BATS_TEST_TMPDIR/out" && break
|
|
sleep 0.1
|
|
done
|
|
# a newer su-dummy-c appears in the repository after the review
|
|
rpm_publish su-dummy-c 3.0
|
|
echo >&"$w"
|
|
exec {w}>&-
|
|
rc=0
|
|
wait "$pid" || rc=$?
|
|
rpm_unpublish su-dummy-c 3.0
|
|
[[ $rc -eq 0 ]] || fail "exit $rc: $(cat "$BATS_TEST_TMPDIR/out")"
|
|
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
|
}
|
|
|
|
@test "pacman: package databases synced elsewhere during the review: refused (exit 2), nothing changed" {
|
|
[[ $BE == pacman ]] || skip "pacman only"
|
|
session_start
|
|
sleep 1
|
|
pacman_publish su-dummy-c 3.0
|
|
pacman -Sy >/dev/null 2>&1 # another terminal: the system's database is now newer than the reviewed one
|
|
synced=$(sha256sum /var/lib/pacman/sync/su-dummy.db | cut -d' ' -f1)
|
|
session_finish ''
|
|
pacman_unpublish
|
|
pacman -Sy >/dev/null 2>&1
|
|
[[ $SESSION_RC -eq 2 ]] || fail "exit $SESSION_RC: $(cat "$BATS_TEST_TMPDIR/out")"
|
|
grep -q 'package databases changed since the review (su-dummy); run skeptical-updater again' "$BATS_TEST_TMPDIR/out"
|
|
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
|
[[ ! -e /var/lib/pacman/db.lck ]]
|
|
[[ -n $synced ]]
|
|
}
|
|
|
|
@test "pacman: <enter> installs exactly the reviewed metadata (the real sync DB becomes the reviewed one)" {
|
|
[[ $BE == pacman ]] || skip "pacman only"
|
|
# the system's database is older than what the review will see (su-dummy-c 3.0) ...
|
|
# (1 s apart from the previous repo rebuild: pacman compares database mtimes in seconds)
|
|
sleep 1
|
|
pacman_publish su-dummy-c 3.0
|
|
session_start
|
|
reviewed=$(sha256sum /tmp/skeptical-updater-db."$SESSION_PID".*/sync/su-dummy.db | cut -d' ' -f1)
|
|
# ... and su-dummy-c 4.0 appears in the repository after the review
|
|
sleep 1
|
|
pacman_publish su-dummy-c 4.0
|
|
session_finish ''
|
|
pacman_unpublish
|
|
[[ $SESSION_RC -eq 0 ]] || fail "exit $SESSION_RC: $(cat "$BATS_TEST_TMPDIR/out")"
|
|
grep -qE '^\+ cp -p -- /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+/sync/su-dummy\.db /var/lib/pacman/sync/$' "$BATS_TEST_TMPDIR/out"
|
|
[[ -n $reviewed ]]
|
|
[[ $(sha256sum /var/lib/pacman/sync/su-dummy.db | cut -d' ' -f1) == "$reviewed" ]] || fail "the system's su-dummy.db is not the reviewed one"
|
|
[[ $(versions) == "a=2.0 b=2.0 c=3.0" ]] || fail "versions after the run: $(versions)"
|
|
[[ ! -e /var/lib/pacman/db.lck ]]
|
|
run ls /tmp
|
|
refute_output --partial 'skeptical-updater-db.'
|
|
}
|