diff --git a/regress/server-access.sh b/regress/server-access.sh new file mode 100644 index 000000000..a6e44efca --- /dev/null +++ b/regress/server-access.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +# Exercise the unprivileged server-access surface: user and group ACL entries, +# read-only/writable transitions, listing, removal, implied addition, and +# command errors. Actual connections from another UID require privileges and +# deliberately do not form part of this test. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +OUT=$(mktemp) || exit 1 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null + rm -f "$OUT" +} +trap cleanup 0 1 15 + +expect_fail() +{ + message=$1 + shift + if $TMUX "$@" >"$OUT" 2>&1; then + fail "$message: command unexpectedly succeeded" + fi +} + +assert_entry() +{ + entry=$1 + $TMUX server-access -l >"$OUT" || exit 1 + grep -Fxq "$entry" "$OUT" || + fail "missing ACL entry '$entry'" +} + +assert_no_entry() +{ + entry=$1 + $TMUX server-access -l >"$OUT" || exit 1 + if grep -Fxq "$entry" "$OUT"; then + fail "unexpected ACL entry '$entry'" + fi +} + +owner_uid=$(id -u) +owner_name=$(id -un) + +# nobody is the non-owner test identity. Derive its primary group rather than +# assuming whether the system calls it nobody, nogroup, or something else. +acl_user=nobody +acl_group=$(id -gn "$acl_user" 2>/dev/null) || + fail "the nobody account is unavailable" +[ "$owner_name" != "$acl_user" ] || fail "test cannot run as nobody" + +$TMUX new-session -d -s access 'exec sleep 100' || exit 1 + +# The owner is installed as writable at server startup (root is intentionally +# omitted from display output). +if [ "$owner_uid" != 0 ]; then + assert_entry "$owner_name (U,W)" +fi + +# User ACL lifecycle, including duplicate and missing-entry errors. +$TMUX server-access -a "$acl_user" || exit 1 +assert_entry "$acl_user (U,W)" +expect_fail "duplicate user addition" server-access -a "$acl_user" +$TMUX server-access -r "$acl_user" || exit 1 +assert_entry "$acl_user (U,R)" +$TMUX server-access -w "$acl_user" || exit 1 +assert_entry "$acl_user (U,W)" +$TMUX server-access -d "$acl_user" || exit 1 +assert_no_entry "$acl_user (U,W)" +expect_fail "removing absent user" server-access -d "$acl_user" + +# -r and -w imply -a when an entry is absent. +$TMUX server-access -r "$acl_user" || exit 1 +assert_entry "$acl_user (U,R)" +$TMUX server-access -d "$acl_user" || exit 1 + +# The same lifecycle for a group exercises the separate ACL key space. +$TMUX server-access -g -a -r "$acl_group" || exit 1 +assert_entry "$acl_group (G,R)" +$TMUX server-access -g -w "$acl_group" || exit 1 +assert_entry "$acl_group (G,W)" +expect_fail "duplicate group addition" server-access -g -a "$acl_group" +$TMUX server-access -g -d "$acl_group" || exit 1 +assert_no_entry "$acl_group (G,W)" +expect_fail "removing absent group" server-access -g -d "$acl_group" + +# Parser and lookup errors, plus the immutable owner/root entries. +expect_fail "missing ACL subject" server-access -a +expect_fail "unknown user" server-access -a "tmux-no-user-$$" +expect_fail "unknown group" server-access -g -a "tmux-no-group-$$" +expect_fail "conflicting add/delete" server-access -a -d "$acl_user" +expect_fail "conflicting read/write" server-access -r -w "$acl_user" +expect_fail "changing server owner" server-access -r "$owner_name" +if [ "$owner_name" != root ]; then + expect_fail "changing root access" server-access -a root +fi + +$TMUX has-session -t access || fail "server exited during ACL updates" +exit 0 diff --git a/regress/switch-mode.sh b/regress/switch-mode.sh new file mode 100644 index 000000000..5f3fe8c12 --- /dev/null +++ b/regress/switch-mode.sh @@ -0,0 +1,203 @@ +#!/bin/sh + +# Exercise switch-mode's session and window lists with a real attached client: +# fuzzy filtering, movement and selection, the default and custom commands, +# no-match cancellation, resize, zoom restoration, and -k mode teardown. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +DIR=$(mktemp -d) || exit 1 +TMUX_TMPDIR=$DIR +export TMUX_TMPDIR +INNER="$TEST_TMUX -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +capture() +{ + $OUTER capture-pane -p -t outer:0.0 2>/dev/null +} + +wait_capture() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + captured=$(capture) + printf '%s\n' "$captured" | grep -Fq "$marker" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "timed out waiting for '$marker'" +} + +wait_count() +{ + marker=$1 + want=$2 + i=0 + while [ "$i" -lt 50 ]; do + captured=$(capture) + count=$(printf '%s\n' "$captured" | grep -Fc "$marker") + [ "$count" -eq "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "found $count '$marker' rows, expected $want" +} + +wait_format() +{ + target=$1 + format=$2 + want=$3 + i=0 + while [ "$i" -lt 50 ]; do + got=$($INNER display-message -p -t "$target" "$format" \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "$target format $format is '$got', expected '$want'" +} + +wait_client_session() +{ + want=$1 + i=0 + while [ "$i" -lt 50 ]; do + got=$($INNER list-clients -F '#{client_session}' 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "client session is '$got', expected '$want'" +} + +# Three alphabetically ordered sessions. alpha:main has two panes so -Z can be +# observed; the two pick-* windows make a deterministic two-row window filter. +$INNER new-session -d -s alpha -n main -x60 -y15 'exec sleep 100' || exit 1 +$INNER split-window -d -h -t alpha:main 'exec sleep 100' || exit 1 +$INNER new-window -d -t alpha: -n pick-one 'exec sleep 100' || exit 1 +$INNER new-session -d -s bravo -n pick-two -x60 -y15 \ + 'exec sleep 100' || exit 1 +$INNER new-session -d -s charlie -n last -x60 -y15 \ + 'exec sleep 100' || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER select-window -t alpha:main || exit 1 + +$OUTER new-session -d -s outer -x60 -y15 "$INNER attach -t alpha" || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +wait_client_session alpha + +# Session rows are sorted alpha, bravo, charlie. Up from the first row wraps to +# charlie; Enter runs a custom command with the selected session target. +$INNER set-option -g @picked '' || exit 1 +$INNER switch-mode -s -t alpha:main.0 -F 'SESSION #{session_name}' \ + "set-option -g @picked '%%'" || exit 1 +wait_count 'SESSION ' 3 +$INNER send-keys -t alpha:main.0 Up Enter || exit 1 +wait_format alpha:main.0 '#{@picked}' '=charlie:' +wait_format alpha:main.0 '#{pane_in_mode}' 0 + +# Fuzzy filtering keeps only bravo for "brv". The default command switches the +# real client to that session and closes the mode. +$INNER switch-mode -s -t alpha:main.0 -F 'SESSION #{session_name}' || exit 1 +wait_count 'SESSION ' 3 +$INNER send-keys -t alpha:main.0 -l brv || exit 1 +wait_count 'SESSION ' 1 +printf '%s\n' "$captured" | grep -Fq 'SESSION bravo' || + fail "fuzzy session filter did not select bravo" +$INNER send-keys -t alpha:main.0 Enter || exit 1 +wait_client_session bravo +client=$($INNER list-clients -F '#{client_name}') +$INNER switch-client -c "$client" -t alpha || exit 1 +wait_client_session alpha + +# Window mode sees pick-one then pick-two for the "pick" filter. Down selects +# the second row, and the custom command receives its window target. +$INNER set-option -g @picked '' || exit 1 +$INNER switch-mode -w -t alpha:main.0 -F \ + 'WINDOW #{session_name}:#{window_name}' \ + "set-option -g @picked '%%'" || exit 1 +wait_capture 'WINDOW ' +$INNER send-keys -t alpha:main.0 -l pick || exit 1 +wait_count 'WINDOW ' 2 +$INNER send-keys -t alpha:main.0 Down Enter || exit 1 +picked=$($INNER show-option -gv @picked) +case "$picked" in +=bravo:0.) ;; +*) fail "window selection produced '$picked', expected '=bravo:0.'" ;; +esac + +# Navigation with an empty result set must be harmless, and Escape cancels +# without running the custom command. +$INNER set-option -g @picked unchanged || exit 1 +$INNER switch-mode -s -t alpha:main.0 -F 'NONE #{session_name}' \ + "set-option -g @picked '%%'" || exit 1 +wait_capture 'NONE ' +$INNER send-keys -t alpha:main.0 -l no-such-session || exit 1 +wait_count 'NONE ' 0 +$INNER send-keys -t alpha:main.0 Up Down PPage NPage Home End || exit 1 +wait_format alpha:main.0 '#{pane_mode}' switch-mode +$INNER send-keys -t alpha:main.0 Escape || exit 1 +wait_format alpha:main.0 '#{pane_in_mode}' 0 +[ "$($INNER show-option -gv @picked)" = unchanged ] || + fail "cancelled switch-mode ran its command" + +# -Z temporarily zooms an unzoomed window. Resizing while the mode is open +# rebuilds and redraws it; exiting restores the original unzoomed state. +$INNER switch-mode -Zs -t alpha:main.0 -F 'RESIZE #{session_name}' || exit 1 +wait_capture 'RESIZE alpha' +wait_format alpha:main.0 '#{window_zoomed_flag}' 1 +$INNER resize-window -t alpha:main -x50 -y12 || exit 1 +wait_format alpha:main.0 '#{window_width}x#{window_height}' 50x12 +wait_format alpha:main.0 '#{pane_mode}' switch-mode +$INNER send-keys -t alpha:main.0 Escape || exit 1 +wait_format alpha:main.0 '#{window_zoomed_flag}' 0 + +# If the window was already zoomed, leaving -Z mode must keep it zoomed. +$INNER resize-pane -Z -t alpha:main.0 || exit 1 +$INNER switch-mode -Zs -t alpha:main.0 -F 'ZOOMED #{session_name}' || exit 1 +wait_capture 'ZOOMED alpha' +$INNER send-keys -t alpha:main.0 Escape || exit 1 +wait_format alpha:main.0 '#{window_zoomed_flag}' 1 +$INNER resize-pane -Z -t alpha:main.0 || exit 1 + +# -k uses the shared mode teardown flag. Put it on a disposable pane and make +# sure exiting the mode removes that pane. +kill_pane=$($INNER split-window -d -P -F '#{pane_id}' -t alpha:main \ + 'exec sleep 100') || exit 1 +$INNER switch-mode -ks -t "$kill_pane" -F 'KILL #{session_name}' || exit 1 +wait_format "$kill_pane" '#{pane_mode}' switch-mode +$INNER send-keys -t "$kill_pane" Escape || exit 1 +i=0 +while [ "$i" -lt 50 ]; do + $INNER list-panes -s -t alpha -F '#{pane_id}' 2>/dev/null | \ + grep -q -x "$kill_pane" || break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "switch-mode -k did not kill its pane" + +exit 0