Skip to content

Commit 2fcfca3

Browse files
garyshengclaude
andcommitted
test: add relay.sh test suite (20 tests)
Tests cover: - CLI flags (--help, --status, --stop, --daemon) - Health endpoint - Play endpoint (valid files, missing file, missing param) - Path traversal protection (../, encoded, absolute paths) - Notify endpoint (valid JSON, empty body, invalid JSON) - Unknown routes - Daemon lifecycle (start, status, stop, duplicate prevention) - Volume handling (clamping, invalid values) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 48b6c45 commit 2fcfca3

1 file changed

Lines changed: 255 additions & 0 deletions

File tree

tests/relay.bats

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env bats
2+
# Tests for relay.sh — the peon-ping audio relay server
3+
4+
load setup
5+
6+
# Use the real curl, not the mock from setup.bash
7+
REAL_CURL="$(command -v curl 2>/dev/null || echo /usr/bin/curl)"
8+
9+
setup() {
10+
setup_test_env
11+
12+
# Derive relay.sh path from PEON_SH (set in setup.bash from its own location)
13+
RELAY_SH="$(dirname "$PEON_SH")/relay.sh"
14+
RELAY_PORT=19876 # Use non-default port to avoid conflicts
15+
RELAY_PID=""
16+
}
17+
18+
teardown() {
19+
# Kill relay if running
20+
if [ -n "$RELAY_PID" ] && kill -0 "$RELAY_PID" 2>/dev/null; then
21+
kill "$RELAY_PID" 2>/dev/null
22+
wait "$RELAY_PID" 2>/dev/null || true
23+
fi
24+
# Also stop via pidfile in case daemon tests left one
25+
if [ -f "$TEST_DIR/.relay.pid" ]; then
26+
pid=$(cat "$TEST_DIR/.relay.pid" 2>/dev/null)
27+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
28+
kill "$pid" 2>/dev/null
29+
wait "$pid" 2>/dev/null || true
30+
fi
31+
rm -f "$TEST_DIR/.relay.pid"
32+
fi
33+
teardown_test_env
34+
}
35+
36+
# Helper: start relay and wait for it to be ready
37+
start_relay() {
38+
bash "$RELAY_SH" --port="$RELAY_PORT" --peon-dir="$TEST_DIR" > /dev/null 2>&1 &
39+
RELAY_PID=$!
40+
41+
# Wait for relay to start (up to 3 seconds)
42+
for i in $(seq 1 30); do
43+
if "$REAL_CURL" -sf "http://127.0.0.1:$RELAY_PORT/health" > /dev/null 2>&1; then
44+
return 0
45+
fi
46+
sleep 0.1
47+
done
48+
echo "Relay failed to start" >&2
49+
return 1
50+
}
51+
52+
# ── Help and CLI ──────────────────────────────────────────────────────────────
53+
54+
@test "relay --help shows usage" {
55+
run bash "$RELAY_SH" --help
56+
[ "$status" -eq 0 ]
57+
[[ "$output" == *"Usage:"* ]]
58+
[[ "$output" == *"--port"* ]]
59+
[[ "$output" == *"--daemon"* ]]
60+
}
61+
62+
@test "relay exits with error if packs dir missing" {
63+
rm -rf "$TEST_DIR/packs"
64+
run bash "$RELAY_SH" --port="$RELAY_PORT" --peon-dir="$TEST_DIR"
65+
[ "$status" -eq 1 ]
66+
[[ "$output" == *"packs not found"* ]]
67+
}
68+
69+
# ── Health endpoint ───────────────────────────────────────────────────────────
70+
71+
@test "relay /health returns 200 OK" {
72+
start_relay
73+
run "$REAL_CURL" -sf "http://127.0.0.1:$RELAY_PORT/health"
74+
[ "$status" -eq 0 ]
75+
[ "$output" = "OK" ]
76+
}
77+
78+
# ── Play endpoint ─────────────────────────────────────────────────────────────
79+
80+
@test "relay /play plays a valid sound file" {
81+
start_relay
82+
run "$REAL_CURL" -sf "http://127.0.0.1:$RELAY_PORT/play?file=packs/peon/sounds/Hello1.wav" \
83+
-H "X-Volume: 0.7"
84+
[ "$status" -eq 0 ]
85+
[ "$output" = "OK" ]
86+
}
87+
88+
@test "relay /play returns 400 without file parameter" {
89+
start_relay
90+
run "$REAL_CURL" -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$RELAY_PORT/play"
91+
[ "$output" = "400" ]
92+
}
93+
94+
@test "relay /play returns 404 for nonexistent file" {
95+
start_relay
96+
run "$REAL_CURL" -s -o /dev/null -w "%{http_code}" \
97+
"http://127.0.0.1:$RELAY_PORT/play?file=packs/peon/sounds/nonexistent.wav"
98+
[ "$output" = "404" ]
99+
}
100+
101+
# ── Path traversal protection ─────────────────────────────────────────────────
102+
103+
@test "relay blocks path traversal with .." {
104+
start_relay
105+
run "$REAL_CURL" -s -o /dev/null -w "%{http_code}" \
106+
"http://127.0.0.1:$RELAY_PORT/play?file=../../../etc/passwd"
107+
[ "$output" = "403" ]
108+
}
109+
110+
@test "relay blocks path traversal with encoded .." {
111+
start_relay
112+
run "$REAL_CURL" -s -o /dev/null -w "%{http_code}" \
113+
"http://127.0.0.1:$RELAY_PORT/play?file=..%2F..%2F..%2Fetc%2Fpasswd"
114+
[ "$output" = "403" ]
115+
}
116+
117+
@test "relay blocks absolute paths" {
118+
start_relay
119+
run "$REAL_CURL" -s -o /dev/null -w "%{http_code}" \
120+
"http://127.0.0.1:$RELAY_PORT/play?file=/etc/passwd"
121+
[ "$output" = "403" ]
122+
}
123+
124+
# ── Notify endpoint ───────────────────────────────────────────────────────────
125+
126+
@test "relay /notify accepts POST with JSON body" {
127+
start_relay
128+
run "$REAL_CURL" -sf -X POST "http://127.0.0.1:$RELAY_PORT/notify" \
129+
-H "Content-Type: application/json" \
130+
-d '{"title":"peon-ping","message":"Test notification"}'
131+
[ "$status" -eq 0 ]
132+
[ "$output" = "OK" ]
133+
}
134+
135+
@test "relay /notify accepts empty POST body" {
136+
start_relay
137+
run "$REAL_CURL" -sf -X POST "http://127.0.0.1:$RELAY_PORT/notify" \
138+
-H "Content-Length: 0"
139+
[ "$status" -eq 0 ]
140+
[ "$output" = "OK" ]
141+
}
142+
143+
@test "relay /notify returns 400 for invalid JSON" {
144+
start_relay
145+
run "$REAL_CURL" -s -o /dev/null -w "%{http_code}" -X POST \
146+
"http://127.0.0.1:$RELAY_PORT/notify" \
147+
-H "Content-Type: application/json" \
148+
-d 'not json at all'
149+
[ "$output" = "400" ]
150+
}
151+
152+
# ── Unknown routes ────────────────────────────────────────────────────────────
153+
154+
@test "relay returns 404 for unknown GET route" {
155+
start_relay
156+
run "$REAL_CURL" -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$RELAY_PORT/unknown"
157+
[ "$output" = "404" ]
158+
}
159+
160+
@test "relay returns 404 for POST to /play" {
161+
start_relay
162+
run "$REAL_CURL" -s -o /dev/null -w "%{http_code}" -X POST "http://127.0.0.1:$RELAY_PORT/play"
163+
# POST to /play has no handler — BaseHTTPRequestHandler returns 501
164+
[[ "$output" =~ ^(404|405|501)$ ]]
165+
}
166+
167+
# ── Daemon mode ───────────────────────────────────────────────────────────────
168+
169+
@test "relay --status reports not running when no pidfile" {
170+
run bash "$RELAY_SH" --status --peon-dir="$TEST_DIR"
171+
[ "$status" -eq 1 ]
172+
[[ "$output" == *"not running"* ]]
173+
}
174+
175+
@test "relay --stop reports not running when no pidfile" {
176+
run bash "$RELAY_SH" --stop --peon-dir="$TEST_DIR"
177+
[ "$status" -eq 0 ]
178+
[[ "$output" == *"not running"* ]]
179+
}
180+
181+
@test "relay --daemon starts and --stop stops" {
182+
run bash "$RELAY_SH" --daemon --port="$RELAY_PORT" --peon-dir="$TEST_DIR"
183+
[ "$status" -eq 0 ]
184+
[[ "$output" == *"started in background"* ]]
185+
186+
# Verify pidfile was created
187+
[ -f "$TEST_DIR/.relay.pid" ]
188+
RELAY_PID=$(cat "$TEST_DIR/.relay.pid")
189+
190+
# Wait for it to be ready
191+
for i in $(seq 1 30); do
192+
if "$REAL_CURL" -sf "http://127.0.0.1:$RELAY_PORT/health" > /dev/null 2>&1; then
193+
break
194+
fi
195+
sleep 0.1
196+
done
197+
198+
# --status should report running
199+
run bash "$RELAY_SH" --status --peon-dir="$TEST_DIR" --port="$RELAY_PORT"
200+
[ "$status" -eq 0 ]
201+
[[ "$output" == *"running"* ]]
202+
203+
# --stop should stop it
204+
run bash "$RELAY_SH" --stop --peon-dir="$TEST_DIR"
205+
[ "$status" -eq 0 ]
206+
[[ "$output" == *"stopped"* ]]
207+
208+
# pidfile should be removed
209+
[ ! -f "$TEST_DIR/.relay.pid" ]
210+
RELAY_PID=""
211+
}
212+
213+
@test "relay --daemon prevents duplicate start" {
214+
# Start first instance
215+
bash "$RELAY_SH" --daemon --port="$RELAY_PORT" --peon-dir="$TEST_DIR" > /dev/null 2>&1
216+
RELAY_PID=$(cat "$TEST_DIR/.relay.pid" 2>/dev/null)
217+
218+
# Wait for it to be ready
219+
for i in $(seq 1 30); do
220+
if "$REAL_CURL" -sf "http://127.0.0.1:$RELAY_PORT/health" > /dev/null 2>&1; then
221+
break
222+
fi
223+
sleep 0.1
224+
done
225+
226+
# Try starting again — should say already running
227+
run bash "$RELAY_SH" --daemon --port="$RELAY_PORT" --peon-dir="$TEST_DIR"
228+
[ "$status" -eq 0 ]
229+
[[ "$output" == *"already running"* ]]
230+
}
231+
232+
# ── Volume handling ───────────────────────────────────────────────────────────
233+
234+
@test "relay clamps volume to valid range" {
235+
start_relay
236+
# Volume > 1.0 should be clamped (no error)
237+
run "$REAL_CURL" -sf "http://127.0.0.1:$RELAY_PORT/play?file=packs/peon/sounds/Hello1.wav" \
238+
-H "X-Volume: 5.0"
239+
[ "$status" -eq 0 ]
240+
[ "$output" = "OK" ]
241+
242+
# Volume < 0 should be clamped (no error)
243+
run "$REAL_CURL" -sf "http://127.0.0.1:$RELAY_PORT/play?file=packs/peon/sounds/Hello1.wav" \
244+
-H "X-Volume: -1.0"
245+
[ "$status" -eq 0 ]
246+
[ "$output" = "OK" ]
247+
}
248+
249+
@test "relay handles invalid volume gracefully" {
250+
start_relay
251+
run "$REAL_CURL" -sf "http://127.0.0.1:$RELAY_PORT/play?file=packs/peon/sounds/Hello1.wav" \
252+
-H "X-Volume: notanumber"
253+
[ "$status" -eq 0 ]
254+
[ "$output" = "OK" ]
255+
}

0 commit comments

Comments
 (0)