From 7c5dbdb1d03ea9fe3738dc02bf7ec47d03148533 Mon Sep 17 00:00:00 2001 From: pjoyce Date: Wed, 22 Apr 2026 16:22:22 -0400 Subject: [PATCH 1/2] add tests --- .github/workflows/test.yml | 26 ++ .gitignore | 1 + Makefile | 8 + go.mod | 5 +- go.sum | 12 +- src/cmd/list.go | 5 +- src/cmd/list_test.go | 71 ++++++ src/cmd/root.go | 27 +- src/cmd/root_test.go | 129 ++++++++++ src/utils/aws.go | 10 +- src/utils/aws_test.go | 136 ++++++++++ src/utils/common.go | 35 ++- src/utils/common_test.go | 235 ++++++++++++++++++ src/utils/testutils/testutils.go | 66 +++++ testdata/aws_config_examples/basic_config | 14 ++ testdata/aws_config_examples/complex_config | 34 +++ testdata/aws_config_examples/malformed_config | 14 ++ 17 files changed, 792 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 src/cmd/list_test.go create mode 100644 src/cmd/root_test.go create mode 100644 src/utils/aws_test.go create mode 100644 src/utils/common_test.go create mode 100644 src/utils/testutils/testutils.go create mode 100644 testdata/aws_config_examples/basic_config create mode 100644 testdata/aws_config_examples/complex_config create mode 100644 testdata/aws_config_examples/malformed_config diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d76511e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version: '1.26' + check-latest: true + cache: true + cache-dependency-path: | + **/go.sum + **/go.mod + + - name: Run tests with coverage + run: | + go test ./... -coverprofile=coverage.out + go tool cover -func=coverage.out diff --git a/.gitignore b/.gitignore index 57c413f..89675ff 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +coverage.* # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/Makefile b/Makefile index 131c217..0f9a58d 100644 --- a/Makefile +++ b/Makefile @@ -23,3 +23,11 @@ uninstall: ## Uninstall Target rm -f ${BINDIR}/_awsd rm -f ${BINDIR}/_awsd_autocomplete rm -f ${BINDIR}/_awsd_prompt + +.PHONY: test test-coverage +test: ## Run tests + go test ./... + +test-coverage: ## Run tests with coverage report + go test ./... -coverprofile=coverage.out + go tool cover -func=coverage.out diff --git a/go.mod b/go.mod index 55eab3c..21b40ff 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,16 @@ go 1.23.5 require ( github.com/radiusmethod/promptui v0.10.3 github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 gopkg.in/ini.v1 v1.67.1 ) require ( github.com/chzyer/readline v1.5.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.9 // indirect - github.com/stretchr/testify v1.11.1 // indirect golang.org/x/sys v0.12.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 67449af..22fdf73 100644 --- a/go.sum +++ b/go.sum @@ -15,14 +15,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/radiusmethod/promptui v0.10.3 h1:JeayJuCR/bPvZp5cGqd+sAk4NDZG9CCt0vBVTFIiCo8= github.com/radiusmethod/promptui v0.10.3/go.mod h1:DYozY3lsgSlf+M+LXX4QF7/QY246KqP69zhdIvPBg+8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -32,16 +26,14 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/cmd/list.go b/src/cmd/list.go index f39063f..137d151 100644 --- a/src/cmd/list.go +++ b/src/cmd/list.go @@ -26,7 +26,10 @@ func init() { } func runProfileLister() error { - profiles := utils.GetProfiles() + profiles, err := utils.GetProfiles() + if err != nil { + return err + } for _, p := range profiles { fmt.Println(p) } diff --git a/src/cmd/list_test.go b/src/cmd/list_test.go new file mode 100644 index 0000000..cf1c67e --- /dev/null +++ b/src/cmd/list_test.go @@ -0,0 +1,71 @@ +package cmd + +import ( + "testing" + + "github.com/radiusmethod/awsd/src/utils/testutils" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestListCommand(t *testing.T) { + cmd := listCmd + assert.NotNil(t, cmd) + assert.Equal(t, "list", cmd.Use) + assert.Equal(t, "List AWS profiles command.", cmd.Short) + assert.Equal(t, "This lists all your AWS profiles.", cmd.Long) + assert.Equal(t, []string{"l"}, cmd.Aliases) +} + +func TestRunProfileLister(t *testing.T) { + tempDir := testutils.CreateTempDir(t) + defer testutils.CleanupTempDir(t, tempDir) + + configPath := testutils.CreateMockAWSConfig(t, tempDir) + testutils.SetTestEnv(t, "AWS_CONFIG_FILE", configPath) + defer testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") + + err := runProfileLister() + assert.NoError(t, err) +} + +func TestListCommandIntegration(t *testing.T) { + tempDir := testutils.CreateTempDir(t) + defer testutils.CleanupTempDir(t, tempDir) + + configPath := testutils.CreateMockAWSConfig(t, tempDir) + testutils.SetTestEnv(t, "AWS_CONFIG_FILE", configPath) + defer testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") + + testutils.SetTestEnv(t, "HOME", tempDir) + defer testutils.UnsetTestEnv(t, "HOME") + + cmd := &cobra.Command{ + Use: "awsd", + Short: "awsd - switch between AWS profiles.", + Long: "Allows for switching AWS profiles files.", + } + + listCmd := &cobra.Command{ + Use: "list", + Short: "List AWS profiles command.", + Aliases: []string{"l"}, + Long: "This lists all your AWS profiles.", + Run: func(cmd *cobra.Command, args []string) { + err := runProfileLister() + if err != nil { + t.Fatal(err) + } + }, + } + cmd.AddCommand(listCmd) + + aliases := []string{"list", "l"} + for _, alias := range aliases { + t.Run(alias, func(t *testing.T) { + cmd.SetArgs([]string{alias}) + err := cmd.Execute() + assert.NoError(t, err) + }) + } +} diff --git a/src/cmd/root.go b/src/cmd/root.go index f67dc4d..9f58117 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -2,11 +2,12 @@ package cmd import ( "fmt" + "log" + "os" + "github.com/radiusmethod/awsd/src/utils" "github.com/radiusmethod/promptui" "github.com/spf13/cobra" - "log" - "os" ) var rootCmd = &cobra.Command{ @@ -39,7 +40,10 @@ func runRootCmd() { } func runProfileSwitcher() error { - profiles := utils.GetProfiles() + profiles, err := utils.GetProfiles() + if err != nil { + return err + } fmt.Printf(utils.NoticeColor, "AWS Profile Switcher\n") profile, err := getProfileFromPrompt(profiles) if err != nil { @@ -49,7 +53,11 @@ func runProfileSwitcher() error { fmt.Printf(utils.NoticeColor, "? ") fmt.Printf(utils.CyanColor, profile) fmt.Println() - return utils.WriteFile(profile, utils.GetHomeDir()) + homeDir, err := utils.GetHomeDir() + if err != nil { + return err + } + return utils.WriteFile(profile, homeDir) } func shouldRunDirectProfileSwitch() bool { @@ -58,12 +66,19 @@ func shouldRunDirectProfileSwitch() bool { } func directProfileSwitch(desiredProfile string) error { - profiles := utils.GetProfiles() + profiles, err := utils.GetProfiles() + if err != nil { + return err + } if utils.Contains(profiles, desiredProfile) { printColoredMessage("Profile ", utils.PromptColor) printColoredMessage(desiredProfile, utils.CyanColor) printColoredMessage(" set.\n", utils.PromptColor) - return utils.WriteFile(desiredProfile, utils.GetHomeDir()) + homeDir, err := utils.GetHomeDir() + if err != nil { + return err + } + return utils.WriteFile(desiredProfile, homeDir) } printColoredMessage("WARNING: Profile ", utils.NoticeColor) printColoredMessage(desiredProfile, utils.CyanColor) diff --git a/src/cmd/root_test.go b/src/cmd/root_test.go new file mode 100644 index 0000000..df4acd3 --- /dev/null +++ b/src/cmd/root_test.go @@ -0,0 +1,129 @@ +package cmd + +import ( + "os" + "path/filepath" + "testing" + + "github.com/radiusmethod/awsd/src/utils/testutils" + "github.com/stretchr/testify/assert" +) + +func TestShouldRunDirectProfileSwitch(t *testing.T) { + tests := []struct { + name string + args []string + expected bool + }{ + { + name: "Direct profile switch", + args: []string{"awsd", "dev"}, + expected: true, + }, + { + name: "List command", + args: []string{"awsd", "list"}, + expected: false, + }, + { + name: "Help command", + args: []string{"awsd", "--help"}, + expected: false, + }, + { + name: "Version command", + args: []string{"awsd", "version"}, + expected: false, + }, + { + name: "No arguments", + args: []string{"awsd"}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Args = tt.args + result := shouldRunDirectProfileSwitch() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestDirectProfileSwitch(t *testing.T) { + tempDir := testutils.CreateTempDir(t) + defer testutils.CleanupTempDir(t, tempDir) + + configPath := testutils.CreateMockAWSConfig(t, tempDir) + testutils.SetTestEnv(t, "AWS_CONFIG_FILE", configPath) + defer testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") + + testutils.SetTestEnv(t, "HOME", tempDir) + defer testutils.UnsetTestEnv(t, "HOME") + + tests := []struct { + name string + profile string + expectError bool + expectFile bool + expectContent string + }{ + { + name: "Valid profile", + profile: "dev", + expectError: false, + expectFile: true, + expectContent: "dev", + }, + { + name: "Invalid profile", + profile: "invalid", + expectError: false, + expectFile: false, + expectContent: "", + }, + { + name: "Default profile", + profile: "default", + expectError: false, + expectFile: true, + expectContent: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + awsdFile := filepath.Join(tempDir, ".awsd") + _ = os.Remove(awsdFile) + + err := directProfileSwitch(tt.profile) + if tt.expectError { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + if tt.expectFile { + content, err := os.ReadFile(awsdFile) + assert.NoError(t, err) + assert.Equal(t, tt.expectContent, string(content)) + } else { + _, err := os.Stat(awsdFile) + assert.True(t, os.IsNotExist(err), "File should not exist for invalid profile") + } + }) + } +} + +func TestRootCommand(t *testing.T) { + cmd := rootCmd + assert.NotNil(t, cmd) + assert.Equal(t, "awsd", cmd.Use) + assert.Equal(t, "awsd - switch between AWS profiles.", cmd.Short) + assert.Equal(t, "Allows for switching AWS profiles files.", cmd.Long) +} + +func TestPrintColoredMessage(t *testing.T) { + printColoredMessage("test", "test") +} diff --git a/src/utils/aws.go b/src/utils/aws.go index d8e7087..795be31 100644 --- a/src/utils/aws.go +++ b/src/utils/aws.go @@ -1,10 +1,10 @@ package utils import ( - "gopkg.in/ini.v1" - "log" "sort" "strings" + + "gopkg.in/ini.v1" ) const ( @@ -12,11 +12,11 @@ const ( defaultProfile = "default" ) -func GetProfiles() []string { +func GetProfiles() ([]string, error) { profileFileLocation := GetCurrentProfileFile() cfg, err := ini.Load(profileFileLocation) if err != nil { - log.Fatalf("Failed to load profiles: %v", err) + return nil, err } sections := cfg.SectionStrings() profiles := make([]string, 0, len(sections)+1) @@ -29,5 +29,5 @@ func GetProfiles() []string { } profiles = AppendIfNotExists(profiles, defaultProfile) sort.Strings(profiles) - return profiles + return profiles, nil } diff --git a/src/utils/aws_test.go b/src/utils/aws_test.go new file mode 100644 index 0000000..f118408 --- /dev/null +++ b/src/utils/aws_test.go @@ -0,0 +1,136 @@ +package utils + +import ( + "os" + "path/filepath" + "testing" + + "github.com/radiusmethod/awsd/src/utils/testutils" + "github.com/stretchr/testify/assert" +) + +func setupTestEnvironment(t *testing.T) (string, func()) { + origHome := os.Getenv("HOME") + origConfigFile := os.Getenv("AWS_CONFIG_FILE") + + tempDir := testutils.CreateTempDir(t) + + awsDir := filepath.Join(tempDir, ".aws") + err := os.MkdirAll(awsDir, 0755) + assert.NoError(t, err) + + os.Setenv("HOME", tempDir) + os.Setenv("AWS_CONFIG_FILE", filepath.Join(tempDir, "config")) + + cleanup := func() { + os.Setenv("HOME", origHome) + os.Setenv("AWS_CONFIG_FILE", origConfigFile) + testutils.CleanupTempDir(t, tempDir) + } + + return tempDir, cleanup +} + +func TestGetProfiles(t *testing.T) { + tempDir, cleanup := setupTestEnvironment(t) + defer cleanup() + + configPath := testutils.CreateMockAWSConfig(t, tempDir) + os.Setenv("AWS_CONFIG_FILE", configPath) + + profiles, err := GetProfiles() + assert.NoError(t, err, "Should not return error") + + expectedProfiles := []string{"default", "dev", "prod"} + assert.Equal(t, expectedProfiles, profiles, "Expected profiles should match") +} + +func TestGetProfilesWithComplexConfig(t *testing.T) { + tempDir, cleanup := setupTestEnvironment(t) + defer cleanup() + + complexConfigPath := filepath.Join("..", "..", "testdata", "aws_config_examples", "complex_config") + configContent, err := os.ReadFile(complexConfigPath) + if err != nil { + t.Fatalf("Failed to read complex config: %v", err) + } + + configPath := filepath.Join(tempDir, "config") + if err := os.WriteFile(configPath, configContent, 0600); err != nil { + t.Fatalf("Failed to write complex config: %v", err) + } + + profiles, err := GetProfiles() + assert.NoError(t, err, "Should not return error") + + expectedProfiles := []string{ + "default", + "dev", + "dev.admin", + "dev.readonly", + "prod", + "prod.admin", + "prod.readonly", + } + assert.Equal(t, expectedProfiles, profiles, "Expected profiles should match") +} + +func TestGetProfilesWithMalformedConfig(t *testing.T) { + tempDir, cleanup := setupTestEnvironment(t) + defer cleanup() + + configPath := filepath.Join(tempDir, "config") + malformedContent := `[default] +aws_access_key_id = test_access_key +aws_secret_access_key = test_secret_key +region = us-east-1 + +[profile dev +aws_access_key_id = dev_access_key +aws_secret_access_key = dev_secret_key +region = us-west-2` + + err := os.WriteFile(configPath, []byte(malformedContent), 0600) + assert.NoError(t, err) + + profiles, err := GetProfiles() + assert.Error(t, err, "Should return an error for malformed config") + assert.Nil(t, profiles, "Should return nil profiles for malformed config") + assert.Contains(t, err.Error(), "unclosed section", "Error should mention unclosed section") +} + +func TestGetProfilesWithError(t *testing.T) { + tempDir, cleanup := setupTestEnvironment(t) + defer cleanup() + + configPath := testutils.CreateMockAWSConfig(t, tempDir) + os.Setenv("AWS_CONFIG_FILE", configPath) + + profiles, err := GetProfiles() + assert.NoError(t, err, "Should not return error for valid config") + expectedProfiles := []string{"default", "dev", "prod"} + assert.Equal(t, expectedProfiles, profiles, "Expected profiles should match") + + malformedContent := `[default] +aws_access_key_id = test_access_key +aws_secret_access_key = test_secret_key +region = us-east-1 + +[profile dev +aws_access_key_id = dev_access_key +aws_secret_access_key = dev_secret_key +region = us-west-2` + + err = os.WriteFile(configPath, []byte(malformedContent), 0600) + assert.NoError(t, err) + + profiles, err = GetProfiles() + assert.Error(t, err, "Should return error for malformed config") + assert.Nil(t, profiles, "Should return nil profiles for malformed config") + assert.Contains(t, err.Error(), "unclosed section", "Error should mention unclosed section") + + os.Setenv("AWS_CONFIG_FILE", "/nonexistent/config") + profiles, err = GetProfiles() + assert.Error(t, err, "Should return error for non-existent config") + assert.Nil(t, profiles, "Should return nil profiles for non-existent config") +} diff --git a/src/utils/common.go b/src/utils/common.go index 21cc817..50f2c10 100644 --- a/src/utils/common.go +++ b/src/utils/common.go @@ -16,14 +16,18 @@ func TouchFile(name string) error { } func WriteFile(config, loc string) error { - if err := TouchFile(fmt.Sprintf("%s/.awsd", GetHomeDir())); err != nil { + homeDir, err := GetHomeDir() + if err != nil { + return err + } + if err := TouchFile(fmt.Sprintf("%s/.awsd", homeDir)); err != nil { return err } s := []byte("") if config != "default" { s = []byte(config) } - err := os.WriteFile(fmt.Sprintf("%s/.awsd", loc), s, 0644) + err = os.WriteFile(fmt.Sprintf("%s/.awsd", loc), s, 0644) if err != nil { log.Fatal(err) } @@ -49,25 +53,30 @@ func CheckError(err error) { } } -func GetHomeDir() string { - homeDir, err := os.UserHomeDir() - if err != nil { - log.Fatalf("Error getting user home directory: %v\n", err) +func GetHomeDir() (string, error) { + if homeDir := os.Getenv("HOME"); homeDir != "" { + return homeDir, nil + } + if homeDir, err := os.UserHomeDir(); err == nil { + return homeDir, nil } - return homeDir + return "", fmt.Errorf("error getting user home directory: $HOME is not defined and os.UserHomeDir() failed") } func GetProfileFileLocation() string { - configFileLocation := filepath.Join(GetHomeDir(), ".aws") - if IsDirectoryExists(configFileLocation) { - return filepath.Join(configFileLocation) + homeDir, err := GetHomeDir() + if err != nil { + log.Fatal(err) } - log.Fatalf("~/.aws directory does not exist!") - return "" + return filepath.Join(homeDir, ".aws") } func GetCurrentProfileFile() string { - return GetEnv("AWS_CONFIG_FILE", filepath.Join(GetHomeDir(), ".aws/config")) + homeDir, err := GetHomeDir() + if err != nil { + log.Fatal(err) + } + return GetEnv("AWS_CONFIG_FILE", filepath.Join(homeDir, ".aws/config")) } func IsDirectoryExists(path string) bool { diff --git a/src/utils/common_test.go b/src/utils/common_test.go new file mode 100644 index 0000000..d3f47ee --- /dev/null +++ b/src/utils/common_test.go @@ -0,0 +1,235 @@ +package utils + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/radiusmethod/awsd/src/utils/testutils" + "github.com/stretchr/testify/assert" +) + +func TestTouchFile(t *testing.T) { + tempDir := testutils.CreateTempDir(t) + defer testutils.CleanupTempDir(t, tempDir) + + filePath := filepath.Join(tempDir, "test.txt") + err := TouchFile(filePath) + assert.NoError(t, err, "Should create file without error") + + _, err = os.Stat(filePath) + assert.NoError(t, err, "File should exist") +} + +func TestWriteFile(t *testing.T) { + tempDir := testutils.CreateTempDir(t) + defer testutils.CleanupTempDir(t, tempDir) + + origHome := os.Getenv("HOME") + defer func() { + if origHome != "" { + os.Setenv("HOME", origHome) + } else { + os.Unsetenv("HOME") + } + }() + testutils.SetTestEnv(t, "HOME", tempDir) + + err := WriteFile("test-profile", tempDir) + assert.NoError(t, err, "Should write file without error") + + filePath := filepath.Join(tempDir, ".awsd") + content, err := os.ReadFile(filePath) + assert.NoError(t, err, "Should read file without error") + assert.Equal(t, "test-profile", string(content), "File content should match") + + err = WriteFile("default", tempDir) + assert.NoError(t, err, "Should write default profile without error") + + content, err = os.ReadFile(filePath) + assert.NoError(t, err, "Should read file without error") + assert.Equal(t, "", string(content), "Default profile should write empty string") +} + +func TestGetEnv(t *testing.T) { + testutils.SetTestEnv(t, "TEST_VAR", "test-value") + defer testutils.UnsetTestEnv(t, "TEST_VAR") + + value := GetEnv("TEST_VAR", "fallback") + assert.Equal(t, "test-value", value, "Should return environment variable value") + + value = GetEnv("NONEXISTENT_VAR", "fallback") + assert.Equal(t, "fallback", value, "Should return fallback value") +} + +func TestGetHomeDir(t *testing.T) { + origHome := os.Getenv("HOME") + defer func() { + if origHome != "" { + os.Setenv("HOME", origHome) + } else { + os.Unsetenv("HOME") + } + }() + + testutils.SetTestEnv(t, "HOME", "/test/home") + homeDir, err := GetHomeDir() + assert.NoError(t, err, "Should not return error when HOME is set") + assert.Equal(t, "/test/home", homeDir, "Should return HOME environment variable value") + + testutils.UnsetTestEnv(t, "HOME") + homeDir, err = GetHomeDir() + if err != nil { + assert.Contains(t, err.Error(), "error getting user home directory") + } else { + assert.NotEmpty(t, homeDir, "Should return UserHomeDir value") + } +} + +func TestGetProfileFileLocation(t *testing.T) { + origHome := os.Getenv("HOME") + defer func() { + if origHome != "" { + os.Setenv("HOME", origHome) + } else { + os.Unsetenv("HOME") + } + }() + + tempDir := testutils.CreateTempDir(t) + defer testutils.CleanupTempDir(t, tempDir) + + testutils.SetTestEnv(t, "HOME", tempDir) + expectedPath := filepath.Join(tempDir, ".aws") + actualPath := GetProfileFileLocation() + assert.Equal(t, expectedPath, actualPath, "Should return correct .aws directory path") +} + +func TestGetCurrentProfileFile(t *testing.T) { + testutils.SetTestEnv(t, "AWS_CONFIG_FILE", "/test/config") + defer testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") + + file := GetCurrentProfileFile() + assert.Equal(t, "/test/config", file, "Should return AWS_CONFIG_FILE value") + + testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") + file = GetCurrentProfileFile() + assert.Contains(t, file, ".aws/config", "Should return default config path") +} + +func TestIsDirectoryExists(t *testing.T) { + tempDir := testutils.CreateTempDir(t) + defer testutils.CleanupTempDir(t, tempDir) + + exists := IsDirectoryExists(tempDir) + assert.True(t, exists, "Should return true for existing directory") + + exists = IsDirectoryExists("/nonexistent/directory") + assert.False(t, exists, "Should return false for non-existing directory") +} + +func TestAppendIfNotExists(t *testing.T) { + slice := []string{"a", "b", "c"} + result := AppendIfNotExists(slice, "d") + assert.Equal(t, []string{"a", "b", "c", "d"}, result, "Should append new item") + + result = AppendIfNotExists(slice, "b") + assert.Equal(t, []string{"a", "b", "c"}, result, "Should not append existing item") + + var emptySlice []string + result = AppendIfNotExists(emptySlice, "a") + assert.Equal(t, []string{"a"}, result, "Should append to empty slice") +} + +func TestContains(t *testing.T) { + slice := []string{"a", "b", "c"} + + assert.True(t, Contains(slice, "b"), "Should find existing item") + + assert.False(t, Contains(slice, "d"), "Should not find non-existing item") + + var emptySlice []string + assert.False(t, Contains(emptySlice, "a"), "Should return false for empty slice") +} + +func TestCheckError(t *testing.T) { + if os.Getenv("TEST_CHECK_ERROR_DEL") == "1" { + CheckError(fmt.Errorf("^D")) + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestCheckError") + cmd.Env = append(os.Environ(), "TEST_CHECK_ERROR_DEL=1") + err := cmd.Run() + if e, ok := err.(*exec.ExitError); ok && !e.Success() { + return + } + t.Fatalf("Process ran with err %v, want exit status 1", err) +} + +func TestCheckErrorCtrlC(t *testing.T) { + if os.Getenv("TEST_CHECK_ERROR_CTRL_C") == "1" { + CheckError(fmt.Errorf("^C")) + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestCheckErrorCtrlC") + cmd.Env = append(os.Environ(), "TEST_CHECK_ERROR_CTRL_C=1") + err := cmd.Run() + if e, ok := err.(*exec.ExitError); ok && !e.Success() { + return + } + t.Fatalf("Process ran with err %v, want exit status 1", err) +} + +func TestCheckErrorOther(t *testing.T) { + if os.Getenv("TEST_CHECK_ERROR_OTHER") == "1" { + CheckError(fmt.Errorf("other error")) + return + } + cmd := exec.Command(os.Args[0], "-test.run=TestCheckErrorOther") + cmd.Env = append(os.Environ(), "TEST_CHECK_ERROR_OTHER=1") + err := cmd.Run() + if e, ok := err.(*exec.ExitError); ok && !e.Success() { + return + } + t.Fatalf("Process ran with err %v, want exit status 1", err) +} + +func TestBellSkipper(t *testing.T) { + bs := &BellSkipper{} + + n, err := bs.Write([]byte{7}) // ASCII bell + assert.NoError(t, err, "Write should not return error") + assert.Equal(t, 0, n, "Write should skip bell character") + + n, err = bs.Write([]byte("test")) + assert.NoError(t, err, "Write should not return error") + assert.Equal(t, 4, n, "Write should return correct number of bytes written") +} + +func TestNewPromptUISearcher(t *testing.T) { + items := []string{"test1", "test2", "another", "something"} + searcher := NewPromptUISearcher(items) + + result := searcher("test1", 0) + assert.True(t, result, "Should find exact match") + + result = searcher("test", 0) + assert.True(t, result, "Should find partial match") + + result = searcher("TEST1", 0) + assert.True(t, result, "Should find case insensitive match") + + result = searcher("nonexistent", 0) + assert.False(t, result, "Should not find non-existent item") + + result = searcher("", 0) + assert.True(t, result, "Should match on empty search") + + result = searcher("test", 1) + assert.True(t, result, "Should find match at different index") + + result = searcher("test", len(items)-1) + assert.False(t, result, "Should handle index at boundary") +} diff --git a/src/utils/testutils/testutils.go b/src/utils/testutils/testutils.go new file mode 100644 index 0000000..48469f2 --- /dev/null +++ b/src/utils/testutils/testutils.go @@ -0,0 +1,66 @@ +package testutils + +import ( + "os" + "path/filepath" + "testing" +) + +// CreateTempDir creates a temporary directory for testing and returns its path +func CreateTempDir(t *testing.T) string { + t.Helper() + dir, err := os.MkdirTemp("", "awsd-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + return dir +} + +// CreateMockAWSConfig creates a mock AWS credentials file with test profiles +func CreateMockAWSConfig(t *testing.T, dir string) string { + t.Helper() + config := `[default] +aws_access_key_id = test_access_key +aws_secret_access_key = test_secret_key +region = us-east-1 + +[profile dev] +aws_access_key_id = dev_access_key +aws_secret_access_key = dev_secret_key +region = us-west-2 + +[profile prod] +aws_access_key_id = prod_access_key +aws_secret_access_key = prod_secret_key +region = eu-west-1` + + configPath := filepath.Join(dir, "config") + if err := os.WriteFile(configPath, []byte(config), 0600); err != nil { + t.Fatalf("Failed to write mock AWS config: %v", err) + } + return configPath +} + +// CleanupTempDir removes a temporary directory and its contents +func CleanupTempDir(t *testing.T, dir string) { + t.Helper() + if err := os.RemoveAll(dir); err != nil { + t.Errorf("Failed to cleanup temp dir: %v", err) + } +} + +// SetTestEnv sets up test environment variables +func SetTestEnv(t *testing.T, key, value string) { + t.Helper() + if err := os.Setenv(key, value); err != nil { + t.Fatalf("Failed to set environment variable %s: %v", key, err) + } +} + +// UnsetTestEnv removes test environment variables +func UnsetTestEnv(t *testing.T, key string) { + t.Helper() + if err := os.Unsetenv(key); err != nil { + t.Errorf("Failed to unset environment variable %s: %v", key, err) + } +} diff --git a/testdata/aws_config_examples/basic_config b/testdata/aws_config_examples/basic_config new file mode 100644 index 0000000..4183aaf --- /dev/null +++ b/testdata/aws_config_examples/basic_config @@ -0,0 +1,14 @@ +[default] +aws_access_key_id = test_access_key +aws_secret_access_key = test_secret_key +region = us-east-1 + +[profile dev] +aws_access_key_id = dev_access_key +aws_secret_access_key = dev_secret_key +region = us-west-2 + +[profile prod] +aws_access_key_id = prod_access_key +aws_secret_access_key = prod_secret_key +region = eu-west-1 diff --git a/testdata/aws_config_examples/complex_config b/testdata/aws_config_examples/complex_config new file mode 100644 index 0000000..248f05c --- /dev/null +++ b/testdata/aws_config_examples/complex_config @@ -0,0 +1,34 @@ +[default] +aws_access_key_id = test_access_key +aws_secret_access_key = test_secret_key +region = us-east-1 + +[profile dev] +aws_access_key_id = dev_access_key +aws_secret_access_key = dev_secret_key +region = us-west-2 + +[profile dev.admin] +aws_access_key_id = dev_admin_key +aws_secret_access_key = dev_admin_secret +region = us-west-2 + +[profile dev.readonly] +aws_access_key_id = dev_readonly_key +aws_secret_access_key = dev_readonly_secret +region = us-west-2 + +[profile prod] +aws_access_key_id = prod_access_key +aws_secret_access_key = prod_secret_key +region = eu-west-1 + +[profile prod.admin] +aws_access_key_id = prod_admin_key +aws_secret_access_key = prod_admin_secret +region = eu-west-1 + +[profile prod.readonly] +aws_access_key_id = prod_readonly_key +aws_secret_access_key = prod_readonly_secret +region = eu-west-1 diff --git a/testdata/aws_config_examples/malformed_config b/testdata/aws_config_examples/malformed_config new file mode 100644 index 0000000..cdb87e9 --- /dev/null +++ b/testdata/aws_config_examples/malformed_config @@ -0,0 +1,14 @@ +[default] +aws_access_key_id = test_access_key +aws_secret_access_key = test_secret_key +region = us-east-1 + +[profile dev +aws_access_key_id = dev_access_key +aws_secret_access_key = dev_secret_key +region = us-west-2 + +[profile prod] +aws_access_key_id = prod_access_key +aws_secret_access_key = prod_secret_key +region = eu-west-1 From 2378432645bc183836e1c97c6f6e13d63e06f45b Mon Sep 17 00:00:00 2001 From: pjoyce Date: Wed, 22 Apr 2026 16:27:14 -0400 Subject: [PATCH 2/2] update tests --- .github/workflows/pull-request.yml | 19 ++++++++++++++- .github/workflows/test.yml | 26 -------------------- src/cmd/list_test.go | 10 +++----- src/cmd/root_test.go | 7 ++---- src/utils/aws_test.go | 37 ++++++++++------------------ src/utils/common_test.go | 39 +++++------------------------- src/utils/testutils/testutils.go | 12 +++------ 7 files changed, 45 insertions(+), 105 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 0e1370a..3dd78ea 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,4 +1,4 @@ -name: Lint +name: PR on: pull_request: @@ -14,3 +14,20 @@ jobs: go-version: '1.26' - name: golangci-lint uses: golangci/golangci-lint-action@v9 + + test: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version: '1.26' + check-latest: true + cache: true + cache-dependency-path: | + **/go.sum + **/go.mod + - name: Run tests with coverage + run: | + go test ./... -coverprofile=coverage.out + go tool cover -func=coverage.out diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index d76511e..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Test - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: '1.26' - check-latest: true - cache: true - cache-dependency-path: | - **/go.sum - **/go.mod - - - name: Run tests with coverage - run: | - go test ./... -coverprofile=coverage.out - go tool cover -func=coverage.out diff --git a/src/cmd/list_test.go b/src/cmd/list_test.go index cf1c67e..8861644 100644 --- a/src/cmd/list_test.go +++ b/src/cmd/list_test.go @@ -22,8 +22,7 @@ func TestRunProfileLister(t *testing.T) { defer testutils.CleanupTempDir(t, tempDir) configPath := testutils.CreateMockAWSConfig(t, tempDir) - testutils.SetTestEnv(t, "AWS_CONFIG_FILE", configPath) - defer testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") + t.Setenv("AWS_CONFIG_FILE", configPath) err := runProfileLister() assert.NoError(t, err) @@ -34,11 +33,8 @@ func TestListCommandIntegration(t *testing.T) { defer testutils.CleanupTempDir(t, tempDir) configPath := testutils.CreateMockAWSConfig(t, tempDir) - testutils.SetTestEnv(t, "AWS_CONFIG_FILE", configPath) - defer testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") - - testutils.SetTestEnv(t, "HOME", tempDir) - defer testutils.UnsetTestEnv(t, "HOME") + t.Setenv("AWS_CONFIG_FILE", configPath) + t.Setenv("HOME", tempDir) cmd := &cobra.Command{ Use: "awsd", diff --git a/src/cmd/root_test.go b/src/cmd/root_test.go index df4acd3..9ceefd1 100644 --- a/src/cmd/root_test.go +++ b/src/cmd/root_test.go @@ -56,11 +56,8 @@ func TestDirectProfileSwitch(t *testing.T) { defer testutils.CleanupTempDir(t, tempDir) configPath := testutils.CreateMockAWSConfig(t, tempDir) - testutils.SetTestEnv(t, "AWS_CONFIG_FILE", configPath) - defer testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") - - testutils.SetTestEnv(t, "HOME", tempDir) - defer testutils.UnsetTestEnv(t, "HOME") + t.Setenv("AWS_CONFIG_FILE", configPath) + t.Setenv("HOME", tempDir) tests := []struct { name string diff --git a/src/utils/aws_test.go b/src/utils/aws_test.go index f118408..d1efa4d 100644 --- a/src/utils/aws_test.go +++ b/src/utils/aws_test.go @@ -9,34 +9,26 @@ import ( "github.com/stretchr/testify/assert" ) -func setupTestEnvironment(t *testing.T) (string, func()) { - origHome := os.Getenv("HOME") - origConfigFile := os.Getenv("AWS_CONFIG_FILE") - +func setupTestEnvironment(t *testing.T) string { + t.Helper() tempDir := testutils.CreateTempDir(t) + t.Cleanup(func() { testutils.CleanupTempDir(t, tempDir) }) awsDir := filepath.Join(tempDir, ".aws") err := os.MkdirAll(awsDir, 0755) assert.NoError(t, err) - os.Setenv("HOME", tempDir) - os.Setenv("AWS_CONFIG_FILE", filepath.Join(tempDir, "config")) - - cleanup := func() { - os.Setenv("HOME", origHome) - os.Setenv("AWS_CONFIG_FILE", origConfigFile) - testutils.CleanupTempDir(t, tempDir) - } + t.Setenv("HOME", tempDir) + t.Setenv("AWS_CONFIG_FILE", filepath.Join(tempDir, "config")) - return tempDir, cleanup + return tempDir } func TestGetProfiles(t *testing.T) { - tempDir, cleanup := setupTestEnvironment(t) - defer cleanup() + tempDir := setupTestEnvironment(t) configPath := testutils.CreateMockAWSConfig(t, tempDir) - os.Setenv("AWS_CONFIG_FILE", configPath) + t.Setenv("AWS_CONFIG_FILE", configPath) profiles, err := GetProfiles() assert.NoError(t, err, "Should not return error") @@ -46,8 +38,7 @@ func TestGetProfiles(t *testing.T) { } func TestGetProfilesWithComplexConfig(t *testing.T) { - tempDir, cleanup := setupTestEnvironment(t) - defer cleanup() + tempDir := setupTestEnvironment(t) complexConfigPath := filepath.Join("..", "..", "testdata", "aws_config_examples", "complex_config") configContent, err := os.ReadFile(complexConfigPath) @@ -76,8 +67,7 @@ func TestGetProfilesWithComplexConfig(t *testing.T) { } func TestGetProfilesWithMalformedConfig(t *testing.T) { - tempDir, cleanup := setupTestEnvironment(t) - defer cleanup() + tempDir := setupTestEnvironment(t) configPath := filepath.Join(tempDir, "config") malformedContent := `[default] @@ -100,11 +90,10 @@ region = us-west-2` } func TestGetProfilesWithError(t *testing.T) { - tempDir, cleanup := setupTestEnvironment(t) - defer cleanup() + tempDir := setupTestEnvironment(t) configPath := testutils.CreateMockAWSConfig(t, tempDir) - os.Setenv("AWS_CONFIG_FILE", configPath) + t.Setenv("AWS_CONFIG_FILE", configPath) profiles, err := GetProfiles() assert.NoError(t, err, "Should not return error for valid config") @@ -129,7 +118,7 @@ region = us-west-2` assert.Nil(t, profiles, "Should return nil profiles for malformed config") assert.Contains(t, err.Error(), "unclosed section", "Error should mention unclosed section") - os.Setenv("AWS_CONFIG_FILE", "/nonexistent/config") + t.Setenv("AWS_CONFIG_FILE", "/nonexistent/config") profiles, err = GetProfiles() assert.Error(t, err, "Should return error for non-existent config") assert.Nil(t, profiles, "Should return nil profiles for non-existent config") diff --git a/src/utils/common_test.go b/src/utils/common_test.go index d3f47ee..bc9a666 100644 --- a/src/utils/common_test.go +++ b/src/utils/common_test.go @@ -27,15 +27,7 @@ func TestWriteFile(t *testing.T) { tempDir := testutils.CreateTempDir(t) defer testutils.CleanupTempDir(t, tempDir) - origHome := os.Getenv("HOME") - defer func() { - if origHome != "" { - os.Setenv("HOME", origHome) - } else { - os.Unsetenv("HOME") - } - }() - testutils.SetTestEnv(t, "HOME", tempDir) + t.Setenv("HOME", tempDir) err := WriteFile("test-profile", tempDir) assert.NoError(t, err, "Should write file without error") @@ -54,8 +46,7 @@ func TestWriteFile(t *testing.T) { } func TestGetEnv(t *testing.T) { - testutils.SetTestEnv(t, "TEST_VAR", "test-value") - defer testutils.UnsetTestEnv(t, "TEST_VAR") + t.Setenv("TEST_VAR", "test-value") value := GetEnv("TEST_VAR", "fallback") assert.Equal(t, "test-value", value, "Should return environment variable value") @@ -65,20 +56,12 @@ func TestGetEnv(t *testing.T) { } func TestGetHomeDir(t *testing.T) { - origHome := os.Getenv("HOME") - defer func() { - if origHome != "" { - os.Setenv("HOME", origHome) - } else { - os.Unsetenv("HOME") - } - }() - - testutils.SetTestEnv(t, "HOME", "/test/home") + t.Setenv("HOME", "/test/home") homeDir, err := GetHomeDir() assert.NoError(t, err, "Should not return error when HOME is set") assert.Equal(t, "/test/home", homeDir, "Should return HOME environment variable value") + // t.Setenv can't unset, so go via the helper which checks the error. testutils.UnsetTestEnv(t, "HOME") homeDir, err = GetHomeDir() if err != nil { @@ -89,27 +72,17 @@ func TestGetHomeDir(t *testing.T) { } func TestGetProfileFileLocation(t *testing.T) { - origHome := os.Getenv("HOME") - defer func() { - if origHome != "" { - os.Setenv("HOME", origHome) - } else { - os.Unsetenv("HOME") - } - }() - tempDir := testutils.CreateTempDir(t) defer testutils.CleanupTempDir(t, tempDir) - testutils.SetTestEnv(t, "HOME", tempDir) + t.Setenv("HOME", tempDir) expectedPath := filepath.Join(tempDir, ".aws") actualPath := GetProfileFileLocation() assert.Equal(t, expectedPath, actualPath, "Should return correct .aws directory path") } func TestGetCurrentProfileFile(t *testing.T) { - testutils.SetTestEnv(t, "AWS_CONFIG_FILE", "/test/config") - defer testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") + t.Setenv("AWS_CONFIG_FILE", "/test/config") file := GetCurrentProfileFile() assert.Equal(t, "/test/config", file, "Should return AWS_CONFIG_FILE value") diff --git a/src/utils/testutils/testutils.go b/src/utils/testutils/testutils.go index 48469f2..f31e072 100644 --- a/src/utils/testutils/testutils.go +++ b/src/utils/testutils/testutils.go @@ -49,15 +49,9 @@ func CleanupTempDir(t *testing.T, dir string) { } } -// SetTestEnv sets up test environment variables -func SetTestEnv(t *testing.T, key, value string) { - t.Helper() - if err := os.Setenv(key, value); err != nil { - t.Fatalf("Failed to set environment variable %s: %v", key, err) - } -} - -// UnsetTestEnv removes test environment variables +// UnsetTestEnv removes an environment variable and checks the error. +// Use t.Setenv for set/auto-restore; this exists for tests that need to +// explicitly observe the "unset" state mid-test. func UnsetTestEnv(t *testing.T, key string) { t.Helper() if err := os.Unsetenv(key); err != nil {