shortcuts.rewrites applies regex replacements after the domain suffix is stripped, e.g. ^([a-z]+)[0-9]+- -> $1 turns fsn1-bx1.hoster.de into fsnbx1. The shortcut works for connecting (cached hosts), shell aliases, and Tab completion, where it is offered when the full name does not match the typed text. Rules are editable in netssh configure, and netssh alias --test HOSTNAME shows a name's shortcut and the cached hosts it matches. The shortcut lookup only ran when domains or strip_hyphens were set; it now also runs for rewrite-only configurations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
199 lines
5.4 KiB
Go
199 lines
5.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)
|
|
}
|
|
}
|
|
}
|