Comprehensive test plan covering every feature and code path of the sqssh suite.
Tests are split into automated (can run non-interactively with an unencrypted test key) and manual (require interactive terminal input, passphrase entry, or physical actions like resizing a window).
# Choose your mode:
# automated — run all automated tests only
# manual — run manual tests only (prompted one at a time)
# both — run automated first, then prompt for manual tests
./tests/run.sh # uses default server
./tests/run.sh root@your-server # custom server
SERVER_A=root@host ./tests/run.sh # via env varOutput streams in real-time with colored PASS/FAIL per test and section summaries. Stops on first failure.
When running manually (without the script), report results after each test section (A0, A1, A2, etc.) before proceeding to the next. If any test fails, stop immediately and report the failure — do not continue to subsequent sections until the failure is resolved.
# Server A: sqsshd running, accessible via SSH for setup
SERVER_A=root@167.235.197.87
# Server B: sqsshd running (used for server-to-server and benchmark tests)
SERVER_B=root@46.224.104.133
All tests below can run non-interactively using -i /tmp/test_key.
This section generates the test key, deploys it, restarts sqsshd to ensure the whitelist is fresh, and verifies end-to-end connectivity. If any step fails, all subsequent tests should be skipped.
MISSING=0
for bin in sqssh sqscp sqsftp sqssh-keygen sqssh-agent sqssh-add sqssh-copy-id sqssh-keyscan sqsshctl; do
$bin --help > /dev/null 2>&1 && echo "$bin: OK" || { echo "$bin: MISSING"; MISSING=1; }
done
[ $MISSING -eq 0 ] || { echo "SETUP FAILED: missing binaries"; exit 1; }
ssh -o ConnectTimeout=5 $SERVER_A "echo ssh-ok" || { echo "SETUP FAILED: cannot SSH to $SERVER_A (is sshd running?)"; exit 1; }
ssh $SERVER_A "systemctl is-active sqsshd" | grep -q active || { echo "SETUP FAILED: sqsshd not running on $SERVER_A"; exit 1; }
rm -f /tmp/test_key /tmp/test_key.pub
echo "" | sqssh-keygen -f /tmp/test_key -C "automated-test"
[ -f /tmp/test_key ] && [ -f /tmp/test_key.pub ] || { echo "SETUP FAILED: key generation"; exit 1; }
TESTPUB=$(awk '{print $2}' /tmp/test_key.pub)
ssh $SERVER_A "mkdir -p ~/.sqssh && chmod 700 ~/.sqssh"
ssh $SERVER_A "grep -qF '$TESTPUB' ~/.sqssh/authorized_keys 2>/dev/null || \
(echo 'sqssh-ed25519 $TESTPUB automated-test' >> ~/.sqssh/authorized_keys && \
chmod 600 ~/.sqssh/authorized_keys)"
ssh $SERVER_A "systemctl restart sqsshd"
sleep 2
ssh $SERVER_A "systemctl is-active sqsshd" | grep -q active || { echo "SETUP FAILED: sqsshd not active"; exit 1; }
sqssh -i /tmp/test_key $SERVER_A "echo setup-ok" | grep -q "setup-ok" || { echo "SETUP FAILED: sqssh connection"; exit 1; }
echo "A0 SETUP COMPLETE"
echo "" | sqssh-keygen -f /tmp/test_key_a -C "test"
head -1 /tmp/test_key_a # Expect: SQSSH-ED25519-PRIVATE-KEY
cat /tmp/test_key_a.pub # Expect: sqssh-ed25519 <base58> test
printf "pass\npass\n" | sqssh-keygen -f /tmp/test_key_enc -C "encrypted"
head -1 /tmp/test_key_enc # Expect: SQSSH-ED25519-ENCRYPTED-KEY
echo "" | sqssh-keygen -f /tmp/test_key_c -C "work laptop"
grep "work laptop" /tmp/test_key_c.pub # Should match
stat -f "%Lp" /tmp/test_key_a # Expect: 600
stat -f "%Lp" /tmp/test_key_a.pub # Expect: 644
stat -f "%Lp" ~/.sqssh # Expect: 700
PUBKEY=$(ssh $SERVER_A "sqsshd --show-pubkey")
sqssh-keyscan add test.example.com $PUBKEY
sqssh-keyscan list | grep test.example.com # Should match
sqssh-keyscan remove test.example.com
sqssh-keyscan list | grep test.example.com # Should NOT match
sqssh-keyscan add "*.internal" $PUBKEY
sqssh-keyscan list | grep '*.internal' # Should match
sqssh-keyscan remove "*.internal"
sqssh -i /tmp/test_key $SERVER_A "echo hello"
# Expect: hello
# Exit code: 0
sqssh -i /tmp/test_key $SERVER_A "exit 42"
echo $? # Expect: 42
sqssh -i /tmp/test_key $SERVER_A "echo err >&2; echo out"
# Expect both "err" and "out" in output
sqssh -i /tmp/test_key user@192.0.2.1
# Expect: "unknown host" error, immediate exit
sqssh -i /tmp/test_key user@unknown.example.com
# Expect: "unknown host" error
sqssh -i /tmp/test_key $SERVER_A "echo conn1" &
sqssh -i /tmp/test_key $SERVER_A "echo conn2" &
sqssh -i /tmp/test_key $SERVER_A "echo conn3" &
wait
# All three should print their output
dd if=/dev/urandom of=/tmp/test_upload bs=1M count=10 2>/dev/null
sqscp -i /tmp/test_key /tmp/test_upload $SERVER_A:/tmp/test_upload_v
LOCAL=$(md5sum /tmp/test_upload | cut -d' ' -f1) # or md5 -q on macOS
REMOTE=$(ssh $SERVER_A "md5sum /tmp/test_upload_v" | cut -d' ' -f1)
[ "$LOCAL" = "$REMOTE" ] && echo PASS || echo FAIL
sqscp -i /tmp/test_key $SERVER_A:/tmp/test_upload_v /tmp/test_download
# Checksum should match A4.1
sqscp -i /tmp/test_key /tmp/test_upload $SERVER_A:~
ssh $SERVER_A "test -f ~/test_upload" && echo PASS
sqscp -i /tmp/test_key $SERVER_A:~/test_upload /tmp/test_from_home
# Checksum should match
echo "aaa" > /tmp/multi_a.txt
echo "bbb" > /tmp/multi_b.txt
sqscp -i /tmp/test_key /tmp/multi_a.txt /tmp/multi_b.txt $SERVER_A:/tmp/
ssh $SERVER_A "cat /tmp/multi_a.txt /tmp/multi_b.txt"
# Expect: aaa\nbbb
mkdir -p /tmp/test_dir/sub
echo "file1" > /tmp/test_dir/a.txt
echo "file2" > /tmp/test_dir/sub/b.txt
sqscp -i /tmp/test_key -r /tmp/test_dir $SERVER_A:/tmp/
ssh $SERVER_A "find /tmp/test_dir -type f | sort"
# Expect: a.txt and sub/b.txt
sqscp -i /tmp/test_key -r $SERVER_A:/tmp/test_dir /tmp/test_dir_dl
diff -r /tmp/test_dir /tmp/test_dir_dl/test_dir
# Expect: no differences
sqscp -i /tmp/test_key -p /tmp/test_upload $SERVER_A:/tmp/test_ts
# Local and remote mtime should match
chmod 755 /tmp/test_upload
sqscp -i /tmp/test_key /tmp/test_upload $SERVER_A:/tmp/test_pm
ssh $SERVER_A "stat -c %a /tmp/test_pm"
# Expect: 755
sqscp -i /tmp/test_key /tmp/test_dir $SERVER_A:/tmp/
# Expect: "is a directory (use -r)" error, non-zero exit
ssh $SERVER_A "sqsshctl reload-keys"
# Expect: "reloaded keys for 'root'"
ssh $SERVER_A "sqsshctl reload-keys --all"
# Expect: "reloaded all keys (N total)"
ssh $SERVER_A "sqsshd --show-pubkey"
# Expect: base58 public key string
sqssh -i /tmp/test_key $SERVER_A "sleep 30" &
PID=$!
sleep 2
ssh $SERVER_A "systemctl stop sqsshd"
sleep 8
kill -0 $PID 2>/dev/null && echo FAIL || echo PASS
# IMPORTANT: restart sqsshd immediately — remaining tests need it
ssh $SERVER_A "systemctl start sqsshd"
sleep 2
ssh $SERVER_A "systemctl is-active sqsshd" | grep -q active || { echo "CRITICAL: sqsshd failed to restart"; exit 1; }
echo "" | sqssh-keygen -f /tmp/unknown_key -C "unknown"
timeout 5 sqssh -i /tmp/unknown_key $SERVER_A "echo should-not-reach" 2>&1
# Expect: timeout or connection error (key not in whitelist)
ssh $SERVER_A "touch ~/.hushlogin"
sqssh -i /tmp/test_key $SERVER_A "echo after-hushlogin"
# Expect: no MOTD or last login message
ssh $SERVER_A "rm -f ~/.hushlogin"
ssh $SERVER_A "ln -sf /etc/passwd ~/.hushlogin"
sqssh -i /tmp/test_key $SERVER_A "echo after-symlink"
# Expect: MOTD and last login SHOULD appear (symlink ignored)
ssh $SERVER_A "rm -f ~/.hushlogin"
sqssh -i /tmp/test_key $SERVER_A "echo c1" &
sqssh -i /tmp/test_key $SERVER_A "echo c2" &
sqssh -i /tmp/test_key $SERVER_A "echo c3" &
wait
# All three should succeed
sqssh -i /tmp/test_key user@192.0.2.1
# Expect: "unknown host" error
sqscp -i /tmp/test_key $SERVER_A:/nonexistent/file /tmp/
# Expect: "No such file or directory" error
sqscp -i /tmp/test_key /tmp/test_dir $SERVER_A:/tmp/
# Expect: "is a directory (use -r)" error
echo "wrong" | sqssh $SERVER_A
# Expect: "decryption failed (wrong passphrase?)"
SQSSH_AGENT_SOCK=/tmp/nonexistent.sock sqssh-add -l
# Expect: connection error
echo -e "pwd\ncd /tmp\npwd\nquit" | sqsftp -i /tmp/test_key $SERVER_A
# Expect: first pwd shows home dir, second shows /tmp
echo -e "lcd /tmp\nput test_upload\nget test_upload /tmp/sftp_dl\nquit" | sqsftp -i /tmp/test_key $SERVER_A
md5sum /tmp/test_upload /tmp/sftp_dl
# Checksums should match
echo -e "mkdir /tmp/sftp_auto_test\nls /tmp/sftp_auto_test\nrm /tmp/sftp_auto_test\nquit" | sqsftp -i /tmp/test_key $SERVER_A
echo -e "stat /etc/motd\nquit" | sqsftp -i /tmp/test_key $SERVER_A
# Expect: shows path, type, size, mode
echo -e "help\nfoobar\nquit" | sqsftp -i /tmp/test_key $SERVER_A
# Expect: help output, then "unknown command: foobar"
echo -e "lpwd\nlcd /tmp\nlpwd\nquit" | sqsftp -i /tmp/test_key $SERVER_A
# Expect: second lpwd shows /tmp
echo -e "cd ~\npwd\nquit" | sqsftp -i /tmp/test_key $SERVER_A
# Expect: pwd shows home directory
# Start agent, add unencrypted test key, verify
sqssh-agent &
AGENT_PID=$!
export SQSSH_AGENT_SOCK=~/.sqssh/agent.sock
sleep 1
sqssh-add /tmp/test_key
sqssh-add -l # Should show the key
sqssh-add -d /tmp/test_key
sqssh-add -l # Should be empty
sqssh-add /tmp/test_key
sqssh-add -D
sqssh-add -l # Should be empty
kill $AGENT_PID
touch ~/.sqssh/agent.sock
sqssh-agent 2>&1
# Expect: error about existing socket
rm -f ~/.sqssh/agent.sock
cat > /tmp/sqssh_test_config << EOF
Host testhost
Hostname 167.235.197.87
User root
IdentityFile /tmp/test_key
EOF
sqssh -F /tmp/sqssh_test_config testhost "echo config-works"
# Expect: "config-works"
cat > /tmp/sqssh_test_config << EOF
Host *.testdomain
Hostname 167.235.197.87
User root
IdentityFile /tmp/test_key
EOF
sqssh -F /tmp/sqssh_test_config server1.testdomain "echo wildcard-works"
# Expect: "wildcard-works"
cat > /tmp/sqssh_test_config << EOF
Host testhost
Hostname 167.235.197.87
User root
IdentityFile /tmp/test_key
EOF
sqscp -F /tmp/sqssh_test_config -i /tmp/test_key /tmp/test_upload root@testhost:/tmp/
# Expect: upload succeeds using config hostname resolution
echo -e "pwd\nquit" | sqsftp -F /tmp/sqssh_test_config -i /tmp/test_key testhost
# Expect: shows home directory (config provides Hostname + User)
stat -f "%Lp" ~/.sqssh/id_ed25519 # Expect: 600
stat -f "%Lp" ~/.sqssh/id_ed25519.pub # Expect: 644
stat -f "%Lp" ~/.sqssh # Expect: 700
stat -f "%Lp" ~/.sqssh/known_hosts # Expect: 644
ssh $SERVER_A "stat -c %a /etc/sqssh/host_key" # Expect: 600
ssh $SERVER_A "stat -c %a ~/.sqssh" # Expect: 700
ssh $SERVER_A "stat -c %a ~/.sqssh/authorized_keys" # Expect: 600
ssh $SERVER_A "stat -c %a /run/sqssh/control.sock 2>/dev/null || \
stat -c %a /var/run/sqssh/control.sock" # Expect: 666
sqssh --version
# Expect: "sqssh 0.1.0" (or current version)
sqssh -i /tmp/test_key -l root 167.235.197.87 "whoami"
# Expect: root
timeout 3 sqssh -i /tmp/test_key -N $SERVER_A 2>&1
# Expect: exits after timeout, no shell prompt
echo "should not reach" | sqssh -i /tmp/test_key -n $SERVER_A "cat"
# Expect: cat exits immediately (stdin is /dev/null)
sqssh -i /tmp/test_key -q $SERVER_A "echo quiet-test" 2>&1
# Expect: only "quiet-test", no warnings or diagnostics
sqssh -i /tmp/test_key -L 8080:localhost:80 $SERVER_A 2>&1
echo $?
# Expect: "sqssh: local port forwarding (-L) is not yet implemented"
# Exit code: 1
sqssh -i /tmp/test_key -R 8080:localhost:80 $SERVER_A 2>&1
echo $?
# Expect: "sqssh: remote port forwarding (-R) is not yet implemented"
# Exit code: 1
sqssh -i /tmp/test_key -D 1080 $SERVER_A 2>&1
echo $?
# Expect: "sqssh: dynamic port forwarding (-D) is not yet implemented"
# Exit code: 1
sqssh -i /tmp/test_key -J bastion $SERVER_A 2>&1
echo $?
# Expect: "sqssh: ProxyJump (-J) is not yet implemented"
# Exit code: 1
sqssh -i /tmp/test_key -o StrictHostKeyChecking=no $SERVER_A "echo opt-test"
# Expect: "opt-test" (option accepted but ignored)
sqscp --version
# Expect: "sqscp 0.1.0"
sqscp -J bastion /tmp/test_upload $SERVER_A:/tmp/ 2>&1
echo $?
# Expect: "sqscp: ProxyJump (-J) is not yet implemented"
# Exit code: 1
sqscp -i /tmp/test_key -o Compression=yes /tmp/test_upload $SERVER_A:/tmp/test_opt
# Expect: upload succeeds
sqsftp --version
# Expect: "sqsftp 0.1.0"
echo "pwd\nquit" | sqsftp -i /tmp/test_key -P 22 $SERVER_A
# Expect: same as -p 22
cat > /tmp/sftp_batch << EOF
pwd
cd /tmp
pwd
quit
EOF
sqsftp -i /tmp/test_key -b /tmp/sftp_batch $SERVER_A
# Expect: shows home dir, then /tmp, no sftp> prompt
rm /tmp/sftp_batch
echo -e "pwd\nquit" | sqsftp -i /tmp/test_key -q $SERVER_A 2>&1
# Expect: output with no extra diagnostics
sqssh-keygen --version
# Expect: "sqssh-keygen 0.1.0"
sqssh-keygen -l /tmp/test_key
# Expect: same output as --fingerprint
# Generate unencrypted key, then add passphrase
sqssh-keygen -f /tmp/test_key_pp -C "pp-test" -N ""
printf "new\nnew\n" | sqssh-keygen -p /tmp/test_key_pp
head -1 /tmp/test_key_pp
# Expect: SQSSH-ED25519-ENCRYPTED-KEY
# Now remove passphrase (enter old passphrase, then empty new passphrase)
printf "new\n\n\n" | sqssh-keygen -p /tmp/test_key_pp
head -1 /tmp/test_key_pp
# Expect: SQSSH-ED25519-PRIVATE-KEY (passphrase removed)
rm -f /tmp/test_key_pp /tmp/test_key_pp.pub
sqssh-keygen -f /tmp/test_key_ni -C "ni-test" -N ""
head -1 /tmp/test_key_ni
# Expect: SQSSH-ED25519-PRIVATE-KEY (no passphrase)
sqssh-keygen -f /tmp/test_key_ni2 -C "ni-test2" -N "secret"
head -1 /tmp/test_key_ni2
# Expect: SQSSH-ED25519-ENCRYPTED-KEY
rm -f /tmp/test_key_ni /tmp/test_key_ni.pub /tmp/test_key_ni2 /tmp/test_key_ni2.pub
sqssh-keygen -y -f /tmp/test_key
# Expect: sqssh-ed25519 <base58>
echo "" | sqssh-keygen -q -f /tmp/test_key_quiet -C "quiet"
# Expect: no "Generated" message, just creates files
rm -f /tmp/test_key_quiet /tmp/test_key_quiet.pub
sqssh-keygen -t ed25519 -f /tmp/test_key_t -N "" 2>&1
# Expect: generates key (ed25519 is valid)
sqssh-keygen -t rsa -f /tmp/test_key_rsa -N "" 2>&1
echo $?
# Expect: error about unsupported key type, exit 1
rm -f /tmp/test_key_t /tmp/test_key_t.pub
sqssh-keyscan scan 167.235.197.87 2>&1
# Expect: message about sQUIC silent servers, not an error
echo $?
# Expect: 0
sqssh-keyscan --version
# Expect: "sqssh-keyscan 0.1.0"
sqssh-agent &
AGENT_PID=$!
sleep 1
sqssh-agent -k
sleep 1
kill -0 $AGENT_PID 2>/dev/null && echo FAIL || echo PASS
rm -f ~/.sqssh/agent.sock
sqssh-agent --version
# Expect: "sqssh-agent 0.1.0"
sqssh-agent &
AGENT_PID=$!
export SQSSH_AGENT_SOCK=~/.sqssh/agent.sock
sleep 1
sqssh-add /tmp/test_key
sqssh-add -L
# Expect: sqssh-ed25519 <base58> <comment>
sqssh-add -l
# Expect: fingerprint only
kill $AGENT_PID
rm -f ~/.sqssh/agent.sock
sqssh-agent &
AGENT_PID=$!
export SQSSH_AGENT_SOCK=~/.sqssh/agent.sock
sleep 1
sqssh-add -q /tmp/test_key 2>&1
# Expect: no "Identity added" message
kill $AGENT_PID
rm -f ~/.sqssh/agent.sock
sqssh-add -x 2>&1
# Expect: "not yet implemented"
sqssh-add -X 2>&1
# Expect: "not yet implemented"
sqssh-add --version
# Expect: "sqssh-add 0.1.0"
sqssh-copy-id -n -i /tmp/test_key.pub $SERVER_A 2>&1
# Expect: "Would deploy key: <pubkey> to <host>" without connecting
sqssh-copy-id --version
# Expect: "sqssh-copy-id 0.1.0"
sqsshctl --version
# Expect: "sqsshctl 0.1.0"
Tests that every --long-form flag works identically to its -short counterpart.
See tests/run.sh section A15 for the full list. Covers:
sqssh --port --identity --login-name --no-command --quiet --local-forward --proxy-jump --optionsqscp --port --preserve --recursive --proxy-jumpsqsftp --batchsqssh-keygen --file --comment --new-passphrase --print-public --fingerprint --typesqssh-keyscan scan --portsqssh-agent --killsqssh-add --list --list-public --delete-all --quietsqssh-copy-id --dry-run --identitysqsshctl --socket
Warning: These tests temporarily modify authorized_keys on the server. They run last (after all other tests) to avoid breaking connectivity.
ssh $SERVER_A "cp ~/.sqssh/authorized_keys ~/.sqssh/ak_backup"
ssh $SERVER_A "rm ~/.sqssh/authorized_keys && ln -s /tmp/evil ~/.sqssh/authorized_keys"
ssh $SERVER_A "sqsshctl reload-keys"
# Expect: error about symlink
ssh $SERVER_A "rm ~/.sqssh/authorized_keys && mv ~/.sqssh/ak_backup ~/.sqssh/authorized_keys"
ssh $SERVER_A "chmod 666 ~/.sqssh/authorized_keys"
ssh $SERVER_A "sqsshctl reload-keys"
# Expect: error about permissions
ssh $SERVER_A "chmod 600 ~/.sqssh/authorized_keys"
rm -f /tmp/test_key /tmp/test_key.pub /tmp/test_key_a /tmp/test_key_a.pub
rm -f /tmp/test_key_enc /tmp/test_key_enc.pub /tmp/test_key_c /tmp/test_key_c.pub
rm -f /tmp/test_upload /tmp/test_download /tmp/test_from_home /tmp/sftp_dl
rm -f /tmp/multi_a.txt /tmp/multi_b.txt
rm -rf /tmp/test_dir /tmp/test_dir_dl
rm -f /tmp/unknown_key /tmp/unknown_key.pub
rm -f /tmp/sqssh_test_config /tmp/sftp_batch
rm -f ~/.sqssh/config.bak
# Server-side: remove test keys from authorized_keys and reload whitelist
ssh SERVER "sed -i '/automated-test/d' ~/.sqssh/authorized_keys"
ssh SERVER "sqsshctl reload-keys --all"
Note: cleanup runs via a trap on EXIT, ensuring test keys are removed from the server even when tests fail or are interrupted.
These tests require a real terminal (PTY rendering, escape sequences, window resize, passphrase entry, or physical network changes).
sqssh -i /tmp/test_key $SERVER_A
htop
# Expect: htop renders correctly with colors and layout
# Press q to quit htop
exit
sqssh -i /tmp/test_key $SERVER_A
sleep 100
# Press Ctrl+C
# Expect: sleep interrupted, shell still alive (requires PTY mode)
exit
sqssh -i /tmp/test_key $SERVER_A
# Resize terminal window by dragging
stty size
# Expect: output reflects new dimensions
exit
sqssh -i /tmp/test_key $SERVER_A
# Press Enter, then type ~.
# Expect: immediate disconnect
sqssh -i /tmp/test_key $SERVER_A
# Press Enter, then type ~?
# Expect: escape sequence help listing
sqssh -i /tmp/test_key $SERVER_A
# Press Enter, then type ~~
# Expect: single ~ sent to remote shell
sqssh $SERVER_A
# Expect: passphrase prompt
# Enter correct passphrase
# Expect: connection succeeds
exit
eval $(sqssh-agent)
sqssh-add # Enter passphrase when prompted
sqssh-add -l # Expect: key listed
sqssh $SERVER_A
# Expect: no passphrase prompt, direct connection
exit
# Terminal 1:
sqssh -i /tmp/test_key $SERVER_A
tail -f /var/log/sqsshd.log
# Terminal 2:
ssh $SERVER_A "systemctl reload sqsshd"
# Expect in Terminal 1:
# "Server restarting. Reconnecting..."
# Session reconnects, tail resumes
# No MOTD or last login on reconnect
# Same as M3.1 — verify tail process PID is the same before and after
sqssh -i /tmp/test_key $SERVER_A
# Switch networks (WiFi to Ethernet, toggle VPN, etc.)
# Expect: session continues without interruption
# Server logs should show "client migrated from X to Y"
# Set MaxSessions 2 in /etc/sqssh/sqsshd.conf, restart sqsshd
# Open 2 sqssh sessions — both should succeed
# Open 3rd session — should be rejected
# Reset MaxSessions and restart
rm -f /tmp/test_key /tmp/test_key.pub
rm -f /tmp/bench_1kb /tmp/bench_10mb /tmp/bench_100mb /tmp/bench_1gb /tmp/bench_dl
ssh $SERVER_A "rm -f /tmp/test_upload* /tmp/test_download /tmp/test_ts /tmp/test_pm /tmp/test_perms"
ssh $SERVER_A "rm -rf /tmp/test_dir /tmp/bench_*"