最後活躍 1 month ago

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