Files
Classroom_Desktop/tests/test-classroom-notify-test.sh
T
ekklesiaandClaude Opus 5 ffd18edf7e fix: deliver test notifications into the classroom session
The classroom desktop belongs to the classroom user and mako sits on
that user's D-Bus session. An administrator over SSH is in a different
session where nothing owns org.freedesktop.Notifications, so notify-send
asked D-Bus to start a second mako, which had no display to open and
timed out with NoReply.

Find the running sway process for the classroom user and deliver into
its session instead. XDG_RUNTIME_DIR and the bus address are read from
that process, falling back to /run/user/<uid> and its bus socket when
the environment cannot be read — root has ptrace access on the endpoint,
but that is not somewhere to be brittle. The Wayland display is read the
same way, and failing that from the wayland-N.lock sway holds open, so
no socket name is ever assumed. notify-send only needs the bus, so a
display that cannot be resolved is not worth refusing to send over.

Refuses to send when mako is not already running, rather than letting
D-Bus start one, and says plainly what is wrong when there is no
session, no privileges, or no bus socket. --dry-run reports what it
resolved without sending.

Behaviour inside the session is unchanged, and the sample notifications
and styling are untouched.

The tests stand a real background process in for sway so /proc/<pid>/environ
is genuine rather than mocked, and cover both invocation paths plus the
fallbacks and every refusal. The build runs them.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 17:38:44 -05:00

293 lines
8.8 KiB
Bash
Executable File

