mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-15 07:57:49 +00:00
Optimisation & fixes
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
// Package complete provides shell completion generation for kportal.
|
||||
// It supports bash, zsh, and fish shells with context-aware completions
|
||||
// for flags, subcommands, config values, and Kubernetes resources.
|
||||
package complete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Shell represents a supported shell type
|
||||
type Shell string
|
||||
|
||||
const (
|
||||
ShellBash Shell = "bash"
|
||||
ShellZsh Shell = "zsh"
|
||||
ShellFish Shell = "fish"
|
||||
)
|
||||
|
||||
// Installer handles shell completion installation
|
||||
type Installer struct {
|
||||
shell Shell
|
||||
prefixDir string
|
||||
}
|
||||
|
||||
// NewInstaller creates a new completion installer for the specified shell
|
||||
func NewInstaller(shell Shell) *Installer {
|
||||
return &Installer{
|
||||
shell: shell,
|
||||
prefixDir: getDefaultPrefixDir(shell),
|
||||
}
|
||||
}
|
||||
|
||||
// getDefaultPrefixDir returns the default completion directory for a shell
|
||||
func getDefaultPrefixDir(shell Shell) string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
home = os.Getenv("HOME")
|
||||
}
|
||||
if home == "" {
|
||||
home = "/tmp"
|
||||
}
|
||||
|
||||
switch shell {
|
||||
case ShellBash:
|
||||
// Check common bash completion directories
|
||||
dirs := []string{
|
||||
"/etc/bash_completion.d",
|
||||
filepath.Join(home, ".local", "share", "bash-completion", "completions"),
|
||||
filepath.Join(home, ".bash_completion.d"),
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
if pathExists(dir) {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
// Fallback to user dir (best-effort create; Install reports write errors)
|
||||
userDir := filepath.Join(home, ".local", "share", "bash-completion", "completions")
|
||||
ensureDir(userDir)
|
||||
return userDir
|
||||
|
||||
case ShellZsh:
|
||||
dirs := []string{
|
||||
filepath.Join(home, ".zsh", "completions"),
|
||||
filepath.Join(home, ".oh-my-zsh", "completions"),
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
if pathExists(dir) {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
// Fallback to standard zsh site-functions
|
||||
usrShare := "/usr/local/share/zsh/site-functions"
|
||||
if pathExists(usrShare) {
|
||||
return usrShare
|
||||
}
|
||||
userDir := filepath.Join(home, ".zsh", "completions")
|
||||
ensureDir(userDir)
|
||||
return userDir
|
||||
|
||||
case ShellFish:
|
||||
dir := filepath.Join(home, ".config", "fish", "completions")
|
||||
ensureDir(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Install installs the completion script for the shell
|
||||
func (i *Installer) Install() error {
|
||||
script, err := Generate(i.shell)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate completion script: %w", err)
|
||||
}
|
||||
|
||||
filename := i.getCompletionFilename()
|
||||
filepath := filepath.Join(i.prefixDir, filename)
|
||||
|
||||
// Check if already installed
|
||||
if pathExists(filepath) {
|
||||
return fmt.Errorf("completion file already exists: %s (remove it first to reinstall)", filepath)
|
||||
}
|
||||
|
||||
// #nosec G306 -- completion scripts are non-secret and must be world-readable by the shell
|
||||
if err := os.WriteFile(filepath, []byte(script), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write completion file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uninstall removes the completion script
|
||||
func (i *Installer) Uninstall() error {
|
||||
filename := i.getCompletionFilename()
|
||||
filepath := filepath.Join(i.prefixDir, filename)
|
||||
|
||||
if err := os.Remove(filepath); err != nil {
|
||||
return fmt.Errorf("failed to remove completion file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCompletionFilename returns the filename for the completion script
|
||||
func (i *Installer) getCompletionFilename() string {
|
||||
switch i.shell {
|
||||
case ShellBash:
|
||||
return "_kportal"
|
||||
case ShellZsh:
|
||||
return "_kportal"
|
||||
case ShellFish:
|
||||
return "kportal.fish"
|
||||
}
|
||||
return "_kportal"
|
||||
}
|
||||
|
||||
// Print prints the completion script to stdout
|
||||
func Print(shell Shell) error {
|
||||
script, err := Generate(shell)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(script)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate generates the completion script for the specified shell
|
||||
func Generate(shell Shell) (string, error) {
|
||||
switch shell {
|
||||
case ShellBash:
|
||||
return generateBash()
|
||||
case ShellZsh:
|
||||
return generateZsh()
|
||||
case ShellFish:
|
||||
return generateFish()
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported shell: %s", shell)
|
||||
}
|
||||
}
|
||||
|
||||
// generateBash generates bash completion script
|
||||
func generateBash() (string, error) {
|
||||
var sb strings.Builder
|
||||
|
||||
bashScript := `# kportal shell completion - bash
|
||||
# Generated by kportal
|
||||
|
||||
# Don't interfere with other completions
|
||||
if [[ -n "${BASH_COMPLETION_VERSINFO:-}" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
_kportal()
|
||||
{
|
||||
local cur prev words cword split=false
|
||||
_init_completion -s || return
|
||||
|
||||
# Complete the value expected after the previous flag
|
||||
case "$prev" in
|
||||
-c|--config)
|
||||
_filedir yaml
|
||||
return
|
||||
;;
|
||||
--log-format)
|
||||
COMPREPLY=( $(compgen -W "text json" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
--convert)
|
||||
_filedir json
|
||||
return
|
||||
;;
|
||||
--convert-output)
|
||||
_filedir yaml
|
||||
return
|
||||
;;
|
||||
--context)
|
||||
# Complete from kubectl contexts
|
||||
if command -v kubectl &> /dev/null; then
|
||||
COMPREPLY=( $(compgen -W "$(kubectl config get-contexts -o name 2>/dev/null)" -- "$cur") )
|
||||
fi
|
||||
return
|
||||
;;
|
||||
--shell)
|
||||
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
# Handle option splitting (e.g., -c=value)
|
||||
[[ "$cur" == -*=* ]] && split=true
|
||||
[[ "$split" == true ]] && prev="${cur%%=*}"
|
||||
|
||||
# Subcommand-specific completion
|
||||
case "${words[1]}" in
|
||||
generate)
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=( $(compgen -W "--context --config --dry-run" -- "$cur") )
|
||||
elif command -v kubectl &> /dev/null; then
|
||||
COMPREPLY=( $(compgen -W "$(kubectl config get-contexts -o name 2>/dev/null)" -- "$cur") )
|
||||
fi
|
||||
return
|
||||
;;
|
||||
completion)
|
||||
COMPREPLY=( $(compgen -W "--install --uninstall --shell" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
# Top-level options
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=( $(compgen -W "-c -v --check --headless --log-format --version --update --convert --convert-output" -- "$cur") )
|
||||
return
|
||||
fi
|
||||
|
||||
# Top-level subcommands
|
||||
COMPREPLY=( $(compgen -W "generate completion" -- "$cur") )
|
||||
}
|
||||
|
||||
# Register completion
|
||||
complete -F _kportal kportal
|
||||
|
||||
# Also complete for common aliases
|
||||
complete -F _kportal kp
|
||||
`
|
||||
|
||||
sb.WriteString(bashScript)
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// generateZsh generates zsh completion script
|
||||
func generateZsh() (string, error) {
|
||||
var sb strings.Builder
|
||||
|
||||
// Per-flag zsh _arguments descriptors. Completion scripts are static text, so
|
||||
// these are literals (single source of truth — keep in sync with the CLI flags).
|
||||
flagDescs := []string{
|
||||
`'-c[Path to configuration file]:config file:_files -g "*.yaml"'`,
|
||||
`'-v[Enable verbose logging]'`,
|
||||
`'--version[Show version and exit]'`,
|
||||
`'--update[Check for updates]'`,
|
||||
`'--check[Validate configuration]'`,
|
||||
`'--headless[Run without UI]'`,
|
||||
`'--log-format[Log format: text or json]:format:(text json)'`,
|
||||
`'--convert[Convert kftray config]:input file:_files -g "*.json"'`,
|
||||
`'--convert-output[Output file]:output file:_files -g "*.yaml"'`,
|
||||
}
|
||||
|
||||
sb.WriteString(`#compdef kportal
|
||||
|
||||
# kportal shell completion - zsh
|
||||
# Generated by kportal
|
||||
|
||||
_kportal()
|
||||
{
|
||||
local -a commands flags generate_flags completion_flags
|
||||
|
||||
commands=(
|
||||
'generate:Interactively generate forwards from cluster'
|
||||
'completion:Generate shell completion scripts'
|
||||
)
|
||||
|
||||
flags=(
|
||||
`)
|
||||
|
||||
for _, desc := range flagDescs {
|
||||
fmt.Fprintf(&sb, " %s\n", desc)
|
||||
}
|
||||
|
||||
sb.WriteString(` )
|
||||
|
||||
generate_flags=(
|
||||
'--context[Kubernetes context]:context:->ctx'
|
||||
'--config[Config file]:file:_files -g "*.yaml"'
|
||||
'--dry-run[Print without saving]'
|
||||
)
|
||||
|
||||
completion_flags=(
|
||||
'--install[Install completions for the shell]'
|
||||
'--uninstall[Remove installed completions]'
|
||||
'--shell[Shell type]:shell:(bash zsh fish)'
|
||||
)
|
||||
|
||||
_arguments -s $flags '1: :->command' '*:: :->args'
|
||||
|
||||
case $state in
|
||||
command)
|
||||
_describe 'command' commands
|
||||
;;
|
||||
args)
|
||||
case ${words[1]} in
|
||||
generate)
|
||||
_arguments -s $generate_flags
|
||||
if [[ "$words[CURRENT]" == --context ]]; then
|
||||
if command -v kubectl &> /dev/null; then
|
||||
local -a contexts
|
||||
contexts=(${(f)"$(kubectl config get-contexts -o name 2>/dev/null)"})
|
||||
_describe 'context' contexts
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
completion)
|
||||
_arguments -s $completion_flags
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_kportal "$@"
|
||||
`)
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// generateFish generates fish completion script
|
||||
func generateFish() (string, error) {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(`# kportal shell completion - fish
|
||||
# Generated by kportal
|
||||
|
||||
# Main completion
|
||||
complete -c kportal -f
|
||||
|
||||
# Subcommands
|
||||
complete -c kportal -n '__fish_use_subcommand' -a 'generate' -d 'Interactively generate forwards from cluster'
|
||||
complete -c kportal -n '__fish_use_subcommand' -a 'completion' -d 'Generate shell completion scripts'
|
||||
|
||||
# Global flags (main command uses single-dash -c and -v; words accept --)
|
||||
complete -c kportal -s c -r -f -a '( __fish_complete_suffix .yaml )' -d 'Path to configuration file'
|
||||
complete -c kportal -s v -d 'Enable verbose logging'
|
||||
complete -c kportal -l version -d 'Show version and exit'
|
||||
complete -c kportal -l update -d 'Check for updates'
|
||||
complete -c kportal -l check -d 'Validate configuration'
|
||||
complete -c kportal -l headless -d 'Run without UI'
|
||||
complete -c kportal -l log-format -d 'Log format' -a 'text json' -f
|
||||
complete -c kportal -l convert -r -f -a '( __fish_complete_suffix .json )' -d 'Convert kftray config'
|
||||
complete -c kportal -l convert-output -r -f -a '( __fish_complete_suffix .yaml )' -d 'Output file'
|
||||
|
||||
# generate subcommand flags
|
||||
complete -c kportal -n '__fish_seen_subcommand_from generate' -l context -d 'Kubernetes context' -a '(kubectl config get-contexts -o name 2>/dev/null)' -f
|
||||
complete -c kportal -n '__fish_seen_subcommand_from generate' -l config -r -f -a '( __fish_complete_suffix .yaml )' -d 'Config file'
|
||||
complete -c kportal -n '__fish_seen_subcommand_from generate' -l dry-run -d 'Print without saving'
|
||||
|
||||
# completion subcommand flags
|
||||
complete -c kportal -n '__fish_seen_subcommand_from completion' -l install -d 'Install completions for the shell'
|
||||
complete -c kportal -n '__fish_seen_subcommand_from completion' -l uninstall -d 'Remove installed completions'
|
||||
complete -c kportal -n '__fish_seen_subcommand_from completion' -l shell -d 'Shell type' -a 'bash zsh fish' -f
|
||||
|
||||
# Aliases
|
||||
complete -c kp -f
|
||||
`)
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// InstallCompletions installs completions for the specified shell
|
||||
// Prints instructions for manual installation if auto-install fails
|
||||
func InstallCompletions(shell Shell) error {
|
||||
installer := NewInstaller(shell)
|
||||
|
||||
if err := installer.Install(); err != nil {
|
||||
// Check if it's just "already exists" error
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
fmt.Printf("Completion already installed at: %s/%s\n",
|
||||
installer.prefixDir, installer.getCompletionFilename())
|
||||
fmt.Println("Remove it first to reinstall, or source it manually:")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to print the script instead
|
||||
fmt.Println("Could not auto-install completions. To install manually, run:")
|
||||
fmt.Printf(" # %s\n", getInstallInstructions(shell))
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Completions installed to: %s/%s\n",
|
||||
installer.prefixDir, installer.getCompletionFilename())
|
||||
fmt.Printf("\nTo enable %s completions, restart your shell or run:\n", shell)
|
||||
fmt.Printf(" source ~/%s\n", getSourceInstruction(shell))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getInstallInstructions(shell Shell) string {
|
||||
return fmt.Sprintf("kportal completion %s > <your-completion-dir>/_kportal", shell)
|
||||
}
|
||||
|
||||
func getSourceInstruction(shell Shell) string {
|
||||
switch shell {
|
||||
case ShellBash:
|
||||
return ".bashrc"
|
||||
case ShellZsh:
|
||||
return ".zshrc"
|
||||
case ShellFish:
|
||||
return ".config/fish/config.fish # completions load automatically"
|
||||
}
|
||||
return "shell config"
|
||||
}
|
||||
|
||||
// AutoDetectShell detects the current shell
|
||||
func AutoDetectShell() Shell {
|
||||
shell := os.Getenv("SHELL")
|
||||
if shell == "" {
|
||||
return ShellBash // Default
|
||||
}
|
||||
|
||||
if strings.HasSuffix(shell, "/bash") {
|
||||
return ShellBash
|
||||
}
|
||||
if strings.HasSuffix(shell, "/zsh") {
|
||||
return ShellZsh
|
||||
}
|
||||
if strings.HasSuffix(shell, "/fish") {
|
||||
return ShellFish
|
||||
}
|
||||
|
||||
return ShellBash
|
||||
}
|
||||
|
||||
// pathExists reports whether path exists on disk.
|
||||
// #nosec G703 -- callers pass fixed completion paths derived from the user's own
|
||||
// HOME and constant subdirectory names, never external/untrusted input.
|
||||
func pathExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ensureDir best-effort creates a completion directory. Errors are intentionally
|
||||
// ignored: Install() surfaces any later write failure with a clear message.
|
||||
// #nosec G703 -- path is derived from the user's own HOME and constant
|
||||
// subdirectory names; 0o750 is appropriate for user-owned completion dirs.
|
||||
func ensureDir(path string) {
|
||||
_ = os.MkdirAll(path, 0o750)
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package complete
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateBash(t *testing.T) {
|
||||
script, err := Generate(ShellBash)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate(ShellBash) failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify script contains key elements
|
||||
if !strings.Contains(script, "_kportal()") {
|
||||
t.Error("Bash script missing _kportal function")
|
||||
}
|
||||
if !strings.Contains(script, "complete -F _kportal kportal") {
|
||||
t.Error("Bash script missing completion registration")
|
||||
}
|
||||
if !strings.Contains(script, "--version") {
|
||||
t.Error("Bash script missing --version flag")
|
||||
}
|
||||
if !strings.Contains(script, "generate") {
|
||||
t.Error("Bash script missing generate subcommand")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateZsh(t *testing.T) {
|
||||
script, err := Generate(ShellZsh)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate(ShellZsh) failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify script contains key elements
|
||||
if !strings.Contains(script, "#compdef kportal") {
|
||||
t.Error("Zsh script missing #compdef directive")
|
||||
}
|
||||
if !strings.Contains(script, "_kportal()") {
|
||||
t.Error("Zsh script missing _kportal function")
|
||||
}
|
||||
if !strings.Contains(script, "'generate:") {
|
||||
t.Error("Zsh script missing generate subcommand")
|
||||
}
|
||||
if !strings.Contains(script, "'--context[") {
|
||||
t.Error("Zsh script missing --context flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateFish(t *testing.T) {
|
||||
script, err := Generate(ShellFish)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate(ShellFish) failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify script contains key elements
|
||||
if !strings.Contains(script, "complete -c kportal") {
|
||||
t.Error("Fish script missing complete directive")
|
||||
}
|
||||
if !strings.Contains(script, "-n '__fish_use_subcommand'") {
|
||||
t.Error("Fish script missing subcommand detection")
|
||||
}
|
||||
if !strings.Contains(script, "-l context") {
|
||||
t.Error("Fish script missing --context flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateUnsupported(t *testing.T) {
|
||||
_, err := Generate(Shell("unsupported"))
|
||||
if err == nil {
|
||||
t.Error("Expected error for unsupported shell")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoDetectShell(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
shellEnv string
|
||||
expected Shell
|
||||
}{
|
||||
{"bash", "/bin/bash", ShellBash},
|
||||
{"zsh", "/usr/bin/zsh", ShellZsh},
|
||||
{"fish", "/usr/local/bin/fish", ShellFish},
|
||||
{"tcsh", "/bin/tcsh", ShellBash}, // Falls back to bash
|
||||
{"empty", "", ShellBash}, // Falls back to bash
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
detected := autoDetectShellFromEnv(tt.shellEnv)
|
||||
if detected != tt.expected {
|
||||
t.Errorf("AutoDetectShell() = %v, want %v", detected, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// autoDetectShellFromEnv is a test helper that simulates shell detection
|
||||
func autoDetectShellFromEnv(shellEnv string) Shell {
|
||||
if shellEnv == "" {
|
||||
return ShellBash
|
||||
}
|
||||
if strings.HasSuffix(shellEnv, "/bash") {
|
||||
return ShellBash
|
||||
}
|
||||
if strings.HasSuffix(shellEnv, "/zsh") {
|
||||
return ShellZsh
|
||||
}
|
||||
if strings.HasSuffix(shellEnv, "/fish") {
|
||||
return ShellFish
|
||||
}
|
||||
return ShellBash
|
||||
}
|
||||
|
||||
func TestInstaller(t *testing.T) {
|
||||
// Test with bash
|
||||
installer := NewInstaller(ShellBash)
|
||||
if installer.prefixDir == "" {
|
||||
t.Error("Installer should have a prefix directory")
|
||||
}
|
||||
|
||||
// Test filename generation
|
||||
t.Run("filename", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
shell Shell
|
||||
expected string
|
||||
}{
|
||||
{ShellBash, "_kportal"},
|
||||
{ShellZsh, "_kportal"},
|
||||
{ShellFish, "kportal.fish"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
inst := NewInstaller(tt.shell)
|
||||
got := inst.getCompletionFilename()
|
||||
if got != tt.expected {
|
||||
t.Errorf("getCompletionFilename() for %v = %v, want %v", tt.shell, got, tt.expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstallCompletion(t *testing.T) {
|
||||
// Temp directory (auto-removed by the test framework)
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Test installation to temp directory
|
||||
installer := &Installer{
|
||||
shell: ShellBash,
|
||||
prefixDir: tempDir,
|
||||
}
|
||||
|
||||
// Should fail because file doesn't exist (doesn't matter, just test the method exists)
|
||||
_ = installer.Install()
|
||||
}
|
||||
|
||||
func TestPrint(t *testing.T) {
|
||||
// Test that Print doesn't crash and outputs something
|
||||
orig := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
err := Print(ShellBash)
|
||||
|
||||
_ = w.Close()
|
||||
os.Stdout = orig
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Print(ShellBash) failed: %v", err)
|
||||
}
|
||||
|
||||
// Read output
|
||||
buf := make([]byte, 1024)
|
||||
n, _ := r.Read(buf)
|
||||
output := string(buf[:n])
|
||||
|
||||
if !strings.Contains(output, "_kportal") {
|
||||
t.Error("Print output should contain _kportal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCompletionFilename(t *testing.T) {
|
||||
tests := []struct {
|
||||
shell Shell
|
||||
expected string
|
||||
}{
|
||||
{ShellBash, "_kportal"},
|
||||
{ShellZsh, "_kportal"},
|
||||
{ShellFish, "kportal.fish"},
|
||||
{Shell("unknown"), "_kportal"}, // Falls back
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
inst := &Installer{shell: tt.shell}
|
||||
got := inst.getCompletionFilename()
|
||||
if got != tt.expected {
|
||||
t.Errorf("getCompletionFilename() for %v = %v, want %v", tt.shell, got, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBashCompletionIncludesContextCompletion(t *testing.T) {
|
||||
script, err := Generate(ShellBash)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate(ShellBash) failed: %v", err)
|
||||
}
|
||||
|
||||
// Should include kubectl context completion
|
||||
if !strings.Contains(script, "kubectl config get-contexts") {
|
||||
t.Error("Bash completion should include kubectl context completion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZshCompletionIncludesContextCompletion(t *testing.T) {
|
||||
script, err := Generate(ShellZsh)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate(ShellZsh) failed: %v", err)
|
||||
}
|
||||
|
||||
// Should include kubectl context completion
|
||||
if !strings.Contains(script, "kubectl config get-contexts") {
|
||||
t.Error("Zsh completion should include kubectl context completion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFishCompletionIncludesContextCompletion(t *testing.T) {
|
||||
script, err := Generate(ShellFish)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate(ShellFish) failed: %v", err)
|
||||
}
|
||||
|
||||
// Should include kubectl context completion
|
||||
if !strings.Contains(script, "kubectl config get-contexts") {
|
||||
t.Error("Fish completion should include kubectl context completion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllFlagsPresent(t *testing.T) {
|
||||
script, err := Generate(ShellBash)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate(ShellBash) failed: %v", err)
|
||||
}
|
||||
|
||||
// Check all main flags are present. The main command exposes single-dash
|
||||
// -c/-v (no --config/--verbose long forms); word flags use --.
|
||||
flags := []string{
|
||||
"-c",
|
||||
"-v",
|
||||
"--version",
|
||||
"--update",
|
||||
"--check",
|
||||
"--headless",
|
||||
"--log-format",
|
||||
"--convert",
|
||||
"--convert-output",
|
||||
}
|
||||
|
||||
for _, flag := range flags {
|
||||
if !strings.Contains(script, flag) {
|
||||
t.Errorf("Bash completion missing flag: %s", flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubcommandsPresent(t *testing.T) {
|
||||
for _, shell := range []Shell{ShellBash, ShellZsh, ShellFish} {
|
||||
script, err := Generate(shell)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate(%v) failed: %v", shell, err)
|
||||
}
|
||||
|
||||
for _, sub := range []string{"generate", "completion"} {
|
||||
if !strings.Contains(script, sub) {
|
||||
t.Errorf("%s completion missing %q subcommand", shell, sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateFlagsPresent(t *testing.T) {
|
||||
script, err := Generate(ShellBash)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate(ShellBash) failed: %v", err)
|
||||
}
|
||||
|
||||
generateFlags := []string{
|
||||
"--context",
|
||||
"--config",
|
||||
"--dry-run",
|
||||
}
|
||||
|
||||
for _, flag := range generateFlags {
|
||||
if !strings.Contains(script, flag) {
|
||||
t.Errorf("Bash completion missing generate flag: %s", flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompletionScriptPermissions(t *testing.T) {
|
||||
// Create temp file and verify permissions handling
|
||||
tempDir := t.TempDir()
|
||||
|
||||
installer := &Installer{
|
||||
shell: ShellBash,
|
||||
prefixDir: tempDir,
|
||||
}
|
||||
|
||||
filename := filepath.Join(tempDir, installer.getCompletionFilename())
|
||||
|
||||
// Write a test file
|
||||
err := os.WriteFile(filename, []byte("test"), 0o600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists and is readable
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read test file: %v", err)
|
||||
}
|
||||
|
||||
if string(data) != "test" {
|
||||
t.Error("File content mismatch")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user