On the pacman backend, an installed AUR helper (paru, else yay;
--aur-helper=paru|yay|none, --no-aur) adds the AUR updates to the review
list. The helper never runs as root: when the tool is root through sudo it
runs as SUDO_USER (runuser -u, else sudo -u, with that user's HOME, from
that home); unprivileged it runs as is; a root login gets no AUR, with a
warning. The banner names the helper ("+ AUR via paru").
- listing: <helper> -Qua (also with --no-refresh), rows with repository
"aur"; [ignored] is shown [held]; a failing -Qua warns and the session
goes on with the repository rows
- every row has a marker column after the number: "*" for AUR rows, a
space otherwise (on every backend)
- N?? merges pacman -Qi with <helper> -Sia; N?, N+, Nx stay with pacman;
N- excludes through the helper's --ignore
- <enter>: the repository step exactly as before, then <helper> -Sua
--ignore <excluded> as the invoking user (--noconfirm only when stdin is
not a terminal); pacman is skipped when only AUR rows are left; a helper
failure is reported and its status returned; --dry-run prints it
- other backends never run a helper; --aur-helper is ignored with a warning
paru 2.1.0 and yay 13.0.1 formats verified in discovery/pacman.md section
15 (proof: discovery/pacman/aur.sh). Unit tests replay the recorded output
(fake paru, yay, runuser, getent); the archlinux E2E run installs the
foreign su-dummy-aur and drives a fake paru through a fake sudo built on
passwordless su for wheel.
261 lines
9.4 KiB
Bash
261 lines
9.4 KiB
Bash
#!/usr/bin/env bats
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
# Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
# The statefile: creation, format, sorting, dedupe, comments, atomic writes, trust/untrust/refresh.
|
|
|
|
load test_helper
|
|
|
|
setup() { common_setup; }
|
|
|
|
@test "trust creates the directory (0755) and the file (0644) with the header" {
|
|
run skup trust su-dummy-c
|
|
assert_success
|
|
assert_output 'trusted su-dummy-c'
|
|
[[ $(stat -c %a "$STATE") == 755 ]]
|
|
[[ $(stat -c %a "$STATE_FILE") == 644 ]]
|
|
run cat "$STATE_FILE"
|
|
assert_line --index 0 "$HEADER"
|
|
assert_line --index 1 --regexp "^apt${T}su-dummy-c${T}user${T}${DATE_RE}\$"
|
|
[[ ${#lines[@]} -eq 2 ]]
|
|
}
|
|
|
|
@test "intermediate directories are created too" {
|
|
run skup --state-dir="$BATS_TEST_TMPDIR/a/b/c" trust su-dummy-c
|
|
assert_success
|
|
[[ -f $BATS_TEST_TMPDIR/a/b/c/trusted.tsv ]]
|
|
[[ $(stat -c %a "$BATS_TEST_TMPDIR/a/b/c") == 755 ]]
|
|
}
|
|
|
|
@test "trust writes the user row and dep:<root> rows for the installed closure" {
|
|
run skup trust su-dummy-a
|
|
assert_success
|
|
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
|
run cat "$STATE_FILE"
|
|
assert_line --index 1 --regexp "^apt${T}su-dummy-a${T}user${T}${DATE_RE}\$"
|
|
assert_line --index 2 --regexp "^apt${T}su-dummy-b${T}dep:su-dummy-a${T}${DATE_RE}\$"
|
|
log_has "dpkg-query -W -f \${db:Status-Abbrev}\\t\${Package}\\t\${Pre-Depends}\\t\${Depends}\\t\${Provides}\\n"
|
|
}
|
|
|
|
@test "the apt closure follows Provides (virtual packages) and alternatives" {
|
|
run skup trust su-dummy-w
|
|
assert_success
|
|
assert_output 'trusted su-dummy-w (+1 dependency: su-dummy-v)'
|
|
}
|
|
|
|
@test "the apt closure walks real chains through virtual packages (base-files -> awk -> mawk)" {
|
|
run skup trust base-files
|
|
assert_success
|
|
assert_output 'trusted base-files (+4 dependencies: gcc-12-base libc6 libgcc-s1 mawk)'
|
|
run cut -f2,3 "$STATE_FILE"
|
|
assert_line "mawk${T}dep:base-files"
|
|
assert_line "gcc-12-base${T}dep:base-files"
|
|
}
|
|
|
|
@test "a not-installed (rc) package is not part of a closure" {
|
|
run skup trust su-dummy-f
|
|
assert_success
|
|
assert_output 'trusted su-dummy-f'
|
|
}
|
|
|
|
@test "records are sorted on (backend, package); comments are kept in order" {
|
|
write_state '# my own note' \
|
|
"pacman${T}zsh${T}user${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}zsh${T}user${T}2026-01-01T00:00:00Z" \
|
|
'# second note' \
|
|
"apt${T}foo-bar${T}user${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}foo${T}dep:zsh${T}2026-01-01T00:00:00Z" \
|
|
"dnf${T}abc${T}user${T}2026-01-01T00:00:00Z"
|
|
run skup trust su-dummy-c
|
|
assert_success
|
|
run cat "$STATE_FILE"
|
|
assert_line --index 0 '# my own note'
|
|
assert_line --index 1 '# second note'
|
|
assert_line --index 2 "apt${T}foo${T}dep:zsh${T}2026-01-01T00:00:00Z"
|
|
assert_line --index 3 "apt${T}foo-bar${T}user${T}2026-01-01T00:00:00Z"
|
|
assert_line --index 4 --regexp "^apt${T}su-dummy-c${T}user${T}"
|
|
assert_line --index 5 "apt${T}zsh${T}user${T}2026-01-01T00:00:00Z"
|
|
assert_line --index 6 "dnf${T}abc${T}user${T}2026-01-01T00:00:00Z"
|
|
assert_line --index 7 "pacman${T}zsh${T}user${T}2026-01-01T00:00:00Z"
|
|
[[ ${#lines[@]} -eq 8 ]]
|
|
}
|
|
|
|
@test "duplicates are collapsed: a user row wins over a dep row, else the first one" {
|
|
write_state "$HEADER" \
|
|
"apt${T}b${T}dep:x${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}b${T}user${T}2026-02-02T00:00:00Z" \
|
|
"apt${T}c${T}dep:x${T}2026-03-03T00:00:00Z" \
|
|
"apt${T}c${T}dep:y${T}2026-04-04T00:00:00Z"
|
|
run skup trust su-dummy-c
|
|
assert_success
|
|
run cat "$STATE_FILE"
|
|
assert_line "apt${T}b${T}user${T}2026-02-02T00:00:00Z"
|
|
assert_line "apt${T}c${T}dep:x${T}2026-03-03T00:00:00Z"
|
|
refute_line --partial 'dep:y'
|
|
refute_line --partial "b${T}dep:x"
|
|
}
|
|
|
|
@test "a user row is never downgraded to dep:" {
|
|
run skup trust su-dummy-b
|
|
run skup trust su-dummy-a
|
|
assert_success
|
|
assert_output 'trusted su-dummy-a (its installed dependency was already trusted)'
|
|
run cut -f2,3 "$STATE_FILE"
|
|
assert_line "su-dummy-b${T}user"
|
|
refute_line --partial 'dep:'
|
|
}
|
|
|
|
@test "an existing dep: row is upgraded to user, keeping one row" {
|
|
run skup trust su-dummy-a
|
|
run skup trust su-dummy-b
|
|
assert_success
|
|
assert_output 'trusted su-dummy-b'
|
|
run cut -f2,3 "$STATE_FILE"
|
|
assert_line "su-dummy-b${T}user"
|
|
[[ $(grep -c su-dummy-b "$STATE_FILE") -eq 1 ]]
|
|
}
|
|
|
|
@test "trusting an already trusted package reports it and changes nothing" {
|
|
run skup trust su-dummy-c
|
|
before=$(cat "$STATE_FILE")
|
|
run skup trust su-dummy-c
|
|
assert_success
|
|
assert_output 'su-dummy-c was already trusted'
|
|
[[ $(cat "$STATE_FILE") == "$before" ]]
|
|
}
|
|
|
|
@test "malformed lines are kept verbatim, with a warning" {
|
|
write_state "$HEADER" "apt${T}only-two" "apt${T}x${T}weird${T}2026-01-01T00:00:00Z"
|
|
run skup trust su-dummy-c
|
|
assert_success
|
|
assert_output --partial "trusted.tsv:2: malformed line kept verbatim: apt${T}only-two"
|
|
assert_output --partial "trusted.tsv:3: malformed line kept verbatim"
|
|
run cat "$STATE_FILE"
|
|
assert_line "apt${T}only-two"
|
|
assert_line "apt${T}x${T}weird${T}2026-01-01T00:00:00Z"
|
|
}
|
|
|
|
@test "blank lines and CRLF line ends are tolerated" {
|
|
printf '%s\r\n\r\n%s\r\n\n' "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" >"$BATS_TEST_TMPDIR/st"
|
|
mkdir -p "$STATE"
|
|
mv "$BATS_TEST_TMPDIR/st" "$STATE_FILE"
|
|
run skup list --no-refresh
|
|
assert_line '3 upgradable, 1 trusted (hidden, use --all to show), 2 to review:'
|
|
}
|
|
|
|
@test "writes are atomic (new file renamed over the old one) and leave no temp file" {
|
|
run skup trust su-dummy-c
|
|
ino1=$(stat -c %i "$STATE_FILE")
|
|
run skup trust su-dummy-a
|
|
assert_success
|
|
ino2=$(stat -c %i "$STATE_FILE")
|
|
[[ $ino1 != "$ino2" ]]
|
|
run ls -A "$STATE"
|
|
assert_output "$(printf '%s\n' trusted.lock trusted.tsv)"
|
|
}
|
|
|
|
@test "a failing write leaves the old file intact" {
|
|
run skup trust su-dummy-c
|
|
before=$(cat "$STATE_FILE")
|
|
# make the state dir read-only (root ignores directory modes: skip then)
|
|
chmod 0555 "$STATE"
|
|
if [[ -w $STATE ]]; then
|
|
chmod 0755 "$STATE"
|
|
skip "running as root: directory modes are not enforced"
|
|
fi
|
|
run skup trust su-dummy-a
|
|
chmod 0755 "$STATE"
|
|
assert_failure 1
|
|
[[ $(cat "$STATE_FILE") == "$before" ]]
|
|
}
|
|
|
|
@test "untrust removes the row for PKG and every dep:PKG row, nothing else" {
|
|
write_state "$HEADER" \
|
|
"apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}x${T}dep:y${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z" \
|
|
"pacman${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
|
"pacman${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
|
|
run skup untrust su-dummy-a
|
|
assert_success
|
|
assert_output 'untrusted su-dummy-a (2 rows removed)'
|
|
run cat "$STATE_FILE"
|
|
assert_output "$(printf '%s\n' "$HEADER" \
|
|
"apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}x${T}dep:y${T}2026-01-01T00:00:00Z" \
|
|
"pacman${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
|
"pacman${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z")"
|
|
}
|
|
|
|
@test "untrust of a dependency-only package removes its dep row" {
|
|
run skup trust su-dummy-a
|
|
run skup untrust su-dummy-b
|
|
assert_success
|
|
assert_output 'untrusted su-dummy-b (1 row removed)'
|
|
run cut -f2 "$STATE_FILE"
|
|
assert_line su-dummy-a
|
|
refute_line su-dummy-b
|
|
}
|
|
|
|
@test "untrust of a package that is not trusted warns, exit 0" {
|
|
run skup untrust nope
|
|
assert_success
|
|
assert_output --partial 'warning: nope is not trusted (no row for backend apt)'
|
|
[[ ! -e $STATE_FILE ]]
|
|
}
|
|
|
|
@test "refresh recomputes closures: adds missing rows, drops stale ones, keeps the rest" {
|
|
write_state "$HEADER" \
|
|
"apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}orphan${T}dep:gone${T}2026-01-01T00:00:00Z" \
|
|
"apt${T}su-dummy-w${T}user${T}2026-01-01T00:00:00Z" \
|
|
"pacman${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
|
|
run skup refresh
|
|
assert_success
|
|
assert_output 'refreshed the dependency closures of 2 trusted roots: 1 added, 2 removed'
|
|
run cat "$STATE_FILE"
|
|
assert_line "apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
|
|
assert_line --regexp "^apt${T}su-dummy-v${T}dep:su-dummy-w${T}${DATE_RE}\$"
|
|
refute_line --partial "apt${T}stale"
|
|
refute_line --partial 'orphan'
|
|
assert_line "pacman${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
|
|
assert_line "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
|
|
}
|
|
|
|
@test "refresh with no user roots for this backend does nothing" {
|
|
write_state "$HEADER" "pacman${T}x${T}user${T}2026-01-01T00:00:00Z"
|
|
run skup refresh
|
|
assert_success
|
|
assert_output 'No trusted root packages for backend apt: nothing to refresh.'
|
|
}
|
|
|
|
@test "trusted prints the statefile verbatim" {
|
|
write_state "$HEADER" '# note' "apt${T}x${T}user${T}2026-01-01T00:00:00Z"
|
|
run skup trusted
|
|
assert_success
|
|
assert_output "$(printf '%s\n' "$HEADER" '# note' "apt${T}x${T}user${T}2026-01-01T00:00:00Z")"
|
|
}
|
|
|
|
@test "trusted without a statefile says so, exit 0" {
|
|
run skup trusted
|
|
assert_success
|
|
assert_output "No trusted packages yet ($STATE_FILE does not exist)."
|
|
}
|
|
|
|
@test "only rows of the current backend hide packages" {
|
|
write_state "$HEADER" "pacman${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
|
|
run skup list --no-refresh
|
|
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
|
assert_line --regexp '^ \[1\] su-dummy-a '
|
|
}
|
|
|
|
@test "the reason does not matter for hiding: dep rows hide too" {
|
|
write_state "$HEADER" "apt${T}su-dummy-b${T}dep:whatever${T}2026-01-01T00:00:00Z"
|
|
run skup list --no-refresh
|
|
assert_line '3 upgradable, 1 trusted (hidden, use --all to show), 2 to review:'
|
|
refute_output --partial 'su-dummy-b'
|
|
}
|