package hook_test import ( "bytes" "os" "os/exec" "path/filepath" "strings" "testing" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/hook" "github.com/spf13/cobra" ) func completionEnvironment(t *testing.T) map[string]string { t.Helper() home := t.TempDir() t.Setenv("HOME", home) t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, "config")) t.Setenv("XDG_CACHE_HOME", filepath.Join(home, "cache")) if err := os.MkdirAll(hook.FishConfigDir(), 0o755); err != nil { t.Fatal(err) } return map[string]string{ "bash": filepath.Join(home, ".local", "share", "bash-completion", "completions", "j"), "zsh": filepath.Join(home, ".zfunc", "_j"), "fish": filepath.Join(hook.FishConfigDir(), "completions", "j.fish"), } } func TestCommandCompletionTracksAliasChanges(t *testing.T) { paths := completionEnvironment(t) if err := hook.WriteAliasFiles([]hook.AliasEntry{{Name: "j"}}); err != nil { t.Fatal(err) } for _, path := range paths { if _, err := os.Stat(path); err != nil { t.Fatal(err) } } // A manually replaced completion file must survive removal of the old alias. custom := "# user-owned completion\n" if err := os.WriteFile(paths["bash"], []byte(custom), 0o644); err != nil { t.Fatal(err) } if err := hook.WriteAliasFiles([]hook.AliasEntry{{Name: "k"}}); err != nil { t.Fatal(err) } for shell, path := range paths { if shell == "bash" { data, err := os.ReadFile(path) if err != nil || string(data) != custom { t.Fatal("user completion was removed") } } else if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("stale %s completion remains: %v", shell, err) } } if err := hook.WriteAliasFiles(nil); err != nil { t.Fatal(err) } for _, path := range []string{ filepath.Join(os.Getenv("HOME"), ".local", "share", "bash-completion", "completions", "k"), filepath.Join(os.Getenv("HOME"), ".zfunc", "_k"), filepath.Join(hook.FishConfigDir(), "completions", "k.fish"), } { if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("disabled completion remains: %s: %v", path, err) } } } func TestCommandAliasCompletesServerNamesAcrossShells(t *testing.T) { for _, shell := range []string{"bash", "zsh", "fish"} { t.Run(shell, func(t *testing.T) { executable, err := exec.LookPath(shell) if err != nil { t.Skipf("%s unavailable", shell) } paths := completionEnvironment(t) if err := hook.SyncCommandCompletion("j"); err != nil { t.Fatal(err) } fixtures := t.TempDir() root := &cobra.Command{Use: "netssh"} var canonical bytes.Buffer switch shell { case "bash": err = root.GenBashCompletionV2(&canonical, true) case "zsh": _, err = canonical.WriteString(hook.ZshCompletionScript) case "fish": err = root.GenFishCompletion(&canonical, true) } if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(fixtures, shell), canonical.Bytes(), 0o644); err != nil { t.Fatal(err) } // Exercise Cobra's actual generated shell code with a deterministic backend. backend := `#!/bin/bash case "$1" in completion) cat "$COMPLETION_FIXTURES/$2" ;; __complete|__completeNoDesc) printf 'server01\n:4\n' ;; *) exit 1 ;; esac ` if err := os.WriteFile(filepath.Join(fixtures, "netssh"), []byte(backend), 0o755); err != nil { t.Fatal(err) } t.Setenv("COMPLETION_FIXTURES", fixtures) t.Setenv("ALIAS_COMPLETION", paths[shell]) t.Setenv("PATH", fixtures+string(os.PathListSeparator)+os.Getenv("PATH")) var script string var args []string switch shell { case "bash": library := "" for _, path := range []string{"/usr/share/bash-completion/bash_completion", "/etc/bash_completion"} { if _, err := os.Stat(path); err == nil { library = path break } } if library == "" { t.Skip("bash-completion unavailable") } t.Setenv("BASH_COMPLETION_LIBRARY", library) args = []string{"--noprofile", "--norc", "-c"} script = `source "$BASH_COMPLETION_LIBRARY" shopt -s expand_aliases alias j=netssh _completion_loader j COMP_WORDS=(j ser) COMP_CWORD=1 COMP_LINE='j ser' COMP_POINT=5 COMP_TYPE=9 COMP_KEY=9 __start_netssh j ser j printf '%s\n' "${COMPREPLY[@]}" ` case "zsh": args = []string{"-f", "-c"} script = `fpath=("$HOME/.zfunc" $fpath) autoload -Uz compinit compinit -u -D alias j=netssh autoload -Uz _j # Capture matches without requiring an interactive ZLE completion widget. _describe() { print -rl -- "${(P)4}"; } compadd() { print -rl -- "$@"; } words=(j ser) CURRENT=2 _j ` case "fish": args = []string{"--no-config", "-c"} script = `alias j netssh set -p fish_complete_path "$XDG_CONFIG_HOME/fish/completions" complete --do-complete 'j ser' ` } cmd := exec.Command(executable, append(args, script)...) out, err := cmd.CombinedOutput() if err != nil || !strings.Contains(string(out), "server01") { t.Fatalf("%s completion: %v\n%s", shell, err, out) } }) } } func TestZshCompletionSplitsHostsAndCommands(t *testing.T) { zsh, err := exec.LookPath("zsh") if err != nil { t.Skip("zsh unavailable") } dir := t.TempDir() backend := "#!/bin/sh\nprintf 'web01\\nweb:02\\ncache\\tManage the cache\\n:36\\n'\n" if err := os.WriteFile(filepath.Join(dir, "netssh"), []byte(backend), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "_netssh"), []byte(hook.ZshCompletionScript), 0o644); err != nil { t.Fatal(err) } script := `compdef() { :; } source "$DIR/_netssh" _describe() { print -r -- "describe ${(P)4}"; } compadd() { print -r -- "compadd $*"; } _files() { print -r -- files; } words=(netssh "") CURRENT=2 _netssh ` cmd := exec.Command(zsh, "-f", "-c", script) cmd.Env = append(os.Environ(), "DIR="+dir, "PATH="+dir+string(os.PathListSeparator)+os.Getenv("PATH")) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("%v\n%s", err, out) } // Directive 36 = NoFileComp|KeepOrder: unsorted host group, no file fallback. want := "describe cache:Manage the cache\ncompadd -V hosts -X %Bhosts%b -- web01 web:02\n" if string(out) != want { t.Fatalf("got:\n%s\nwant:\n%s", out, want) } }