Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/testscript.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: E2E Testscript Tests

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
local-tests:
name: Local 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: Build binary
run: make build

- name: Run local tests
run: |
cd tests/e2e
# Run local tests (no build tag required)
go test -v

# 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
# 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: Build binary
# run: make build
#
# - 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 <<EOF
# # default_account = "ci-test"
# #
# # [[accounts]]
# # name = "ci-test"
# # key = "$EXOSCALE_API_KEY"
# # secret = "$EXOSCALE_API_SECRET"
# # EOF
#
# - name: Run API tests
# env:
# EXOSCALE_API_KEY: ${{ secrets.EXOSCALE_TEST_API_KEY }}
# EXOSCALE_API_SECRET: ${{ secrets.EXOSCALE_TEST_API_SECRET }}
# run: |
# cd tests/e2e
# # Run API tests with build tag
# go test -v -tags=api -timeout 30m
#
# - name: Cleanup test resources
# if: always()
# env:
# EXOSCALE_API_KEY: ${{ secrets.EXOSCALE_TEST_API_KEY }}
# EXOSCALE_API_SECRET: ${{ secrets.EXOSCALE_API_SECRET }}
# run: |
# # Optional: Clean up any leaked test resources
# # ./cleanup-test-resources.sh || true
# echo "Cleanup complete"
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ require (
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,8 @@ 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=
github.com/rs/zerolog v1.11.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
Expand Down
9 changes: 7 additions & 2 deletions internal/integ/go.mod → tests/e2e/go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
module github.com/exoscale/cli/internal/integ

go 1.22.2
go 1.23

require github.com/stretchr/testify v1.9.0
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
)
6 changes: 6 additions & 0 deletions internal/integ/go.sum → tests/e2e/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ 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=
Expand Down
62 changes: 62 additions & 0 deletions tests/e2e/scenarios/README.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions tests/e2e/scenarios/local/basic_no_api.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# This test runs commands that don't need API access
# Perfect for understanding how testscript works ~~

# Test 1: Version command
exec exo version
stdout 'exo'
! stderr .

# Test 2: Help output
exec exo --help
stdout 'Usage:'
stdout 'Available Commands:'
! stderr 'error'

# Test 3: Zone list (just checking command works)
exec exo zone --help
stdout 'zone'
! stderr .

# Test 4: Invalid command should fail
! exec exo not-a-real-command
stderr .
29 changes: 29 additions & 0 deletions tests/e2e/scenarios/local/config_isolated.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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'
25 changes: 25 additions & 0 deletions tests/e2e/testscript_api_test.go
Original file line number Diff line number Diff line change
@@ -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)
},
})
}
47 changes: 47 additions & 0 deletions tests/e2e/testscript_local_test.go
Original file line number Diff line number Diff line change
@@ -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
}
61 changes: 61 additions & 0 deletions tests/e2e/testscript_test.go
Original file line number Diff line number Diff line change
@@ -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
}
File renamed without changes.
Loading