With several NetBox instances, netssh offers "prod/fsn1-bx1…" for the typed text "fsn1". compadd discarded these matches because they do not start with the typed word, so <Tab> found nothing. The matcher 'l:|=*' lets zsh accept text before the typed word; netssh has already filtered the candidates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
6.0 KiB
Go
208 lines
6.0 KiB
Go
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 -M l:|=* -- web01 web:02\n"
|
|
if string(out) != want {
|
|
t.Fatalf("got:\n%s\nwant:\n%s", out, want)
|
|
}
|
|
}
|