209 lines
7.1 KiB
Go
209 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache"
|
|
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
|
|
)
|
|
|
|
func prepareInstancesTest(t *testing.T, officeURL, labURL string) *config.Config {
|
|
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 := fmt.Sprintf("netboxes:\n - name: office\n url: %q\n token: office-token\n - name: lab\n url: %q\n token: lab-token\nresolver:\n strategies: [primary_ip]\n", officeURL, labURL)
|
|
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)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func multiHost(name, ip string) connectionTestHost {
|
|
return connectionTestHost{ID: 1, Name: name, PrimaryIP4: &struct {
|
|
Address string `json:"address"`
|
|
}{Address: ip + "/24"}}
|
|
}
|
|
|
|
func TestRefreshStoresSameHostnameFromBothInstances(t *testing.T) {
|
|
office := connectionTestServer(t, []connectionTestHost{multiHost("web", "10.0.0.1")}, nil)
|
|
lab := connectionTestServer(t, []connectionTestHost{multiHost("web", "10.0.0.2")}, nil)
|
|
cfg := prepareInstancesTest(t, office.URL, lab.URL)
|
|
cmd := rootCmd()
|
|
cmd.SetArgs([]string{"cache", "refresh"})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
|
if err := c.Load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, ip := range map[string]string{"office/web": "10.0.0.1", "lab/web": "10.0.0.2"} {
|
|
got, fresh := c.Get(name)
|
|
if !fresh || got.IP != ip {
|
|
t.Fatalf("cache source collision: %+v, fresh=%v", got, fresh)
|
|
}
|
|
}
|
|
if len(c.All()) != 2 || c.NeedsRefresh(time.Hour) {
|
|
t.Fatal("full refresh is incomplete")
|
|
}
|
|
}
|
|
|
|
func TestRefreshFailureAndPerInstanceRefresh(t *testing.T) {
|
|
office := connectionTestServer(t, []connectionTestHost{multiHost("web", "10.0.0.1")}, nil)
|
|
failed := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "offline", 503) }))
|
|
defer failed.Close()
|
|
cfg := prepareInstancesTest(t, office.URL, failed.URL)
|
|
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
|
c.Upsert(cache.Entry{Name: "lab/old", Kind: "device", IP: "10.0.0.2"})
|
|
if err := c.Save(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd := rootCmd()
|
|
cmd.SetArgs([]string{"cache", "refresh"})
|
|
if err := cmd.Execute(); err == nil {
|
|
t.Fatal("failed instance marked as full refresh")
|
|
}
|
|
if err := c.Load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(c.All()) != 1 || !c.NeedsRefresh(time.Hour) {
|
|
t.Fatal("partial refresh changed cache or full-refresh stamp")
|
|
}
|
|
cmd = rootCmd()
|
|
cmd.SetArgs([]string{"cache", "refresh", "--instance", "office"})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.Load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(c.All()) != 2 || !c.NeedsRefresh(time.Hour) {
|
|
t.Fatal("per-instance refresh lost other source or updated full stamp")
|
|
}
|
|
}
|
|
|
|
func TestLocalNotesCLIKeepsInstancesSeparate(t *testing.T) {
|
|
cfg := prepareInstancesTest(t, "https://office.example", "https://lab.example")
|
|
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
|
for _, name := range []string{"office/web", "lab/web"} {
|
|
c.Upsert(cache.Entry{Name: name, Kind: "device", IP: "10.0.0.1"})
|
|
}
|
|
if err := c.Save(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
run := func(args ...string) (string, error) {
|
|
var out bytes.Buffer
|
|
cmd := rootCmd()
|
|
cmd.SetArgs(append([]string{"notes"}, args...))
|
|
cmd.SetOut(&out)
|
|
cmd.SetErr(&out)
|
|
err := cmd.Execute()
|
|
return out.String(), err
|
|
}
|
|
for _, source := range []string{"office", "lab"} {
|
|
if _, err := run("set", source+"/web", source+" note"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, source := range []string{"office", "lab"} {
|
|
got, err := run("show", source+"/web")
|
|
if err != nil || got != source+" note" {
|
|
t.Fatalf("CLI notes crossed sources: %q, %v", got, err)
|
|
}
|
|
}
|
|
if _, err := run("set", "web", "wrong note"); err == nil {
|
|
t.Fatal("ambiguous note target accepted")
|
|
}
|
|
}
|
|
|
|
func TestMultiInstanceWrapperHelper(t *testing.T) {
|
|
if target := os.Getenv("NETSSH_TEST_MULTI_TARGET"); target != "" {
|
|
runSSHWrapper([]string{target})
|
|
return
|
|
}
|
|
}
|
|
|
|
func TestWrapperRoutesQualifiedTargetsAndRejectsAmbiguity(t *testing.T) {
|
|
office := connectionTestServer(t, []connectionTestHost{multiHost("web", "10.0.0.1")}, nil)
|
|
lab := connectionTestServer(t, []connectionTestHost{multiHost("web", "10.0.0.2")}, nil)
|
|
cfg := prepareInstancesTest(t, office.URL, lab.URL)
|
|
c := cache.New(cfg.CachePath(), 3600)
|
|
if err := c.SetRefreshed(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bin := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(bin, "ssh"), []byte("#!/bin/sh\nprintf '%s\\n' \"$@\"\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for target, want := range map[string]string{"office/web": "10.0.0.1", "lab/web": "10.0.0.2", "web": "ambiguous"} {
|
|
cmd := exec.Command(os.Args[0], "-test.run=^TestMultiInstanceWrapperHelper$")
|
|
cmd.Env = append(os.Environ(), "NETSSH_TEST_MULTI_TARGET="+target, "PATH="+bin+":"+os.Getenv("PATH"))
|
|
out, err := cmd.CombinedOutput()
|
|
if want == "ambiguous" {
|
|
if err == nil || !strings.Contains(string(out), "2 exact matches") {
|
|
t.Fatalf("ambiguous connection accepted: %s, %v", out, err)
|
|
}
|
|
} else if err != nil || !strings.Contains(string(out), want+"\n") {
|
|
t.Fatalf("wrong SSH destination for %s: %s, %v", target, out, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQualifiedShortcutDoesNotMatchOtherInstance(t *testing.T) {
|
|
cfg := config.ShortcutsConfig{Domains: []string{".example.com"}, StripHyphens: true}
|
|
normalize := normalizeTarget(cfg, true)
|
|
if normalize("office/web-01.example.com") != normalize("office/web01") {
|
|
t.Fatal("qualified shortcut does not normalize")
|
|
}
|
|
if normalize("office/web01") == normalize("lab/web01") {
|
|
t.Fatal("qualified shortcut lost instance")
|
|
}
|
|
}
|
|
|
|
func TestRefreshCachesIndependentDevicesAndVMs(t *testing.T) {
|
|
office := connectionTestServer(t, []connectionTestHost{multiHost("router", "10.1.0.1")}, []connectionTestHost{multiHost("app", "10.1.0.2")})
|
|
lab := connectionTestServer(t, []connectionTestHost{multiHost("switch", "10.2.0.1")}, []connectionTestHost{multiHost("test-vm", "10.2.0.2")})
|
|
cfg := prepareInstancesTest(t, office.URL, lab.URL)
|
|
cmd := rootCmd()
|
|
cmd.SetArgs([]string{"cache", "refresh"})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c := cache.New(cfg.CachePath(), cfg.Cache.TTL)
|
|
if err := c.Load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]struct{ kind, ip string }{
|
|
"office/router": {"device", "10.1.0.1"}, "office/app": {"vm", "10.1.0.2"},
|
|
"lab/switch": {"device", "10.2.0.1"}, "lab/test-vm": {"vm", "10.2.0.2"},
|
|
}
|
|
if len(c.All()) != len(want) {
|
|
t.Fatalf("missing inventory entries: %+v", c.All())
|
|
}
|
|
for name, entry := range want {
|
|
got, fresh := c.Get(name)
|
|
if !fresh || got.Kind != entry.kind || got.IP != entry.ip {
|
|
t.Fatalf("wrong cached host %s: %+v", name, got)
|
|
}
|
|
}
|
|
}
|