273 lines
15 KiB
Bash
Executable file
273 lines
15 KiB
Bash
Executable file
#!/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"
|