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