Files
ssh-netbox-wrapper/internal/hook/aliases_test.go
T
Sebastian UnterschützandClaude Opus 5.5 878701aba7 feat(shortcuts): regex rewrite rules for host shortcuts
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>
2026-11-13 22:42:13 +01:00

256 lines
7.8 KiB
Go

package hook_test
import (
"os"
"path/filepath"
"strings"
"testing"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/cache"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/hook"
)
func TestAliasesPath_UnderCacheDir(t *testing.T) {
dir := t.TempDir()
orig := os.Getenv("XDG_CACHE_HOME")
os.Setenv("XDG_CACHE_HOME", dir)
defer os.Setenv("XDG_CACHE_HOME", orig)
p := hook.AliasesPath()
if !strings.HasPrefix(p, dir) {
t.Errorf("AliasesPath should be under XDG_CACHE_HOME, got %q", p)
}
if !strings.HasSuffix(p, "aliases.sh") {
t.Errorf("AliasesPath should end with aliases.sh, got %q", p)
}
}
func TestFishAliasesPath_UnderConfigDir(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)
p := hook.FishAliasesPath()
if !strings.HasPrefix(p, dir) {
t.Errorf("FishAliasesPath should be under XDG_CONFIG_HOME, got %q", p)
}
if !strings.HasSuffix(p, "netssh.fish") {
t.Errorf("FishAliasesPath should end with netssh.fish, got %q", p)
}
}
// TestWriteAliasFiles_ShFile verifies the bash/zsh file is always written.
func TestWriteAliasFiles_ShFile(t *testing.T) {
cacheDir := t.TempDir()
orig := os.Getenv("XDG_CACHE_HOME")
os.Setenv("XDG_CACHE_HOME", cacheDir)
defer os.Setenv("XDG_CACHE_HOME", orig)
entries := []hook.AliasEntry{
{Name: "web01", Host: "web01.example.com"},
{Name: "db01", Host: "db01.example.com"},
}
if err := hook.WriteAliasFiles(entries); err != nil {
t.Fatalf("WriteAliasFiles: %v", err)
}
data, err := os.ReadFile(hook.AliasesPath())
if err != nil {
t.Fatalf("aliases.sh not created: %v", err)
}
content := string(data)
for _, want := range []string{
"alias web01='netssh web01.example.com'",
"alias db01='netssh db01.example.com'",
} {
if !strings.Contains(content, want) {
t.Errorf("aliases.sh missing %q\ncontent:\n%s", want, content)
}
}
}
// TestWriteAliasFiles_FishFile verifies the fish file is written when fish config dir exists.
func TestWriteAliasFiles_FishFile(t *testing.T) {
configDir := t.TempDir()
orig := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", configDir)
defer os.Setenv("XDG_CONFIG_HOME", orig)
// Create the fish config directory so WriteAliasFiles knows fish is set up.
fishDir := filepath.Join(configDir, "fish")
os.MkdirAll(fishDir, 0o755)
cacheDir := t.TempDir()
origCache := os.Getenv("XDG_CACHE_HOME")
os.Setenv("XDG_CACHE_HOME", cacheDir)
defer os.Setenv("XDG_CACHE_HOME", origCache)
entries := []hook.AliasEntry{{Name: "web01", Host: "web01.example.com"}}
if err := hook.WriteAliasFiles(entries); err != nil {
t.Fatalf("WriteAliasFiles: %v", err)
}
data, err := os.ReadFile(hook.FishAliasesPath())
if err != nil {
t.Fatalf("netssh.fish not created: %v", err)
}
if !strings.Contains(string(data), "alias web01 'netssh web01.example.com'") {
t.Errorf("fish alias file missing expected line\ncontent:\n%s", string(data))
}
}
// TestWriteAliasFiles_SkipsFishWhenNotConfigured verifies that fish file is NOT written
// when fish is not set up (no ~/.config/fish directory).
func TestWriteAliasFiles_SkipsFishWhenNotConfigured(t *testing.T) {
configDir := t.TempDir() // no fish subdir
orig := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", configDir)
defer os.Setenv("XDG_CONFIG_HOME", orig)
cacheDir := t.TempDir()
origCache := os.Getenv("XDG_CACHE_HOME")
os.Setenv("XDG_CACHE_HOME", cacheDir)
defer os.Setenv("XDG_CACHE_HOME", origCache)
entries := []hook.AliasEntry{{Name: "web01", Host: "web01.example.com"}}
if err := hook.WriteAliasFiles(entries); err != nil {
t.Fatalf("WriteAliasFiles: %v", err)
}
if _, err := os.Stat(hook.FishAliasesPath()); err == nil {
t.Error("fish alias file should not be created when fish is not configured")
}
}
// TestWriteAliasFiles_EmptyEntries verifies an empty (but valid) file is written.
func TestWriteAliasFiles_EmptyEntries(t *testing.T) {
cacheDir := t.TempDir()
orig := os.Getenv("XDG_CACHE_HOME")
os.Setenv("XDG_CACHE_HOME", cacheDir)
defer os.Setenv("XDG_CACHE_HOME", orig)
if err := hook.WriteAliasFiles(nil); err != nil {
t.Fatalf("WriteAliasFiles with nil entries: %v", err)
}
data, err := os.ReadFile(hook.AliasesPath())
if err != nil {
t.Fatalf("aliases.sh not created: %v", err)
}
// Should contain only the header comment, no alias lines.
if strings.Contains(string(data), "alias ") {
t.Error("aliases.sh should have no alias lines for empty entries")
}
}
// --- InstallAliasesSource ---
func TestInstallAliasesSource_Fresh(t *testing.T) {
profile := writeProfile(t, "existing line\n")
installed, err := hook.InstallAliasesSource(profile)
if err != nil {
t.Fatalf("InstallAliasesSource: %v", err)
}
if !installed {
t.Error("should report installed=true on first install")
}
data, _ := os.ReadFile(profile)
if !strings.Contains(string(data), hook.AliasesMarker) {
t.Error("profile should contain aliases source line")
}
}
func TestInstallAliasesSource_Idempotent(t *testing.T) {
profile := writeProfile(t, "")
hook.InstallAliasesSource(profile)
installed, err := hook.InstallAliasesSource(profile)
if err != nil {
t.Fatalf("second InstallAliasesSource: %v", err)
}
if installed {
t.Error("second call should report installed=false")
}
data, _ := os.ReadFile(profile)
if count := strings.Count(string(data), hook.AliasesMarker); count != 1 {
t.Errorf("aliases source line should appear once, got %d", count)
}
}
// TestUninstall_AlsoRemovesAliasesSourceLine verifies that Uninstall removes both lines.
func TestUninstall_AlsoRemovesAliasesSourceLine(t *testing.T) {
content := "export PATH=$PATH\n" +
hook.Line + "\n" +
hook.AliasesSourceLine + "\n" +
"export FOO=bar\n"
profile := writeProfile(t, content)
removed, err := hook.Uninstall(profile)
if err != nil {
t.Fatalf("Uninstall: %v", err)
}
if !removed {
t.Error("should report removed=true")
}
data, _ := os.ReadFile(profile)
s := string(data)
if strings.Contains(s, hook.Marker) {
t.Error("shell-init line should be removed")
}
if strings.Contains(s, hook.AliasesMarker) {
t.Error("aliases source line should be removed")
}
if !strings.Contains(s, "export PATH") || !strings.Contains(s, "export FOO") {
t.Error("other content should be preserved")
}
}
func TestCommandAliasGenerationAndCollision(t *testing.T) {
cfg := config.Config{Shell: config.ShellConfig{Alias: "j"}}
entries := hook.BuildAliasEntries([]cache.Entry{{Name: "j"}, {Name: "netssh"}, {Name: "web-01"}}, cfg)
if len(entries) != 2 {
t.Fatalf("entries: %+v", entries)
}
if got := entries[0].Definition("bash"); got != "alias j='netssh'" {
t.Fatal(got)
}
if got := entries[0].Definition("fish"); got != "alias j 'netssh'" {
t.Fatal(got)
}
if got := entries[1].Definition("bash"); got != "alias web-01='netssh web-01'" {
t.Fatal(got)
}
}
func TestAliasesIncludeInstanceName(t *testing.T) {
cfg := config.Config{Shortcuts: config.ShortcutsConfig{Domains: []string{".example.com"}}}
entries := hook.BuildAliasEntries([]cache.Entry{{Name: "office/web01.example.com"}, {Name: "lab/web01.example.com"}}, cfg)
if len(entries) != 2 {
t.Fatalf("missing qualified aliases: %+v", entries)
}
got := map[string]string{}
for _, entry := range entries {
got[entry.Name] = entry.Host
}
if got["office_web01"] != "office/web01.example.com" || got["lab_web01"] != "lab/web01.example.com" {
t.Fatalf("alias lost source: %+v", entries)
}
}
func TestAliasesUseRewriteRules(t *testing.T) {
cfg := config.Config{Shortcuts: config.ShortcutsConfig{
Domains: []string{".hoster.de"},
Rewrites: []config.ShortcutRewrite{{Pattern: `^([a-z]+)[0-9]+-`, Replace: "$1"}},
}}
entries := hook.BuildAliasEntries([]cache.Entry{{Name: "fsn1-bx1.hoster.de"}}, cfg)
if len(entries) != 1 || entries[0].Definition("bash") != "alias fsnbx1='netssh fsn1-bx1.hoster.de'" {
t.Fatalf("entries: %+v", entries)
}
}