diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c11186c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + check-latest: true + cache: true + cache-dependency-path: | + **/go.sum + **/go.mod + + - name: golangci-lint + uses: golangci/golangci-lint-action@v7 + + - 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..7d6c43a 100644 --- a/Makefile +++ b/Makefile @@ -23,3 +23,8 @@ uninstall: ## Uninstall Target rm -f ${BINDIR}/_awsd rm -f ${BINDIR}/_awsd_autocomplete rm -f ${BINDIR}/_awsd_prompt + +.PHONY: test-coverage +test-coverage: + go test ./... -coverprofile=coverage.out + go tool cover -func=coverage.out diff --git a/go.mod b/go.mod index c2a72bd..f2151bb 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.9.1 + github.com/stretchr/testify v1.8.4 gopkg.in/ini.v1 v1.67.0 ) 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.6 // indirect - github.com/stretchr/testify v1.10.0 // 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 9bd9129..57748a6 100644 --- a/go.sum +++ b/go.sum @@ -18,11 +18,12 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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= 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..06ad70e --- /dev/null +++ b/src/cmd/list_test.go @@ -0,0 +1,77 @@ +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) + + // Create mock AWS config + 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) + + // Create mock AWS config + configPath := testutils.CreateMockAWSConfig(t, tempDir) + testutils.SetTestEnv(t, "AWS_CONFIG_FILE", configPath) + defer testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") + + // Set HOME environment variable + testutils.SetTestEnv(t, "HOME", tempDir) + defer testutils.UnsetTestEnv(t, "HOME") + + // Create a new command instance + cmd := &cobra.Command{ + Use: "awsd", + Short: "awsd - switch between AWS profiles.", + Long: "Allows for switching AWS profiles files.", + } + + // Add the list command + 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) + + // Test both list and l aliases + 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..b25d357 --- /dev/null +++ b/src/cmd/root_test.go @@ -0,0 +1,134 @@ +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) + + // Create mock AWS config + configPath := testutils.CreateMockAWSConfig(t, tempDir) + testutils.SetTestEnv(t, "AWS_CONFIG_FILE", configPath) + defer testutils.UnsetTestEnv(t, "AWS_CONFIG_FILE") + + // Set HOME environment variable + 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) { + // Remove any existing .awsd file before each test + 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) { + // This is a simple test that just ensures the function doesn't panic + // since we can't easily capture stdout in tests + 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..175c191 --- /dev/null +++ b/src/utils/aws_test.go @@ -0,0 +1,152 @@ +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()) { + // Save original environment + origHome := os.Getenv("HOME") + origConfigFile := os.Getenv("AWS_CONFIG_FILE") + + // Create temporary directory + tempDir := testutils.CreateTempDir(t) + + // Create .aws directory + awsDir := filepath.Join(tempDir, ".aws") + err := os.MkdirAll(awsDir, 0755) + assert.NoError(t, err) + + // Set environment variables + os.Setenv("HOME", tempDir) + os.Setenv("AWS_CONFIG_FILE", filepath.Join(tempDir, "config")) + + // Return cleanup function + 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() + + // Create a mock AWS config file + configPath := testutils.CreateMockAWSConfig(t, tempDir) + os.Setenv("AWS_CONFIG_FILE", configPath) + + // Test getting profiles + profiles, err := GetProfiles() + assert.NoError(t, err, "Should not return error") + + // Verify the profiles + 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() + + // Copy complex config to temp directory + 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) + } + + // Test getting profiles + profiles, err := GetProfiles() + assert.NoError(t, err, "Should not return error") + + // Verify the profiles + 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() + + // Create a malformed config file + 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) + + // Test getting profiles with error handling + 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() + + // Test with valid config + 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") + + // Test with malformed 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 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") + + // Test with non-existent config file + 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..149fcca --- /dev/null +++ b/src/utils/common_test.go @@ -0,0 +1,259 @@ +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") + + // Verify file exists + _, 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) + + // Test writing a profile + err := WriteFile("test-profile", tempDir) + assert.NoError(t, err, "Should write file without error") + + // Verify file exists and contains correct content + 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") + + // Test writing default profile + 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) { + // Test with environment variable set + 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") + + // Test with environment variable not set + value = GetEnv("NONEXISTENT_VAR", "fallback") + assert.Equal(t, "fallback", value, "Should return fallback value") +} + +func TestGetHomeDir(t *testing.T) { + // Save original HOME value + origHome := os.Getenv("HOME") + defer func() { + if origHome != "" { + os.Setenv("HOME", origHome) + } else { + os.Unsetenv("HOME") + } + }() + + // Test with HOME set + 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") + + // Test with HOME unset + testutils.UnsetTestEnv(t, "HOME") + homeDir, err = GetHomeDir() + if err != nil { + // This is okay - on some systems UserHomeDir() might fail when HOME is unset + assert.Contains(t, err.Error(), "error getting user home directory") + } else { + assert.NotEmpty(t, homeDir, "Should return UserHomeDir value") + } +} + +func TestGetProfileFileLocation(t *testing.T) { + // Save original HOME value + 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) + + // Set HOME environment variable + 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) { + // Test with AWS_CONFIG_FILE set + 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") + + // Test without AWS_CONFIG_FILE set + 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) + + // Test existing directory + exists := IsDirectoryExists(tempDir) + assert.True(t, exists, "Should return true for existing directory") + + // Test non-existing directory + exists = IsDirectoryExists("/nonexistent/directory") + assert.False(t, exists, "Should return false for non-existing directory") +} + +func TestAppendIfNotExists(t *testing.T) { + // Test appending new item + slice := []string{"a", "b", "c"} + result := AppendIfNotExists(slice, "d") + assert.Equal(t, []string{"a", "b", "c", "d"}, result, "Should append new item") + + // Test appending existing item + result = AppendIfNotExists(slice, "b") + assert.Equal(t, []string{"a", "b", "c"}, result, "Should not append existing item") + + // Test appending to empty slice + 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"} + + // Test existing item + assert.True(t, Contains(slice, "b"), "Should find existing item") + + // Test non-existing item + assert.False(t, Contains(slice, "d"), "Should not find non-existing item") + + // Test empty slice + var emptySlice []string + assert.False(t, Contains(emptySlice, "a"), "Should return false for empty slice") +} + +func TestCheckError(t *testing.T) { + // Test ^D error + 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) { + // Test ^C error + 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) { + // Test other error + 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{} + + // Test writing bell character + n, err := bs.Write([]byte{7}) // ASCII bell character + assert.NoError(t, err, "Write should not return error") + assert.Equal(t, 0, n, "Write should skip bell character") + + // Test writing normal text + 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) + + // Test exact match + result := searcher("test1", 0) + assert.True(t, result, "Should find exact match") + + // Test partial match + result = searcher("test", 0) + assert.True(t, result, "Should find partial match") + + // Test case insensitive match + result = searcher("TEST1", 0) + assert.True(t, result, "Should find case insensitive match") + + // Test no match + result = searcher("nonexistent", 0) + assert.False(t, result, "Should not find non-existent item") + + // Test empty search + result = searcher("", 0) + assert.True(t, result, "Should match on empty search") + + // Test different index + result = searcher("test", 1) + assert.True(t, result, "Should find match at different index") + + // Test index at boundary + 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..44979ad --- /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 \ No newline at end of file diff --git a/testdata/aws_config_examples/complex_config b/testdata/aws_config_examples/complex_config new file mode 100644 index 0000000..f297760 --- /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 \ No newline at end of file diff --git a/testdata/aws_config_examples/malformed_config b/testdata/aws_config_examples/malformed_config new file mode 100644 index 0000000..24a2d9c --- /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 \ No newline at end of file