One liveness primitive instead of two. The PID argument was a vestige (format-checked, never probed, since the kill -0 removal); dropping it means a roster with a garbage SERVER_PID can no longer mask a live socket in host/moderation/role resolution.
151 lines
4.7 KiB
Bash
151 lines
4.7 KiB
Bash
#!/usr/bin/env bats
|
|
|
|
load 'helpers'
|
|
|
|
setup() {
|
|
setup_party_sandbox
|
|
load_party_lib
|
|
}
|
|
|
|
# party name validation
|
|
# =====================
|
|
|
|
@test "validate_party_name accepts plain names" {
|
|
validate_party_name "debug-the-deploy"
|
|
validate_party_name "mob_coding"
|
|
validate_party_name "p"
|
|
validate_party_name "Party-2026"
|
|
}
|
|
|
|
@test "validate_party_name rejects empty" {
|
|
run validate_party_name ""
|
|
[ "$status" -ne 0 ]
|
|
}
|
|
|
|
@test "validate_party_name rejects path separators" {
|
|
run validate_party_name "../etc"
|
|
[ "$status" -ne 0 ]
|
|
run validate_party_name "a/b"
|
|
[ "$status" -ne 0 ]
|
|
}
|
|
|
|
@test "validate_party_name rejects names over 63 chars" {
|
|
long=$(printf 'a%.0s' $(seq 1 64))
|
|
run validate_party_name "$long"
|
|
[ "$status" -ne 0 ]
|
|
}
|
|
|
|
@test "validate_party_name rejects whitespace and shell metas" {
|
|
for bad in "a b" "a;b" 'a$b' 'a`b' "a'b" 'a"b'; do
|
|
run validate_party_name "$bad"
|
|
[ "$status" -ne 0 ] || { echo "should have rejected: $bad"; false; }
|
|
done
|
|
}
|
|
|
|
@test "validate_party_name rejects dots (tmux rewrites '.' in session names)" {
|
|
# tmux session_check_name() silently maps '.' and ':' to '_', so a
|
|
# dotted party name desyncs from its tmux session and every later
|
|
# `-t <name>` target fails. Verified on tmux 3.3a through 3.6b.
|
|
for bad in "my.party" "." ".." "a.d"; do
|
|
run validate_party_name "$bad"
|
|
[ "$status" -ne 0 ] || { echo "should have rejected: $bad"; false; }
|
|
done
|
|
}
|
|
|
|
@test "validate_party_name rejects leading dash (unusable through the CLI)" {
|
|
run validate_party_name "-flag"
|
|
[ "$status" -ne 0 ]
|
|
}
|
|
|
|
# time
|
|
# ====
|
|
|
|
@test "iso_now produces ISO 8601 UTC with Z suffix" {
|
|
out=$(iso_now)
|
|
case "$out" in
|
|
[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z) ;;
|
|
*) echo "bad timestamp: $out"; false ;;
|
|
esac
|
|
}
|
|
|
|
# user existence
|
|
# ==============
|
|
|
|
@test "user_exists returns 0 for the running user" {
|
|
user_exists "$USER"
|
|
}
|
|
|
|
@test "user_exists returns nonzero for a fictional user" {
|
|
! user_exists "nonexistent_$(date +%s)_user"
|
|
}
|
|
|
|
# liveness
|
|
# ========
|
|
#
|
|
# party_conn_state must not probe PIDs with kill -0. POSIX kill(2) is
|
|
# allowed to return EPERM when the caller can't signal the target, and
|
|
# illumos/BSD/macOS honor that, so kill -0 against another user's PID
|
|
# returns nonzero even when the process exists. Linux and Linux-ABI
|
|
# zones (LX-branded) are the outliers that return 0; that's why the
|
|
# bug hid in the test matrix until a native illumos run surfaced it.
|
|
# Liveness rides on `tmux -S list-clients` alone, which is strictly
|
|
# stronger (filters PID-reuse, dead servers, AF_UNIX impostors).
|
|
|
|
@test "party_conn_state does not reach for kill -0 (cross-user discovery)" {
|
|
body=$(declare -f party_conn_state)
|
|
[[ "$body" != *"kill "* ]] || { echo "$body"; false; }
|
|
}
|
|
|
|
@test "party_conn_state answers dead when no tmux server is at the socket" {
|
|
[ "$(party_conn_state "$PARTY_TMP/no-such-sock")" = dead ]
|
|
}
|
|
|
|
@test "validate_socket_dir_parent refuses whitespace in PARTY_SOCKET_DIR" {
|
|
spaced="$PARTY_TMP/with space"
|
|
mkdir -p "$spaced"
|
|
chmod 0700 "$spaced"
|
|
PARTY_SOCKET_DIR="$spaced" run validate_socket_dir_parent
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"whitespace"* ]]
|
|
}
|
|
|
|
@test "pick_live_party without a tty fails with guidance, not a silent death" {
|
|
# Two live-looking parties force the numbered picker; stdin is not a
|
|
# tty under bats, so the guard must fire before read.
|
|
stub="$PARTY_TMP/tmux-ok"
|
|
printf '#!/bin/sh\nexit 0\n' > "$stub"
|
|
chmod +x "$stub"
|
|
export PARTY_TMUX="$stub"
|
|
ensure_party_dir "$USER" one
|
|
ensure_party_dir "$USER" two
|
|
for n in one two; do
|
|
d="$PARTY_SOCKET_DIR/party-$USER:$n.d"
|
|
printf 'SERVER_PID=1\nGROUP=%s\n' "$(id -gn)" > "$d/roster"
|
|
done
|
|
run pick_live_party
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"pass a name"* ]]
|
|
}
|
|
|
|
@test "party_conn_state classifies ok / unauthorized / dead" {
|
|
stub="$PARTY_TMP/conn-stub"
|
|
cat > "$stub" <<'S'
|
|
#!/bin/sh
|
|
case "${MODE:-}" in
|
|
ok) exit 0 ;;
|
|
deny) echo "access not allowed" >&2; exit 0 ;;
|
|
deny2) echo "connection declined by policy (hypothetical future wording)" >&2; exit 0 ;;
|
|
*) echo "error connecting to /x (No such file or directory)" >&2; exit 1 ;;
|
|
esac
|
|
S
|
|
chmod +x "$stub"
|
|
export PARTY_TMUX="$stub"
|
|
export MODE=ok; [ "$(party_conn_state /x)" = "ok" ]
|
|
# The trap this function exists for: tmux answers unauthorized users
|
|
# with EXIT 0 + a stderr message, so rc alone says "authorized".
|
|
export MODE=deny; [ "$(party_conn_state /x)" = "unauthorized" ]
|
|
# Classification must not depend on tmux's denial wording (adversarial
|
|
# review finding): rc 0 with ANY stderr output is a refusal.
|
|
export MODE=deny2; [ "$(party_conn_state /x)" = "unauthorized" ]
|
|
export MODE=dead; [ "$(party_conn_state /x)" = "dead" ]
|
|
}
|