Files
ssh-netbox-wrapper/internal/config/config_test.go
T
Sebastian UnterschützandClaude Fable 5.2 4287b7b650 fix: harden config loading and ssh addresses, stop resolver on API errors
- read netssh.yaml from the user config directory only, not from the
  working directory
- refuse addresses from NetBox or the cache that are neither an IP
  address nor a hostname (ssh read "-oProxyCommand=…" as an option)
- resolver chain stops at an API failure; only "no address" falls
  through to the next strategy
- netssh --help and -h show netssh's help
- start at most one background cache refresh per 5 minutes
- validate the legacy netbox.url
- page through all IP addresses of a host
- CI: run format check, vet, and tests on pull requests and main
- changelog for v0.0.25

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-15 23:25:28 +01:00

223 lines
6.4 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func writeConfig(t *testing.T, content string) string {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "netssh.yaml")
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
return dir
}
func loadFromDir(t *testing.T, dir string) *Config {
t.Helper()
// Override UserConfigDir by pointing viper at our temp dir via env isn't
// straightforward, so we exercise Load() by temporarily changing XDG_CONFIG_HOME.
orig := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", dir)
t.Cleanup(func() { os.Setenv("XDG_CONFIG_HOME", orig) })
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
return cfg
}
func TestLoad_V2TokenVersion_Preserved(t *testing.T) {
dir := writeConfig(t, `
netbox:
url: "https://netbox.example.com"
token: "nbt_abc123"
token_version: 2
`)
cfg := loadFromDir(t, dir)
if cfg.NetBox.TokenVersion != 2 {
t.Errorf("TokenVersion: got %d, want 2", cfg.NetBox.TokenVersion)
}
}
func TestLoad_V1TokenVersion_Preserved(t *testing.T) {
dir := writeConfig(t, `
netbox:
url: "https://netbox.example.com"
token: "legacyhex123"
token_version: 1
`)
cfg := loadFromDir(t, dir)
if cfg.NetBox.TokenVersion != 1 {
t.Errorf("TokenVersion: got %d, want 1", cfg.NetBox.TokenVersion)
}
}
func TestLoad_AutoDetectsV2_WhenFieldMissing(t *testing.T) {
dir := writeConfig(t, `
netbox:
url: "https://netbox.example.com"
token: "nbt_mytoken"
`)
cfg := loadFromDir(t, dir)
if cfg.NetBox.TokenVersion != 2 {
t.Errorf("TokenVersion: got %d, want 2 (auto-detected from nbt_ prefix)", cfg.NetBox.TokenVersion)
}
}
func TestLoad_AutoDetectsV1_WhenFieldMissing(t *testing.T) {
dir := writeConfig(t, `
netbox:
url: "https://netbox.example.com"
token: "abc123def456"
`)
cfg := loadFromDir(t, dir)
if cfg.NetBox.TokenVersion != 1 {
t.Errorf("TokenVersion: got %d, want 1 (auto-detected from plain token)", cfg.NetBox.TokenVersion)
}
}
func TestLoad_TokenVersionZero_WhenNoToken(t *testing.T) {
dir := writeConfig(t, `
netbox:
url: "https://netbox.example.com"
`)
cfg := loadFromDir(t, dir)
if cfg.NetBox.TokenVersion != 0 {
t.Errorf("TokenVersion: got %d, want 0 (no token present)", cfg.NetBox.TokenVersion)
}
}
func TestLoad_Defaults(t *testing.T) {
dir := writeConfig(t, `
netbox:
url: "https://netbox.example.com"
token: "nbt_x"
`)
cfg := loadFromDir(t, dir)
if cfg.Cache.TTL != 3600 {
t.Errorf("default cache.ttl: got %d, want 3600", cfg.Cache.TTL)
}
if cfg.Cache.Path == "" {
t.Error("cache.path should be auto-set when empty")
}
}
func TestLoad_MissingFile_ReturnsEmptyConfig(t *testing.T) {
orig := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", t.TempDir()) // dir exists but no netssh.yaml
defer os.Setenv("XDG_CONFIG_HOME", orig)
cfg, err := Load()
if err != nil {
t.Fatalf("Load on missing file should not error: %v", err)
}
if cfg.NetBox.URL != "" {
t.Errorf("expected empty URL, got %q", cfg.NetBox.URL)
}
}
func TestLoad_InvalidYAML_ReturnsError(t *testing.T) {
dir := writeConfig(t, "not: valid: yaml: [[[")
orig := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", dir)
defer os.Setenv("XDG_CONFIG_HOME", orig)
_, err := Load()
if err == nil {
t.Error("invalid YAML should return an error")
}
}
func TestPath_ReturnsNonEmpty(t *testing.T) {
p := Path()
if p == "" {
t.Error("Path() should return a non-empty string")
}
if filepath.Base(p) != "netssh.yaml" {
t.Errorf("Path() base: got %q, want netssh.yaml", filepath.Base(p))
}
}
func TestEnvironmentOverridesWithoutConfigKeys(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("NETSSH_NETBOX_URL", "https://env.example")
t.Setenv("NETSSH_NETBOX_TOKEN", "nbt_env")
t.Setenv("NETSSH_SSH_DEFAULT_PORT", "2222")
cfg, err := Load()
if err != nil {
t.Fatal(err)
}
if cfg.NetBox.URL != "https://env.example" || cfg.NetBox.Token != "nbt_env" || cfg.NetBox.TokenVersion != 2 || cfg.SSH.DefaultPort != 2222 {
t.Fatalf("env ignored: %+v", cfg)
}
}
func TestCommandAliasEnvironmentAndValidation(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("NETSSH_SHELL_ALIAS", "j")
cfg, err := Load()
if err != nil || cfg.Shell.Alias != "j" {
t.Fatalf("alias env: %+v, %v", cfg, err)
}
for _, name := range []string{"netssh", "if", "alias", "j;touch /tmp/bad", "a b", "1bad"} {
t.Setenv("NETSSH_SHELL_ALIAS", name)
if _, err := Load(); err == nil {
t.Errorf("unsafe alias accepted: %q", name)
}
}
for _, name := range []string{"", "j", "jump_server", "goSSH"} {
if err := ValidateCommandAlias(name); err != nil {
t.Errorf("valid alias rejected: %s: %v", name, err)
}
}
}
func TestLoad_ShortcutRewrites(t *testing.T) {
dir := writeConfig(t, "shortcuts:\n rewrites:\n - pattern: '^([a-z]+)[0-9]+-'\n replace: '$1'\n")
cfg := loadFromDir(t, dir)
want := []ShortcutRewrite{{Pattern: `^([a-z]+)[0-9]+-`, Replace: "$1"}}
if len(cfg.Shortcuts.Rewrites) != 1 || cfg.Shortcuts.Rewrites[0] != want[0] || !cfg.Shortcuts.Enabled() {
t.Fatalf("rewrites: %+v", cfg.Shortcuts)
}
}
func TestLoad_InvalidShortcutRewrite(t *testing.T) {
for _, rule := range []string{"pattern: '[a-'", "replace: x"} {
t.Setenv("XDG_CONFIG_HOME", writeConfig(t, "shortcuts:\n rewrites:\n - "+rule+"\n"))
if _, err := Load(); err == nil || !strings.Contains(err.Error(), "shortcuts.rewrites[0]") {
t.Errorf("%s: got %v", rule, err)
}
}
}
// A netssh.yaml in the working directory (e.g. of a cloned repository) must
// not be loaded: it could inject ssh options such as ProxyCommand.
func TestLoad_IgnoresConfigInWorkingDirectory(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("NETSSH_NETBOX_URL", "")
t.Chdir(writeConfig(t, "hosts:\n - name: evil\n address: 10.0.0.1\nssh:\n options:\n ProxyCommand: touch /tmp/pwned\n"))
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(cfg.Hosts) != 0 || len(cfg.SSH.Options) != 0 {
t.Errorf("config from the working directory was loaded: %+v", cfg)
}
}
func TestLoad_RejectsInvalidLegacyNetBoxURL(t *testing.T) {
for _, bad := range []string{"netbox.example.com", "ftp://netbox.example.com", "https://netbox.example.com/?x=1"} {
t.Setenv("XDG_CONFIG_HOME", writeConfig(t, "netbox:\n url: \""+bad+"\"\n token: t\n"))
if _, err := Load(); err == nil || !strings.Contains(err.Error(), "netbox.url") {
t.Errorf("%s: got %v, want a netbox.url error", bad, err)
}
}
}