package main import ( "fmt" "os" "path/filepath" "strings" "testing" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config" ) // prepareConfig writes data as the only config, with empty cache and home. func prepareConfig(t *testing.T, data string) string { 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", "") path := filepath.Join(dir, "netssh.yaml") if err := os.WriteFile(path, []byte(data), 0o600); err != nil { t.Fatal(err) } return path } const staticOnlyConfig = `ssh: default_user: ops rules: - match: {kind: static, tag: home} host_key_checking: accept-new shortcuts: domains: [.lan] hosts: - name: router.lan address: 192.168.1.1 user: admin port: 2222 tags: [home] - name: nas address: nas.example.org ssh: proxy_jump: jh.example.com ` // Static hosts work without any NetBox and with their own user, port, tags, and options. func TestStaticHostsWithoutNetBox(t *testing.T) { prepareConfig(t, staticOnlyConfig) for _, tc := range []struct { args []string want string }{ {[]string{"--dry-run", "router.lan"}, "ssh -p 2222 -l admin -o StrictHostKeyChecking=accept-new 192.168.1.1"}, {[]string{"--dry-run", "router"}, "ssh -p 2222 -l admin -o StrictHostKeyChecking=accept-new 192.168.1.1"}, // shortcut {[]string{"--dry-run", "-l", "root", "nas", "uptime"}, "ssh -l root -J jh.example.com nas.example.org uptime"}, {[]string{"sftp", "--dry-run", "router:/etc"}, "sftp -P 2222 -o User=admin -o StrictHostKeyChecking=accept-new 192.168.1.1:/etc"}, {[]string{"scp", "--dry-run", "a.txt", "nas:/tmp/"}, "scp -o User=ops -J jh.example.com a.txt nas.example.org:/tmp/"}, } { 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) } } if out, err := runNetssh(t, "--dry-run", "unknown"); err == nil || !strings.Contains(out, "no NetBox is configured") { t.Errorf("unknown host: %s (%v)", out, err) } if out, _ := runNetssh(t, "__complete", "rou"); !strings.HasPrefix(out, "router.lan\n") { t.Errorf("completion: %q", out) } if out, _ := runNetssh(t, "alias", "--shell", "bash"); !strings.Contains(out, "alias router='netssh router.lan'") || !strings.Contains(out, "alias nas='netssh nas'") { t.Errorf("aliases: %s", out) } } func TestHostsCommand(t *testing.T) { path := prepareConfig(t, "hosts:\n - {name: nas, address: nas.example.org, ssh: {proxy_jump: jh}}\n") steps := []struct { args []string wantErr string }{ {[]string{"hosts", "add", "router", "192.168.1.1", "--user", "admin", "--port", "2222", "--tag", "home", "--tag", "lab"}, ""}, {[]string{"hosts", "add", "router", "10.0.0.1"}, "already exists"}, {[]string{"hosts", "add", "nas", "10.0.0.5", "--replace"}, ""}, {[]string{"hosts", "add", "bad name", "10.0.0.1"}, "must start with"}, {[]string{"hosts", "remove", "missing"}, "no static host"}, } for _, step := range steps { out, err := runNetssh(t, step.args...) if step.wantErr == "" && err != nil || step.wantErr != "" && !strings.Contains(out, step.wantErr) { t.Fatalf("%v: %s (%v)", step.args, out, err) } } cfg, err := config.Load() if err != nil { t.Fatal(err) } router, nas := cfg.StaticHost("router"), cfg.StaticHost("nas") if router == nil || router.User != "admin" || router.Port != 2222 || strings.Join(router.Tags, ",") != "home,lab" { t.Fatalf("router: %+v", router) } if nas == nil || nas.Address != "10.0.0.5" || nas.SSH.ProxyJump != "jh" { t.Fatalf("--replace must keep SSH options: %+v", nas) } out, err := runNetssh(t, "hosts", "list") if err != nil || !strings.Contains(out, "router 192.168.1.1 admin 2222 home,lab") { t.Fatalf("list:\n%s (%v)", out, err) } if out, err := runNetssh(t, "hosts", "rm", "router"); err != nil { t.Fatalf("remove: %s (%v)", out, err) } data, _ := os.ReadFile(path) if strings.Contains(string(data), "router") || strings.Contains(string(data), "netbox:") { t.Fatalf("config after remove:\n%s", data) } } // The hostname strategy passes the NetBox name (plus domain) instead of an IP. func TestHostnameStrategyConnects(t *testing.T) { srv := connectionTestServer(t, []connectionTestHost{{ID: 1, Name: "web01"}, multiHost("db01", "10.0.0.7")}, nil) prepareConfig(t, fmt.Sprintf("netbox:\n url: %q\n token: t\nresolver:\n strategies: [primary_ip, hostname]\n hostname_domain: example.com\n", srv.URL)) for host, want := range map[string]string{"web01": "ssh web01.example.com", "db01": "ssh 10.0.0.7"} { if out, err := runNetssh(t, "--dry-run", host); err != nil || out != want { t.Errorf("%s: got %s (%v), want %s", host, out, err, want) } } }