package config import ( "os" "path/filepath" "reflect" "strings" "testing" ) func loadYAML(t *testing.T, data string) (*Config, error) { t.Helper() dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) t.Setenv("HOME", t.TempDir()) if err := os.WriteFile(filepath.Join(dir, "netssh.yaml"), []byte(data), 0o600); err != nil { t.Fatal(err) } return Load() } func TestSSHOptionsLayering(t *testing.T) { cfg, err := loadYAML(t, ` netboxes: - name: prod url: https://prod.example.com ssh: proxy_jump: jh-prod.example.com host_key_checking: strict - name: staging url: https://staging.example.com ssh: host_key_checking: off ssh: proxy_jump: jh.example.com extra_args: ["-4"] options: ServerAliveInterval: 60 ServerAliveCountMax: 5 rules: - match: {tag: dmz} proxy_jump: jh-dmz.example.com options: {ServerAliveInterval: 10} - match: {instance: prod, name: "db*"} proxy_jump: none completion: max_results: 200 `) if err != nil { t.Fatal(err) } if cfg.Completion.MaxResults != 200 { t.Fatalf("max_results: %d", cfg.Completion.MaxResults) } for _, tc := range []struct { host SSHHost jump, keys, alive string }{ {SSHHost{Name: "web"}, "jh.example.com", "", "60"}, {SSHHost{Instance: "prod", Name: "web"}, "jh-prod.example.com", "strict", "60"}, {SSHHost{Instance: "staging", Name: "web", Tags: []string{"DMZ"}}, "jh-dmz.example.com", "off", "10"}, {SSHHost{Instance: "prod", Name: "db01", Tags: []string{"dmz"}}, "none", "strict", "10"}, } { got := cfg.SSHOptionsFor(tc.host) if got.ProxyJump != tc.jump || got.HostKeyChecking != tc.keys || got.Options["serveraliveinterval"] != tc.alive || got.Options["serveralivecountmax"] != "5" { t.Errorf("%+v: got %+v", tc.host, got) } if !reflect.DeepEqual(got.ExtraArgs, []string{"-4"}) { t.Errorf("extra args: %v", got.ExtraArgs) } } } func TestSSHOptionsValidation(t *testing.T) { for _, data := range []string{ "ssh:\n host_key_checking: maybe\n", "ssh:\n target: dns\n", "ssh:\n address_family: ipx\n", "ssh:\n proxy_jump: \"-oProxyCommand=x\"\n", "ssh:\n options:\n \"bad key\": 1\n", "ssh:\n rules:\n - proxy_jump: jh\n", "netboxes:\n - name: a\n url: https://a.example\n ssh:\n target: nope\n", } { if _, err := loadYAML(t, data); err == nil { t.Errorf("accepted invalid config:\n%s", data) } else if !strings.Contains(err.Error(), "ssh") { t.Errorf("unclear error %v", err) } } } func TestForwardsLayering(t *testing.T) { cfg, err := loadYAML(t, ` netboxes: - name: prod url: https://prod.example token: t ssh: forwards: db: {local: "5432:localhost:5432"} ssh: forwards: web: {local: "8080:localhost:80"} socks: {dynamic: "1080"} rules: - match: {tag: dmz} forwards: web: {local: "8443:localhost:443"} `) if err != nil { t.Fatal(err) } got := cfg.SSHOptionsFor(SSHHost{Instance: "prod", Name: "web01", Tags: []string{"dmz"}}).Forwards want := map[string]Forward{"web": {Local: "8443:localhost:443"}, "socks": {Dynamic: "1080"}, "db": {Local: "5432:localhost:5432"}} if !reflect.DeepEqual(got, want) { t.Fatalf("got %+v", got) } if got := cfg.SSHOptionsFor(SSHHost{Instance: "other", Name: "web01"}).Forwards; len(got) != 2 || got["web"].Local != "8080:localhost:80" { t.Fatalf("other instance: %+v", got) } if f := (Forward{Remote: "9000:localhost:9000"}); f.String() != "-R 9000:localhost:9000" { t.Fatal(f.String()) } } func TestForwardsValidation(t *testing.T) { for _, forwards := range []string{ `web: {local: "8080:a:80", dynamic: "1080"}`, `web: {local: "-oProxyCommand=x"}`, `web: {local: "8080 a:80"}`, `"bad name": {local: "8080:a:80"}`, } { if _, err := loadYAML(t, "ssh:\n forwards:\n "+forwards+"\n"); err == nil || !strings.Contains(err.Error(), "forwards") { t.Errorf("%s: got %v", forwards, err) } } } func TestStaticHosts(t *testing.T) { cfg, err := loadYAML(t, ` resolver: hostname_domain: example.com ssh: proxy_jump: jh rules: - match: {kind: static, tag: home} host_key_checking: off hosts: - name: Router address: 192.168.1.1 user: admin port: 2222 tags: [home] ssh: proxy_jump: none `) if err != nil { t.Fatal(err) } if !cfg.HasHosts() || cfg.HasNetBox() || cfg.Resolver.HostnameDomain != "example.com" { t.Fatalf("got %+v", cfg) } host := cfg.StaticHost("router") if host == nil || host.Address != "192.168.1.1" || host.User != "admin" || host.Port != 2222 { t.Fatalf("host: %+v", host) } opts := cfg.SSHOptionsFor(SSHHost{Name: "Router", Kind: StaticKind, Tags: []string{"home"}}) if opts.ProxyJump != "none" || opts.HostKeyChecking != "off" { t.Fatalf("static host options: %+v", opts) } // A NetBox host of the same name does not get the static host's options. if got := cfg.SSHOptionsFor(SSHHost{Name: "Router", Kind: "device"}); got.ProxyJump != "jh" { t.Fatalf("netbox host: %+v", got) } } func TestStaticHostsValidation(t *testing.T) { for _, hosts := range []string{ "- {name: a, address: 10.0.0.1}\n - {name: A, address: 10.0.0.2}", "- {name: 'bad name', address: 10.0.0.1}", "- {name: prod/a, address: 10.0.0.1}", "- {name: a}", "- {name: a, address: '-oProxyCommand=x'}", "- {name: a, address: 10.0.0.1, port: 70000}", "- {name: a, address: 10.0.0.1, user: 'a b'}", "- {name: a, address: 10.0.0.1, ssh: {target: nope}}", } { if _, err := loadYAML(t, "hosts:\n "+hosts+"\n"); err == nil || !strings.Contains(err.Error(), "hosts[") { t.Errorf("%s: got %v", hosts, err) } } }