Add netssh sftp and netssh scp. Both resolve the host like a connection does (cache, shortcuts and rewrites, instance/host, default user and port, SSH options and rules, --jump/--no-jump, --dry-run) and get shell completion. The per-client flag tables handle the differences from ssh: the port is -P, -l is a bandwidth limit (the default user goes in as -o User=), and IPv6 addresses are bracketed in host:path. All remote scp operands must name the same host. Add named port forwards under ssh.forwards (global, per instance, per rule; a more specific layer replaces a forward of the same name), netssh tunnel HOST NAME|SPEC... (ssh -N with ExitOnForwardFailure) and netssh --forward NAME. Specs such as 8080:localhost:80 work without configuration. Forwards are editable in netssh configure and completed in the shell; the TUI opens SFTP with ctrl+o and a tunnel with ctrl+t. Host resolution moves out of runSSHWrapper into resolveTarget, shared by ssh, sftp, scp, tunnel, and the TUI. Options added by netssh are now inserted before the first operand or "--": after "--", ssh read them as the destination, and scp stops reading options at the first operand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
4.5 KiB
Go
131 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache"
|
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
|
)
|
|
|
|
// TestNetsshHelper runs main() with the arguments in NETSSH_TEST_ARGS.
|
|
func TestNetsshHelper(t *testing.T) {
|
|
if args := os.Getenv("NETSSH_TEST_ARGS"); args != "" {
|
|
os.Args = append([]string{"netssh"}, strings.Split(args, "\x1f")...)
|
|
main()
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
// prepareTransferTest writes a config whose only host is cached; NetBox is
|
|
// unreachable, so every result must come from cache, shortcuts, and options.
|
|
func prepareTransferTest(t *testing.T) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
t.Setenv("HOME", t.TempDir())
|
|
t.Setenv("XDG_CONFIG_HOME", dir)
|
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
t.Setenv("NETSSH_NETBOX_URL", "")
|
|
t.Setenv("NETSSH_NETBOX_TOKEN", "")
|
|
t.Setenv("NETSSH_CACHE_PATH", filepath.Join(t.TempDir(), "hosts.json"))
|
|
t.Setenv("NETSSH_SHELL_ALIAS", "")
|
|
data := `netbox:
|
|
url: http://127.0.0.1:1
|
|
token: t
|
|
ssh:
|
|
default_user: ops
|
|
default_port: 2222
|
|
extra_args: ["-A"]
|
|
forwards:
|
|
web: {local: "8080:localhost:80"}
|
|
rules:
|
|
- match: {tag: dmz}
|
|
proxy_jump: jh.example.com
|
|
forwards:
|
|
socks: {dynamic: "1080"}
|
|
shortcuts:
|
|
domains: [.hoster.de]
|
|
rewrites:
|
|
- pattern: '^([a-z]+)[0-9]+-'
|
|
replace: '$1'
|
|
`
|
|
if err := os.WriteFile(filepath.Join(dir, "netssh.yaml"), []byte(data), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c := cache.New(cfg.CachePath(), 3600)
|
|
c.Upsert(cache.Entry{Name: "fsn1-bx1.hoster.de", IP: "10.0.0.9", Kind: "device", Tags: []string{"dmz"}})
|
|
if err := c.Save(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func runNetssh(t *testing.T, args ...string) (string, error) {
|
|
t.Helper()
|
|
cmd := exec.Command(os.Args[0], "-test.run=^TestNetsshHelper$")
|
|
cmd.Env = append(os.Environ(), "NETSSH_TEST_ARGS="+strings.Join(args, "\x1f"))
|
|
out, err := cmd.CombinedOutput()
|
|
return strings.TrimSpace(string(out)), err
|
|
}
|
|
|
|
func TestTransferAndTunnelCommands(t *testing.T) {
|
|
prepareTransferTest(t)
|
|
for _, tc := range []struct {
|
|
args []string
|
|
want string
|
|
}{
|
|
// sftp: -P and -o User, rule jump host, path kept, no ssh-only extra_args.
|
|
{[]string{"sftp", "--dry-run", "fsnbx1:/var/log"}, "sftp -P 2222 -o User=ops -J jh.example.com 10.0.0.9:/var/log"},
|
|
{[]string{"sftp", "--dry-run", "-P", "22", "root@fsnbx1"}, "sftp -P 22 -J jh.example.com root@10.0.0.9"},
|
|
{[]string{"scp", "--dry-run", "-r", "a.txt", "fsn1-bx1.hoster.de:/tmp/"}, "scp -P 2222 -o User=ops -r -J jh.example.com a.txt 10.0.0.9:/tmp/"},
|
|
{[]string{"scp", "--dry-run", "--no-jump", "fsnbx1:/etc/hosts", "."}, "scp -P 2222 -o User=ops -o ProxyJump=none 10.0.0.9:/etc/hosts ."},
|
|
// Forwards come from the global options and the matching rule.
|
|
{[]string{"tunnel", "--dry-run", "fsnbx1", "web", "socks", "5432:db:5432"}, "ssh -A -p 2222 -l ops -J jh.example.com -L 8080:localhost:80 -D 1080 -L 5432:db:5432 -o ExitOnForwardFailure=yes -N 10.0.0.9"},
|
|
{[]string{"--dry-run", "--forward", "web", "fsnbx1"}, "ssh -A -p 2222 -l ops -J jh.example.com -L 8080:localhost:80 10.0.0.9"},
|
|
} {
|
|
out, err := runNetssh(t, tc.args...)
|
|
if err != nil || out != tc.want {
|
|
t.Errorf("%v:\n got %s (%v)\n want %s", tc.args, out, err, tc.want)
|
|
}
|
|
}
|
|
for _, tc := range []struct {
|
|
args []string
|
|
want string
|
|
}{
|
|
{[]string{"tunnel", "--dry-run", "fsnbx1"}, "forwards for fsn1-bx1.hoster.de: socks, web"},
|
|
{[]string{"tunnel", "--dry-run", "fsnbx1", "db"}, `unknown forward "db"; available: socks, web`},
|
|
{[]string{"sftp", "--forward", "web", "fsnbx1"}, "only available for ssh"},
|
|
{[]string{"scp", "fsnbx1:a", "other:b"}, "not supported"},
|
|
} {
|
|
out, err := runNetssh(t, tc.args...)
|
|
if err == nil || !strings.Contains(out, tc.want) {
|
|
t.Errorf("%v: got %s (%v), want error containing %q", tc.args, out, err, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTransferCompletion(t *testing.T) {
|
|
prepareTransferTest(t)
|
|
for _, tc := range []struct {
|
|
args []string
|
|
want string
|
|
}{
|
|
{[]string{"__complete", "sftp", "fsnb"}, "fsnbx1\n"},
|
|
{[]string{"__complete", "scp", "a.txt", "fsnb"}, "fsnbx1:\n"},
|
|
{[]string{"__complete", "tunnel", "fsnbx1", ""}, "socks\nweb\n"},
|
|
{[]string{"__complete", "tunnel", "fsnbx1", "web", ""}, "socks\n"},
|
|
{[]string{"__complete", "--forward", ""}, "socks\nweb\n"},
|
|
} {
|
|
out, err := runNetssh(t, tc.args...)
|
|
if err != nil || !strings.HasPrefix(out+"\n", tc.want) {
|
|
t.Errorf("%v: got %q (%v), want prefix %q", tc.args, out, err, tc.want)
|
|
}
|
|
}
|
|
}
|