Files
VoiletSshTools/smoketest/run-goscp.sh
T
liyp 4365e80075 sshtools: 非交互 SSH 工具集(golink / goscp)
golink: 一条命令在远端执行,退出码原样透传,从不提示、不分配终端。
goscp:  用 SCP 协议上传,-files 一个参数同时接受文件和目录。

- 纯 Go,唯一依赖 golang.org/x/crypto,可离线交叉编译到六个桌面平台
- internal/sshx 统一建连、认证(password + keyboard-interactive)与主机密钥策略
- smoketest/ 自带迷你 sshd,57 项端到端测试(golink 38 + goscp 19)
2026-10-19 10:22:53 +08:00

179 lines
6.9 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# End-to-end smoke test for goscp against the bundled miniature sshd.
#
# ./smoketest/run-goscp.sh
#
# The test server runs commands through /bin/sh, so the real scp binary on this
# machine plays the remote side of the protocol. Everything stays local; the
# uploaded trees are checked directly on disk.
set -uo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(dirname "$here")"
work="$root/.smoketest-goscp"
export GOCACHE="${GOCACHE:-$root/.gocache}"
USER_NAME=tester
PASS_WORD=s3cr3t
HOST=127.0.0.1
pass=0
fail=0
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
bad() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; [ -n "${2:-}" ] && printf ' %s\n' "$2"; }
command -v scp >/dev/null 2>&1 || { echo "scp binary not found; cannot run this test" >&2; exit 1; }
rm -rf "$work"
mkdir -p "$work/bin" "$work/src/sub" "$work/server-cwd"
printf 'building\n'
go build -o "$work/bin/goscp" ./cmd/goscp || exit 1
go build -o "$work/bin/sshd" ./smoketest/sshd || exit 1
printf 'hello\n' >"$work/src/a.txt"
: >"$work/src/empty.bin"
printf 'companion data\n' >"$work/src/notes.md"
printf 'space content\n' >"$work/src/sub/name with spaces.txt"
head -c 1048576 /dev/urandom >"$work/src/sub/big.bin"
printf '#!/bin/sh\necho hi\n' >"$work/src/run.sh"
BIG_SUM="$(sha256sum "$work/src/sub/big.bin" | cut -d' ' -f1)"
sshd_pids=()
cleanup() {
local p
for p in ${sshd_pids[@]+"${sshd_pids[@]}"}; do kill "$p" 2>/dev/null; done
wait 2>/dev/null
}
trap cleanup EXIT
# The server runs with cwd $work/server-cwd so that "default destination"
# (the remote home, ".")
( cd "$work/server-cwd" && exec "$work/bin/sshd" -addr 127.0.0.1:0 -user "$USER_NAME" -pass "$PASS_WORD" \
-hostkey "$work/hostkey" ) >"$work/sshd.out" 2>"$work/sshd.err" &
sshd_pids+=($!)
for i in $(seq 1 100); do
grep -q '^HOSTKEY_SHA256=' "$work/sshd.out" 2>/dev/null && break
sleep 0.05
done
adr="$(sed -n 's/^ADDR=//p' "$work/sshd.out" | head -1)"
PORT="${adr##*:}"
if [ -z "$PORT" ]; then
echo "sshd did not start: $(cat "$work/sshd.err")" >&2
exit 1
fi
TARGET="$USER_NAME@$HOST"
G=( "$TARGET" -p "$PASS_WORD" -port "$PORT" )
gscp() { timeout 60 "$work/bin/goscp" "$@" </dev/null; }
printf '\n== single file, default destination ==\n'
gscp "${G[@]}" -files "$work/src/a.txt" >/dev/null 2>"$work/err.txt"
rc=$?
if [ "$rc" -eq 0 ] && [ "$(cat "$work/server-cwd/a.txt" 2>/dev/null)" = "hello" ]; then
ok "file lands in the remote home"
else
bad "file lands in the remote home" "exit=$rc stderr=$(head -1 "$work/err.txt")"
fi
gscp "${G[@]}" -files "$work/src/empty.bin" -dest "$work/dest1" >/dev/null 2>&1
if [ -f "$work/dest1/empty.bin" ] && [ ! -s "$work/dest1/empty.bin" ]; then
ok "zero byte file"
else
bad "zero byte file" "missing or non-empty"
fi
printf '\n== directory tree ==\n'
gscp "${G[@]}" -files "$work/src" -dest "$work/dest2" >/dev/null 2>"$work/err.txt"
rc=$?
if [ "$rc" -eq 0 ] && [ -f "$work/dest2/src/sub/big.bin" ] && [ -f "$work/dest2/src/sub/name with spaces.txt" ]; then
ok "nested tree with spaces in names"
else
bad "nested tree with spaces in names" "exit=$rc stderr=$(head -1 "$work/err.txt")"
fi
got="$(sha256sum "$work/dest2/src/sub/big.bin" 2>/dev/null | cut -d' ' -f1)"
[ "$got" = "$BIG_SUM" ] && ok "1 MiB binary is byte identical" || bad "1 MiB binary is byte identical" "sha256 mismatch"
if [ -d "$work/dest2/src/sub" ] && [ "$(stat -c %a "$work/dest2/src/sub")" = "755" ]; then
ok "directories are 0755"
else
bad "directories are 0755" "mode=$(stat -c %a "$work/dest2/src/sub" 2>/dev/null)"
fi
mode="$(stat -c %a "$work/dest2/src/a.txt" 2>/dev/null)"
[ "$mode" = "644" ] && ok "plain files are 0644 by default" || bad "plain files are 0644 by default" "mode=$mode"
printf '\n== file modes ==\n'
chmod 755 "$work/src/run.sh"
gscp "${G[@]}" -files "$work/src/run.sh" -dest "$work/dest3" >/dev/null 2>&1
mode="$(stat -c %a "$work/dest3/run.sh" 2>/dev/null)"
[ "$mode" = "755" ] && ok "local exec bit is preserved" || bad "local exec bit is preserved" "mode=$mode"
printf '\n== multiple files and extra positionals ==\n'
gscp "${G[@]}" -files "$work/src/a.txt" -files "$work/src/notes.md" -dest "$work/dest4" >/dev/null 2>&1
if [ -f "$work/dest4/a.txt" ] && [ -f "$work/dest4/notes.md" ]; then
ok "two -files flags"
else
bad "two -files flags" "missing output"
fi
# one -files followed by several bare paths: everything after the target is
# treated as a path, so both spellings below are equivalent
gscp "${G[@]}" -files "$work/src/a.txt" "$work/src/notes.md" "$work/src/empty.bin" -dest "$work/dest4b" >/dev/null 2>&1
if [ -f "$work/dest4b/a.txt" ] && [ -f "$work/dest4b/notes.md" ] && [ -f "$work/dest4b/empty.bin" ]; then
ok "-files with several bare paths"
else
bad "-files with several bare paths" "missing output"
fi
gscp "${G[@]}" -dest "$work/dest5" "$work/src/a.txt" "$work/src/notes.md" >/dev/null 2>&1
if [ -f "$work/dest5/a.txt" ] && [ -f "$work/dest5/notes.md" ]; then
ok "trailing positional paths"
else
bad "trailing positional paths" "missing output"
fi
printf '\n== destination handling ==\n'
gscp "${G[@]}" -files "$work/src/a.txt" -dest "$work/deep/nested/dir" >/dev/null 2>&1
[ -f "$work/deep/nested/dir/a.txt" ] && ok "missing destination directories are created" || bad "missing destination directories are created" "not created"
gscp "${G[@]}" -files "$work/src/a.txt" -dest "$work/rel" >/dev/null 2>&1
[ -f "$work/rel/a.txt" ] && ok "absolute -dest" || bad "absolute -dest" "missing"
printf '\n== output and exit codes ==\n'
out="$(gscp "${G[@]}" -files "$work/src/a.txt" -dest "$work/dest6" -v 2>&1 >/dev/null)"
if printf '%s' "$out" | grep -q 'a.txt'; then
ok "-v lists the uploaded entry"
else
bad "-v lists the uploaded entry" "stderr: $(printf '%s' "$out" | head -2 | tr '\n' '|')"
fi
out="$(gscp "${G[@]}" -files "$work/src/a.txt" -dest "$work/dest7" -q 2>&1 >/dev/null)"
[ -z "$out" ] && ok "-q keeps stderr empty" || bad "-q keeps stderr empty" "stderr: $out"
gscp "${G[@]}" -files "$work/does-not-exist" -dest "$work/dest8" >/dev/null 2>"$work/err.txt"
rc=$?
[ "$rc" -eq 1 ] && grep -q 'does-not-exist' "$work/err.txt" && ok "missing local path -> exit 1" \
|| bad "missing local path -> exit 1" "exit=$rc"
gscp "${G[@]}" -dest "$work/dest9" >/dev/null 2>&1
[ $? -eq 2 ] && ok "no -files -> exit 2" || bad "no -files -> exit 2"
gscp "$TARGET" -p wrong -port "$PORT" -files "$work/src/a.txt" >/dev/null 2>&1
[ $? -eq 255 ] && ok "wrong password -> exit 255" || bad "wrong password -> exit 255"
gscp "$TARGET" -port "$PORT" -files "$work/src/a.txt" >/dev/null 2>&1
[ $? -eq 2 ] && ok "no credentials -> exit 2" || bad "no credentials -> exit 2"
printf '\n== password from environment ==\n'
GOSCP_PASSWORD="$PASS_WORD" timeout 60 "$work/bin/goscp" "$TARGET" -port "$PORT" -files "$work/src/a.txt" -dest "$work/dest10" </dev/null >/dev/null 2>&1
[ -f "$work/dest10/a.txt" ] && ok "GOSCP_PASSWORD works" || bad "GOSCP_PASSWORD works" "missing output"
printf '\n%d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]