test: add a local Docker test environment for all features

testenv/ starts two real NetBox 4.7 instances (v2 and v1 token), SSH
targets on public, mgmt, and prod networks, a jump host, an IPv6-only
host, a static host, and a client with ssh, sftp, scp, tmux, zellij,
bash, zsh, and fish. A seed script fills NetBox through the REST API.

141 end-to-end checks cover cache and refresh filters, connecting,
all resolver strategies, multiple instances, shortcuts and rewrites,
SSH options and rules, static hosts, sftp/scp incl. IPv6 and jump
hosts, tunnels, local notes and NetBox comments, completion and shell
hooks, the TUI (driven through tmux), the configurator, and uninstall.

Local only: testenv/up.sh, test.sh, shell.sh, down.sh. Not part of
CI/CD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sebastian UnterschützandClaude Opus 5.5 committed 2026-11-14 12:30:24 +01:00
1 parent 55e7a5106b
commit dbb99bdded
34 files changed
+1215

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
# Cache refresh across both NetBox instances, filters, list, search, clear.
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
EOF
expect_out "refresh both instances" "9 entries written" netssh cache refresh
expect_out "list shows mgmt address" "fsn/fsn1-pve1.hoster.test 10.231.1.11" netssh cache list
expect_out "list shows lab VM" "lab/web1 10.231.1.42 vm" netssh cache list
expect_out "IPv6-only host" "fsn/v6-host fd00:30::60" netssh cache list
expect_out "refresh one instance" "2 entries written" netssh cache refresh --instance lab
expect_out "refresh by tag" "3 entries written" netssh cache refresh --tag pve
expect_out "refresh by kind" "3 entries written" netssh cache refresh --kind vm
expect_eq "search" "fsn/fsn1-pve1.hoster.test
fsn/fsn1-pve2.hoster.test
fsn/fsn1-pve3.hoster.test" bash -c 'netssh search fsn1 | sort'
check "clear" netssh cache clear
expect_eq "empty after clear" "" bash -c 'netssh search fsn'
+29
View File
@@ -0,0 +1,29 @@
# Connecting with the resolved address, default user and port, overrides.
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
ssh:
default_user: ops
EOF
expect_eq "connect via NetBox" "fsn1-pve1" netssh fsn/fsn1-pve1.hoster.test hostname
expect_eq "unique name without instance" "fsn1-pve2" netssh fsn1-pve2.hoster.test hostname
expect_eq "default user" "ops" netssh fsn/fsn1-pve1.hoster.test whoami
expect_eq "user@ wins" "admin" netssh admin@fsn/fsn1-pve1.hoster.test whoami
expect_eq "-l wins" "admin" netssh -l admin fsn/fsn1-pve1.hoster.test whoami
expect_eq "remote command with arguments" "a b" netssh fsn/fsn1-pve1.hoster.test echo a b
expect_eq "dry run" "ssh -l ops 10.231.1.11" netssh --dry-run fsn/fsn1-pve1.hoster.test
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
ssh:
default_user: ops
default_port: 2222
EOF
expect_out "default port" " 2222" netssh fsn/fsn1-pve1.hoster.test 'echo $SSH_CONNECTION'
expect_out "-p wins over default port" " 22" netssh -p 22 fsn/fsn1-pve1.hoster.test 'echo $SSH_CONNECTION'
expect_eq "dry run with port" "ssh -p 2222 -l ops 10.231.1.11" netssh --dry-run fsn/fsn1-pve1.hoster.test
expect_eq "version" "netssh" bash -c 'netssh --version | cut -d" " -f1'
+63
View File
@@ -0,0 +1,63 @@
# Every resolver strategy against real NetBox data. cache.ttl 0 makes every
# connection ask NetBox, so each config change takes effect immediately.
jump=' - match: {instance: fsn}
proxy_jump: ops@10.231.0.30
- match: {name: "bastion*"}
proxy_jump: none'
config <<EOF
cache: {ttl: 0}
resolver: {strategies: [primary_ip]}
ssh:
default_user: ops
rules:
$jump
EOF
expect_eq "primary_ip uses prod address via bastion" "ssh -l ops -J ops@10.231.0.30 10.231.2.11" netssh --dry-run fsn/fsn1-pve1.hoster.test
expect_eq "primary_ip connects through the jump host" "fsn1-pve1" netssh fsn/fsn1-pve1.hoster.test hostname
expect_eq "bastion rule disables the jump" "ssh -l ops 10.231.0.30" netssh --dry-run fsn/bastion.hoster.test
expect_eq "IPv6 primary address" "ssh -l ops -J ops@10.231.0.30 fd00:30::60" netssh --dry-run fsn/v6-host
config <<'EOF'
cache: {ttl: 0}
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
ssh: {default_user: ops}
EOF
expect_eq "management_subnet" "ssh -l ops 10.231.1.12" netssh --dry-run fsn/fsn1-pve2.hoster.test
expect_eq "management_subnet falls back to primary_ip" "ssh -l ops 10.231.2.21" netssh --dry-run fsn/web1
config <<'EOF'
cache: {ttl: 0}
resolver:
strategies: [interface_name, primary_ip]
interface_name: mgmt0
ssh: {default_user: ops}
EOF
expect_eq "interface_name" "ssh -l ops 10.231.1.13" netssh --dry-run fsn/fsn1-pve3.hoster.test
expect_eq "interface_name connects" "fsn1-pve3" netssh fsn/fsn1-pve3.hoster.test hostname
expect_eq "interface_name falls back for VMs" "ssh -l ops 10.231.2.22" netssh --dry-run fsn/web2
# eth0 holds the prod address, which sorts after mgmt: catches an ignored filter.
sed -i 's/interface_name: mgmt0/interface_name: eth0/' "$XDG_CONFIG_HOME/netssh.yaml"
expect_eq "interface_name picks the named interface" "ssh -l ops 10.231.2.13" netssh --dry-run fsn/fsn1-pve3.hoster.test
config <<'EOF'
cache: {ttl: 0}
resolver:
strategies: [hostname]
hostname_domain: example.org
ssh: {default_user: ops}
EOF
expect_eq "hostname strategy" "ssh -l ops fsn1-pve1.hoster.test" netssh --dry-run fsn/fsn1-pve1.hoster.test
expect_eq "hostname connects via DNS" "fsn1-pve1" netssh fsn/fsn1-pve1.hoster.test hostname
expect_eq "hostname domain for names without a dot" "ssh -l ops lab-db1.example.org" netssh --dry-run lab/lab-db1
config <<'EOF'
cache: {ttl: 0}
resolver:
strategies: [interface_name]
interface_name: mgmt0
EOF
expect_fail "no strategy finds an address" "no strategy resolved an IP" netssh --dry-run fsn/web1
+20
View File
@@ -0,0 +1,20 @@
# Two NetBox instances: qualified names, ambiguity, an unreachable instance.
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
ssh:
default_user: ops
rules:
- match: {kind: vm, instance: fsn}
proxy_jump: ops@10.231.0.30
EOF
expect_fail "same name in both instances is ambiguous" "found 2 exact matches" netssh --dry-run web1
expect_eq "fsn/web1 through the bastion" "web1" netssh fsn/web1 hostname
expect_eq "lab/web1 directly" "lab-web1" netssh lab/web1 hostname
expect_eq "lab uses its v1 token" "lab-db1" netssh lab/lab-db1 hostname
# lab unreachable: qualified fsn targets still work, unqualified ones need every instance.
sed -i 's#http://10.231.0.21:8080#http://10.231.0.21:9#' "$XDG_CONFIG_HOME/netssh.yaml"
expect_eq "qualified target with another instance down" "fsn1-pve1" netssh fsn/fsn1-pve1.hoster.test hostname
expect_fail "unqualified target needs every instance" "10.231.0.21:9" netssh --dry-run fsn1-pve1.hoster.test
+31
View File
@@ -0,0 +1,31 @@
# Domains, hyphen folding, rewrite rules, alias --test, completion, aliases.
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
ssh: {default_user: ops}
shortcuts:
domains: [.hoster.test]
rewrites:
- pattern: '^([a-z]+)[0-9]+-'
replace: '$1'
EOF
check "refresh" netssh cache refresh
expect_eq "domain expansion via NetBox" "fsn1-pve1" netssh fsn/fsn1-pve1 hostname
expect_eq "rewrite shortcut, qualified" "fsn1-pve2" netssh fsn/fsnpve2 hostname
expect_eq "rewrite shortcut, unqualified" "fsn1-pve3" netssh fsnpve3 hostname
expect_out "alias --test" "fsn1-pve1.hoster.test → fsnpve1" netssh alias --test fsn1-pve1.hoster.test
expect_out "completion offers shortcuts" "fsnpve1" netssh __complete fsnp
expect_out "aliases use the shortcut" "alias fsn_fsnpve1='netssh " netssh alias --shell bash
expect_fail "unknown shortcut" "host not found" netssh --dry-run fsnpve9
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
shortcuts:
domains: [.hoster.test]
strip_hyphens: true
EOF
check "refresh" netssh cache refresh
expect_eq "hyphen folding" "ssh 10.231.1.12" netssh --dry-run fsn/fsn1pve2
+34
View File
@@ -0,0 +1,34 @@
# Jump hosts, rules, host key checking, connection target, options, extra args.
known="$HOME/known_hosts"
config <<EOF
cache: {ttl: 0}
resolver: {strategies: [primary_ip]}
ssh:
default_user: ops
options:
UserKnownHostsFile: $known
ServerAliveInterval: "30"
extra_args: ["-T"]
rules:
- match: {tag: web}
proxy_jump: ops@10.231.0.30
- match: {tag: pve}
proxy_jump: ops@10.231.0.30
host_key_checking: strict
- match: {name: "fsn1-pve2*"}
host_key_checking: accept-new
- match: {name: "fsn1-pve3*"}
host_key_checking: "off"
target: name
EOF
expect_eq "rule by tag adds jump host" "ssh -T -l ops -J ops@10.231.0.30 -o serveraliveinterval=30 -o userknownhostsfile=$known 10.231.2.21" netssh --dry-run fsn/web1
expect_eq "connect through rule jump" "web1" netssh fsn/web1 hostname
expect_out "--no-jump removes it" "-o ProxyJump=none" netssh --dry-run --no-jump fsn/web1
expect_out "--jump overrides it" "-J ops@10.231.0.30" netssh --dry-run --jump ops@10.231.0.30 fsn/v6-host
expect_fail "strict rejects an unknown host key" "Host key verification failed" netssh fsn/fsn1-pve1.hoster.test hostname
expect_eq "later rule accept-new wins" "fsn1-pve2" netssh fsn/fsn1-pve2.hoster.test hostname
expect_out "accept-new stored the key" "10.231.2.12" cat "$known"
expect_out "off prints a notice" "host key checking is disabled" netssh fsn/fsn1-pve3.hoster.test hostname
expect_out "target name keeps the address" "-o HostName=10.231.2.13 -o HostKeyAlias=fsn1-pve3.hoster.test" netssh --dry-run fsn/fsn1-pve3.hoster.test
expect_out "target name destination" " fsn1-pve3.hoster.test" netssh --dry-run fsn/fsn1-pve3.hoster.test
expect_out "user -o wins over config" "-o ServerAliveInterval=5" netssh --dry-run -o ServerAliveInterval=5 fsn/web1
+25
View File
@@ -0,0 +1,25 @@
# Static hosts without any NetBox: config, CLI, own user/port/options, rules.
config_only <<'EOF'
ssh:
default_user: ops
rules:
- match: {kind: static, tag: legacy}
options: {ServerAliveInterval: "15"}
hosts:
- name: legacy-box
address: 10.231.0.50
user: admin
tags: [legacy]
EOF
expect_eq "connect to a static host" "legacy-box" netssh legacy-box hostname
expect_eq "static user" "admin" netssh legacy-box whoami
expect_out "rule by kind and tag" "-o serveraliveinterval=15" netssh --dry-run legacy-box
expect_eq "-l still wins" "ops" netssh -l ops legacy-box whoami
check "hosts add" netssh hosts add box2 10.231.0.50 --port 2222 --tag lab
expect_eq "hosts list" "box2 10.231.0.50 2222 lab" bash -c 'netssh hosts list | grep "^box2" | tr -s " "'
expect_out "static port" " 2222" netssh box2 'echo $SSH_CONNECTION'
expect_out "completion" "box2" netssh __complete bo
expect_fail "duplicate" "already exists" netssh hosts add box2 10.0.0.1
check "hosts remove" netssh hosts remove box2
expect_fail "removed host is gone" "no NetBox is configured" netssh --dry-run box2
expect_eq "sftp to a static host" "Remote working directory: /home/admin" bash -c 'echo pwd | netssh sftp -b - legacy-box | grep Remote'
+24
View File
@@ -0,0 +1,24 @@
# sftp and scp: upload, download, jump host, IPv6, errors.
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
ssh:
default_user: ops
rules:
- match: {kind: vm, instance: fsn}
proxy_jump: ops@10.231.0.30
EOF
echo "payload $$" >"$HOME/up.txt"
check "scp upload" netssh scp "$HOME/up.txt" fsn/fsn1-pve1.hoster.test:/tmp/up.txt
expect_eq "uploaded content" "payload $$" netssh fsn/fsn1-pve1.hoster.test cat /tmp/up.txt
check "scp download" netssh scp fsn/fsn1-pve1.hoster.test:/tmp/up.txt "$HOME/down.txt"
expect_eq "downloaded content" "payload $$" cat "$HOME/down.txt"
check "scp to a VM through the bastion" netssh scp "$HOME/up.txt" fsn/web1:/tmp/up.txt
expect_eq "VM content" "payload $$" netssh fsn/web1 cat /tmp/up.txt
check "scp to IPv6 host" netssh scp "$HOME/up.txt" fsn/v6-host:/tmp/v6.txt
expect_eq "IPv6 content" "payload $$" netssh fsn/v6-host cat /tmp/v6.txt
expect_out "sftp starts in the path" "Remote working directory: /tmp" bash -c 'echo pwd | netssh sftp -b - fsn/fsn1-pve2.hoster.test:/tmp'
expect_out "sftp to IPv6 host" "v6.txt" bash -c 'echo "ls /tmp" | netssh sftp -b - fsn/v6-host'
expect_out "sftp through the bastion" "up.txt" bash -c 'echo "ls /tmp" | netssh sftp -b - fsn/web1'
expect_fail "two remote hosts" "not supported" netssh scp fsn/web1:/a fsn/web2:/b
+40
View File
@@ -0,0 +1,40 @@
# Port forwards: named, ad hoc, --forward with a shell, port in use.
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
ssh:
default_user: ops
forwards:
web: {local: "18081:127.0.0.1:8081"}
rules:
- match: {kind: vm, instance: fsn}
proxy_jump: ops@10.231.0.30
EOF
tunnel_test() { # NAME PORT WANT ARGS...
local name=$1 port=$2 want=$3 pid out
shift 3
"$@" >"$HOME/tunnel.log" 2>&1 &
pid=$!
if wait_for 15 curl -sf "http://127.0.0.1:$port/"; then
out=$(curl -s "http://127.0.0.1:$port/")
[[ $out == *"$want"* ]] && ok "$name" || bad "$name" "curl: $out"
else
bad "$name" "tunnel did not open: $(cat "$HOME/tunnel.log")"
fi
kill "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
}
tunnel_test "named forward" 18081 "hello from fsn1-pve1" netssh tunnel fsn/fsn1-pve1.hoster.test web
expect_out "tunnel notice" "forwarding -L 18081:127.0.0.1:8081 via fsn1-pve1.hoster.test" cat "$HOME/tunnel.log"
tunnel_test "ad hoc spec through the bastion" 18082 "hello from web1" netssh tunnel fsn/web1 18082:127.0.0.1:8081
tunnel_test "--forward with a remote command" 18081 "hello from fsn1-pve2" netssh --forward web fsn/fsn1-pve2.hoster.test sleep 30
netssh tunnel fsn/fsn1-pve1.hoster.test web >/dev/null 2>&1 &
holder=$!
wait_for 15 curl -sf http://127.0.0.1:18081/
expect_fail "port in use fails instead of hanging" "Address in use" timeout 20 netssh tunnel fsn/fsn1-pve2.hoster.test web
kill "$holder"; wait "$holder" 2>/dev/null
expect_fail "unknown forward lists the configured ones" "available: web" netssh tunnel fsn/fsn1-pve1.hoster.test db
expect_fail "tunnel without forward" "forwards for fsn/fsn1-pve1.hoster.test: web" netssh tunnel fsn/fsn1-pve1.hoster.test
+22
View File
@@ -0,0 +1,22 @@
# Local notes and NetBox comments (v2 token on fsn, v1 token on lab).
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
EOF
check "refresh" netssh cache refresh
host=fsn/fsn1-pve1.hoster.test
check "set local note" netssh notes set $host "first line"
check "append local note" netssh notes append $host "second line"
expect_eq "show local note" "first line
second line" netssh notes show $host
expect_eq "note from stdin" "piped" bash -c "echo piped | netssh notes set $host && netssh notes show $host"
expect_eq "read NetBox comments (v2 token)" "Proxmox node 1" netssh notes show $host --netbox
check "append NetBox comments" netssh notes append $host --netbox "checked by netssh test"
expect_out "NetBox has the new line" "checked by netssh test" curl -s -H "Authorization: Bearer $FSN_TOKEN" "$FSN_URL/api/dcim/devices/?name=fsn1-pve1.hoster.test"
check "restore NetBox comments" netssh notes set $host --netbox "Proxmox node 1"
check "set NetBox comments (v1 token)" netssh notes set lab/lab-db1 --netbox "lab note"
expect_eq "read them back" "lab note" netssh notes show lab/lab-db1 --netbox
check "clear them" netssh notes set lab/lab-db1 --netbox ""
+32
View File
@@ -0,0 +1,32 @@
# Completion, aliases, hook install for bash, zsh, fish, and the command alias.
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
ssh:
default_user: ops
forwards:
web: {local: "18081:127.0.0.1:8081"}
shortcuts:
domains: [.hoster.test]
shell:
alias: j
EOF
check "refresh" netssh cache refresh
expect_out "complete hosts" "fsn/fsn1-pve2.hoster.test" netssh __complete fsn/fsn1
expect_out "complete scp host:" "fsn/fsn1-pve1.hoster.test:" netssh __complete scp a.txt fsn/fsn1-pve1
expect_out "complete forwards after the host" "web" netssh __complete tunnel fsn/fsn1-pve1.hoster.test ""
expect_out "complete --forward" "web" netssh __complete --forward ""
expect_out "fish aliases" "alias fsn_fsn1-pve1 " netssh alias --shell fish
check "hook install bash" netssh hook install --shell bash
# Interactive bash without a terminal warns about job control on stderr.
expect_eq "bash loads host aliases" "fsn1-pve1" sh -c "bash -ic 'fsn_fsn1-pve1 hostname' 2>/dev/null </dev/null"
expect_eq "bash command alias" "fsn1-pve2" sh -c "bash -ic 'j fsn/fsn1-pve2.hoster.test hostname' 2>/dev/null </dev/null"
check "hook install zsh" netssh hook install --shell zsh
expect_eq "zsh loads host aliases" "fsn1-pve3" zsh -ic 'fsn_fsn1-pve3 hostname' </dev/null
mkdir -p "$XDG_CONFIG_HOME/fish"
check "hook install fish" netssh hook install --shell fish
expect_eq "fish loads host aliases" "fsn1-pve1" fish -c 'fsn_fsn1-pve1 hostname' </dev/null
check "completion install bash" netssh completion install --shell bash
expect_out "bash completes hosts" "fsn/fsn1-pve2.hoster.test" bash -ic '. ~/.local/share/bash-completion/completions/netssh; COMP_WORDS=(netssh fsn/fsn1-pve2); COMP_CWORD=1; COMP_LINE="netssh fsn/fsn1-pve2"; COMP_POINT=${#COMP_LINE}; __start_netssh netssh; printf "%s\n" "${COMPREPLY[@]}"' </dev/null
+64
View File
@@ -0,0 +1,64 @@
# The interactive TUI, driven through tmux: search, connect, sftp, tunnel, edit.
config <<'EOF'
resolver:
strategies: [management_subnet, primary_ip]
management_subnets: [10.231.1.0/24]
ssh:
default_user: ops
forwards:
web: {local: "18085:127.0.0.1:8081"}
EOF
check "refresh" netssh cache refresh
export TMUX_TMPDIR="$HOME/tmux"
mkdir -p "$TMUX_TMPDIR"
screen() { tmux capture-pane -p -t tui; }
shows() { screen | grep -qF -- "$1"; }
tui() { # start netssh in a fresh tmux session
tmux kill-server 2>/dev/null
tmux new-session -d -s tui -x 160 -y 40 -e HOME="$HOME" -e XDG_CONFIG_HOME="$XDG_CONFIG_HOME" -e XDG_CACHE_HOME="$XDG_CACHE_HOME" netssh
wait_for 10 shows "Search hostname"
}
step() { # NAME TEXT — wait until the pane shows TEXT
if wait_for 15 shows "$2"; then ok "$1"; else bad "$1" "$(screen)"; fi
}
tui && ok "TUI starts" || bad "TUI starts" "$(screen)"
tmux send-keys -t tui "fsn1-pve2"
step "search shows the host" "fsn/fsn1-pve2.hoster.test"
tmux send-keys -t tui Enter
sleep 1
tmux send-keys -t tui "hostname" Enter
step "enter connects" "fsn1-pve2"
tmux send-keys -t tui "exit" Enter
tui
tmux send-keys -t tui "fsn1-pve3"
wait_for 10 shows "fsn/fsn1-pve3.hoster.test"
tmux send-keys -t tui C-o
step "ctrl+o opens sftp" "sftp>"
tmux send-keys -t tui "pwd" Enter
step "sftp works" "Remote working directory: /home/ops"
tmux send-keys -t tui "bye" Enter
tui
tmux send-keys -t tui "fsn1-pve1"
wait_for 10 shows "fsn/fsn1-pve1.hoster.test"
tmux send-keys -t tui C-t
step "ctrl+t lists configured forwards" "forwards: web"
tmux send-keys -t tui "web" Enter
step "tunnel notice" "forwarding -L 18085:127.0.0.1:8081"
expect_out "tunnel from the TUI works" "hello from fsn1-pve1" curl -s --retry 10 --retry-connrefused --retry-delay 1 http://127.0.0.1:18085/
tmux send-keys -t tui C-c
tui
tmux send-keys -t tui "fsn1-pve1"
wait_for 10 shows "fsn/fsn1-pve1.hoster.test"
tmux send-keys -t tui C-e
step "ctrl+e opens the editor" "Connect as: ops@fsn/fsn1-pve1.hoster.test:22"
tmux send-keys -t tui C-u "admin@fsn/fsn1-pve1.hoster.test:22" Enter
sleep 1
tmux send-keys -t tui "whoami" Enter
step "edited user is used" "admin"
tmux send-keys -t tui "exit" Enter
tmux kill-server 2>/dev/null
+9
View File
@@ -0,0 +1,9 @@
# The configurator in huh's accessible mode: without NetBox, add a static host,
# save, then connect. Menu numbers follow the order in internal/setup.
config_only </dev/null
rm -f "$XDG_CONFIG_HOME/netssh.yaml"
# no NetBox, static hosts, add, name, address, user, port, tags, back, save
out=$(python3 /tests/drive.py 2 4 1 legacy-box 10.231.0.50 admin "" legacy 5 8)
[[ $out == *"Config saved"* ]] && ok "configure saves" || bad "configure saves" "$out"
expect_out "config has the host" 'address: "10.231.0.50"' cat "$XDG_CONFIG_HOME/netssh.yaml"
expect_eq "connect to the configured host" "admin" netssh legacy-box whoami
+12
View File
@@ -0,0 +1,12 @@
# Uninstall in dry-run mode lists everything netssh created.
config <<'EOF'
resolver: {strategies: [primary_ip]}
EOF
check "refresh" netssh cache refresh
check "hook install" netssh hook install --shell bash
expect_out "lists the profile" ".bashrc" netssh uninstall --dry-run
expect_out "lists the config" "netssh.yaml" netssh uninstall --dry-run
expect_out "lists the cache" ".cache/netssh" netssh uninstall --dry-run
out=$(netssh uninstall --dry-run --keep-config 2>&1)
[[ $out != *"netssh.yaml"* ]] && ok "--keep-config keeps the config" || bad "--keep-config keeps the config" "$out"
expect_out "nothing was removed" "netssh shell-init" cat "$HOME/.bashrc"
+32
View File
@@ -0,0 +1,32 @@
"""Drive `netssh configure` in huh's accessible mode (TERM=dumb): one answer
per prompt, sent after a short pause so each form reads its own line."""
import os
import pty
import select
import sys
import time
answers = sys.argv[1:]
pid, fd = pty.fork()
if pid == 0:
os.execvpe("netssh", ["netssh", "configure"], dict(os.environ, TERM="dumb"))
out = b""
def drain(seconds):
global out
end = time.time() + seconds
while time.time() < end:
ready, _, _ = select.select([fd], [], [], 0.05)
if ready:
try:
out += os.read(fd, 4096)
except OSError:
return
for answer in answers:
drain(0.5)
os.write(fd, (answer + "\n").encode())
drain(2)
sys.stdout.write(out.decode(errors="replace"))
+78
View File
@@ -0,0 +1,78 @@
# Helpers for the netssh test environment. Sourced by run.sh, the tests, and
# shell.sh inside the client container.
# shellcheck disable=SC1091
. /state/env
RESULTS=${RESULTS:-/tmp/netssh-results}
# new_home gives each test file its own netssh config, cache, notes, and rc files.
new_home() {
HOME=$(mktemp -d /tmp/home.XXXXXX)
export HOME XDG_CONFIG_HOME="$HOME/.config" XDG_CACHE_HOME="$HOME/.cache" SHELL=/bin/bash
mkdir -p "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"
unset TMUX ZELLIJ NETSSH_NETBOX_URL NETSSH_NETBOX_TOKEN
cd "$HOME" || exit 1
}
# base_config prints both NetBox instances followed by stdin.
base_config() { cat /state/netssh.yaml -; }
# config writes the netssh config: both NetBox instances plus stdin.
config() { base_config >"$XDG_CONFIG_HOME/netssh.yaml"; }
# config_only writes stdin as the whole config (no NetBox).
config_only() { cat >"$XDG_CONFIG_HOME/netssh.yaml"; }
ok() {
echo "ok $CURRENT: $1" >>"$RESULTS"
printf ' \033[32mok\033[0m %s\n' "$1"
}
bad() {
echo "FAIL $CURRENT: $1" >>"$RESULTS"
printf ' \033[31mFAIL\033[0m %s\n' "$1"
printf '%s\n' "$2" | sed 's/^/ /'
}
# check NAME CMD... — CMD must succeed.
check() {
local name=$1 out
shift
if out=$("$@" 2>&1); then ok "$name"; else bad "$name" "exit $?: $out"; fi
}
# expect_out NAME WANT CMD... — CMD must succeed and print WANT (substring).
expect_out() {
local name=$1 want=$2 out rc
shift 2
out=$("$@" 2>&1) && rc=0 || rc=$?
if [ "$rc" = 0 ] && [[ $out == *"$want"* ]]; then ok "$name"; else bad "$name" "want: $want
got (exit $rc): $out"; fi
}
# expect_eq NAME WANT CMD... — CMD must succeed and print exactly WANT.
expect_eq() {
local name=$1 want=$2 out rc
shift 2
out=$("$@" 2>&1) && rc=0 || rc=$?
if [ "$rc" = 0 ] && [ "$out" = "$want" ]; then ok "$name"; else bad "$name" "want: $want
got (exit $rc): $out"; fi
}
# expect_fail NAME WANT CMD... — CMD must fail and print WANT (substring).
expect_fail() {
local name=$1 want=$2 out rc
shift 2
out=$("$@" 2>&1) && rc=0 || rc=$?
if [ "$rc" != 0 ] && [[ $out == *"$want"* ]]; then ok "$name"; else bad "$name" "want failure with: $want
got (exit $rc): $out"; fi
}
# wait_for TIMEOUT CMD... — retry CMD every 0.2 s until it succeeds.
wait_for() {
local deadline=$((SECONDS + $1))
shift
until "$@" >/dev/null 2>&1; do
[ $SECONDS -ge $deadline ] && return 1
sleep 0.2
done
}
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Runs every tests/NN-*.sh (or those matching $1) in a fresh netssh home and
# prints a summary. Exit code 1 if any check failed.
set -u
export RESULTS=/tmp/netssh-results
: >"$RESULTS"
filter=${1:-}
for file in /tests/[0-9][0-9]-*.sh; do
name=$(basename "$file" .sh)
[[ -n $filter && $name != *"$filter"* ]] && continue
echo "== $name"
(
export CURRENT=$name
. /tests/lib.sh
new_home
. "$file"
)
done
passed=$(grep -c '^ok ' "$RESULTS" || true)
failed=$(grep -c '^FAIL ' "$RESULTS" || true)
echo
echo "passed: $passed, failed: $failed"
if [ "$failed" -gt 0 ]; then
grep '^FAIL ' "$RESULTS" | sed 's/^/ /'
exit 1
fi