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
5.3 KiB
Go
131 lines
5.3 KiB
Go
package ssh
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
|
)
|
|
|
|
func TestExtractWrapperFlags(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
args []string
|
|
flags WrapperFlags
|
|
rest []string
|
|
}{
|
|
{[]string{"--dry-run", "web"}, WrapperFlags{DryRun: true}, []string{"web"}},
|
|
{[]string{"-l", "--dry-run", "--jump", "jh", "web"}, WrapperFlags{Jump: "jh"}, []string{"-l", "--dry-run", "web"}},
|
|
{[]string{"--jump=jh", "-vp", "22", "web", "--dry-run"}, WrapperFlags{Jump: "jh"}, []string{"-vp", "22", "web", "--dry-run"}},
|
|
{[]string{"--no-jump", "--", "web"}, WrapperFlags{NoJump: true}, []string{"--", "web"}},
|
|
} {
|
|
flags, rest, err := ExtractWrapperFlags(tc.args)
|
|
if err != nil || !reflect.DeepEqual(flags, tc.flags) || !reflect.DeepEqual(rest, tc.rest) {
|
|
t.Errorf("%v: got %+v %v %v", tc.args, flags, rest, err)
|
|
}
|
|
}
|
|
for _, args := range [][]string{{"--jump"}, {"--jump", "-x", "web"}, {"--jump=a", "--no-jump", "web"}, {"--version-typo", "web"}} {
|
|
if _, _, err := ExtractWrapperFlags(args); err == nil {
|
|
t.Errorf("%v accepted", args)
|
|
}
|
|
}
|
|
}
|
|
|
|
func apply(args []string, opts config.SSHOptions, flags WrapperFlags) Result {
|
|
return Apply(args, len(args)-1, Host{Name: "web.example.com", IP: "192.0.2.10"}, opts, flags)
|
|
}
|
|
|
|
func TestApplyDefaultConnectsToIP(t *testing.T) {
|
|
got := apply([]string{"-l", "root", "web"}, config.SSHOptions{}, WrapperFlags{})
|
|
if want := []string{"-l", "root", "192.0.2.10"}; !reflect.DeepEqual(got.Args, want) {
|
|
t.Fatalf("got %v", got.Args)
|
|
}
|
|
}
|
|
|
|
func TestApplyTargetName(t *testing.T) {
|
|
got := apply([]string{"admin@web"}, config.SSHOptions{Target: "name"}, WrapperFlags{})
|
|
want := []string{"-o", "HostName=192.0.2.10", "-o", "HostKeyAlias=web.example.com", "admin@web.example.com"}
|
|
if !reflect.DeepEqual(got.Args, want) {
|
|
t.Fatalf("got %v", got.Args)
|
|
}
|
|
got = apply([]string{"web"}, config.SSHOptions{Target: "hostname"}, WrapperFlags{})
|
|
if want := []string{"web.example.com"}; !reflect.DeepEqual(got.Args, want) {
|
|
t.Fatalf("got %v", got.Args)
|
|
}
|
|
// Names that are not plain hostnames fall back to the IP address.
|
|
got = Apply([]string{"x"}, 0, Host{Name: "rack 1 switch", IP: "192.0.2.1"}, config.SSHOptions{Target: "name"}, WrapperFlags{})
|
|
if want := []string{"192.0.2.1"}; !reflect.DeepEqual(got.Args, want) {
|
|
t.Fatalf("got %v", got.Args)
|
|
}
|
|
}
|
|
|
|
func TestApplyJumpPrecedence(t *testing.T) {
|
|
opts := config.SSHOptions{ProxyJump: "jh.example.com"}
|
|
for _, tc := range []struct {
|
|
args []string
|
|
flags WrapperFlags
|
|
want string
|
|
}{
|
|
{[]string{"web"}, WrapperFlags{}, "-J jh.example.com 192.0.2.10"},
|
|
{[]string{"web"}, WrapperFlags{Jump: "other"}, "-J other 192.0.2.10"},
|
|
{[]string{"web"}, WrapperFlags{NoJump: true}, "-o ProxyJump=none 192.0.2.10"},
|
|
{[]string{"-J", "mine", "web"}, WrapperFlags{}, "-J mine 192.0.2.10"},
|
|
{[]string{"-oProxyCommand=nc %h %p", "web"}, WrapperFlags{}, "-oProxyCommand=nc %h %p 192.0.2.10"},
|
|
} {
|
|
if got := strings.Join(apply(tc.args, opts, tc.flags).Args, " "); got != tc.want {
|
|
t.Errorf("%v %+v: got %q, want %q", tc.args, tc.flags, got, tc.want)
|
|
}
|
|
}
|
|
if got := apply([]string{"web"}, config.SSHOptions{ProxyJump: "none"}, WrapperFlags{}); len(got.Args) != 1 {
|
|
t.Fatalf("proxy_jump none still jumps: %v", got.Args)
|
|
}
|
|
}
|
|
|
|
func TestApplyHostKeyChecking(t *testing.T) {
|
|
got := apply([]string{"web"}, config.SSHOptions{HostKeyChecking: "off"}, WrapperFlags{})
|
|
want := "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR 192.0.2.10"
|
|
if strings.Join(got.Args, " ") != want || got.Notice == "" {
|
|
t.Fatalf("got %v, notice %q", got.Args, got.Notice)
|
|
}
|
|
// An explicit user choice disables the whole "off" bundle.
|
|
got = apply([]string{"-o", "StrictHostKeyChecking=yes", "web"}, config.SSHOptions{HostKeyChecking: "off"}, WrapperFlags{})
|
|
if strings.Join(got.Args, " ") != "-o StrictHostKeyChecking=yes 192.0.2.10" || got.Notice != "" {
|
|
t.Fatalf("user choice overridden: %v", got.Args)
|
|
}
|
|
got = apply([]string{"web"}, config.SSHOptions{HostKeyChecking: "accept-new"}, WrapperFlags{})
|
|
if strings.Join(got.Args, " ") != "-o StrictHostKeyChecking=accept-new 192.0.2.10" {
|
|
t.Fatalf("got %v", got.Args)
|
|
}
|
|
}
|
|
|
|
func TestApplyOptionsAndExtraArgs(t *testing.T) {
|
|
opts := config.SSHOptions{
|
|
AddressFamily: "inet",
|
|
ExtraArgs: []string{"-A"},
|
|
Options: map[string]string{"serveraliveinterval": "60", "user": "ignored"},
|
|
}
|
|
got := apply([]string{"-o", "User=me", "web"}, opts, WrapperFlags{})
|
|
want := "-A -o User=me -o AddressFamily=inet -o serveraliveinterval=60 192.0.2.10"
|
|
if strings.Join(got.Args, " ") != want {
|
|
t.Fatalf("got %q", strings.Join(got.Args, " "))
|
|
}
|
|
got = apply([]string{"-v6", "web"}, config.SSHOptions{AddressFamily: "inet"}, WrapperFlags{})
|
|
if strings.Join(got.Args, " ") != "-v6 192.0.2.10" {
|
|
t.Fatalf("address family overrides -6: %v", got.Args)
|
|
}
|
|
}
|
|
|
|
func TestApplyKeepsRemoteCommand(t *testing.T) {
|
|
got := Apply([]string{"web", "uptime", "-o", "x"}, 0, Host{Name: "web", IP: "192.0.2.1"}, config.SSHOptions{ProxyJump: "jh"}, WrapperFlags{})
|
|
if want := []string{"-J", "jh", "192.0.2.1", "uptime", "-o", "x"}; !reflect.DeepEqual(got.Args, want) {
|
|
t.Fatalf("got %v", got.Args)
|
|
}
|
|
}
|
|
|
|
func TestQuote(t *testing.T) {
|
|
got := Quote([]string{"ssh", "-o", "ProxyCommand=nc %h %p", "it's", "", "a@b:22"})
|
|
if want := `ssh -o 'ProxyCommand=nc %h %p' 'it'\''s' '' a@b:22`; got != want {
|
|
t.Fatalf("got %s", got)
|
|
}
|
|
}
|