Last active 1 month ago

nuno revised this gist 1 month ago. Go to revision

1 file changed, 486 insertions

gistfile1.txt(file created)

@@ -0,0 +1,486 @@
1 + #!/usr/bin/env bash
2 +
3 + WGCTL_BINARY="$(command -v wgctl)"
4 +
5 + # ============================================
6 + # Lifecycle
7 + # ============================================
8 +
9 + function cmd::test::on_load() {
10 + flag::register --destructive
11 + flag::register --section
12 + flag::register --fn
13 + flag::register --function
14 + }
15 +
16 + function cmd::test::help() {
17 + cat <<EOF
18 + Usage: wgctl test [options]
19 +
20 + Run the wgctl test suite.
21 +
22 + Options:
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 +
26 + Examples:
27 + wgctl test
28 + wgctl test --section rules
29 + wgctl test --destructive
30 + EOF
31 + }
32 +
33 + # ============================================
34 + # Test helpers
35 + # ============================================
36 +
37 + function 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 +
79 + function 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 +
107 + function 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 +
138 + function 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 +
150 + function 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 +
158 + function 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 +
165 + function 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 +
174 + function 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 +
181 + function 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 +
188 + function 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 +
197 + function 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 + }
207 + function 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 +
236 + function 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 +
316 + function 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 +
337 + function 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 +
360 + function 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 +
386 + function 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 +
404 + function 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 +
423 + function 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 + }
Newer Older