170 lines
7.1 KiB
Bash
170 lines
7.1 KiB
Bash
#!/usr/bin/env bash
|
|
# test_wiki_lock.sh — unit tests for scripts/wiki-lock.sh.
|
|
#
|
|
# Hermetic: creates a throwaway vault under mktemp, no network, no external
|
|
# deps beyond bash + standard POSIX utilities. Covers:
|
|
# - acquire returns 0 on first call, 75 on second call from a holding context
|
|
# - release frees the lock and re-acquire works
|
|
# - list shows held locks; reflects releases
|
|
# - clear-stale removes locks for dead PIDs
|
|
# - peek is read-only and reports unheld/held correctly
|
|
# - path validation rejects absolute paths and traversal
|
|
#
|
|
# Usage: bash tests/test_wiki_lock.sh
|
|
|
|
set -uo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
LOCK_SH="$ROOT/scripts/wiki-lock.sh"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
assert_eq() {
|
|
local label="$1" expected="$2" actual="$3"
|
|
if [ "$expected" = "$actual" ]; then
|
|
echo "OK $label"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "FAIL $label: expected '$expected', got '$actual'"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
assert_true() {
|
|
local label="$1"
|
|
shift
|
|
if "$@"; then
|
|
echo "OK $label"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "FAIL $label"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
# Set up a sandbox vault for the duration of this run
|
|
SANDBOX=$(mktemp -d /tmp/wiki-lock-test-XXXXXX)
|
|
trap 'rm -rf "$SANDBOX"' EXIT
|
|
mkdir -p "$SANDBOX/.vault-meta/locks"
|
|
export WIKI_LOCK_VAULT="$SANDBOX"
|
|
|
|
# Helper: run wiki-lock.sh against the sandbox; return rc
|
|
wl() {
|
|
bash "$LOCK_SH" "$@"
|
|
}
|
|
|
|
echo "=== test_wiki_lock.sh ==="
|
|
echo "sandbox: $SANDBOX"
|
|
echo ""
|
|
|
|
# ── acquire on a fresh path returns 0 ────────────────────────────────────────
|
|
wl acquire wiki/concepts/Foo.md >/dev/null
|
|
assert_eq "first acquire rc" "0" "$?"
|
|
|
|
# ── second acquire while the lock is fresh returns 75 ────────────────────────
|
|
# With age-based staleness (STALE_AFTER_SEC=60 default), the lock is held until
|
|
# either an explicit release OR 60 seconds elapse. A second acquire immediately
|
|
# after the first should refuse.
|
|
RC2=$( (wl acquire wiki/concepts/Foo.md >/dev/null); echo $? )
|
|
assert_eq "second acquire while fresh rc" "75" "$RC2"
|
|
|
|
# ── peek shows the lock ──────────────────────────────────────────────────────
|
|
PEEK_OUT=$(wl peek wiki/concepts/Foo.md)
|
|
case "$PEEK_OUT" in
|
|
*"wiki/concepts/Foo.md"*) assert_eq "peek includes path" "yes" "yes" ;;
|
|
*) assert_eq "peek includes path" "yes" "no($PEEK_OUT)" ;;
|
|
esac
|
|
|
|
# ── list shows the held lock ─────────────────────────────────────────────────
|
|
LIST_OUT=$(wl list)
|
|
case "$LIST_OUT" in
|
|
*"wiki/concepts/Foo.md"*) assert_eq "list shows held lock" "yes" "yes" ;;
|
|
*) assert_eq "list shows held lock" "yes" "no" ;;
|
|
esac
|
|
|
|
# ── release frees the lock (cross-process release is allowed by design) ─────
|
|
wl release wiki/concepts/Foo.md
|
|
LIST_AFTER_RELEASE=$(wl list)
|
|
assert_eq "list empty after release" "" "$LIST_AFTER_RELEASE"
|
|
|
|
# ── re-acquire after release succeeds ───────────────────────────────────────
|
|
wl acquire wiki/concepts/Foo.md >/dev/null
|
|
assert_eq "re-acquire after release rc" "0" "$?"
|
|
wl release wiki/concepts/Foo.md
|
|
|
|
# ── short --stale-after-sec lets us test age-based reap quickly ─────────────
|
|
# Acquire with a 1-second stale window, sleep 2s, second acquire should succeed
|
|
wl --stale-after-sec 1 acquire wiki/concepts/Aged.md >/dev/null 2>&1 || \
|
|
bash "$LOCK_SH" acquire --stale-after-sec 1 wiki/concepts/Aged.md >/dev/null 2>&1
|
|
# (flag order tolerance) — make sure the lock exists
|
|
PEEK_AGED=$(wl peek wiki/concepts/Aged.md)
|
|
case "$PEEK_AGED" in
|
|
*Aged.md*) : ;;
|
|
*) echo "DEBUG: aged peek was: $PEEK_AGED" ;;
|
|
esac
|
|
sleep 2
|
|
RC_AGED=$( (bash "$LOCK_SH" --stale-after-sec 1 acquire wiki/concepts/Aged.md >/dev/null 2>&1); echo $? )
|
|
assert_eq "age-based stale reap allows re-acquire" "0" "$RC_AGED"
|
|
wl release wiki/concepts/Aged.md
|
|
|
|
# ── clear-stale with max-age=0 reaps everything ──────────────────────────────
|
|
# First seed a lock to reap
|
|
wl acquire wiki/concepts/Reap.md >/dev/null
|
|
REMOVED=$(wl clear-stale --max-age 0)
|
|
# Should have removed 1 (the Reap.md lock)
|
|
case "$REMOVED" in
|
|
[1-9]*) assert_eq "clear-stale removed count >=1" "yes" "yes" ;;
|
|
*) assert_eq "clear-stale removed count >=1" "yes" "no($REMOVED)" ;;
|
|
esac
|
|
LIST_AFTER_CLEAR=$(wl list)
|
|
assert_eq "list empty after clear-stale" "" "$LIST_AFTER_CLEAR"
|
|
|
|
# ── peek on unheld path ──────────────────────────────────────────────────────
|
|
PEEK_UNHELD=$(wl peek wiki/concepts/Never.md)
|
|
assert_eq "peek unheld" "unheld" "$PEEK_UNHELD"
|
|
|
|
# ── path validation: absolute path rejected ──────────────────────────────────
|
|
RC_ABS=$( (wl acquire /etc/passwd >/dev/null 2>&1); echo $? )
|
|
assert_eq "acquire absolute path rejected" "4" "$RC_ABS"
|
|
|
|
# ── path validation: traversal rejected ──────────────────────────────────────
|
|
RC_DOTDOT=$( (wl acquire ../escape.md >/dev/null 2>&1); echo $? )
|
|
assert_eq "acquire ../ rejected" "4" "$RC_DOTDOT"
|
|
|
|
# ── path validation: empty rejected ──────────────────────────────────────────
|
|
RC_EMPTY=$( (wl acquire "" >/dev/null 2>&1); echo $? )
|
|
assert_eq "acquire empty path rejected" "4" "$RC_EMPTY"
|
|
|
|
# ── path validation: newline rejected (v1.7.2; closes audit M4) ──────────────
|
|
# Newlines in lock paths would break the meta-lock line format (key=value lines
|
|
# separated by literal \n). Must be rejected at validate_path() time.
|
|
RC_NL=$( (wl acquire $'wiki/concepts/Foo\nbar.md' >/dev/null 2>&1); echo $? )
|
|
assert_eq "acquire newline path rejected" "4" "$RC_NL"
|
|
|
|
# ── path validation: carriage return rejected (v1.7.2; closes audit M4) ──────
|
|
RC_CR=$( (wl acquire $'wiki/concepts/Foo\rbar.md' >/dev/null 2>&1); echo $? )
|
|
assert_eq "acquire carriage-return path rejected" "4" "$RC_CR"
|
|
|
|
# ── stress: 10 unique paths all acquire cleanly ──────────────────────────────
|
|
for i in $(seq 1 10); do
|
|
wl acquire "wiki/stress/page-$i.md" >/dev/null
|
|
rc=$?
|
|
if [ $rc -ne 0 ]; then
|
|
echo "FAIL stress acquire $i: rc=$rc"
|
|
FAIL=$((FAIL + 1))
|
|
break
|
|
fi
|
|
done
|
|
LIST_COUNT=$(wl list | wc -l)
|
|
assert_eq "10 unique paths all acquired" "10" "$LIST_COUNT"
|
|
wl clear-stale --max-age 0 >/dev/null
|
|
|
|
# ── summary ──────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo "Pass: $PASS Fail: $FAIL"
|
|
if [ $FAIL -gt 0 ]; then
|
|
exit 1
|
|
fi
|
|
echo "All wiki-lock tests passed."
|