Skip to content
Open
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
9 changes: 7 additions & 2 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@ on:
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 50
# Only run if secrets are available (skip on forks).
if: github.repository == 'api7/a7'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
go-version: '1.23.0'

- name: Download Go modules
run: go mod download

- name: Run E2E tests
env:
A7_ADMIN_URL: ${{ secrets.DEMO_WEBSITE }}
A7_TOKEN: ${{ secrets.DEMO_TOKEN }}
A7_GATEWAY_GROUP: default
A7_GATEWAY_URL: ${{ secrets.A7_GATEWAY_URL }}
HTTPBIN_URL: ${{ secrets.HTTPBIN_URL }}
run: make test-e2e
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ docker-down:
docker compose -f test/e2e/docker-compose.yml down -v

test-e2e:
go test ./test/e2e/... -count=1 -v -tags=e2e -timeout 25m
go run github.com/onsi/ginkgo/v2/ginkgo -r --procs=1 --tags=e2e --timeout=45m ./test/e2e/...
214 changes: 59 additions & 155 deletions docs/testing-strategy.md
Original file line number Diff line number Diff line change
@@ -1,183 +1,87 @@
# Testing Strategy

## Test Requirements
- Every exported function must have at least one corresponding test.
- Every command must be tested for:
- Success cases
- Error cases
- TTY output (Table)
- Non-TTY output (JSON)
- Aim for a code coverage target of 80% or higher for packages within the `pkg/` directory.
## Principles

## Test File Location
Tests should be located in the same directory as the code they test. For example, `list.go` should have its tests in `list_test.go`.
- CLI behavior that depends on API7 EE or APISIX Admin API must be verified with E2E tests against a real environment.
- Unit tests are only allowed for self-contained logic that does not need mocked external systems.
- Do not add new command-level tests that mock the Admin API, gateway, or control-plane behavior.
- New or modified E2E coverage must be written with Ginkgo.

Store test fixtures in `test/fixtures/<resource>_<action>.json`.
## Test Pyramid For `a7`

## Test Naming Convention
Follow the pattern `func Test<Function>_<Scenario>(t *testing.T) {}`.
### Pure unit tests

Allowed targets:

- parsing and normalization helpers
- config merge / override rules
- output formatting helpers
- deterministic business logic with no network, process, or filesystem side effects beyond temp files

Examples:
- `func TestRouteList_ReturnsTable(t *testing.T) {}`
- `func TestRouteList_EmptyResponse(t *testing.T) {}`
- `func TestRouteList_APIError(t *testing.T) {}`
- `func TestRouteList_JSONOutput(t *testing.T) {}`
- `func TestRouteList_NonTTY(t *testing.T) {}`

## HTTP Mocking Pattern
Use the project's internal `pkg/httpmock` package instead of external mock libraries.

```go
func TestRouteList_Success(t *testing.T) {
// 1. Create mock registry
reg := &httpmock.Registry{}

// 2. Register expected request and response
reg.Register(
http.MethodGet,
"/apisix/admin/routes", // Full path with dual-API prefix
httpmock.JSONResponse("../../../../test/fixtures/route_list.json"),
)

// 3. Create test factory with mock dependencies
ios, _, out, _ := iostreams.Test()
f := &cmd.Factory{
IOStreams: ios,
HttpClient: func() (*http.Client, error) {
return reg.GetClient(), nil
},
Config: func() (config.Config, error) {
return &mockConfig{
baseURL: "https://localhost:7443",
token: "a7ee-test-token",
gatewayGroup: "default",
}, nil
},
}

// 4. Create and execute command
cmd := list.NewCmdList(f)
err := cmd.Execute()

// 5. Verify results
require.NoError(t, err)
assert.Contains(t, out.String(), "users-api")
reg.Verify(t)
}
```

## Test Categories

### Unit Tests
Required for every command to verify:
- Command flag parsing
- HTTP request construction (URL, query parameters including `gateway_group_id`)
- Response parsing (handles `ListResponse[T]` and `SingleResponse[T]`)
- Output formatting for both table and JSON
- Error handling for API errors, network issues, and authentication failures

### TTY vs Non-TTY Tests
Every command must have tests for both TTY and non-TTY environments:

```go
func TestRouteList_TTY(t *testing.T) {
ios, _, _, _ := iostreams.Test()
ios.SetStdoutTTY(true)
// Verify table output
}

func TestRouteList_NonTTY(t *testing.T) {
ios, _, _, _ := iostreams.Test()
ios.SetStdoutTTY(false)
// Verify JSON output
}
```
- `internal/config`
- `internal/update`
- helper logic in `pkg/cmd/debug/logs`
- helper logic in `pkg/cmd/debug/trace`
- API client envelope / transport helpers that can be tested with stub round trippers instead of HTTP servers

