From 1c9b336910158023f222b8aa357e92f102e8ae4e Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:51:31 +0100 Subject: [PATCH 1/7] feat: Higlight Exo CLI Bug exo config add panics when new first config without it being the default one --- .../scenarios/local/config/add_panic.txtar | 37 ++++++++++++++ .../commands_when_no_default_account.txtar | 41 ++++++++++++++++ .../show_when_missing_config_file.txtar | 7 +++ .../config/show_when_valid_config_file.txtar | 28 +++++++++++ .../e2e/scenarios/local/config_isolated.txtar | 29 ----------- tests/e2e/testscript_api_test.go | 25 ---------- tests/e2e/testscript_local_test.go | 23 ++++++++- tests/integ/blockstorage_test.go | 8 ++-- tests/integ/config_panic_test.go | 48 +++++++++++++++++++ tests/integ/go.mod | 7 +-- tests/integ/go.sum | 6 --- tests/integ/suite.go | 2 +- 12 files changed, 188 insertions(+), 73 deletions(-) create mode 100644 tests/e2e/scenarios/local/config/add_panic.txtar create mode 100644 tests/e2e/scenarios/local/config/commands_when_no_default_account.txtar create mode 100644 tests/e2e/scenarios/local/config/show_when_missing_config_file.txtar create mode 100644 tests/e2e/scenarios/local/config/show_when_valid_config_file.txtar delete mode 100644 tests/e2e/scenarios/local/config_isolated.txtar delete mode 100644 tests/e2e/testscript_api_test.go create mode 100644 tests/integ/config_panic_test.go diff --git a/tests/e2e/scenarios/local/config/add_panic.txtar b/tests/e2e/scenarios/local/config/add_panic.txtar new file mode 100644 index 000000000..bb9a50528 --- /dev/null +++ b/tests/e2e/scenarios/local/config/add_panic.txtar @@ -0,0 +1,37 @@ +# Bug: exo config add panics when declining to set first account as default +# Panic location: cmd/config/config.go:160 +# Panic: interface conversion: interface {} is nil, not string +# +# Note: Cannot reproduce `exo config add` directly in testscript because it uses +# interactive prompts (promptui) for zone selection and account info, which don't +# work in testscript's non-interactive environment. See tests/integ/config_panic_test.go +# for integration test coverage. +# +# E2E test: This file documents the BROKEN STATE left after the panic + +# Simulate the broken state: config with account but no defaultaccount field +mkdir -p .config/exoscale +cp broken-config.toml .config/exoscale/exoscale.toml + +# Verify commands fail when account exists but no defaultaccount +! exec exo config show +stderr 'default account not defined' + +! exec exo config list +stderr 'default account not defined' + +# Circular dependency: config set requires a default account to set a default account! +! exec exo config set Exoscale-Panic +stderr 'default account not defined' + +# Workaround: --use-account flag works +exec exo --use-account Exoscale-Panic config show +stdout 'Exoscale-Panic' + +# Config in broken state: account without defaultaccount field +-- broken-config.toml -- +[[accounts]] +name = "Exoscale-Panic" +key = "EXOpanic123" +secret = "testsecret123" +defaultZone = "ch-gva-2" diff --git a/tests/e2e/scenarios/local/config/commands_when_no_default_account.txtar b/tests/e2e/scenarios/local/config/commands_when_no_default_account.txtar new file mode 100644 index 000000000..4c83ebcaf --- /dev/null +++ b/tests/e2e/scenarios/local/config/commands_when_no_default_account.txtar @@ -0,0 +1,41 @@ +# Bug reproduction: Commands fail when account exists but no default is set +# This simulates the state after `exo config add` panic when declining to set default +# See: Bug Report - CLI exo config add panics when adding first non-default account + +# Test with default config location (no --config flag) +! exec exo config show +stderr 'default account not defined' + +! exec exo config list +stderr 'default account not defined' + +! exec exo config set Exoscale-Test +stderr 'default account not defined' + +# Workaround: --use-account flag bypasses the default account requirement +exec exo --use-account Exoscale-Test config show +stdout 'Exoscale-Test' +stdout 'EXOtest123' + +# Test with explicit --config flag (same results) +! exec exo --config .config/exoscale/exoscale.toml config show +stderr 'default account not defined' + +! exec exo --config .config/exoscale/exoscale.toml config list +stderr 'default account not defined' + +! exec exo --config .config/exoscale/exoscale.toml config set Exoscale-Test +stderr 'default account not defined' + +exec exo --config .config/exoscale/exoscale.toml --use-account Exoscale-Test config show +stdout 'Exoscale-Test' +stdout 'EXOtest123' + +# Config file in broken state: has account but no defaultaccount +# This is what exists after the panic when declining to set as default +-- .config/exoscale/exoscale.toml -- +[[accounts]] +name = "Exoscale-Test" +key = "EXOtest123" +secret = "testsecret123" +defaultZone = "ch-gva-2" diff --git a/tests/e2e/scenarios/local/config/show_when_missing_config_file.txtar b/tests/e2e/scenarios/local/config/show_when_missing_config_file.txtar new file mode 100644 index 000000000..839c3ca1d --- /dev/null +++ b/tests/e2e/scenarios/local/config/show_when_missing_config_file.txtar @@ -0,0 +1,7 @@ +# Test config show fails when config file does not exist + +! exec exo config show +stderr 'no accounts configured' + +! exec exo --config ./i-do-not-exist.toml config show +stderr 'no such file or directory' diff --git a/tests/e2e/scenarios/local/config/show_when_valid_config_file.txtar b/tests/e2e/scenarios/local/config/show_when_valid_config_file.txtar new file mode 100644 index 000000000..f8a38a316 --- /dev/null +++ b/tests/e2e/scenarios/local/config/show_when_valid_config_file.txtar @@ -0,0 +1,28 @@ +# Test config show displays account information from valid config file + +exec exo config show +stdout 'test-account' +stdout 'EXOtest1234' +stdout 'ch-gva-2' +stdout 'https://sos-{zone}.exo.io' +stdout '.config/exoscale/exoscale.toml' +stdout '×××××××××××' +! stdout 'testsecret1234' + +exec exo --config .config/exoscale/exoscale.toml config show +stdout 'test-account' +stdout 'EXOtest1234' +stdout 'ch-gva-2' +stdout 'https://sos-{zone}.exo.io' +stdout '.config/exoscale/exoscale.toml' +stdout '×××××××××××' +! stdout 'testsecret1234' + +-- .config/exoscale/exoscale.toml -- +defaultaccount = "test-account" + +[[accounts]] +name = "test-account" +key = "EXOtest1234" +secret = "testsecret1234" +defaultZone = "ch-gva-2" diff --git a/tests/e2e/scenarios/local/config_isolated.txtar b/tests/e2e/scenarios/local/config_isolated.txtar deleted file mode 100644 index 004aa1841..000000000 --- a/tests/e2e/scenarios/local/config_isolated.txtar +++ /dev/null @@ -1,29 +0,0 @@ -# Test config command with isolated config file -# Each test gets its own temp directory, so config changes are isolated - -# Create a config directory structure (txtar file archives can include files) --- .config/exoscale/exoscale.toml -- -default_account = "test-account" - -[[accounts]] -name = "test-account" -key = "EXOtest1234" -secret = "testsecret1234" -default_zone = "ch-gva-2" --- END -- - -# Now test config commands that read this file -exec exo config show -stdout 'test-account' -stdout 'EXOtest1234' - -# Add a new account (modifies the isolated config, not your real one!) -# exec exo config add new-account --key EXOnew123 --secret newsecret -# -# # Verify it was added -# exec exo config show -# stdout 'new-account' - -# Read default account -# exec exo config default -# stdout 'test-account' diff --git a/tests/e2e/testscript_api_test.go b/tests/e2e/testscript_api_test.go deleted file mode 100644 index dc6a4aac3..000000000 --- a/tests/e2e/testscript_api_test.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build api - -package e2e_test - -import ( - "testing" - - "github.com/rogpeppe/go-internal/testscript" -) - -// TestScriptsAPI runs testscript scenarios that require API access. -// These tests only run when built with the 'api' build tag: -// -// go test -tags=api -// -// API tests require EXOSCALE_API_KEY and EXOSCALE_API_SECRET -// environment variables to be set with valid API credentials. -func TestScriptsAPI(t *testing.T) { - testscript.Run(t, testscript.Params{ - Dir: "scenarios/api", - Setup: func(e *testscript.Env) error { - return setupTestEnv(e, true) - }, - }) -} diff --git a/tests/e2e/testscript_local_test.go b/tests/e2e/testscript_local_test.go index 1dbfd19c3..af08dba35 100644 --- a/tests/e2e/testscript_local_test.go +++ b/tests/e2e/testscript_local_test.go @@ -11,14 +11,35 @@ import ( // TestScriptsLocal runs testscript scenarios that don't require API access. // These tests run by default without any build tags. func TestScriptsLocal(t *testing.T) { + // Find all txtar files recursively in scenarios/local + files, err := findTestScripts("scenarios/local") + if err != nil { + t.Fatal(err) + } + testscript.Run(t, testscript.Params{ - Dir: "scenarios/local", + Files: files, Setup: func(e *testscript.Env) error { return setupTestEnv(e, false) }, }) } +// findTestScripts recursively finds all .txtar and .txt files in a directory +func findTestScripts(dir string) ([]string, error) { + var files []string + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && (filepath.Ext(path) == ".txtar" || filepath.Ext(path) == ".txt") { + files = append(files, path) + } + return nil + }) + return files, err +} + // setupTestEnv configures the test environment for testscript scenarios. // withAPI controls whether API credentials should be forwarded. func setupTestEnv(e *testscript.Env, withAPI bool) error { diff --git a/tests/integ/blockstorage_test.go b/tests/integ/blockstorage_test.go index 7b884d188..84d1a3287 100644 --- a/tests/integ/blockstorage_test.go +++ b/tests/integ/blockstorage_test.go @@ -1,11 +1,9 @@ -package integ_test +package integ import ( "fmt" "math/rand" "testing" - - "github.com/exoscale/cli/internal/integ/test" ) type blockStorageShowOutput struct { @@ -36,10 +34,10 @@ func TestBlockStorage(t *testing.T) { NewSnapshotName: fmt.Sprintf("test-snap-name-%d-renamed", rand.Int()), } - s := test.Suite{ + s := Suite{ Zone: "ch-gva-2", Parameters: params, - Steps: []test.Step{ + Steps: []Step{ { Description: "create volume", Command: "exo compute block-storage create {{.VolumeName}}" + diff --git a/tests/integ/config_panic_test.go b/tests/integ/config_panic_test.go new file mode 100644 index 000000000..71668240f --- /dev/null +++ b/tests/integ/config_panic_test.go @@ -0,0 +1,48 @@ +package integ_test + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +var Binary = "../../bin/exo" + +// TestConfigPanic tests the bug where adding a first account without setting +// it as default causes a panic at cmd/config/config.go:160 +// +// Note: This is an integration test rather than a testscript (e2e) test because +// `exo config add` uses interactive prompts (promptui) for zone selection and +// account information, which cannot be properly simulated in testscript's +// non-interactive environment. Instead, we test the broken state that results +// from the panic - a config file with accounts but no defaultAccount field. +func TestConfigPanic(t *testing.T) { + tmpHome := t.TempDir() + tmpConfigDir := filepath.Join(tmpHome, ".config", "exoscale") + err := os.MkdirAll(tmpConfigDir, 0755) + require.NoError(t, err) + + // Create broken config: account without defaultAccount field + brokenConfig := `[[accounts]] +name = "test-account" +key = "EXOtest123" +secret = "testsecret" +defaultZone = "ch-gva-2" +` + configPath := filepath.Join(tmpConfigDir, "exoscale.toml") + err = os.WriteFile(configPath, []byte(brokenConfig), 0644) + require.NoError(t, err) + + t.Run("commands fail with broken config", func(t *testing.T) { + cmd := exec.Command(Binary, "config", "show") + cmd.Env = append(os.Environ(), "HOME="+tmpHome) + output, err := cmd.CombinedOutput() + + require.Error(t, err) + require.Contains(t, string(output), "default account not defined") + t.Logf("Output: %s", output) + }) +} diff --git a/tests/integ/go.mod b/tests/integ/go.mod index 33006f4f1..75ab676bf 100644 --- a/tests/integ/go.mod +++ b/tests/integ/go.mod @@ -2,15 +2,10 @@ module github.com/exoscale/cli/internal/integ go 1.23 -require ( - github.com/rogpeppe/go-internal v1.14.1 - github.com/stretchr/testify v1.9.0 -) +require github.com/stretchr/testify v1.9.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/tools v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tests/integ/go.sum b/tests/integ/go.sum index bb1a653f5..60ce688a0 100644 --- a/tests/integ/go.sum +++ b/tests/integ/go.sum @@ -2,14 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/tests/integ/suite.go b/tests/integ/suite.go index 695bb2fc7..e08391178 100644 --- a/tests/integ/suite.go +++ b/tests/integ/suite.go @@ -1,4 +1,4 @@ -package integ_test +package integ import ( "bytes" From b5c256432c1669ba9806e54e44309b73f73536da Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:37:29 +0100 Subject: [PATCH 2/7] feat: fix Exo CLI Bug exo config add panics when new first config without it being the default one --- cmd/config/config.go | 3 +- cmd/config/config_add.go | 14 ++++++- .../scenarios/local/config/add_panic.txtar | 17 ++++---- .../commands_when_no_default_account.txtar | 40 +++++++++++++++---- tests/integ/config_panic_test.go | 39 ++++++++++++++---- 5 files changed, 88 insertions(+), 25 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index 9e7aa5785..d1f869407 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -157,7 +157,8 @@ func saveConfig(filePath string, newAccounts *account.Config) error { return err } - conf.DefaultAccount = exocmd.GConfig.Get("defaultAccount").(string) + // Safely get defaultAccount - may be empty/unset if user declined to set default + conf.DefaultAccount = exocmd.GConfig.GetString("defaultAccount") if conf.DefaultAccount == "" { fmt.Println("no default account set") } diff --git a/cmd/config/config_add.go b/cmd/config/config_add.go index df9693ef9..cfa43fe17 100644 --- a/cmd/config/config_add.go +++ b/cmd/config/config_add.go @@ -26,9 +26,21 @@ func init() { } config := &account.Config{Accounts: []account.Account{*newAccount}} - if utils.AskQuestion(exocmd.GContext, "Set ["+newAccount.Name+"] as default account?") { + + // Check if this is the first account + isFirstAccount := account.GAllAccount == nil || len(account.GAllAccount.Accounts) == 0 + + if isFirstAccount { + // First account: automatically set as default config.DefaultAccount = newAccount.Name exocmd.GConfig.Set("defaultAccount", newAccount.Name) + fmt.Printf("Set [%s] as default account (first account)\n", newAccount.Name) + } else { + // Additional account: ask user if it should be the new default + if utils.AskQuestion(exocmd.GContext, "Set ["+newAccount.Name+"] as default account?") { + config.DefaultAccount = newAccount.Name + exocmd.GConfig.Set("defaultAccount", newAccount.Name) + } } return saveConfig(exocmd.GConfig.ConfigFileUsed(), config) diff --git a/tests/e2e/scenarios/local/config/add_panic.txtar b/tests/e2e/scenarios/local/config/add_panic.txtar index bb9a50528..c3c29cb6f 100644 --- a/tests/e2e/scenarios/local/config/add_panic.txtar +++ b/tests/e2e/scenarios/local/config/add_panic.txtar @@ -1,19 +1,20 @@ -# Bug: exo config add panics when declining to set first account as default -# Panic location: cmd/config/config.go:160 -# Panic: interface conversion: interface {} is nil, not string +# Test: Commands work correctly when config has account but no defaultAccount +# Previously this caused a panic at cmd/config/config.go:160 due to unsafe type assertion +# Bug fix: Changed from Get("defaultAccount").(string) to GetString("defaultAccount") # # Note: Cannot reproduce `exo config add` directly in testscript because it uses # interactive prompts (promptui) for zone selection and account info, which don't # work in testscript's non-interactive environment. See tests/integ/config_panic_test.go # for integration test coverage. # -# E2E test: This file documents the BROKEN STATE left after the panic +# E2E test: This file verifies correct behavior with config missing defaultAccount -# Simulate the broken state: config with account but no defaultaccount field +# Simulate config state: account exists but no defaultaccount field mkdir -p .config/exoscale -cp broken-config.toml .config/exoscale/exoscale.toml +cp config-without-default.toml .config/exoscale/exoscale.toml # Verify commands fail when account exists but no defaultaccount +# They should fail gracefully with a clear error message, not panic ! exec exo config show stderr 'default account not defined' @@ -27,8 +28,8 @@ stderr 'default account not defined' # Workaround: --use-account flag works exec exo --use-account Exoscale-Panic config show stdout 'Exoscale-Panic' - -# Config in broken state: account without defaultaccount field +state: account without defaultaccount field +-- config-without-defaulten state: account without defaultaccount field -- broken-config.toml -- [[accounts]] name = "Exoscale-Panic" diff --git a/tests/e2e/scenarios/local/config/commands_when_no_default_account.txtar b/tests/e2e/scenarios/local/config/commands_when_no_default_account.txtar index 4c83ebcaf..dcd250722 100644 --- a/tests/e2e/scenarios/local/config/commands_when_no_default_account.txtar +++ b/tests/e2e/scenarios/local/config/commands_when_no_default_account.txtar @@ -1,8 +1,34 @@ -# Bug reproduction: Commands fail when account exists but no default is set -# This simulates the state after `exo config add` panic when declining to set default -# See: Bug Report - CLI exo config add panics when adding first non-default account +# Test: Commands handle gracefully when account exists but no default is set +# Previously this caused a panic at cmd/config/config.go:160 (unsafe type assertion) +# Bug fix: Changed from Get("defaultAccount").(string) to GetString("defaultAccount") +# +# Note: With the fix, `exo config add` automatically sets the first account as default, +# so this state should not occur through normal CLI usage. However, it can still happen if: +# - User manually edits config file and removes defaultAccount +# - Config file was created by an external tool +# +# This test compares behavior in two similar failure scenarios: +# 1. No config file at all (first-time use) - various errors depending on command +# 2. Config with accounts but no defaultAccount - "default account not defined" +# +# Cannot test `exo config add` interactively in testscript due to promptui. +# See tests/integ/config_panic_test.go for integration test coverage. -# Test with default config location (no --config flag) +# Scenario 1: First test with NO config at all +# The `exo config` command should show helpful setup message +! exec exo config +stdout 'No Exoscale CLI configuration found' +stdout 'In order to set up your configuration profile' +stdout 'https://portal.exoscale.com/iam/keys' + +# Other config commands should fail gracefully +! exec exo config show + +# Scenario 2: Create config with account but NO defaultAccount field +mkdir -p .config/exoscale +cp test-config.toml .config/exoscale/exoscale.toml + +# Now commands should fail with clear "default account not defined" error ! exec exo config show stderr 'default account not defined' @@ -31,9 +57,9 @@ exec exo --config .config/exoscale/exoscale.toml --use-account Exoscale-Test con stdout 'Exoscale-Test' stdout 'EXOtest123' -# Config file in broken state: has account but no defaultaccount -# This is what exists after the panic when declining to set as default --- .config/exoscale/exoscale.toml -- +# Config file without defaultAccount field +# This state can occur from manual config editing or external tools +-- test-config.toml -- [[accounts]] name = "Exoscale-Test" key = "EXOtest123" diff --git a/tests/integ/config_panic_test.go b/tests/integ/config_panic_test.go index 71668240f..d58c9fbef 100644 --- a/tests/integ/config_panic_test.go +++ b/tests/integ/config_panic_test.go @@ -11,38 +11,61 @@ import ( var Binary = "../../bin/exo" -// TestConfigPanic tests the bug where adding a first account without setting -// it as default causes a panic at cmd/config/config.go:160 +// TestConfigPanic tests that config commands handle gracefully when +// defaultAccount field is missing from the config file. +// +// Note: With the fix, `exo config add` now automatically sets the first account +// as default, so this "no default" state should not occur through normal CLI usage. +// However, this state can still occur if: +// - User manually edits the config file and removes defaultAccount +// - Config file was created by an external tool +// +// This test ensures commands fail gracefully (not panic) when defaultAccount is missing. // // Note: This is an integration test rather than a testscript (e2e) test because // `exo config add` uses interactive prompts (promptui) for zone selection and // account information, which cannot be properly simulated in testscript's -// non-interactive environment. Instead, we test the broken state that results -// from the panic - a config file with accounts but no defaultAccount field. +// non-interactive environment. func TestConfigPanic(t *testing.T) { tmpHome := t.TempDir() tmpConfigDir := filepath.Join(tmpHome, ".config", "exoscale") err := os.MkdirAll(tmpConfigDir, 0755) require.NoError(t, err) - // Create broken config: account without defaultAccount field - brokenConfig := `[[accounts]] + // Create config: account without defaultAccount field + // This state can occur from manual config editing or external tools + configWithoutDefault := `[[accounts]] name = "test-account" key = "EXOtest123" secret = "testsecret" defaultZone = "ch-gva-2" ` configPath := filepath.Join(tmpConfigDir, "exoscale.toml") - err = os.WriteFile(configPath, []byte(brokenConfig), 0644) + err = os.WriteFile(configPath, []byte(configWithoutDefault), 0644) require.NoError(t, err) - t.Run("commands fail with broken config", func(t *testing.T) { + t.Run("commands handle missing default account gracefully", func(t *testing.T) { cmd := exec.Command(Binary, "config", "show") cmd.Env = append(os.Environ(), "HOME="+tmpHome) output, err := cmd.CombinedOutput() + // Should fail gracefully with clear error message, not panic require.Error(t, err) require.Contains(t, string(output), "default account not defined") t.Logf("Output: %s", output) }) + + t.Run("use-account flag bypasses default account requirement", func(t *testing.T) { + cmd := exec.Command(Binary, "--use-account", "test-account", "config", "show") + cmd.Env = append(os.Environ(), "HOME="+tmpHome) + output, err := cmd.CombinedOutput() + + // Should work with --use-account flag + if err != nil { + // May fail due to invalid credentials, but shouldn't panic + t.Logf("Command failed (expected with test credentials): %s", output) + } else { + require.Contains(t, string(output), "test-account") + } + }) } From a0cddf8a3a451044125550a6d4574ce39acf5cf1 Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:50:58 +0100 Subject: [PATCH 3/7] feat: fix Exo CLI Bug exo config add panics when new first config without it being the default one --- cmd/config/config_add.go | 18 +++++++-- .../scenarios/local/config/add_panic.txtar | 38 ------------------- 2 files changed, 15 insertions(+), 41 deletions(-) delete mode 100644 tests/e2e/scenarios/local/config/add_panic.txtar diff --git a/cmd/config/config_add.go b/cmd/config/config_add.go index cfa43fe17..bf524c642 100644 --- a/cmd/config/config_add.go +++ b/cmd/config/config_add.go @@ -20,6 +20,21 @@ func init() { Use: "add", Short: "Add a new account to configuration", RunE: func(cmd *cobra.Command, args []string) error { + // Check if this is the first account + isFirstAccount := account.GAllAccount == nil || len(account.GAllAccount.Accounts) == 0 + + if isFirstAccount { + fmt.Println("No Exoscale CLI configuration found") + + fmt.Print(` +In order to set up your configuration profile, you will need to retrieve +Exoscale API credentials from your organization's IAM: + + https://portal.exoscale.com/iam/keys + +`) + } + newAccount, err := promptAccountInformation() if err != nil { return err @@ -27,9 +42,6 @@ func init() { config := &account.Config{Accounts: []account.Account{*newAccount}} - // Check if this is the first account - isFirstAccount := account.GAllAccount == nil || len(account.GAllAccount.Accounts) == 0 - if isFirstAccount { // First account: automatically set as default config.DefaultAccount = newAccount.Name diff --git a/tests/e2e/scenarios/local/config/add_panic.txtar b/tests/e2e/scenarios/local/config/add_panic.txtar deleted file mode 100644 index c3c29cb6f..000000000 --- a/tests/e2e/scenarios/local/config/add_panic.txtar +++ /dev/null @@ -1,38 +0,0 @@ -# Test: Commands work correctly when config has account but no defaultAccount -# Previously this caused a panic at cmd/config/config.go:160 due to unsafe type assertion -# Bug fix: Changed from Get("defaultAccount").(string) to GetString("defaultAccount") -# -# Note: Cannot reproduce `exo config add` directly in testscript because it uses -# interactive prompts (promptui) for zone selection and account info, which don't -# work in testscript's non-interactive environment. See tests/integ/config_panic_test.go -# for integration test coverage. -# -# E2E test: This file verifies correct behavior with config missing defaultAccount - -# Simulate config state: account exists but no defaultaccount field -mkdir -p .config/exoscale -cp config-without-default.toml .config/exoscale/exoscale.toml - -# Verify commands fail when account exists but no defaultaccount -# They should fail gracefully with a clear error message, not panic -! exec exo config show -stderr 'default account not defined' - -! exec exo config list -stderr 'default account not defined' - -# Circular dependency: config set requires a default account to set a default account! -! exec exo config set Exoscale-Panic -stderr 'default account not defined' - -# Workaround: --use-account flag works -exec exo --use-account Exoscale-Panic config show -stdout 'Exoscale-Panic' -state: account without defaultaccount field --- config-without-defaulten state: account without defaultaccount field --- broken-config.toml -- -[[accounts]] -name = "Exoscale-Panic" -key = "EXOpanic123" -secret = "testsecret123" -defaultZone = "ch-gva-2" From b3ddbe4ede685a40e62eeb3b4f05e44fd9c03249 Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:55:27 +0100 Subject: [PATCH 4/7] chore: dry-ing config and config add strings --- cmd/config/config.go | 22 +++++++++++++--------- cmd/config/config_add.go | 10 +--------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index d1f869407..52ff3645e 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -63,15 +63,7 @@ func configCmdRun(cmd *cobra.Command, _ []string) error { return nil } - fmt.Println("No Exoscale CLI configuration found") - - fmt.Print(` -In order to set up your configuration profile, you will need to retrieve -Exoscale API credentials from your organization's IAM: - - https://portal.exoscale.com/iam/keys - -`) + printNoConfigMessage() return addConfigAccount(true) } @@ -255,6 +247,18 @@ func chooseZone(client *v3.Client, zones []string) (string, error) { return result, nil } +func printNoConfigMessage() { + fmt.Println("No Exoscale CLI configuration found") + + fmt.Print(` +In order to set up your configuration profile, you will need to retrieve +Exoscale API credentials from your organization's IAM: + + https://portal.exoscale.com/iam/keys + +`) +} + func init() { exocmd.RootCmd.AddCommand(configCmd) } diff --git a/cmd/config/config_add.go b/cmd/config/config_add.go index bf524c642..5497b49f3 100644 --- a/cmd/config/config_add.go +++ b/cmd/config/config_add.go @@ -24,15 +24,7 @@ func init() { isFirstAccount := account.GAllAccount == nil || len(account.GAllAccount.Accounts) == 0 if isFirstAccount { - fmt.Println("No Exoscale CLI configuration found") - - fmt.Print(` -In order to set up your configuration profile, you will need to retrieve -Exoscale API credentials from your organization's IAM: - - https://portal.exoscale.com/iam/keys - -`) + printNoConfigMessage() } newAccount, err := promptAccountInformation() From e46c0e30970891ea7750aefb6e1ff258170990c9 Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:06:07 +0100 Subject: [PATCH 5/7] docs: fix Exo CLI Bug exo config add panics when new first config without it being the default one --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b36630c..1947f18f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Bug fixes + +- Fix panic when config commands are used without a default account set + ## 1.93.0 ### Features From dffd078e549fe4131c997ffbcc4dc306fd0efd27 Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:44:41 +0100 Subject: [PATCH 6/7] chore: merge multi print when no existing exo config account --- cmd/config/config.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index 52ff3645e..c61471e78 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -248,9 +248,8 @@ func chooseZone(client *v3.Client, zones []string) (string, error) { } func printNoConfigMessage() { - fmt.Println("No Exoscale CLI configuration found") + fmt.Print(`No Exoscale CLI configuration found - fmt.Print(` In order to set up your configuration profile, you will need to retrieve Exoscale API credentials from your organization's IAM: From bf4e3fc1cfbd4470474cb0c01dcf1bcc06334b0f Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:23:03 +0100 Subject: [PATCH 7/7] docs: update CHANGELOG.md Co-authored-by: Pierre-Emmanuel Jacquier <15922119+pierre-emmanuelJ@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1947f18f1..a2bfe6411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Bug fixes -- Fix panic when config commands are used without a default account set +- Fix panic when config commands are used without a default account set #798 ## 1.93.0