arch-system/tests/deploy/test.sh

273 lines
15 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Exécuter seulement une copie de deploy.sh dans un bwrap sans réseau, avec / en lecture seule.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
command -v bwrap >/dev/null || { echo 'bwrap requis pour le confinement' >&2; exit 1; }
TEST_HOME="$(mktemp -d)"
trap 'rm -rf -- "$TEST_HOME"' EXIT
passes=0
case_dir=''
rc=0
fail() { printf 'ÉCHEC : %s\n' "$*" >&2; exit 1; }
pass() { passes=$((passes + 1)); }
expect_rc() { [[ "$rc" == "$1" ]] || fail "code $rc, attendu $1 ; sortie : $(cat "$case_dir/output")"; }
contains() { grep -Fq -- "$1" "$case_dir/output" || fail "texte absent : $1"; }
not_contains() { ! grep -Fq -- "$1" "$case_dir/output" || fail "texte interdit : $1"; }
no_sudo() { ! grep -q '^sudo ' "$case_dir/calls" || fail 'appel sudo dans un scénario sans mutation'; }
clean_nominal() {
[[ ! -s "$case_dir/stderr" ]] || fail "stderr inattendu : $(cat "$case_dir/stderr")"
! grep -Eq 'REFUSED|commande introuvable|command not found|ligne [0-9]+:' "$case_dir/output" ||
fail 'diagnostic parasite dans un scénario nominal'
}
clean_error() {
! grep -Eq 'REFUSED|commande introuvable|command not found|ligne [0-9]+:' "$case_dir/output" ||
fail 'diagnostic parasite dans un scénario d’erreur'
if grep -Ev '^\[!\] |^Option inconnue : |^simulated (sudo refusal|parent inspection error|second inspection error)$' "$case_dir/stderr" > "$case_dir/unexpected-stderr"; then
fail "stderr non prévu : $(cat "$case_dir/unexpected-stderr")"
fi
}
new_case() {
case_dir="$(mktemp -d "$TEST_HOME/case.XXXXXXXX")"
mkdir -p "$case_dir"/{repo/scripts,repo/pkglist,repo/manifests,repo/templates/sudoers.d,repo/rootfs/etc/tlp.d,repo/rootfs/etc/systemd,repo/rootfs/etc/snapper/configs,repo/rootfs/usr/local/sbin,bin,etc/usr,usr-local-sbin,tmp,backups}
cp "$ROOT/scripts/deploy.sh" "$case_dir/repo/scripts/deploy.sh"
chmod 755 "$case_dir/repo/scripts/deploy.sh"
for source in pacman.conf makepkg.conf mkinitcpio.conf nftables.conf limine-snapper-sync.conf; do
printf 'fixture %s\n' "$source" > "$case_dir/repo/rootfs/etc/$source"
done
printf 'fixture tlp\n' > "$case_dir/repo/rootfs/etc/tlp.d/10-archasp.conf"
printf 'fixture zram\n' > "$case_dir/repo/rootfs/etc/systemd/zram-generator.conf"
printf 'fixture snapper\n' > "$case_dir/repo/rootfs/etc/snapper/configs/root"
for helper in batlimit-set vmware-stack; do
printf '#!/usr/bin/env bash\nexit 0\n' > "$case_dir/repo/rootfs/usr/local/sbin/$helper"
done
printf '@USER@ ALL=(root) NOPASSWD: /usr/local/sbin/batlimit-set\n' > "$case_dir/repo/templates/sudoers.d/batlimit"
printf '@USER@ ALL=(root) NOPASSWD: /usr/local/sbin/vmware-stack start\n' > "$case_dir/repo/templates/sudoers.d/vmware-stack"
printf '# comment\n alpha \n\n' > "$case_dir/repo/pkglist/pacman.txt"
printf 'foreign\n' > "$case_dir/repo/pkglist/aur.txt"
printf 'alpha.service\n' > "$case_dir/repo/manifests/system-services.txt"
printf 'beta.timer\n' > "$case_dir/repo/manifests/system-timers.txt"
printf 'ondemand.service\n' > "$case_dir/repo/manifests/on-demand-system-services.txt"
printf 'alpha.service\nbeta.timer\nondemand.service\n' > "$case_dir/units"
printf 'existing config\n' > "$case_dir/etc/pacman.conf"
printf 'fixturehost\n' > "$case_dir/etc/hostname"
uid="$(id -u)" gid="$(id -g)"
printf 'fixtureuser:x:%s:%s:Fixture:/mnt:/usr/bin/bash\n' "$uid" "$gid" > "$case_dir/etc/passwd"
printf 'fixture:x:%s:\n' "$gid" > "$case_dir/etc/group"
cp /etc/nsswitch.conf "$case_dir/etc/nsswitch.conf"
for safe in bash cat dirname find grep sed mktemp rm rmdir; do
[[ -x "/usr/bin/$safe" ]] || fail "outil réel absent : /usr/bin/$safe"
ln -s "/usr/bin/$safe" "$case_dir/bin/$safe"
done
for mocked in sudo systemctl yay pacman nft visudo tlp install id; do
ln -s /mnt/mock-command.sh "$case_dir/bin/$mocked"
done
cp "$ROOT/tests/deploy/mock-command.sh" "$case_dir/mock-command.sh"
chmod 755 "$case_dir/mock-command.sh"
: > "$case_dir/calls"
}
# Le bind de /etc et /usr/local/sbin fournit uniquement des cibles synthétiques.
# / est en lecture seule, /tmp et /mnt sont les seuls espaces inscriptibles,
# le réseau est absent, et PATH n'offre que des lectures sûres ou nos doublures.
run_case() {
local name="$1"
shift
: > "$case_dir/output"
: > "$case_dir/stderr"
set +e
printf '%s\n' "${SIM_ANSWER:-y}" | bwrap --unshare-user --unshare-pid --unshare-net \
--ro-bind / / --bind "$case_dir/etc" /etc \
--bind "$case_dir/usr-local-sbin" /usr/local/sbin \
--bind "$case_dir/tmp" /tmp --bind "$case_dir" /mnt \
--proc /proc --dev /dev \
--setenv PATH /mnt/bin --setenv HOME /mnt --setenv TMPDIR /tmp \
--setenv SIM_FAIL_NFT "${SIM_FAIL_NFT:-0}" \
--setenv SIM_FAIL_VISUDO "${SIM_FAIL_VISUDO:-0}" \
--setenv SIM_FAIL_INSTALL_TARGET "${SIM_FAIL_INSTALL_TARGET:-}" \
--setenv SIM_INTERRUPT_TARGET "${SIM_INTERRUPT_TARGET:-}" \
--setenv SIM_INTERRUPT_SIGNAL "${SIM_INTERRUPT_SIGNAL:-TERM}" \
--setenv SIM_INSPECT_TARGET "${SIM_INSPECT_TARGET:-}" \
--setenv SIM_INSPECT_MODE "${SIM_INSPECT_MODE:-}" \
--setenv SIM_FAIL_SYSTEMCTL "${SIM_FAIL_SYSTEMCTL:-}" \
--setenv SIM_MISSING_UNIT "${SIM_MISSING_UNIT:-}" \
--setenv SIM_UNIT_ENABLED "${SIM_UNIT_ENABLED:-disabled}" \
--setenv SIM_UNIT_ACTIVE "${SIM_UNIT_ACTIVE:-inactive}" \
--setenv SIM_ID_ROOT "${SIM_ID_ROOT:-0}" \
--setenv USER root \
-- /mnt/repo/scripts/deploy.sh "$@" > "$case_dir/stdout" 2> "$case_dir/stderr"
rc=$?
set -e
cat "$case_dir/stdout" "$case_dir/stderr" > "$case_dir/output"
printf '[test] %s : rc=%s\n' "$name" "$rc"
}
new_case
run_case help --help; expect_rc 0; no_sudo; clean_nominal; pass
run_case no_selection; expect_rc 2; no_sudo; clean_error; pass
run_case bad_option --unknown; expect_rc 2; no_sudo; clean_error; pass
for selection in --packages --configs --tools --services --all; do
new_case
run_case "dry_$selection" --dry-run "$selection"
expect_rc 0; contains 'déploiement non effectué'; not_contains 'Déploiement sélectionné terminé'
no_sudo
clean_nominal
if [[ "$selection" == --all ]]; then
contains '/etc/snapper/configs/root'
contains '/etc/sudoers.d/batlimit'
contains 'Activer : alpha.service'
fi
[[ ! -e "$case_dir/counter" ]] || fail 'sauvegarde créée en simulation'
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaire créé en simulation'
pass
done
new_case
rm "$case_dir/repo/rootfs/etc/pacman.conf"
run_case missing_source --dry-run --configs; expect_rc 1; contains 'source obligatoire'; no_sudo; clean_error; pass
new_case
rm "$case_dir/repo/rootfs/etc/pacman.conf"
run_case all_missing_source --all; expect_rc 1; contains 'source obligatoire'; no_sudo; clean_error; pass
new_case
rm "$case_dir/repo/manifests/system-services.txt"
run_case missing_manifest --dry-run --services; expect_rc 1; contains 'liste/manifest absent'; no_sudo; clean_error; pass
new_case
printf 'bad option\n' >> "$case_dir/repo/pkglist/pacman.txt"
run_case bad_line --dry-run --packages; expect_rc 1; contains 'nom de paquet invalide'; no_sudo; clean_error; pass
new_case
: > "$case_dir/repo/pkglist/pacman.txt"
: > "$case_dir/repo/pkglist/aur.txt"
run_case empty_lists --dry-run --packages; expect_rc 0; contains '(0 entrées)'; no_sudo; clean_nominal; pass
run_case empty_lists_apply --packages; expect_rc 0; clean_nominal
! grep -q '^sudo pacman ' "$case_dir/calls" || fail 'pacman appelé pour liste vide'
! grep -q '^yay ' "$case_dir/calls" || fail 'yay appelé pour liste vide'
pass
new_case
rm "$case_dir/bin/yay"
run_case no_yay --packages; expect_rc 1; contains 'commande requise absente : yay'; no_sudo; clean_error; pass
new_case
SIM_ID_ROOT=1 run_case invalid_identity --tools; expect_rc 1; contains 'compte bénéficiaire root refusé'; no_sudo; clean_error; pass
new_case
SIM_FAIL_NFT=1 run_case nft_rejected --configs; expect_rc 1; contains 'validation nftables'
! grep -q '^sudo install ' "$case_dir/calls" || fail 'configuration installée après échec nft'; pass
new_case
SIM_FAIL_VISUDO=1 run_case sudoers_rejected --tools; expect_rc 1; contains 'validation sudoers'
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaire sudoers non nettoyé'
! grep -q '^sudo install ' "$case_dir/calls" || fail 'helper installé avant validation sudoers'; pass
new_case
SIM_FAIL_INSTALL_TARGET=/etc/makepkg.conf run_case partial_after_packages --packages --configs
expect_rc 1; contains 'Étapes achevées : paquets'; not_contains 'Déploiement sélectionné terminé'
contains 'installation:/etc/pacman.conf'
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/existing/etc/pacman.conf" ]] || fail 'sauvegarde existante absente'
clean_error; pass
# R1 : un échec du contrôle sudo/find, même avec un état plausible, n'est jamais ABSENT.
for mode in fail parent_fail empty unknown valid_fail; do
new_case
SIM_INSPECT_TARGET=/etc/pacman.conf SIM_INSPECT_MODE="$mode" run_case "inspect_$mode" --configs
expect_rc 1
if [[ "$mode" == empty || "$mode" == unknown ]]; then
contains 'réponse de contrôle invalide'
else
contains 'contrôle privilégié impossible'
fi
not_contains 'Déploiement sélectionné terminé'
[[ "$(cat "$case_dir/etc/pacman.conf")" == 'existing config' ]] || fail 'cible existante modifiée après contrôle invalide'
[[ ! -e "$case_dir/backups/arch-system-restore.SIM0001/absent/etc/pacman.conf" ]] || fail 'fausse absence après contrôle invalide'
! grep -Fq 'sudo install -D -m 0644' "$case_dir/calls" || fail 'installation après contrôle invalide'
clean_error; pass
done
new_case
SIM_INSPECT_TARGET=/etc/pacman.conf SIM_INSPECT_MODE=second_fail run_case inspect_second_fail --configs
expect_rc 1; contains 'contrôle privilégié impossible'
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/existing/etc/pacman.conf" ]] || fail 'sauvegarde perdue après second contrôle'
[[ "$(cat "$case_dir/etc/pacman.conf")" == 'existing config' ]] || fail 'cible modifiée après second contrôle'
! grep -Fq 'sudo install -D -m 0644 /mnt/repo/rootfs/etc/pacman.conf /etc/pacman.conf' "$case_dir/calls" || fail 'installation après second contrôle'
clean_error; pass
new_case
run_case first_backup --configs; expect_rc 0
clean_nominal
run_case second_backup --configs; expect_rc 0
clean_nominal
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/existing/etc/pacman.conf" ]] || fail 'première sauvegarde absente'
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/absent/etc/makepkg.conf" ]] || fail 'marqueur de cible absente absent'
[[ -e "$case_dir/backups/arch-system-restore.SIM0002/existing/etc/makepkg.conf" ]] || fail 'seconde sauvegarde absente'
[[ "$(cat "$case_dir/backups/arch-system-restore.SIM0001/existing/etc/pacman.conf")" == 'existing config' ]] || fail 'sauvegarde initiale incorrecte'
[[ "$(cat "$case_dir/etc/makepkg.conf")" == 'fixture makepkg.conf' ]] || fail 'installation de cible absente manquante'
pass
new_case
rm "$case_dir/etc/pacman.conf"
ln -s /mnt/escape "$case_dir/etc/pacman.conf"
run_case dangling_target --dry-run --configs; expect_rc 1; contains 'cible liée refusée'; no_sudo; pass
new_case
ln -s /mnt/escape "$case_dir/etc/tlp.d"
run_case linked_parent --dry-run --configs; expect_rc 1; contains 'répertoire cible lié refusé'; no_sudo; pass
new_case
SIM_FAIL_INSTALL_TARGET=/usr/local/sbin/batlimit-set run_case temp_cleanup_failure --tools
expect_rc 1
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaire non nettoyé après erreur'
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/absent/usr/local/sbin/batlimit-set" ]] || fail 'marqueur supprimé après erreur'
pass
new_case
SIM_INTERRUPT_TARGET=/usr/local/sbin/batlimit-set run_case term_cleanup --tools
expect_rc 143
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaire non nettoyé après TERM'
contains 'Interruption TERM (143)'; contains 'Sauvegardes et marqueurs conservés'; clean_error; pass
for signal in INT TERM; do
new_case
SIM_INTERRUPT_TARGET=/usr/local/sbin/vmware-stack SIM_INTERRUPT_SIGNAL="$signal" run_case "interrupt_second_$signal" --tools
if [[ "$signal" == INT ]]; then expect_rc 130; else expect_rc 143; fi
contains "Interruption $signal"; contains 'phase : outils root'
contains 'installation:/usr/local/sbin/batlimit-set'
contains 'Action interrompue, effets à vérifier : installation /usr/local/sbin/vmware-stack'
contains 'Sauvegardes et marqueurs conservés : /root/arch-system-restore.SIM0001'
not_contains 'Déploiement sélectionné terminé'
[[ -e "$case_dir/usr-local-sbin/batlimit-set" ]] || fail 'premier helper non installé avant interruption'
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/absent/usr/local/sbin/vmware-stack" ]] || fail 'marqueur non conservé'
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaires non nettoyés après signal'
clean_error; pass
done
new_case
rm "$case_dir/etc/hostname"
run_case missing_hostname --configs; expect_rc 1; contains '/etc/hostname illisible ou absent'; no_sudo; clean_error; pass
new_case
SIM_ANSWER=n run_case user_cancel --configs; expect_rc 0; contains 'Déploiement annulé'; no_sudo; clean_nominal; pass
new_case
SIM_MISSING_UNIT=alpha.service run_case missing_unit --dry-run --services
expect_rc 1; contains 'unité requise absente'; no_sudo; pass
new_case
SIM_FAIL_SYSTEMCTL=enable run_case failed_enable --services
expect_rc 1; contains 'activation alpha.service'; not_contains 'Déploiement sélectionné terminé'; pass
new_case
SIM_UNIT_ENABLED=enabled SIM_FAIL_SYSTEMCTL=disable run_case failed_disable --tools
expect_rc 1; contains 'désactivation ondemand.service'; not_contains 'Déploiement sélectionné terminé'; pass
new_case
SIM_MISSING_UNIT=alpha.service run_case deferred_unit_failure --packages --services
expect_rc 1; contains 'Étapes achevées : paquets'; contains 'unité requise absente'; pass
new_case
run_case tools_identity --tools; expect_rc 0
clean_nominal
grep -Fq 'fixtureuser ALL=' "$case_dir/etc/sudoers.d/batlimit" || fail 'sudoers utilisateur erroné'
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaire non nettoyé en réussite'
pass
new_case
run_case all_order --all; expect_rc 0
clean_nominal
python3 - "$case_dir/calls" <<'PY'
from pathlib import Path
import sys
s=Path(sys.argv[1]).read_text()
for a,b in [('sudo pacman -Syu','yay -S'),('yay -S','sudo install -D'),('sudo install -D','sudo systemctl enable')]:
if s.find(a)<0 or s.find(b)<0 or s.find(a)>=s.find(b): raise SystemExit(f'order failure: {a} -> {b}')
PY
pass
printf 'Résultat : %s scénarios validés dans bwrap ; aucun script du dépôt exécuté hors copie.\n' "$passes"