test-failclosed.sh
#!/bin/bash
## Fail-closed behavior: every tampered or conflicting input is refused,
## loudly, and for its own specific reason (we check the message, not just
## the exit code, so each case stays tied to the guard it exercises).
set -euo pipefail
cd "$(dirname "$0")"
source ./lib.sh
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
make_corpus "$work/corpus"
source "$work/corpus/FACTS"
"$PATCHETTE" export "$work/corpus/dev" "$work/pristine" "$base" topic/fancy docs-polish >/dev/null
fancy=$'topic\u00a6fancy'
## Set up a fresh tampering workspace from the pristine export.
##
## - @param $1 case name; becomes the folder name under $work
## - @state TAMPER -- path to the fresh copy
clone_layout() {
TAMPER="$work/$1"
cp -r "$work/pristine" "$TAMPER"
}
# A tampered patch must make verify fail. Editing message content keeps the
# diff applying cleanly, so the only witness left is the tip comparison --
# exactly the guard this case is about.
clone_layout tampered
sed -i 's/> The hum is load-bearing./> The hum is decorative./' "$TAMPER/$fancy/teach-gizmo-to-hum.patch"
EXPECT refusal "verify fails on a tampered patch" "MISMATCH" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
# Import onto an existing branch is refused (before any replay work).
git clone -q "$work/corpus/upstream" "$work/occupied"
git -C "$work/occupied" branch docs-polish main
EXPECT refusal "import onto an existing branch is refused" "already exists" -- \
"$PATCHETTE" import "$work/occupied" "$work/pristine"
# A malformed message line is a parse error, never a guess.
clone_layout badmsg
sed -i 's/> Do not ask why./| Do not ask why./' "$TAMPER/$fancy/teach-gizmo-to-hum.patch"
EXPECT refusal "a malformed message line is a parse error" "malformed message line" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
# An unrecognized bare line in directive position (directly after the
# opening flank) is an error.
clone_layout baddirective
sed -i '0,/^----$/s//----\nmystery-directive/' "$TAMPER/$fancy/note-to-self.patch"
EXPECT refusal "an unknown directive line is an error" "unrecognized directive" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
# A directive is only legal before message content; after content has begun,
# the same bare line is just a malformed message line.
clone_layout latedirective
sed -i 's/^> Note to self$/> Note to self\nmessage-no-final-newline/' "$TAMPER/$fancy/note-to-self.patch"
EXPECT refusal "a directive after message content is a parse error" "malformed message line" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
# The directive's pre-spec position (bare line in the header area) must fail
# loudly, not be swallowed as a verbatim header into the replayed commit.
clone_layout olddirective
sed -i '1a message-no-final-newline' "$TAMPER/$fancy/note-to-self.patch"
EXPECT refusal "a bare line in the header area is a parse error" "malformed header line" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
# An unknown parent reference is an error.
clone_layout badparent
sed -i '1s/.*/parents no-such-slug/' "$TAMPER/$fancy/add-sprocket-docs.patch"
EXPECT refusal "an unknown parent reference is an error" "not BASE or an earlier slug" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
# base and tip must be full commit IDs: a ref name or an abbreviation could
# resolve differently in another repo, or later, so neither is guessed at.
clone_layout refbase
sed -i 's/^base .*/base main/' "$TAMPER/$fancy/STACK"
EXPECT refusal "a ref name as base is an error" "base is not a full commit ID" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
clone_layout shorttip
sed -i 's/^tip \(.\{12\}\).*/tip \1/' "$TAMPER/$fancy/STACK"
EXPECT refusal "an abbreviated tip is an error" "tip is not a full commit ID" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
clone_layout emptytip
sed -i 's/^tip .*/tip /' "$TAMPER/$fancy/STACK"
EXPECT refusal "a tip line with no value is an error, not an absent tip" "tip is not a full commit ID" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
# A repeated STACK key is an error: which value is meant can't be known.
clone_layout dupkey
sed -i '1a ref refs/heads/other' "$TAMPER/$fancy/STACK"
EXPECT refusal "a repeated STACK key is an error" "duplicate ref line" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
# A duplicate series entry is an error.
clone_layout dupseries
echo "note-to-self" >>"$TAMPER/$fancy/STACK"
EXPECT refusal "a duplicate series entry is an error" "duplicate series entry" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
# Folder discordances are errors for every reader: an orphan patch file
# (nothing would ever read it) and a series entry naming no file must be
# refused by verify and import alike.
clone_layout orphan
cp "$TAMPER/$fancy/note-to-self.patch" "$TAMPER/$fancy/stray.patch"
EXPECT refusal "an orphan patch file is refused by verify" "orphan" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
git clone -q "$work/corpus/upstream" "$work/orphantarget"
EXPECT refusal "an orphan patch file is refused by import" "orphan" -- \
"$PATCHETTE" import "$work/orphantarget" "$TAMPER"
EXPECT equal "the refused import created no refs" -- \
"$(git -C "$work/orphantarget" for-each-ref --format='%(refname)' 'refs/heads/*' --exclude refs/heads/main)" ""
clone_layout missingfile
rm "$TAMPER/$fancy/note-to-self.patch"
EXPECT refusal "a series entry naming no file is refused by verify" "note-to-self.patch is missing" -- \
"$PATCHETTE" verify "$work/corpus/dev" "$TAMPER"
EXPECT refusal "a series entry naming no file is refused by import" "note-to-self.patch is missing" -- \
"$PATCHETTE" import "$work/orphantarget" "$TAMPER"
# Export never deletes a folder it didn't write.
mkdir -p "$work/notours/$fancy"
touch "$work/notours/$fancy/precious.txt"
EXPECT refusal "export into a non-patchette folder is refused" "no STACK file" -- \
"$PATCHETTE" export "$work/corpus/dev" "$work/notours" "$base" topic/fancy
EXPECT success "export left the non-patchette folder untouched" -- \
test -f "$work/notours/$fancy/precious.txt"
# A failed export must leave the patches dir as it found it: the previous
# snapshot intact, and no half-written folder that could pass for a real one.
# Trigger: rewrite topic/fancy onto a commit forked from BEFORE the base,
# so its parent is out of range and export dies mid-branch.
cp -a "$work/corpus/dev" "$work/poisoned"
git -C "$work/poisoned" switch -qc side "$base^"
git -C "$work/poisoned" commit -q --allow-empty -m "Out of range"
git -C "$work/poisoned" branch -f topic/fancy side
clone_layout atomic
EXPECT refusal "export dies on an out-of-range parent" "out-of-range" -- \
"$PATCHETTE" export "$work/poisoned" "$TAMPER" "$base" topic/fancy docs-polish
EXPECT success "the failed re-export left the previous snapshot untouched" -- \
diff -r "$work/pristine" "$TAMPER"
mkdir "$work/freshout"
EXPECT refusal "export into a fresh dir dies the same way" "out-of-range" -- \
"$PATCHETTE" export "$work/poisoned" "$work/freshout" "$base" topic/fancy
EXPECT equal "the failed fresh export left no debris behind" -- \
"$(ls -A "$work/freshout")" ""
# Importing where the recorded base is absent tells the user what to do.
git init -q -b main "$work/empty"
EXPECT refusal "import without the recorded base points at fetching" "fetch from upstream first" -- \
"$PATCHETTE" import "$work/empty" "$work/pristine"
exit "$RESULT"
tests/test-failclosed.sh · 7.3 KiB · 2df663fec9