## Test Fixtures
- **Location**: `test/fixtures/`
- **Naming**: `<resource>_<action>.json` (e.g., `route_list.json`)
- **Content**: Use realistic API7 EE responses. Redact any sensitive data.
### E2E tests

## What NOT to Test
- Do not test cobra flag binding, as this is handled by the cobra framework.
- Do not test JSON marshaling, which is the responsibility of the standard library.
- Avoid writing integration tests against a real API7 EE instance in unit test files — use e2e tests for that.
Required targets:

## E2E Tests
- all command flows that talk to API7 EE or APISIX Admin API
- CRUD coverage for runtime resources
- traffic assertions that require a real gateway
- auth, route, service, secret, stream-route, and debug flows

E2E tests validate the CLI binary against a real API7 EE environment. They live in `test/e2e/` and use the `//go:build e2e` build tag.
E2E tests live in `test/e2e/` behind the `e2e` build tag and are run with Ginkgo.

### Infrastructure
## What To Remove

E2E tests require a running API7 EE instance:
- Existing unit tests that rely on `pkg/httpmock`
- Existing unit tests that spin up fake HTTP servers to emulate Admin API behavior for command coverage
- Any new tests that validate CLI request construction by mocking remote APIs instead of exercising the real system

| Variable | Default | Purpose |
|---------|---------|---------|
| `A7_SERVER` | `https://127.0.0.1:7443` | API7 EE Control-plane URL |
| `A7_TOKEN` | (required) | API Access Token |
| `A7_GATEWAY_GROUP` | `default` | Gateway Group for tests |
## Local E2E Environment

### Running E2E Tests
Required environment variables:

**Locally** (requires API7 EE accessible):
```bash
export A7_SERVER=https://your-instance:7443
export A7_TOKEN=a7ee-your-token
make test-e2e
```
- `A7_ADMIN_URL`
- `A7_TOKEN`

### E2E Test File Structure
Optional but strongly recommended for traffic coverage:

- `test/e2e/setup_test.go` — `TestMain`, helper functions (`runA7`, `adminAPI`)
- `test/e2e/smoke_test.go` — Basic connectivity checks
- `test/e2e/<resource>_test.go` — Per-resource CRUD lifecycle tests
- `A7_GATEWAY_URL`
- `HTTPBIN_URL`
- `A7_GATEWAY_GROUP`

### Writing E2E Tests
## Running Tests

```go
//go:build e2e
Pure unit tests:

package e2e
```bash
go test ./... -count=1
```

import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
E2E tests:

func TestRoute_CRUD(t *testing.T) {
// 1. Create a route via CLI
stdout, _, err := runA7("route", "create", "--name", "test-route", "--uris", "/test", "--gateway-group", "default")
require.NoError(t, err)
assert.Contains(t, stdout, "created")
```bash
make test-e2e
```

// 2. List via CLI
stdout, _, err = runA7("route", "list", "--gateway-group", "default")
require.NoError(t, err)
assert.Contains(t, stdout, "test-route")
Equivalent direct command:

// 3. Cleanup: delete via CLI
_, _, err = runA7("route", "delete", "test-route", "--gateway-group", "default")
require.NoError(t, err)
}
```bash
go run github.com/onsi/ginkgo/v2/ginkgo -r --procs=1 --tags=e2e --timeout=45m ./test/e2e/...
```

## Running Tests
Use the following commands to run tests:
- `make test`: Runs all unit tests with race detection.
- `make test-verbose`: Runs unit tests with verbose output.
- `make test-e2e`: Runs E2E tests (requires configured environment).
- `make coverage`: Generates and opens a coverage report.

## Assertions
Use the `testify` library for assertions:
```go
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

require.NoError(t, err) // Fatal if an error occurs
assert.Equal(t, expected, actual) // Continue if the assertion fails
assert.Contains(t, output, "ID") // Check for a substring
```
## Authoring Guidance

- Prefer `Ordered` suites only when resource lifecycle or environment reuse requires it.
- Use shared helpers for context creation, cleanup, and propagation polling.
- When live gateway behavior may depend on optional local infrastructure, fail only on product regressions and `Skip` on missing external capabilities.
- Keep E2E assertions aligned with the current EE data model:
- routes should use `service_id`
- auth flows should use `consumer create + credential create`
- file-based updates should not require redundant flags that are already present in the payload
16 changes: 15 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
module github.com/api7/a7

go 1.22.3
go 1.23.0
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module now requires Go 1.23.0, which needs to be consistently reflected across CI (and any pinned local dev tooling). Ensure all workflows and release/build pipelines that use go-version or pinned toolchains are updated to Go 1.23.x to avoid build failures.

Copilot uses AI. Check for mistakes.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

require (
github.com/onsi/ginkgo/v2 v2.27.2
github.com/onsi/gomega v1.38.2
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.10.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
)
60 changes: 59 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
@@ -1,19 +1,77 @@
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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=
Loading
Loading