From 071497a255bd238f08ec2a0dc4dedda0aa006ebc Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:21:46 +0100 Subject: [PATCH 1/6] feat(test): add testscript integration test framework - Add testscript library for declarative CLI testing - Create test runner with config isolation per test - Add example test scenarios for basic CLI commands - Configure GitHub Actions workflow for CI - Use scenarios/ directory for test files This provides a more maintainable and readable alternative to the existing Go-based integration tests, with automatic config isolation and better support for both local and CI environments. Scenarios directory can be customized by changing Dir parameter in testscript.Run(). Tests use runtime.Caller() to locate CLI root, ensuring correct path resolution regardless of working directory. The framework supports: - Config file isolation via XDG_CONFIG_HOME redirection - Environment variable credential forwarding - Parallel test execution - Both unit tests (no API) and integration tests (with API) --- .github/workflows/testscript.yml | 75 +++++++++++ go.mod | 1 + go.sum | 2 + internal/integ/go.mod | 9 +- internal/integ/go.sum | 6 + internal/integ/scenarios/basic_no_api.txtar | 22 +++ .../integ/scenarios/config_isolated.txtar | 32 +++++ internal/integ/testscript_test.go | 126 ++++++++++++++++++ vendor/modules.txt | 2 + 9 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/testscript.yml create mode 100644 internal/integ/scenarios/basic_no_api.txtar create mode 100644 internal/integ/scenarios/config_isolated.txtar create mode 100644 internal/integ/testscript_test.go diff --git a/.github/workflows/testscript.yml b/.github/workflows/testscript.yml new file mode 100644 index 000000000..5ae08c617 --- /dev/null +++ b/.github/workflows/testscript.yml @@ -0,0 +1,75 @@ +name: Testscript Integration Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + unit-tests: + name: Unit Tests (No API) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Run unit tests + run: | + cd internal/integ + # Only run tests that don't need API access + go test -v -run 'TestScripts/(basic_no_api|config_isolated|example)' + + integration-tests: + name: Integration Tests (With API) + runs-on: ubuntu-latest + # Only run on master branch or when manually triggered + if: github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Create test config + env: + EXOSCALE_API_KEY: ${{ secrets.EXOSCALE_TEST_API_KEY }} + EXOSCALE_API_SECRET: ${{ secrets.EXOSCALE_TEST_API_SECRET }} + run: | + # Testscript will use XDG_CONFIG_HOME, but we can also use env vars + echo "Using environment variables for credentials" + # Or create a config file: + # mkdir -p ~/.config/exoscale + # cat > ~/.config/exoscale/exoscale.toml < Date: Thu, 19 Feb 2026 11:30:59 +0100 Subject: [PATCH 2/6] fix(deps): remove duplicate go-internal entry from go.sum --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 612259ccc..81ebc045f 100644 --- a/go.sum +++ b/go.sum @@ -455,8 +455,6 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= From e231a18b781be275943432555bf37b6919976b26 Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:57:02 +0100 Subject: [PATCH 3/6] docs(test): add TODOs and documentation for API-based integration tests - Remove outdated comment about interactive config add - Add TODO for making test flags parametrizable per scenario - Document need for org account setup in future PR for API tests - Add scenarios/README.md explaining current and future test coverage --- .github/workflows/testscript.yml | 2 ++ internal/integ/scenarios/README.md | 32 +++++++++++++++++++ .../integ/scenarios/config_isolated.txtar | 3 -- internal/integ/testscript_test.go | 3 ++ 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 internal/integ/scenarios/README.md diff --git a/.github/workflows/testscript.yml b/.github/workflows/testscript.yml index 5ae08c617..e0496e0da 100644 --- a/.github/workflows/testscript.yml +++ b/.github/workflows/testscript.yml @@ -28,6 +28,8 @@ jobs: name: Integration Tests (With API) runs-on: ubuntu-latest # Only run on master branch or when manually triggered + # TODO: This job is ready but no API-based test scenarios exist yet. + # A future PR will add API tests requiring org account setup. if: github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch' steps: - uses: actions/checkout@v4 diff --git a/internal/integ/scenarios/README.md b/internal/integ/scenarios/README.md new file mode 100644 index 000000000..64f55163f --- /dev/null +++ b/internal/integ/scenarios/README.md @@ -0,0 +1,32 @@ +# Testscript Integration Test Scenarios + +This directory contains testscript scenarios for integration testing the Exoscale CLI. + +## Current Scenarios + +- **basic_no_api.txtar**: Tests basic CLI functionality without API access (version, help commands) +- **config_isolated.txtar**: Tests config file isolation using `XDG_CONFIG_HOME` + +## Running Tests + +```bash +# Run all scenarios +cd internal/integ +go test -v -run TestScripts + +# Run specific scenario +go test -v -run 'TestScripts/basic_no_api' +``` + +## Future Work + +**TODO**: API-based integration tests will be added in a separate PR. These will require: +- Organization test account setup +- Proper API credentials configuration (`EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`) +- Test scenarios covering: + - Block storage operations (create, resize, snapshot, delete) + - Compute instance operations + - Network resources + - Other API-dependent features + +The CI workflow is already configured to run API tests on the master branch when credentials are available. diff --git a/internal/integ/scenarios/config_isolated.txtar b/internal/integ/scenarios/config_isolated.txtar index 3a89270e8..004aa1841 100644 --- a/internal/integ/scenarios/config_isolated.txtar +++ b/internal/integ/scenarios/config_isolated.txtar @@ -27,6 +27,3 @@ stdout 'EXOtest1234' # Read default account # exec exo config default # stdout 'test-account' - -# Note: This test is commented because 'exo config add' might be interactive -# Uncomment if your CLI has non-interactive flags for testing diff --git a/internal/integ/testscript_test.go b/internal/integ/testscript_test.go index fe8302550..06e5b5822 100644 --- a/internal/integ/testscript_test.go +++ b/internal/integ/testscript_test.go @@ -100,11 +100,14 @@ func TestScripts(t *testing.T) { e.Setenv("HOME", e.WorkDir) // Set default flags that all tests need + // TODO: Make these parametrizable per test scenario e.Setenv("EXO_ZONE", "ch-gva-2") e.Setenv("EXO_OUTPUT", "json") // Forward API credentials from environment (for CI and local API tests) // Tests can use either env vars or config files + // TODO: Currently no scenarios use API credentials. Future PR will add + // API-based tests requiring org account setup with proper credentials. if apiKey := os.Getenv("EXOSCALE_API_KEY"); apiKey != "" { e.Setenv("EXOSCALE_API_KEY", apiKey) } From 690a6e848a34ccd171d3e58c00028a79ff655bfb Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:04:32 +0100 Subject: [PATCH 4/6] chore(ci): comment out API-based integration tests job The integration-tests job is commented out until actual API test scenarios are added in a future PR, since currently no testscript scenarios require API credentials. --- .github/workflows/testscript.yml | 103 ++++++++++++++++--------------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/.github/workflows/testscript.yml b/.github/workflows/testscript.yml index e0496e0da..8a2bbf38c 100644 --- a/.github/workflows/testscript.yml +++ b/.github/workflows/testscript.yml @@ -24,54 +24,55 @@ jobs: # Only run tests that don't need API access go test -v -run 'TestScripts/(basic_no_api|config_isolated|example)' - integration-tests: - name: Integration Tests (With API) - runs-on: ubuntu-latest - # Only run on master branch or when manually triggered - # TODO: This job is ready but no API-based test scenarios exist yet. - # A future PR will add API tests requiring org account setup. - if: github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch' - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' - - - name: Create test config - env: - EXOSCALE_API_KEY: ${{ secrets.EXOSCALE_TEST_API_KEY }} - EXOSCALE_API_SECRET: ${{ secrets.EXOSCALE_TEST_API_SECRET }} - run: | - # Testscript will use XDG_CONFIG_HOME, but we can also use env vars - echo "Using environment variables for credentials" - # Or create a config file: - # mkdir -p ~/.config/exoscale - # cat > ~/.config/exoscale/exoscale.toml < ~/.config/exoscale/exoscale.toml < Date: Thu, 19 Feb 2026 12:08:39 +0100 Subject: [PATCH 5/6] refactor(test): use pre-built binary instead of building in tests Instead of building the binary within the test code, use the binary from the existing build pipeline (bin/exo). This: - Eliminates redundant builds in test code - Tests the actual build artifact from the pipeline - Aligns with existing test infrastructure (blockstorage_test.go) - Improves test execution speed GitHub Actions workflow now runs 'make build' before tests. --- .github/workflows/testscript.yml | 6 ++++ internal/integ/scenarios/README.md | 5 +++ internal/integ/testscript_test.go | 51 +++++++----------------------- 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/.github/workflows/testscript.yml b/.github/workflows/testscript.yml index 8a2bbf38c..7f523d728 100644 --- a/.github/workflows/testscript.yml +++ b/.github/workflows/testscript.yml @@ -18,6 +18,9 @@ jobs: with: go-version-file: 'go.mod' + - name: Build binary + run: make build + - name: Run unit tests run: | cd internal/integ @@ -40,6 +43,9 @@ jobs: # with: # go-version-file: 'go.mod' # + # - name: Build binary + # run: make build + # # - name: Create test config # env: # EXOSCALE_API_KEY: ${{ secrets.EXOSCALE_TEST_API_KEY }} diff --git a/internal/integ/scenarios/README.md b/internal/integ/scenarios/README.md index 64f55163f..4edb1fe0d 100644 --- a/internal/integ/scenarios/README.md +++ b/internal/integ/scenarios/README.md @@ -9,7 +9,12 @@ This directory contains testscript scenarios for integration testing the Exoscal ## Running Tests +Tests use the pre-built binary from the existing build pipeline. Build it first: + ```bash +# Build the CLI binary (from repository root) +make build + # Run all scenarios cd internal/integ go test -v -run TestScripts diff --git a/internal/integ/testscript_test.go b/internal/integ/testscript_test.go index 06e5b5822..73af413da 100644 --- a/internal/integ/testscript_test.go +++ b/internal/integ/testscript_test.go @@ -6,17 +6,14 @@ import ( "os/exec" "path/filepath" "runtime" - "sync" "testing" "github.com/rogpeppe/go-internal/testscript" ) var ( - buildOnce sync.Once - buildError error - exoBinary string - cliRoot string // Set at init time before working directory changes + exoBinary string + cliRoot string // Set at init time before working directory changes ) func init() { @@ -29,6 +26,10 @@ func init() { // filename is .../cli/internal/integ/testscript_test.go // We need .../cli cliRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) + + // Use pre-built binary from the existing build pipeline + // Tests should run against the actual build artifact, not rebuild it + exoBinary = filepath.Join(cliRoot, "bin", "exo") } func TestMain(m *testing.M) { @@ -37,45 +38,15 @@ func TestMain(m *testing.M) { })) } -// buildExoBinary builds the exo CLI binary once for all tests -func buildExoBinary() error { - buildOnce.Do(func() { - // Verify go.mod exists - if _, err := os.Stat(filepath.Join(cliRoot, "go.mod")); err != nil { - buildError = fmt.Errorf("go.mod not found in %s: %w", cliRoot, err) - return - } - - // Create bin directory if it doesn't exist - binDir := filepath.Join(cliRoot, "bin") - if err := os.MkdirAll(binDir, 0755); err != nil { - buildError = fmt.Errorf("failed to create bin directory: %w", err) - return - } - - exoBinary = filepath.Join(binDir, "exo") - - // Build the binary - use the main.go path explicitly - mainPath := filepath.Join(cliRoot, "main.go") - cmd := exec.Command("go", "build", "-o", exoBinary, mainPath) - cmd.Dir = cliRoot - output, err := cmd.CombinedOutput() - if err != nil { - buildError = fmt.Errorf("failed to build exo binary: %w\n%s", err, output) - return - } - }) - return buildError -} - func mainExo() int { - // Build binary once if not already built - if err := buildExoBinary(); err != nil { - fmt.Fprintf(os.Stderr, "build error: %v\n", err) + // Check if binary exists + if _, err := os.Stat(exoBinary); err != nil { + fmt.Fprintf(os.Stderr, "exo binary not found at %s\n", exoBinary) + fmt.Fprintf(os.Stderr, "Please build the binary first: make build\n") return 1 } - // Run the compiled binary + // Run the pre-built binary cmd := exec.Command(exoBinary, os.Args[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr From 714681621a6aca87476241260b257ca7c5e2f8ea Mon Sep 17 00:00:00 2001 From: Natalie Perret <11332444+natalie-o-perret@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:57:47 +0100 Subject: [PATCH 6/6] refactor: reorganize tests into declarative e2e and programmatic integ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move testscript scenarios from internal/integ/ to tests/e2e/ - Move programmatic tests from internal/integ/ to tests/integ/ - Rename unit/integration terminology to local/api for clarity - Update build tags from 'integration' to 'api' - Rename test functions: TestScriptsUnit → TestScriptsLocal, TestScriptsIntegration → TestScriptsAPI - Update GitHub Actions workflow for new structure and build tags - Update documentation to reflect new organization This provides cleaner separation between: - tests/e2e/: Declarative testscript scenarios (local vs api) - tests/integ/: Programmatic Go tests with suite helpers Both test approaches are end-to-end from CLI user perspective, just different implementation styles. --- .github/workflows/testscript.yml | 30 +++--- internal/integ/scenarios/README.md | 37 ------- internal/integ/testscript_test.go | 100 ------------------ {internal/integ => tests/e2e}/go.mod | 0 {internal/integ => tests/e2e}/go.sum | 0 tests/e2e/scenarios/README.md | 62 +++++++++++ .../e2e/scenarios/local}/basic_no_api.txtar | 0 .../scenarios/local}/config_isolated.txtar | 0 tests/e2e/testscript_api_test.go | 25 +++++ tests/e2e/testscript_local_test.go | 47 ++++++++ tests/e2e/testscript_test.go | 61 +++++++++++ .../integ/blockstorage_test.go | 0 tests/integ/go.mod | 16 +++ tests/integ/go.sum | 16 +++ {internal/integ/test => tests/integ}/suite.go | 2 +- 15 files changed, 242 insertions(+), 154 deletions(-) delete mode 100644 internal/integ/scenarios/README.md delete mode 100644 internal/integ/testscript_test.go rename {internal/integ => tests/e2e}/go.mod (100%) rename {internal/integ => tests/e2e}/go.sum (100%) create mode 100644 tests/e2e/scenarios/README.md rename {internal/integ/scenarios => tests/e2e/scenarios/local}/basic_no_api.txtar (100%) rename {internal/integ/scenarios => tests/e2e/scenarios/local}/config_isolated.txtar (100%) create mode 100644 tests/e2e/testscript_api_test.go create mode 100644 tests/e2e/testscript_local_test.go create mode 100644 tests/e2e/testscript_test.go rename {internal => tests}/integ/blockstorage_test.go (100%) create mode 100644 tests/integ/go.mod create mode 100644 tests/integ/go.sum rename {internal/integ/test => tests/integ}/suite.go (99%) diff --git a/.github/workflows/testscript.yml b/.github/workflows/testscript.yml index 7f523d728..85ffe9f40 100644 --- a/.github/workflows/testscript.yml +++ b/.github/workflows/testscript.yml @@ -1,4 +1,4 @@ -name: Testscript Integration Tests +name: E2E Testscript Tests on: push: @@ -7,8 +7,8 @@ on: branches: [ master ] jobs: - unit-tests: - name: Unit Tests (No API) + local-tests: + name: Local Tests (No API) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -21,19 +21,17 @@ jobs: - name: Build binary run: make build - - name: Run unit tests + - name: Run local tests run: | - cd internal/integ - # Only run tests that don't need API access - go test -v -run 'TestScripts/(basic_no_api|config_isolated|example)' + cd tests/e2e + # Run local tests (no build tag required) + go test -v - # TODO: Commented out until API-based test scenarios are added in a future PR - # integration-tests: - # name: Integration Tests (With API) + # TODO: Uncomment when API-based test scenarios are added to scenarios/api/ + # api-tests: + # name: API Tests (With API) # runs-on: ubuntu-latest # # Only run on master branch or when manually triggered - # # TODO: This job is ready but no API-based test scenarios exist yet. - # # A future PR will add API tests requiring org account setup. # if: github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch' # steps: # - uses: actions/checkout@v4 @@ -64,14 +62,14 @@ jobs: # # secret = "$EXOSCALE_API_SECRET" # # EOF # - # - name: Run integration tests + # - name: Run API tests # env: # EXOSCALE_API_KEY: ${{ secrets.EXOSCALE_TEST_API_KEY }} # EXOSCALE_API_SECRET: ${{ secrets.EXOSCALE_TEST_API_SECRET }} # run: | - # cd internal/integ - # # Run all tests (including API tests) - # go test -v -run TestScripts -timeout 30m + # cd tests/e2e + # # Run API tests with build tag + # go test -v -tags=api -timeout 30m # # - name: Cleanup test resources # if: always() diff --git a/internal/integ/scenarios/README.md b/internal/integ/scenarios/README.md deleted file mode 100644 index 4edb1fe0d..000000000 --- a/internal/integ/scenarios/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Testscript Integration Test Scenarios - -This directory contains testscript scenarios for integration testing the Exoscale CLI. - -## Current Scenarios - -- **basic_no_api.txtar**: Tests basic CLI functionality without API access (version, help commands) -- **config_isolated.txtar**: Tests config file isolation using `XDG_CONFIG_HOME` - -## Running Tests - -Tests use the pre-built binary from the existing build pipeline. Build it first: - -```bash -# Build the CLI binary (from repository root) -make build - -# Run all scenarios -cd internal/integ -go test -v -run TestScripts - -# Run specific scenario -go test -v -run 'TestScripts/basic_no_api' -``` - -## Future Work - -**TODO**: API-based integration tests will be added in a separate PR. These will require: -- Organization test account setup -- Proper API credentials configuration (`EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`) -- Test scenarios covering: - - Block storage operations (create, resize, snapshot, delete) - - Compute instance operations - - Network resources - - Other API-dependent features - -The CI workflow is already configured to run API tests on the master branch when credentials are available. diff --git a/internal/integ/testscript_test.go b/internal/integ/testscript_test.go deleted file mode 100644 index 73af413da..000000000 --- a/internal/integ/testscript_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package integ_test - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "testing" - - "github.com/rogpeppe/go-internal/testscript" -) - -var ( - exoBinary string - cliRoot string // Set at init time before working directory changes -) - -func init() { - // Use runtime.Caller to get the actual source file location - // This works regardless of current working directory - _, filename, _, ok := runtime.Caller(0) - if !ok { - panic("failed to get source file location") - } - // filename is .../cli/internal/integ/testscript_test.go - // We need .../cli - cliRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) - - // Use pre-built binary from the existing build pipeline - // Tests should run against the actual build artifact, not rebuild it - exoBinary = filepath.Join(cliRoot, "bin", "exo") -} - -func TestMain(m *testing.M) { - os.Exit(testscript.RunMain(m, map[string]func() int{ - "exo": mainExo, - })) -} - -func mainExo() int { - // Check if binary exists - if _, err := os.Stat(exoBinary); err != nil { - fmt.Fprintf(os.Stderr, "exo binary not found at %s\n", exoBinary) - fmt.Fprintf(os.Stderr, "Please build the binary first: make build\n") - return 1 - } - - // Run the pre-built binary - cmd := exec.Command(exoBinary, os.Args[1:]...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - if err := cmd.Run(); err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - return exitErr.ExitCode() - } - return 1 - } - return 0 -} - -func TestScripts(t *testing.T) { - testscript.Run(t, testscript.Params{ - Dir: "scenarios", - Setup: func(e *testscript.Env) error { - // Redirect config directory to test's temp directory - // This isolates config file changes per test - configDir := filepath.Join(e.WorkDir, ".config") - e.Setenv("XDG_CONFIG_HOME", configDir) - e.Setenv("HOME", e.WorkDir) - - // Set default flags that all tests need - // TODO: Make these parametrizable per test scenario - e.Setenv("EXO_ZONE", "ch-gva-2") - e.Setenv("EXO_OUTPUT", "json") - - // Forward API credentials from environment (for CI and local API tests) - // Tests can use either env vars or config files - // TODO: Currently no scenarios use API credentials. Future PR will add - // API-based tests requiring org account setup with proper credentials. - if apiKey := os.Getenv("EXOSCALE_API_KEY"); apiKey != "" { - e.Setenv("EXOSCALE_API_KEY", apiKey) - } - if apiSecret := os.Getenv("EXOSCALE_API_SECRET"); apiSecret != "" { - e.Setenv("EXOSCALE_API_SECRET", apiSecret) - } - - // Alternatively, copy real config for tests that need it: - // (Uncomment if you prefer config file over env vars) - // if realConfig, err := os.ReadFile(filepath.Join(os.Getenv("HOME"), ".config/exoscale/exoscale.toml")); err == nil { - // testConfigPath := filepath.Join(configDir, "exoscale", "exoscale.toml") - // os.MkdirAll(filepath.Dir(testConfigPath), 0755) - // os.WriteFile(testConfigPath, realConfig, 0644) - // } - - return nil - }, - }) -} diff --git a/internal/integ/go.mod b/tests/e2e/go.mod similarity index 100% rename from internal/integ/go.mod rename to tests/e2e/go.mod diff --git a/internal/integ/go.sum b/tests/e2e/go.sum similarity index 100% rename from internal/integ/go.sum rename to tests/e2e/go.sum diff --git a/tests/e2e/scenarios/README.md b/tests/e2e/scenarios/README.md new file mode 100644 index 000000000..a555edaa5 --- /dev/null +++ b/tests/e2e/scenarios/README.md @@ -0,0 +1,62 @@ +# E2E Testscript Scenarios + +This directory contains testscript scenarios for end-to-end testing the Exoscale CLI. + +## Directory Structure + +- **local/**: Test scenarios that don't require API access (run by default) +- **api/**: Test scenarios that require API access (run with `-tags=api`) + +## Current Scenarios + +### Local Tests (scenarios/local/) + +- **basic_no_api.txtar**: Tests basic CLI functionality without API access (version, help commands) +- **config_isolated.txtar**: Tests config file isolation using `XDG_CONFIG_HOME` + +### API Tests (scenarios/api/) + +No API test scenarios yet. These will be added in a future PR. + +## Running Tests + +Tests use the pre-built binary from the existing build pipeline. Build it first: + +```bash +# Build the CLI binary (from repository root) +make build + +# Run local tests only (default - no build tag needed) +cd tests/e2e +go test -v + +# Run API tests only (requires API credentials) +cd tests/e2e +go test -v -tags=api + +# Run all tests (local + API) +cd tests/e2e +go test -v -tags=api +``` + +## Using Build Tags + +The test suite uses Go build tags to separate local tests from API tests: + +- **Local tests** (`testscript_local_test.go`): No build tag, runs by default +- **API tests** (`testscript_api_test.go`): Requires `-tags=api` build tag + +This approach is more maintainable than regex filtering and follows Go best practices. + +## Future Work + +**TODO**: API-based tests will be added in a separate PR to `scenarios/api/`. These will require: +- Organization test account setup +- Proper API credentials configuration (`EXOSCALE_API_KEY`, `EXOSCALE_API_SECRET`) +- Test scenarios covering: + - Block storage operations (create, resize, snapshot, delete) + - Compute instance operations + - Network resources + - Other API-dependent features + +The CI workflow is already configured to run API tests on the master branch when credentials are available. diff --git a/internal/integ/scenarios/basic_no_api.txtar b/tests/e2e/scenarios/local/basic_no_api.txtar similarity index 100% rename from internal/integ/scenarios/basic_no_api.txtar rename to tests/e2e/scenarios/local/basic_no_api.txtar diff --git a/internal/integ/scenarios/config_isolated.txtar b/tests/e2e/scenarios/local/config_isolated.txtar similarity index 100% rename from internal/integ/scenarios/config_isolated.txtar rename to tests/e2e/scenarios/local/config_isolated.txtar diff --git a/tests/e2e/testscript_api_test.go b/tests/e2e/testscript_api_test.go new file mode 100644 index 000000000..dc6a4aac3 --- /dev/null +++ b/tests/e2e/testscript_api_test.go @@ -0,0 +1,25 @@ +//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 new file mode 100644 index 000000000..1dbfd19c3 --- /dev/null +++ b/tests/e2e/testscript_local_test.go @@ -0,0 +1,47 @@ +package e2e_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/rogpeppe/go-internal/testscript" +) + +// TestScriptsLocal runs testscript scenarios that don't require API access. +// These tests run by default without any build tags. +func TestScriptsLocal(t *testing.T) { + testscript.Run(t, testscript.Params{ + Dir: "scenarios/local", + Setup: func(e *testscript.Env) error { + return setupTestEnv(e, false) + }, + }) +} + +// setupTestEnv configures the test environment for testscript scenarios. +// withAPI controls whether API credentials should be forwarded. +func setupTestEnv(e *testscript.Env, withAPI bool) error { + // Redirect config directory to test's temp directory + // This isolates config file changes per test + configDir := filepath.Join(e.WorkDir, ".config") + e.Setenv("XDG_CONFIG_HOME", configDir) + e.Setenv("HOME", e.WorkDir) + + // Set default flags that all tests need + // TODO: Make these parametrizable per test scenario + e.Setenv("EXO_ZONE", "ch-gva-2") + e.Setenv("EXO_OUTPUT", "json") + + // Forward API credentials if requested (for integration tests) + if withAPI { + if apiKey := os.Getenv("EXOSCALE_API_KEY"); apiKey != "" { + e.Setenv("EXOSCALE_API_KEY", apiKey) + } + if apiSecret := os.Getenv("EXOSCALE_API_SECRET"); apiSecret != "" { + e.Setenv("EXOSCALE_API_SECRET", apiSecret) + } + } + + return nil +} diff --git a/tests/e2e/testscript_test.go b/tests/e2e/testscript_test.go new file mode 100644 index 000000000..86d114ee5 --- /dev/null +++ b/tests/e2e/testscript_test.go @@ -0,0 +1,61 @@ +package e2e_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + + "github.com/rogpeppe/go-internal/testscript" +) + +var ( + exoBinary string + cliRoot string // Set at init time before working directory changes +) + +func init() { + // Use runtime.Caller to get the actual source file location + // This works regardless of current working directory + _, filename, _, ok := runtime.Caller(0) + if !ok { + panic("failed to get source file location") + } + // filename is .../cli/internal/integ/testscript_test.go + // We need .../cli + cliRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) + + // Use pre-built binary from the existing build pipeline + // Tests should run against the actual build artifact, not rebuild it + exoBinary = filepath.Join(cliRoot, "bin", "exo") +} + +func TestMain(m *testing.M) { + os.Exit(testscript.RunMain(m, map[string]func() int{ + "exo": mainExo, + })) +} + +func mainExo() int { + // Check if binary exists + if _, err := os.Stat(exoBinary); err != nil { + fmt.Fprintf(os.Stderr, "exo binary not found at %s\n", exoBinary) + fmt.Fprintf(os.Stderr, "Please build the binary first: make build\n") + return 1 + } + + // Run the pre-built binary + cmd := exec.Command(exoBinary, os.Args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + if err := cmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return exitErr.ExitCode() + } + return 1 + } + return 0 +} diff --git a/internal/integ/blockstorage_test.go b/tests/integ/blockstorage_test.go similarity index 100% rename from internal/integ/blockstorage_test.go rename to tests/integ/blockstorage_test.go diff --git a/tests/integ/go.mod b/tests/integ/go.mod new file mode 100644 index 000000000..33006f4f1 --- /dev/null +++ b/tests/integ/go.mod @@ -0,0 +1,16 @@ +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/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 new file mode 100644 index 000000000..bb1a653f5 --- /dev/null +++ b/tests/integ/go.sum @@ -0,0 +1,16 @@ +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= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/integ/test/suite.go b/tests/integ/suite.go similarity index 99% rename from internal/integ/test/suite.go rename to tests/integ/suite.go index 795f4e3af..695bb2fc7 100644 --- a/internal/integ/test/suite.go +++ b/tests/integ/suite.go @@ -1,4 +1,4 @@ -package test +package integ_test import ( "bytes"