From 2edb7418e11e4d0b2b45d42c5f334096a929fda6 Mon Sep 17 00:00:00 2001 From: Arden Shackelford Date: Tue, 16 Jul 2024 16:14:24 -0500 Subject: [PATCH] Adding more tests for commands; Setting up to properly show version on --version --- .../unreleased/Added-20240716-161044.yaml | 5 + .../unreleased/Added-20240716-161123.yaml | 5 + .github/workflows/release.yml | 4 +- .goreleaser.yaml | 4 +- build/version.go | 6 + cmd/activate.go | 21 ++- cmd/activate_test.go | 94 ++++++++++ cmd/cache.go | 4 +- cmd/cache_test.go | 1 + cmd/list.go | 3 +- cmd/list_test.go | 167 ++++++++++++++++++ cmd/root.go | 73 +++++--- cmd/unset.go | 8 +- cmd/use_test.go | 100 +++++++++++ cmd/version.go | 13 ++ config/config_test.go | 3 +- models/consul.go | 6 +- models/nomad.go | 6 +- 18 files changed, 473 insertions(+), 50 deletions(-) create mode 100644 .changes/unreleased/Added-20240716-161044.yaml create mode 100644 .changes/unreleased/Added-20240716-161123.yaml create mode 100644 build/version.go create mode 100644 cmd/activate_test.go create mode 100644 cmd/cache_test.go create mode 100644 cmd/list_test.go create mode 100644 cmd/use_test.go create mode 100644 cmd/version.go diff --git a/.changes/unreleased/Added-20240716-161044.yaml b/.changes/unreleased/Added-20240716-161044.yaml new file mode 100644 index 0000000..47dc6de --- /dev/null +++ b/.changes/unreleased/Added-20240716-161044.yaml @@ -0,0 +1,5 @@ +kind: Added +body: Ensuring we include the version during builds +time: 2024-07-16T16:10:44.764349-05:00 +custom: + Author: Shackelford-Arden diff --git a/.changes/unreleased/Added-20240716-161123.yaml b/.changes/unreleased/Added-20240716-161123.yaml new file mode 100644 index 0000000..de45646 --- /dev/null +++ b/.changes/unreleased/Added-20240716-161123.yaml @@ -0,0 +1,5 @@ +kind: Added +body: Adding more testing for commands +time: 2024-07-16T16:11:23.031283-05:00 +custom: + Author: Shackelford-Arden diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ad8060..602ede8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,12 +18,12 @@ jobs: with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: stable # It all depends on your needs. - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: # either 'goreleaser' (default) or 'goreleaser-pro' distribution: goreleaser diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 64a0e90..a76c897 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -6,7 +6,7 @@ # yaml-language-server: $schema=https://goreleaser.com/static/schema.json # vim: set ts=2 sw=2 tw=0 fo=cnqoj -version: 1 +version: 2 before: hooks: @@ -20,6 +20,8 @@ builds: - linux - windows - darwin + ldflags: + - -s -w -X version.Version={{.Version}} -X version.Commit={{.Commit}} -X version.Date={{.Date}} -X version.BuiltWith=goreleaser archives: - format: tar.gz diff --git a/build/version.go b/build/version.go new file mode 100644 index 0000000..c84ce98 --- /dev/null +++ b/build/version.go @@ -0,0 +1,6 @@ +package build + +var Version = "0.0.0" +var Commit = "" +var Date = "" +var BuiltWith = "" diff --git a/cmd/activate.go b/cmd/activate.go index cfce7f7..dbaaa2d 100644 --- a/cmd/activate.go +++ b/cmd/activate.go @@ -2,8 +2,9 @@ package cmd import ( "fmt" - "github.com/urfave/cli/v2" "os" + + "github.com/urfave/cli/v2" ) // Activate provides the given shell with the commands to set environment variables @@ -11,11 +12,18 @@ import ( func Activate(ctx *cli.Context) error { execPath, _ := os.Executable() + activateScript := "" + + shell := ctx.String("shell") + if shell == "" { + shell = AppConfig.Shell + } - // This bash/zsh script is pretty much the same as what mise has - // Reference: https://github.com/jdx/mise/blob/be34b768d9c09feda3c59d9a949a40609c294dcf/src/shell/zsh.rs#L17 - activateScript := fmt.Sprintf( - ` + switch shell { + default: + // This bash/zsh script is pretty much the same as what mise has + // Reference: https://github.com/jdx/mise/blob/be34b768d9c09feda3c59d9a949a40609c294dcf/src/shell/zsh.rs#L17 + activateScript = fmt.Sprintf(` hctx () { local command HCTX_PATH='%s' @@ -36,7 +44,8 @@ hctx () { command $HCTX_PATH "$command" "$@" } `, execPath, - ) + ) + } fmt.Print(activateScript) diff --git a/cmd/activate_test.go b/cmd/activate_test.go new file mode 100644 index 0000000..81f7c75 --- /dev/null +++ b/cmd/activate_test.go @@ -0,0 +1,94 @@ +package cmd + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "testing" +) + +func TestActivateOutput(t *testing.T) { + app, _ := App() + + t.Run("TestBashOutput", func(t *testing.T) { + + execPath, _ := os.Executable() + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Could not create pipe: %v", err) + } + + // Save the original stdout and stderr + oldStdout := os.Stdout + oldStderr := os.Stderr + + // Set the pipe's writer as stdout and stderr + os.Stdout = w + os.Stderr = w + + // Create a channel to signal when we're done reading the output + done := make(chan bool) + var output string + + // Start a goroutine to read from the pipe + // This allows us to capture the output of the CLI + // being called in the next section. + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + output = buf.String() + done <- true + }() + + args := []string{"hctx", "--shell", "bash", "activate"} + err = app.Run(args) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // Restore the original stdout and stderr + os.Stdout = oldStdout + os.Stderr = oldStderr + + // Close the writer side of the pipe + w.Close() + + // Wait for the reading goroutine to finish + <-done + + // Check for command execution error + if err != nil { + t.Errorf("Command execution failed: %v", err) + } + + expectedContent := strings.TrimSpace(fmt.Sprintf(` +hctx () { + local command + HCTX_PATH='%s' + command="${1:-}" + if [ "$#" = 0 ] + then + command $HCTX_PATH + return + fi + shift + case "$command" in + (use|u|unset|un) if [[ ! " $@ " =~ " --help " ]] && [[ ! " $@ " =~ " -h " ]] + then + eval "$(command $HCTX_PATH "$command" "$@")" + return $? + fi ;; + esac + command $HCTX_PATH "$command" "$@" +} +`, execPath, + )) + + if !strings.Contains(output, expectedContent) { + t.Errorf("Expected output to contain '%s', but got: %s", expectedContent, output) + } + }, + ) +} diff --git a/cmd/cache.go b/cmd/cache.go index e4e2387..f7bf392 100644 --- a/cmd/cache.go +++ b/cmd/cache.go @@ -77,8 +77,8 @@ func ClearCache(ctx *cli.Context) error { // ShowCache shows the content of the cache file. func ShowCache(ctx *cli.Context) error { - cache, _ := AppCache.Get() - fmtCache, fmtErr := json.MarshalIndent(cache, "", " ") + currentCache, _ := AppCache.Get() + fmtCache, fmtErr := json.MarshalIndent(currentCache, "", " ") if fmtErr != nil { return fmt.Errorf("failed to read/format the cache: %s", fmtErr.Error()) } diff --git a/cmd/cache_test.go b/cmd/cache_test.go new file mode 100644 index 0000000..1d619dd --- /dev/null +++ b/cmd/cache_test.go @@ -0,0 +1 @@ +package cmd diff --git a/cmd/list.go b/cmd/list.go index 7ad4a3f..2c38aa7 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -2,9 +2,10 @@ package cmd import ( "fmt" - "github.com/Shackelford-Arden/hctx/types" "os" + "github.com/Shackelford-Arden/hctx/types" + "github.com/urfave/cli/v2" ) diff --git a/cmd/list_test.go b/cmd/list_test.go new file mode 100644 index 0000000..ae39b13 --- /dev/null +++ b/cmd/list_test.go @@ -0,0 +1,167 @@ +package cmd + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "testing" + + "github.com/Shackelford-Arden/hctx/types" +) + +func TestListOutput(t *testing.T) { + app, _ := App() + + testTmpDir := t.TempDir() + + t.Run("TestIndicatorIncluded", func(t *testing.T) { + + tmpConfig, _ := os.CreateTemp(testTmpDir, "indicator-*.hcl") + defer tmpConfig.Close() + + tmpConfig.WriteString(` +stack "test-01" { + nomad { + address = "http://localhost:4646" + } +} +`) + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Could not create pipe: %v", err) + } + + // Save the original stdout and stderr + oldStdout := os.Stdout + oldStderr := os.Stderr + + // Set the pipe's writer as stdout and stderr + os.Stdout = w + os.Stderr = w + + // Create a channel to signal when we're done reading the output + done := make(chan bool) + var output string + + // Start a goroutine to read from the pipe + // This allows us to capture the output of the CLI + // being called in the next section. + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + output = buf.String() + done <- true + }() + + // Set the environment variable to the stack name + t.Setenv(types.StackNameEnv, "test-01") + + args := []string{"hctx", "--config", tmpConfig.Name(), "list"} + err = app.Run(args) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // Restore the original stdout and stderr + os.Stdout = oldStdout + os.Stderr = oldStderr + + // Close the writer side of the pipe + w.Close() + + // Wait for the reading goroutine to finish + <-done + + // Check for command execution error + if err != nil { + t.Errorf("Command execution failed: %v", err) + } + + expectedContent := strings.TrimSpace(fmt.Sprintf(` +Stacks: + test-01 * +`, + )) + + if !strings.Contains(output, expectedContent) { + t.Errorf("Expected output to contain '%s', but got: %s", expectedContent, output) + } + }, + ) + + t.Run("TestNoStackSelected", func(t *testing.T) { + + tmpConfig, _ := os.CreateTemp(testTmpDir, "indicator-*.hcl") + defer tmpConfig.Close() + + tmpConfig.WriteString(` +stack "test-01" { + nomad { + address = "http://localhost:4646" + } +} +`) + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Could not create pipe: %v", err) + } + + // Save the original stdout and stderr + oldStdout := os.Stdout + oldStderr := os.Stderr + + // Set the pipe's writer as stdout and stderr + os.Stdout = w + os.Stderr = w + + // Create a channel to signal when we're done reading the output + done := make(chan bool) + var output string + + // Start a goroutine to read from the pipe + // This allows us to capture the output of the CLI + // being called in the next section. + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + output = buf.String() + done <- true + }() + + args := []string{"hctx", "--config", tmpConfig.Name(), "list"} + err = app.Run(args) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // Restore the original stdout and stderr + os.Stdout = oldStdout + os.Stderr = oldStderr + + // Close the writer side of the pipe + w.Close() + + // Wait for the reading goroutine to finish + <-done + + // Check for command execution error + if err != nil { + t.Errorf("Command execution failed: %v", err) + } + + expectedContent := strings.TrimSpace(fmt.Sprintf(` +Stacks: + test-01 +`, + )) + + if !strings.Contains(output, expectedContent) { + t.Errorf("Expected output to contain '%s', but got: %s", expectedContent, output) + } + }, + ) +} diff --git a/cmd/root.go b/cmd/root.go index 4e3a831..9a8c4c4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "log/slog" "os" + "github.com/Shackelford-Arden/hctx/build" "github.com/Shackelford-Arden/hctx/cache" "github.com/Shackelford-Arden/hctx/config" "github.com/urfave/cli/v2" @@ -15,43 +16,49 @@ var AppCache *cache.Cache func ValidateConfig(ctx *cli.Context) error { - userHome, homeErr := os.UserHomeDir() - if homeErr != nil { - fmt.Printf("failed to get user homedir: %s", homeErr) - os.Exit(10) - } + userConfig := ctx.String("config") + if userConfig == "" { - configPath := fmt.Sprintf("%s/%s/%s", userHome, config.ConfigParentDir, config.ConfigDir) - configFilePath := fmt.Sprintf("%s/%s/%s/%s", userHome, config.ConfigParentDir, config.ConfigDir, config.ConfigFileName) - configOldPath := fmt.Sprintf("%s/%s/%s", userHome, config.ConfigParentDir, config.OldConfigFileName) + userHome, homeErr := os.UserHomeDir() + if homeErr != nil { + fmt.Printf("failed to get user homedir: %s", homeErr) + os.Exit(10) + } - _, err := os.Stat(configPath) - if os.IsNotExist(err) { - // Create the directory - err := os.Mkdir(configPath, 0744) - if err != nil { - return fmt.Errorf("failed to create %s: %s", configPath, err) + configPath := fmt.Sprintf("%s/%s/%s", userHome, config.ConfigParentDir, config.ConfigDir) + configFilePath := fmt.Sprintf("%s/%s/%s/%s", userHome, config.ConfigParentDir, config.ConfigDir, config.ConfigFileName) + configOldPath := fmt.Sprintf("%s/%s/%s", userHome, config.ConfigParentDir, config.OldConfigFileName) + + _, err := os.Stat(configPath) + if os.IsNotExist(err) { + // Create the directory + err := os.Mkdir(configPath, 0744) + if err != nil { + return fmt.Errorf("failed to create %s: %s", configPath, err) + } } - } - oldConfig, _ := os.Stat(configOldPath) - newConfig, newConfigStatErr := os.Stat(configFilePath) + oldConfig, _ := os.Stat(configOldPath) + newConfig, newConfigStatErr := os.Stat(configFilePath) - if oldConfig != nil && newConfig != nil { - slog.InfoContext(ctx.Context, fmt.Sprintf("both %s and %s exist. Only using %s, please merge the config files then remove %s", configPath, configOldPath, configPath, configOldPath)) - } + if oldConfig != nil && newConfig != nil { + slog.InfoContext(ctx.Context, fmt.Sprintf("both %s and %s exist. Only using %s, please merge the config files then remove %s", configPath, configOldPath, configPath, configOldPath)) + } - if oldConfig != nil && os.IsNotExist(newConfigStatErr) { + if oldConfig != nil && os.IsNotExist(newConfigStatErr) { - // Copy old config to new config path - copyErr := os.Rename(configOldPath, configFilePath) - if copyErr != nil { - return fmt.Errorf("failed to copy %s to %s: %s", configOldPath, configFilePath, copyErr) + // Copy old config to new config path + copyErr := os.Rename(configOldPath, configFilePath) + if copyErr != nil { + return fmt.Errorf("failed to copy %s to %s: %s", configOldPath, configFilePath, copyErr) + } } + + userConfig = configFilePath } // Parse config - cfg, cfgErr := config.NewConfig("") + cfg, cfgErr := config.NewConfig(userConfig) if cfgErr != nil { return cfgErr } @@ -72,8 +79,10 @@ func App() (*cli.App, error) { app := &cli.App{ Name: "Hashi Context", + Usage: "Managing your Hashi contexts with style!", HelpName: "hctx", Description: "A CLI tool to help you manage your CLI life interacting with some of HashiCorp's products.", + Version: fmt.Sprintf("%s - %s - built with %s on %s", build.Version, build.Commit, build.BuiltWith, build.Date), Authors: []*cli.Author{ { Name: "Arden Shackelford", @@ -81,6 +90,18 @@ func App() (*cli.App, error) { }, }, Before: ValidateConfig, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "Path to config file to use.", + Hidden: true, + }, + &cli.StringFlag{ + Name: "shell", + Hidden: true, + }, + }, Commands: []*cli.Command{ { Name: "list", diff --git a/cmd/unset.go b/cmd/unset.go index 2b74cf1..71ee909 100644 --- a/cmd/unset.go +++ b/cmd/unset.go @@ -11,18 +11,24 @@ import ( func Unset(ctx *cli.Context) error { currentStack := AppConfig.GetCurrentStack() + configPath := ctx.String("config") if currentStack == nil { return nil } // Get current stacks tokens, if any and cache them + toCache := cache.GetCacheableValues() if AppConfig.CacheAuth { - toCache := cache.GetCacheableValues() updateErr := AppCache.Update(currentStack.Name, toCache) if updateErr != nil { return fmt.Errorf("could not update cache for stack %s: %v", currentStack.Name, updateErr) } + + saveErr := AppCache.Save(configPath) + if saveErr != nil { + return fmt.Errorf("could not save cache for stack %s: %v", currentStack.Name, saveErr) + } } fmt.Println(currentStack.Unset(AppConfig.Shell)) diff --git a/cmd/use_test.go b/cmd/use_test.go new file mode 100644 index 0000000..860fd91 --- /dev/null +++ b/cmd/use_test.go @@ -0,0 +1,100 @@ +package cmd + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "testing" +) + +func TestUse(t *testing.T) { + app, _ := App() + + testTmpDir := t.TempDir() + + t.Run("TestErrOnMissingStack", func(t *testing.T) { + + tmpConfig, _ := os.CreateTemp(testTmpDir, "missing-stack-*.hcl") + defer tmpConfig.Close() + + args := []string{"hctx", "--config", tmpConfig.Name(), "use", "test-01"} + err := app.Run(args) + if err.Error() != "no stack named test-01 in config" { + t.Errorf("Error received was not expected %v", err) + } + }) + + t.Run("TestValidStackOutput", func(t *testing.T) { + + tmpConfig, _ := os.CreateTemp(testTmpDir, "missing-stack-*.hcl") + defer tmpConfig.Close() + + tmpConfig.WriteString(` +stack "test-01" { + nomad { + address = "http://localhost:4646" + } +} +`) + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Could not create pipe: %v", err) + } + + // Save the original stdout and stderr + oldStdout := os.Stdout + oldStderr := os.Stderr + + // Set the pipe's writer as stdout and stderr + os.Stdout = w + os.Stderr = w + + // Create a channel to signal when we're done reading the output + done := make(chan bool) + var output string + + // Start a goroutine to read from the pipe + // This allows us to capture the output of the CLI + // being called in the next section. + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + output = buf.String() + done <- true + }() + + args := []string{"hctx", "--shell", "bash", "--config", tmpConfig.Name(), "use", "test-01"} + err = app.Run(args) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // Restore the original stdout and stderr + os.Stdout = oldStdout + os.Stderr = oldStderr + + // Close the writer side of the pipe + w.Close() + + // Wait for the reading goroutine to finish + <-done + + // Check for command execution error + if err != nil { + t.Errorf("Command execution failed: %v", err) + } + + expectedContent := strings.TrimSpace(fmt.Sprintf(` +export NOMAD_ADDR=http://localhost:4646 +`, + )) + + if !strings.Contains(output, expectedContent) { + t.Errorf("Expected output to contain '%s', but got: %s", expectedContent, output) + } + }, + ) +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..bd90b2f --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,13 @@ +package cmd + +import ( + "fmt" + + "github.com/Shackelford-Arden/hctx/build" + "github.com/urfave/cli/v2" +) + +func ShowVersion(ctx *cli.Context) error { + fmt.Println(fmt.Sprintf("%s %s - Commit %s & built with %s on %s", ctx.App.Name, ctx.App.Version, build.Commit, build.BuiltWith, build.Date)) + return nil +} diff --git a/config/config_test.go b/config/config_test.go index dc9940e..4c7a510 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -88,8 +88,9 @@ func TestNewConfigUnsetNomad(t *testing.T) { expectedOutput := fmt.Sprintf(` unset %s unset %s +unset %s unset %s`, - types.StackNameEnv, models.NomadAddr, models.NomadNamespace) + types.StackNameEnv, models.NomadAddr, models.NomadNamespace, models.NomadToken) if unsetOut != expectedOutput { t.Fatalf("\nExpected: %s\nActual: %s", expectedOutput, unsetOut) diff --git a/models/consul.go b/models/consul.go index 879020c..5bd0c39 100644 --- a/models/consul.go +++ b/models/consul.go @@ -1,7 +1,5 @@ package models -import "os" - type ConsulConfig struct { Address string `hcl:"address,optional"` Namespace string `hcl:"namespace,optional"` @@ -43,9 +41,7 @@ func (n *ConsulConfig) Unset(shell string) []string { unsetCommands = append(unsetCommands, genUnsetCommands(shell, ConsulNamespace)) } - if os.Getenv(ConsulToken) != "" { - unsetCommands = append(unsetCommands, genUnsetCommands(shell, ConsulToken)) - } + unsetCommands = append(unsetCommands, genUnsetCommands(shell, ConsulToken)) return unsetCommands } diff --git a/models/nomad.go b/models/nomad.go index 84253d9..2dca49b 100644 --- a/models/nomad.go +++ b/models/nomad.go @@ -1,7 +1,5 @@ package models -import "os" - type NomadConfig struct { Address string `hcl:"address,optional"` Namespace string `hcl:"namespace,optional"` @@ -43,9 +41,7 @@ func (n *NomadConfig) Unset(shell string) []string { unsetCommands = append(unsetCommands, genUnsetCommands(shell, NomadNamespace)) } - if os.Getenv(NomadToken) != "" { - unsetCommands = append(unsetCommands, genUnsetCommands(shell, NomadToken)) - } + unsetCommands = append(unsetCommands, genUnsetCommands(shell, NomadToken)) return unsetCommands }