Zuletzt aktiv 1 month ago

gistfile1.txt Originalformat
1#!/usr/bin/env bash
2
3WGCTL_BINARY="$(command -v wgctl)"
4
5# ============================================
6# Lifecycle
7# ============================================
8
9function cmd::test::on_load() {
10 flag::register --destructive
11 flag::register --section
12 flag::register --fn
13 flag::register --function
14}
15
16function cmd::test::help() {
17 cat <<EOF
18Usage: wgctl test [options]
19
20Run the wgctl test suite.
21
22Options:
23 --destructive Include tests that modify state (add/remove/block)
24 --section <name> Run only a specific section (list, rules, groups, audit, logs, fw)
25
26Examples:
27 wgctl test
28 wgctl test --section rules
29 wgctl test --destructive
30EOF
31}
32
33# ============================================
34# Test helpers
35# ============================================
36
37function cmd::test::run_cmd() {
38 local desc="$1"
39 local expected="${2:-}"
40 shift 2
41
42 local tmp exit_code
43 tmp=$(mktemp)
44
45 set +e # disable exit on error (return 1)
46
47 timeout 30 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1 &
48 local pid=$!
49 wait $pid
50 exit_code=$?
51
52 set -e # re-enable exit on error
53
54 if [[ $exit_code -eq 124 ]]; then
55 test::warn "${desc} (timed out after 30s)"
56 rm -f "$tmp"
57 return 1
58 fi
59
60 if [[ $exit_code -ne 0 ]]; then
61 test::fail "${desc}"
62 if [[ "${WGCTL_TEST_VERBOSE:-false}" == "true" ]]; then
63 printf " Output: %s\n" "$(cat "$tmp")"
64 fi
65 rm -f "$tmp"
66 return 1
67 fi
68
69 if [[ -n "$expected" ]] && ! grep -qF "$expected" "$tmp"; then
70 local actual
71 actual=$(head -3 "$tmp" | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-100)
72 test::fail "${desc} (expected '${expected}', got: '${actual}')"
73 rm -f "$tmp"
74 return 1
75 fi
76
77 test::pass "$desc"
78 rm -f "$tmp"
79}
80
81function cmd::test::run_cmd_fails() {
82 local desc="$1"
83 shift
84
85 set +e # disable exit on error (return 1)
86
87 local tmp exit_code
88 tmp=$(mktemp)
89 timeout 10 setsid "$WGCTL_BINARY" "$@" > "$tmp" 2>&1
90 exit_code=$?
91
92 set -e # re-enable exit on error
93
94 rm -f "$tmp"
95
96 if [[ $exit_code -eq 124 ]]; then
97 test::warn "${desc} (timed out)"
98 return 1
99 fi
100
101 if [[ $exit_code -eq 0 ]]; then
102 test::fail "${desc} (expected failure but succeeded)"
103 return 1
104 fi
105
106 test::pass "$desc"
107}
108
109function cmd::test::run_function() {
110 local fn="$1"
111
112 local namespace
113 namespace=$(echo "$fn" | cut -d':' -f3)
114 load_command "$namespace" 2>/dev/null || true
115
116 test::reset
117 log::section "Function Test: ${fn}"
118
119 case "$fn" in
120 cmd::block::run) cmd::test::fn_block ;;
121 cmd::unblock::run) cmd::test::fn_unblock ;;
122 cmd::remove::run) cmd::test::fn_remove ;;
123 cmd::rule::assign) cmd::test::fn_rule_assign ;;
124 cmd::rename::run) cmd::test::fn_rename ;;
125 cmd::remove::run) cmd::test::fn_remove ;;
126 cmd::unblock::run) cmd::test::fn_unblock ;;
127 *)
128 log::error "No function test defined for: ${fn}"
129 return 1
130 ;;
131 esac
132
133 test::summary
134}
135
136# ============================================
137# Test sections
138# ============================================
139
140function cmd::test::section_list() {
141 test::section "List"
142 cmd::test::run_cmd "list" "WireGuard Clients" list
143 cmd::test::run_cmd "list --online" "" list --online
144 cmd::test::run_cmd "list --offline" "" list --offline
145 cmd::test::run_cmd "list --blocked" "" list --blocked
146 cmd::test::run_cmd "list --type phone" "" list --type phone
147 cmd::test::run_cmd "list --type guest" "" list --type guest
148 cmd::test::run_cmd "list --detailed" "Client:" list --detailed
149 cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno
150}
151
152function cmd::test::section_inspect() {
153 test::section "Inspect"
154 cmd::test::run_cmd "inspect --name phone-nuno" "IP:" inspect --name phone-nuno
155 cmd::test::run_cmd "inspect --name nuno --type phone" "IP:" inspect --name nuno --type phone
156 cmd::test::run_cmd "inspect --name phone-nuno --config" "PrivateKey" inspect --name phone-nuno --config
157 cmd::test::run_cmd_fails "inspect nonexistent peer" inspect --name nonexistent-peer
158}
159
160function cmd::test::section_config() {
161 test::section "Config & QR"
162 cmd::test::run_cmd "config --name phone-nuno" "PrivateKey" config --name phone-nuno
163 cmd::test::run_cmd "config --name nuno --type phone" "PrivateKey" config --name nuno --type phone
164 cmd::test::run_cmd "qr --name phone-nuno" "" qr --name phone-nuno
165}
166
167function cmd::test::section_rules() {
168 test::section "Rules"
169 cmd::test::run_cmd "rule list" "guest" rule list
170 cmd::test::run_cmd "rule show --name guest" "Description" rule show --name guest
171 cmd::test::run_cmd "rule show --name user" "Description" rule show --name user
172 cmd::test::run_cmd "rule show --name admin" "Description" rule show --name admin
173 cmd::test::run_cmd_fails "rule show nonexistent" rule show --name nonexistent
174}
175
176function cmd::test::section_groups() {
177 test::section "Groups"
178 cmd::test::run_cmd "group list" "Groups" group list
179 cmd::test::run_cmd "group show --name family" "Peers:" group show --name family
180 cmd::test::run_cmd_fails "group show nonexistent" group show --name nonexistent
181}
182
183function cmd::test::section_audit() {
184 test::section "Audit"
185 cmd::test::run_cmd "audit" "passed" audit
186 cmd::test::run_cmd "audit --peer phone-nuno" "passed" audit --peer phone-nuno
187 cmd::test::run_cmd "audit --type phone" "passed" audit --type phone
188}
189
190function cmd::test::section_logs() {
191 test::section "Logs"
192 cmd::test::run_cmd "logs" "Activity" logs
193 cmd::test::run_cmd "logs --name phone-nuno" "Activity" logs --name phone-nuno
194 cmd::test::run_cmd "logs --type guest" "Activity" logs --type guest
195 cmd::test::run_cmd "logs --fw" "Activity" logs --fw
196 cmd::test::run_cmd "logs --wg" "Activity" logs --wg
197}
198
199function cmd::test::section_fw() {
200 test::section "Firewall"
201 cmd::test::run_cmd "fw list" "FORWARD" fw list
202 cmd::test::run_cmd "fw list --peer phone-nuno" "" fw list --peer phone-nuno
203 cmd::test::run_cmd "fw list --no-nflog" "" fw list --no-nflog
204 cmd::test::run_cmd "fw list --no-accept" "" fw list --no-accept
205 cmd::test::run_cmd "fw list --no-drop" "" fw list --no-drop
206 cmd::test::run_cmd "fw nat" "PREROUTING" fw nat
207 cmd::test::run_cmd "fw count" "TOTAL" fw count
208}
209function cmd::test::section_net() {
210 test::section "Net"
211
212 "$WGCTL_BINARY" net rm --name test-svc --force > /dev/null 2>&1 || true
213
214 cmd::test::run_cmd "net add service" "added" \
215 net add --name test-svc --ip 10.0.0.99 --desc "Test service"
216 cmd::test::run_cmd "net add port" "Added" \
217 net add --name test-svc:web --port 9999:tcp
218 cmd::test::run_cmd "net list" "test-svc" \
219 net list
220 cmd::test::run_cmd "net list --detailed" "web" \
221 net list --detailed
222 cmd::test::run_cmd "net show" "9999" \
223 net show --name test-svc
224 cmd::test::run_cmd "net rm port" "Removed" \
225 net rm --name test-svc:web --force
226 cmd::test::run_cmd "net add port again" "Added" \
227 net add --name test-svc:web --port 9999:tcp
228 cmd::test::run_cmd "net rm all ports" "Removed" \
229 net rm --name test-svc:ports --force
230 cmd::test::run_cmd "net rm service" "Removed" \
231 net rm --name test-svc --force
232 cmd::test::run_cmd_fails "net show nonexistent" \
233 net show --name nonexistent-svc
234 cmd::test::run_cmd_fails "net add port no service" \
235 net add --name nonexistent:web --port 80:tcp
236}
237
238function cmd::test::section_destructive() {
239 test::section "Destructive (modifying state)"
240
241 # ── Cleanup from any previous failed run ──
242 "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
243 "$WGCTL_BINARY" group remove --name testgroup --force > /dev/null 2>&1 || true
244 "$WGCTL_BINARY" group remove --name testgroup2 --force > /dev/null 2>&1 || true
245 "$WGCTL_BINARY" net rm --name test-block-svc --force > /dev/null 2>&1 || true
246 "$WGCTL_BINARY" unblock --name phone-testunit > /dev/null 2>&1 || true
247
248 # ── Add test peer ──────────────────────────
249 cmd::test::run_cmd "add phone peer" "added successfully" \
250 add --name testunit --type phone
251
252 # ── Direct block/unblock ───────────────────
253 cmd::test::run_cmd "block peer" "blocked" block --name phone-testunit
254 cmd::test::run_cmd "list shows blocked" "blocked" list --blocked
255 cmd::test::run_cmd "unblock peer" "unblocked" unblock --name phone-testunit
256
257 # ── Specific IP block/unblock ──────────────
258 cmd::test::run_cmd "block peer --ip" "blocked for" \
259 block --name phone-testunit --ip 10.0.0.99
260 cmd::test::run_cmd "list shows restricted" "restricted" \
261 list --name phone-testunit
262 cmd::test::run_cmd "unblock peer --ip" "unblocked" \
263 unblock --name phone-testunit --ip 10.0.0.99
264
265 # ── Service block/unblock ──────────────────
266 "$WGCTL_BINARY" net add --name test-block-svc \
267 --ip 10.0.0.99 > /dev/null 2>&1
268 "$WGCTL_BINARY" net add --name test-block-svc:web \
269 --port 9999:tcp > /dev/null 2>&1
270 cmd::test::run_cmd "block peer --service (ip)" "blocked" \
271 block --name phone-testunit --service test-block-svc
272 cmd::test::run_cmd "block already blocked service" "already" \
273 block --name phone-testunit --service test-block-svc
274 cmd::test::run_cmd "unblock peer --service (ip)" "unblocked" \
275 unblock --name phone-testunit --service test-block-svc
276 cmd::test::run_cmd "unblock not blocked service" "not blocked" \
277 unblock --name phone-testunit --service test-block-svc
278 cmd::test::run_cmd "block peer --service (port)" "blocked" \
279 block --name phone-testunit --service test-block-svc:web
280 cmd::test::run_cmd "unblock peer --service (port)" "unblocked" \
281 unblock --name phone-testunit --service test-block-svc:web
282 "$WGCTL_BINARY" net rm --name test-block-svc --force > /dev/null 2>&1 || true
283
284 # ── Rule assign/unassign ───────────────────
285 cmd::test::run_cmd "rule assign" "Assigned" \
286 rule assign --name user --peer phone-testunit
287 cmd::test::run_cmd "rule unassign" "Unassigned" \
288 rule unassign --peer phone-testunit
289 "$WGCTL_BINARY" rule assign --name user --peer phone-testunit \
290 > /dev/null 2>&1 || true
291
292 # ── Group basic operations ─────────────────
293 cmd::test::run_cmd "group add" "created" \
294 group add --name testgroup --desc "Test group"
295 cmd::test::run_cmd "group peer add" "Added" \
296 group peer add --name testgroup --peer phone-testunit
297 cmd::test::run_cmd "group block" "blocked" \
298 group block --name testgroup
299 cmd::test::run_cmd "group unblock" "unblocked" \
300 group unblock --name testgroup
301
302 # ── M:N group block tracking ───────────────
303 "$WGCTL_BINARY" group add --name testgroup2 \
304 --desc "Test group 2" > /dev/null 2>&1
305 "$WGCTL_BINARY" group peer add --name testgroup2 \
306 --peer phone-testunit > /dev/null 2>&1
307
308 cmd::test::run_cmd "group block first group" "blocked" \
309 group block --name testgroup
310 cmd::test::run_cmd "group block second group" "blocked" \
311 group block --name testgroup2
312
313 "$WGCTL_BINARY" group unblock --name testgroup > /dev/null 2>&1
314 cmd::test::run_cmd "peer stays blocked after partial unblock" "blocked" \
315 list --blocked
316
317 "$WGCTL_BINARY" group unblock --name testgroup2 > /dev/null 2>&1
318 cmd::test::run_cmd "peer unblocked after all groups unblock" "phone-testunit" \
319 list --allowed
320
321 # ── Direct block overrides group block ─────
322 "$WGCTL_BINARY" group block --name testgroup > /dev/null 2>&1
323 cmd::test::run_cmd "direct unblock overrides group block" "unblocked" \
324 unblock --name phone-testunit
325
326 # ── Cleanup ────────────────────────────────
327 cmd::test::run_cmd "group remove" "removed" \
328 group remove --name testgroup --force
329 "$WGCTL_BINARY" group remove --name testgroup2 --force > /dev/null 2>&1 || true
330
331 cmd::test::run_cmd "remove phone peer" "removed" \
332 remove --name phone-testunit --force
333}
334
335# ============================================
336# Function Blocks
337# ============================================
338
339function cmd::test::fn_block() {
340 test::section "cmd::block::run"
341
342 # Setup
343 "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
344 "$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1
345
346 # Tests
347 cmd::test::run_cmd "block peer" "blocked" block --name phone-testunit
348 cmd::test::run_cmd "block already blocked" "already" block --name phone-testunit
349
350 "$WGCTL_BINARY" unblock --name phone-testunit > /dev/null 2>&1 || true
351 cmd::test::run_cmd "block with --type" "blocked" block --name testunit --type phone
352
353 cmd::test::run_cmd_fails "block nonexistent" block --name truly-nonexistent-xyz
354
355 # Cleanup
356 "$WGCTL_BINARY" unblock --name phone-testunit > /dev/null 2>&1 || true
357 "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
358}
359
360function cmd::test::fn_remove() {
361 test::section "cmd::remove::run"
362
363 # Setup
364 "$WGCTL_BINARY" remove --name phone-testunit --force \
365 > /dev/null 2>&1 || true
366 "$WGCTL_BINARY" add --name testunit --type phone \
367 > /dev/null 2>&1
368
369 # Tests
370 # Skip the interactive prompt test — it hangs waiting for input
371 # cmd::test::run_cmd_fails "remove without --force" \
372 # remove --name phone-testunit
373 cmd::test::run_cmd "remove with --force" "removed" \
374 remove --name phone-testunit --force
375 cmd::test::run_cmd_fails "remove nonexistent" \
376 remove --name nonexistent-peer --force
377 cmd::test::run_cmd_fails "remove missing --name" \
378 remove --force
379
380 # Cleanup already done by tests
381}
382
383function cmd::test::fn_rename() {
384 test::section "cmd::rename::run"
385
386 # Setup
387 "$WGCTL_BINARY" remove --name phone-testunit --force \
388 > /dev/null 2>&1 || true
389 "$WGCTL_BINARY" remove --name phone-testunit2 --force \
390 > /dev/null 2>&1 || true
391 "$WGCTL_BINARY" add --name testunit --type phone \
392 > /dev/null 2>&1
393
394 # Tests
395 cmd::test::run_cmd "rename peer" "renamed" \
396 rename --name phone-testunit --new-name phone-testunit2
397 cmd::test::run_cmd_fails "rename to existing" \
398 rename --name phone-testunit2 --new-name phone-nuno
399 cmd::test::run_cmd_fails "rename nonexistent" \
400 rename --name phone-nonexistent --new-name phone-testunit
401 cmd::test::run_cmd_fails "rename missing --new-name" \
402 rename --name phone-testunit2
403
404 # Cleanup
405 "$WGCTL_BINARY" remove --name phone-testunit2 --force \
406 > /dev/null 2>&1 || true
407}
408
409function cmd::test::fn_unblock() {
410 test::section "cmd::unblock::run"
411
412 # Setup — add and block a peer
413 "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
414 "$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1
415 "$WGCTL_BINARY" block --name phone-testunit > /dev/null 2>&1
416
417 # Tests
418 cmd::test::run_cmd "unblock peer" "unblocked" unblock --name phone-testunit
419 cmd::test::run_cmd "unblock not blocked" "not blocked" unblock --name phone-testunit
420 cmd::test::run_cmd_fails "unblock nonexistent" unblock --name nonexistent-peer
421 cmd::test::run_cmd_fails "unblock missing --name" unblock
422
423 # Cleanup
424 "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
425}
426
427function cmd::test::fn_rule_assign() {
428 test::section "cmd::rule::assign"
429
430 "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
431 "$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1
432
433 cmd::test::run_cmd "rule assign" "Assigned" \
434 rule assign --name admin --peer phone-testunit
435
436 "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
437}
438
439# ============================================
440# Run
441# ============================================
442
443function cmd::test::run() {
444 local destructive=false section=""
445 local fn=""
446
447 while [[ $# -gt 0 ]]; do
448 case "$1" in
449 --destructive) destructive=true; shift ;;
450 --section)
451 util::require_flag "--section" "${2:-}" || return 1
452 section="$2"; shift 2
453 ;;
454 --fn|--function) fn="$2"; shift 2 ;;
455 --verbose|-v) WGCTL_TEST_VERBOSE=true; shift ;;
456 --help) cmd::test::help; return ;;
457 *)
458 log::error "Unknown flag: $1"
459 return 1
460 ;;
461 esac
462 done
463
464 # After flag parsing:
465 if [[ -n "$fn" ]]; then
466 cmd::test::run_function "$fn"
467 return
468 fi
469
470 test::reset
471 log::section "wgctl Test Suite"
472
473 if [[ -n "$section" ]]; then
474 case "$section" in
475 list) cmd::test::section_list ;;
476 inspect) cmd::test::section_inspect ;;
477 config) cmd::test::section_config ;;
478 rules) cmd::test::section_rules ;;
479 groups) cmd::test::section_groups ;;
480 audit) cmd::test::section_audit ;;
481 logs) cmd::test::section_logs ;;
482 fw) cmd::test::section_fw ;;
483 net) cmd::test::section_net ;;
484 destructive) cmd::test::section_destructive ;;
485 *)
486 log::error "Unknown section: $section"
487 return 1
488 ;;
489 esac
490 else
491 cmd::test::section_list
492 cmd::test::section_inspect
493 cmd::test::section_config
494 cmd::test::section_rules
495 cmd::test::section_groups
496 cmd::test::section_audit
497 cmd::test::section_logs
498 cmd::test::section_fw
499 fi
500
501 if $destructive; then
502 cmd::test::section_destructive
503 fi
504
505 test::summary
506}