#!/bin/bash
set -uo pipefail
# Tests the session resolution in classroom-notify-test.
#
# sway is stood in for by a real background process started with a known
# environment, so /proc/<pid>/environ is genuine rather than mocked. id,
# pgrep, runuser and notify-send are shimmed onto PATH so every branch
# can be reached from an ordinary account.
#
# tests/test-classroom-notify-test.sh
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT="$ROOT/scripts/classroom-notify-test"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"; kill "${FAKE_SWAY_PID:-}" 2>/dev/null' EXIT
passed=0
failed=0
check() {
local label="$1" expected="$2" actual="$3"
if [[ "$actual" == *"$expected"* ]]; then
echo " ok $label"
passed=$((passed + 1))
else
echo " FAIL $label"
echo " expected to contain: $expected"
echo " got: $actual"
failed=$((failed + 1))
fi
}
check_absent() {
local label="$1" unexpected="$2" actual="$3"
if [[ "$actual" != *"$unexpected"* ]]; then
echo " ok $label"
passed=$((passed + 1))
else
echo " FAIL $label"
echo " expected NOT to contain: $unexpected"
echo " got: $actual"
failed=$((failed + 1))
fi
}
# ---------------------------------------------------------------- shims
mkdir -p "$WORK/bin"
cat > "$WORK/bin/id" <<'SHIM'
#!/bin/bash
if [[ "${1:-}" == "-u" && -n "${2:-}" ]]; then
echo "$FAKE_CLASSROOM_UID"
exit 0
fi
if [[ "${1:-}" == "-u" ]]; then
echo "$FAKE_CURRENT_UID"
exit 0
fi
exec /usr/bin/id "$@"
SHIM
cat > "$WORK/bin/pgrep" <<'SHIM'
#!/bin/bash
case "${!#}" in
sway)
[[ -n "${FAKE_SWAY_PID:-}" ]] || exit 1
echo "$FAKE_SWAY_PID"
;;
mako)
[[ "${FAKE_MAKO:-1}" == "1" ]] || exit 1
echo 4242
;;
*)
exit 1
;;
esac
SHIM
cat > "$WORK/bin/runuser" <<'SHIM'
#!/bin/bash
echo "runuser $*" >> "$RUNUSER_LOG"
shift 2
[[ "${1:-}" == "--" ]] && shift
exec "$@"
SHIM
cat > "$WORK/bin/notify-send" <<'SHIM'
#!/bin/bash
{
echo "args: $*"
echo "XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-}"
echo "DBUS_SESSION_BUS_ADDRESS=${DBUS_SESSION_BUS_ADDRESS:-}"
echo "WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-}"
} >> "$NOTIFY_LOG"
SHIM
chmod +x "$WORK/bin"/*
export PATH="$WORK/bin:$PATH"
export NOTIFY_LOG="$WORK/notify.log"
export RUNUSER_LOG="$WORK/runuser.log"
export FAKE_CLASSROOM_UID=4000
export FAKE_CURRENT_UID=4000
RUNTIME="$WORK/run"
mkdir -p "$RUNTIME"
# A real unix socket, because the script checks the bus is actually there.
python3 -c 'import socket, sys; socket.socket(socket.AF_UNIX).bind(sys.argv[1])' \
"$RUNTIME/bus"
run() {
: > "$NOTIFY_LOG"
: > "$RUNUSER_LOG"
"$SCRIPT" "$@" 2>&1
}
start_fake_sway() {
env "$@" sleep 300 &
FAKE_SWAY_PID=$!
export FAKE_SWAY_PID
# Give the exec time to land so /proc/<pid>/environ is populated.
while [[ ! -r "/proc/$FAKE_SWAY_PID/environ" ]]; do sleep 0.05; done
}
stop_fake_sway() {
[[ -n "${FAKE_SWAY_PID:-}" ]] && kill "$FAKE_SWAY_PID" 2>/dev/null
wait "${FAKE_SWAY_PID:-}" 2>/dev/null
unset FAKE_SWAY_PID
}
unset WAYLAND_DISPLAY DBUS_SESSION_BUS_ADDRESS
# --------------------------------------------- 1. resolve from environ
echo "1. resolves the session from sway's environment"
start_fake_sway \
XDG_RUNTIME_DIR="$RUNTIME" \
DBUS_SESSION_BUS_ADDRESS="unix:path=$RUNTIME/bus" \
WAYLAND_DISPLAY=wayland-7
out="$(run)"
check "reports delivery to the classroom session" "Sent normal notification to the classroom desktop session" "$out"
log="$(cat "$NOTIFY_LOG")"
check "runtime dir taken from sway" "XDG_RUNTIME_DIR=$RUNTIME" "$log"
check "bus address taken from sway" "DBUS_SESSION_BUS_ADDRESS=unix:path=$RUNTIME/bus" "$log"
check "wayland display from sway" "WAYLAND_DISPLAY=wayland-7" "$log"
check "styling preserved" "--app-name=Ekklesia Classroom" "$log"
check "default sample title" "DVD Ready" "$log"
check "default sample body" "A disc is ready to play." "$log"
# ------------------------------------------------- 2. dry run reporting
echo "2. --dry-run reports the resolved session and sends nothing"
out="$(run --dry-run)"
check "shows the sway pid" "sway pid: $FAKE_SWAY_PID" "$out"
check "shows the runtime dir" "XDG_RUNTIME_DIR: $RUNTIME" "$out"
check "shows the wayland display" "WAYLAND_DISPLAY: wayland-7" "$out"
check_absent "nothing was sent" "args:" "$(cat "$NOTIFY_LOG")"
# ------------------------------------------------------ 3. urgency/args
echo "3. urgency and custom text still work"
out="$(run --critical "Parent Call" "Room 3 needs a parent.")"
log="$(cat "$NOTIFY_LOG")"
check "critical urgency passed" "--urgency=critical" "$log"
check "custom title passed" "Parent Call" "$log"
check "custom body passed" "Room 3 needs a parent." "$log"
# ------------------------------------------------ 4. root uses runuser
echo "4. root delivers as the classroom user"
FAKE_CURRENT_UID=0
out="$(run)"
check "runuser targets classroom" "runuser -u classroom --" "$(cat "$RUNUSER_LOG")"
check "notification still sent" "--app-name=Ekklesia Classroom" "$(cat "$NOTIFY_LOG")"
FAKE_CURRENT_UID=4000
# ------------------------------------ 5. unprivileged admin is told how
echo "5. an unprivileged administrator gets told what to do"
FAKE_CURRENT_UID=9999
out="$(run)"
check "explains it needs root" "run this as root (sudo classroom-notify-test)" "$out"
check_absent "nothing was sent" "args:" "$(cat "$NOTIFY_LOG")"
FAKE_CURRENT_UID=4000
# ----------------------------------------- 6. mako must already be up
echo "6. refuses to let D-Bus start a second mako"
export FAKE_MAKO=0
out="$(run)"
check "explains mako is not running" "mako is not running in the classroom session" "$out"
check_absent "nothing was sent" "args:" "$(cat "$NOTIFY_LOG")"
unset FAKE_MAKO
stop_fake_sway
# --------------------------------- 7. display derived from the lock fd
echo "7. derives the display from sway's wayland lock when environ has none"
touch "$RUNTIME/wayland-9.lock"
env -u WAYLAND_DISPLAY \
XDG_RUNTIME_DIR="$RUNTIME" \
DBUS_SESSION_BUS_ADDRESS="unix:path=$RUNTIME/bus" \
bash -c "exec 9< '$RUNTIME/wayland-9.lock'; sleep 300" &
FAKE_SWAY_PID=$!
export FAKE_SWAY_PID
while [[ ! -r "/proc/$FAKE_SWAY_PID/fd/9" ]]; do sleep 0.05; done
out="$(run --dry-run)"
check "display derived from the lock" "WAYLAND_DISPLAY: wayland-9" "$out"
stop_fake_sway
# ------------------------------------------- 8. no session at all
echo "8. says so clearly when no classroom session is running"
out="$(run)"
check "explains there is no session" "no classroom desktop session is running" "$out"
check_absent "nothing was sent" "args:" "$(cat "$NOTIFY_LOG")"
# ------------------------------------- 9. falls back when environ is thin
echo "9. falls back to session defaults when sway's environment is unreadable"
start_fake_sway XDG_RUNTIME_DIR="$RUNTIME"
out="$(run --dry-run)"
check "derives the bus from the runtime dir" \
"DBUS_SESSION_BUS_ADDRESS: unix:path=$RUNTIME/bus" "$out"
out="$(run)"
check "still delivers" "Sent normal notification to the classroom desktop session" "$out"
check "bus reached notify-send" "DBUS_SESSION_BUS_ADDRESS=unix:path=$RUNTIME/bus" \
"$(cat "$NOTIFY_LOG")"
stop_fake_sway
# ------------------------------------------ 10. missing bus is explained
echo "10. explains a missing session bus instead of hanging"
MISSING="$WORK/empty"
mkdir -p "$MISSING"
start_fake_sway XDG_RUNTIME_DIR="$MISSING"
out="$(run)"
check "names the missing socket" "bus socket $MISSING/bus does not exist" "$out"
check_absent "nothing was sent" "args:" "$(cat "$NOTIFY_LOG")"
stop_fake_sway
# ----------------------------- 11. already inside the classroom session
echo "11. sends directly when already inside the classroom session"
# No sway process to find: an in-session caller must not need one.
export WAYLAND_DISPLAY=wayland-4
export DBUS_SESSION_BUS_ADDRESS="unix:path=$RUNTIME/bus"
export XDG_RUNTIME_DIR="$RUNTIME"
out="$(run)"
check "reports delivery to this session" "Sent normal notification to this session" "$out"
check "notification was sent" "--app-name=Ekklesia Classroom" "$(cat "$NOTIFY_LOG")"
check "inherits the session display" "WAYLAND_DISPLAY=wayland-4" "$(cat "$NOTIFY_LOG")"
check_absent "did not go through runuser" "runuser" "$(cat "$RUNUSER_LOG")"
out="$(run --dry-run)"
check "dry run names this session" "Would notify this session" "$out"
unset WAYLAND_DISPLAY DBUS_SESSION_BUS_ADDRESS XDG_RUNTIME_DIR
# ----------------------------------------------------------- summary
echo
echo "passed: $passed failed: $failed"
[[ "$failed" -eq 0 ]]