package setup import ( "os" "path/filepath" "strings" "testing" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config" ) func TestSave_WritesFile(t *testing.T) { dir := t.TempDir() orig := os.Getenv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) defer os.Setenv("XDG_CONFIG_HOME", orig) cfg := config.Config{ NetBox: config.NetBoxConfig{ URL: "https://netbox.example.com", Token: "nbt_abc123", TokenVersion: 2, }, SSH: config.SSHConfig{DefaultUser: "admin"}, Resolver: config.ResolverConfig{ Strategies: []string{"primary_ip", "management_subnet"}, ManagementSubnets: []string{"10.0.0.0/8"}, }, Cache: config.CacheConfig{TTL: 3600}, } if err := save(cfg); err != nil { t.Fatalf("save: %v", err) } data, err := os.ReadFile(filepath.Join(dir, "netssh.yaml")) if err != nil { t.Fatalf("reading saved file: %v", err) } content := string(data) for _, want := range []string{ `"https://netbox.example.com"`, `"nbt_abc123"`, `token_version: 2`, `- primary_ip`, `- management_subnet`, `- 10.0.0.0/8`, `ttl: 3600`, `"admin"`, } { if !strings.Contains(content, want) { t.Errorf("saved config missing %q\nfull content:\n%s", want, content) } } } func TestSave_FilePermissions(t *testing.T) { dir := t.TempDir() orig := os.Getenv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) defer os.Setenv("XDG_CONFIG_HOME", orig) if err := save(config.Config{ NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1}, Cache: config.CacheConfig{TTL: 60}, }); err != nil { t.Fatalf("save: %v", err) } info, err := os.Stat(filepath.Join(dir, "netssh.yaml")) if err != nil { t.Fatalf("stat: %v", err) } if perm := info.Mode().Perm(); perm != 0o600 { t.Errorf("file permissions: got %o, want 600", perm) } } func TestSave_OmitsEmptyOptionalFields(t *testing.T) { dir := t.TempDir() orig := os.Getenv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) defer os.Setenv("XDG_CONFIG_HOME", orig) cfg := config.Config{ NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1}, Cache: config.CacheConfig{TTL: 60}, // No DefaultUser, no ManagementSubnets, no InterfaceName } if err := save(cfg); err != nil { t.Fatalf("save: %v", err) } data, _ := os.ReadFile(filepath.Join(dir, "netssh.yaml")) content := string(data) for _, absent := range []string{"default_user", "management_subnets", "interface_name"} { if strings.Contains(content, absent) { t.Errorf("config should not contain %q when field is empty\nfull content:\n%s", absent, content) } } } func TestSave_CreatesConfigDir(t *testing.T) { dir := filepath.Join(t.TempDir(), "does", "not", "exist") orig := os.Getenv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) defer os.Setenv("XDG_CONFIG_HOME", orig) if err := save(config.Config{ NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1}, Cache: config.CacheConfig{TTL: 60}, }); err != nil { t.Fatalf("save should create missing directories: %v", err) } } func TestSave_DefaultPort(t *testing.T) { dir := t.TempDir() orig := os.Getenv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) defer os.Setenv("XDG_CONFIG_HOME", orig) cfg := config.Config{ NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1}, Cache: config.CacheConfig{TTL: 60}, SSH: config.SSHConfig{DefaultPort: 2222}, } if err := save(cfg); err != nil { t.Fatalf("save: %v", err) } data, _ := os.ReadFile(filepath.Join(dir, "netssh.yaml")) if !strings.Contains(string(data), "default_port: 2222") { t.Errorf("expected default_port: 2222 in config, got:\n%s", string(data)) } } func TestSave_Shortcuts_WritesSection(t *testing.T) { dir := t.TempDir() orig := os.Getenv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) defer os.Setenv("XDG_CONFIG_HOME", orig) cfg := config.Config{ NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1}, Cache: config.CacheConfig{TTL: 60}, Shortcuts: config.ShortcutsConfig{ Domains: []string{".example.com"}, StripHyphens: true, }, } if err := save(cfg); err != nil { t.Fatalf("save: %v", err) } data, _ := os.ReadFile(filepath.Join(dir, "netssh.yaml")) content := string(data) for _, want := range []string{"shortcuts:", "domains:", ".example.com", "strip_hyphens: true"} { if !strings.Contains(content, want) { t.Errorf("expected %q in config, got:\n%s", want, content) } } } func TestSave_OmitsDefaultPortWhenZero(t *testing.T) { dir := t.TempDir() orig := os.Getenv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) defer os.Setenv("XDG_CONFIG_HOME", orig) cfg := config.Config{ NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1}, Cache: config.CacheConfig{TTL: 60}, SSH: config.SSHConfig{DefaultPort: 0}, } if err := save(cfg); err != nil { t.Fatalf("save: %v", err) } data, _ := os.ReadFile(filepath.Join(dir, "netssh.yaml")) if strings.Contains(string(data), "default_port") { t.Errorf("default_port should be omitted when zero, got:\n%s", string(data)) } } func TestSave_OmitsShortcutsWhenEmpty(t *testing.T) { dir := t.TempDir() orig := os.Getenv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) defer os.Setenv("XDG_CONFIG_HOME", orig) cfg := config.Config{ NetBox: config.NetBoxConfig{URL: "http://x", Token: "t", TokenVersion: 1}, Cache: config.CacheConfig{TTL: 60}, Shortcuts: config.ShortcutsConfig{}, // empty } if err := save(cfg); err != nil { t.Fatalf("save: %v", err) } data, _ := os.ReadFile(filepath.Join(dir, "netssh.yaml")) if strings.Contains(string(data), "shortcuts:") { t.Errorf("shortcuts section should be omitted when empty, got:\n%s", string(data)) } } func TestSave_Roundtrip_WithNewFields(t *testing.T) { dir := t.TempDir() orig := os.Getenv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) defer os.Setenv("XDG_CONFIG_HOME", orig) original := config.Config{ NetBox: config.NetBoxConfig{ URL: "https://netbox.example.com", Token: "nbt_test", TokenVersion: 2, }, SSH: config.SSHConfig{ DefaultUser: "admin", DefaultPort: 2222, }, Resolver: config.ResolverConfig{ Strategies: []string{"primary_ip"}, }, Cache: config.CacheConfig{TTL: 3600}, Shortcuts: config.ShortcutsConfig{ Domains: []string{".example.com", ".example.de"}, StripHyphens: true, }, } if err := save(original); err != nil { t.Fatalf("save: %v", err) } loaded, err := config.Load() if err != nil { t.Fatalf("Load after save: %v", err) } if loaded.SSH.DefaultPort != original.SSH.DefaultPort { t.Errorf("DefaultPort: got %d, want %d", loaded.SSH.DefaultPort, original.SSH.DefaultPort) } if len(loaded.Shortcuts.Domains) != len(original.Shortcuts.Domains) { t.Errorf("Shortcuts.Domains length: got %d, want %d", len(loaded.Shortcuts.Domains), len(original.Shortcuts.Domains)) } else { for i, d := range original.Shortcuts.Domains { if loaded.Shortcuts.Domains[i] != d { t.Errorf("Shortcuts.Domains[%d]: got %q, want %q", i, loaded.Shortcuts.Domains[i], d) } } } if loaded.Shortcuts.StripHyphens != original.Shortcuts.StripHyphens { t.Errorf("StripHyphens: got %v, want %v", loaded.Shortcuts.StripHyphens, original.Shortcuts.StripHyphens) } } func TestParseStrategies(t *testing.T) { tests := []struct { in string want []string }{ {"primary_ip", []string{"primary_ip"}}, {"management_subnet, primary_ip", []string{"management_subnet", "primary_ip"}}, {"primary_ip,management_subnet,interface_name", []string{"primary_ip", "management_subnet", "interface_name"}}, {" primary_ip , management_subnet ", []string{"primary_ip", "management_subnet"}}, {"", nil}, {" , ", nil}, } for _, tt := range tests { got := parseStrategies(tt.in) if len(got) != len(tt.want) { t.Errorf("parseStrategies(%q): got %v, want %v", tt.in, got, tt.want) continue } for i := range got { if got[i] != tt.want[i] { t.Errorf("parseStrategies(%q)[%d]: got %q, want %q", tt.in, i, got[i], tt.want[i]) } } } } func TestParseStrategies_PreservesOrder(t *testing.T) { got := parseStrategies("interface_name, management_subnet, primary_ip") want := []string{"interface_name", "management_subnet", "primary_ip"} for i, s := range got { if s != want[i] { t.Errorf("order not preserved at [%d]: got %q, want %q", i, s, want[i]) } } } func TestValidateStrategies_Valid(t *testing.T) { cases := []string{ "primary_ip", "management_subnet, primary_ip", "interface_name, management_subnet, primary_ip", } for _, c := range cases { if err := validateStrategies(c); err != nil { t.Errorf("validateStrategies(%q) should be valid, got: %v", c, err) } } } func TestValidateStrategies_Invalid(t *testing.T) { cases := []string{ "", "unknown_strategy", "primary_ip, typo", } for _, c := range cases { if err := validateStrategies(c); err == nil { t.Errorf("validateStrategies(%q) should return an error", c) } } } func TestSave_RoundtripViaLoad(t *testing.T) { dir := t.TempDir() orig := os.Getenv("XDG_CONFIG_HOME") os.Setenv("XDG_CONFIG_HOME", dir) defer os.Setenv("XDG_CONFIG_HOME", orig) original := config.Config{ NetBox: config.NetBoxConfig{ URL: "https://netbox.zb-server.de", Token: "nbt_supersecret", TokenVersion: 2, }, SSH: config.SSHConfig{DefaultUser: "root"}, Resolver: config.ResolverConfig{ Strategies: []string{"primary_ip"}, ManagementSubnets: []string{"192.168.0.0/16"}, InterfaceName: "eth0", }, Cache: config.CacheConfig{TTL: 7200}, } if err := save(original); err != nil { t.Fatalf("save: %v", err) } loaded, err := config.Load() if err != nil { t.Fatalf("Load after save: %v", err) } if loaded.NetBox.URL != original.NetBox.URL { t.Errorf("URL: got %q, want %q", loaded.NetBox.URL, original.NetBox.URL) } if loaded.NetBox.Token != original.NetBox.Token { t.Errorf("Token: got %q, want %q", loaded.NetBox.Token, original.NetBox.Token) } if loaded.NetBox.TokenVersion != original.NetBox.TokenVersion { t.Errorf("TokenVersion: got %d, want %d", loaded.NetBox.TokenVersion, original.NetBox.TokenVersion) } if loaded.SSH.DefaultUser != original.SSH.DefaultUser { t.Errorf("DefaultUser: got %q, want %q", loaded.SSH.DefaultUser, original.SSH.DefaultUser) } if loaded.Cache.TTL != original.Cache.TTL { t.Errorf("TTL: got %d, want %d", loaded.Cache.TTL, original.Cache.TTL) } }