Skip to content
213 changes: 213 additions & 0 deletions docs/ga-test-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# a7 GA Test Plan — Local API7 EE 3.9.12

Execution plan for **Task #3 (real CLI smoke tests)** and **Task #4 (complete E2E coverage)**
from the GA Readiness Handoff. Run against a locally deployed API7 EE.

## Scope

- **In scope**: management-plane validation of the `a7` CLI — CRUD round-trips, declarative
config, output formats, error handling, and confirmation that unsupported resources are
removed/blocked.
- **Out of scope**: real gateway data-plane traffic forwarding. Per the handoff, traffic
tests stay in the gateway repository. The separately-running gateway container is only
needed to keep the EE deployment healthy; we do **not** assert on proxied traffic here.

## Local environment (assumed already running)

| Component | How it runs | Notes |
|---|---|---|
| API7 EE dashboard / control-plane + dp-manager + PostgreSQL | docker compose | the management API `a7` talks to |
| API7 gateway | standalone docker container | connected to the dp-manager; health only |

### 0. Pre-flight — capture connection facts

Fill these in before doing anything else:

```
A7_ADMIN_URL = https://localhost:7443 # control-plane HTTPS port — confirm
A7_TOKEN = a7ee-<your-access-token> # access token from the dashboard UI / API
A7_GATEWAY_GROUP = default # resolved to a UUID at runtime
```
Comment thread
shreemaan-abhishek marked this conversation as resolved.

Confirm before proceeding:

1. **Reachability** — `curl -k -H "X-API-KEY: $A7_TOKEN" $A7_ADMIN_URL/api/gateway_groups`
returns `200` with a non-empty `list`.
2. **Version is 3.9.12** — check the dashboard image tag in `docker compose ps` /
`docker inspect`, or the dashboard `/api/version` endpoint. Record the exact version.
If it is **not** 3.9.12, note the deviation in the report — the handoff specifically
wants 3.9.12 validated.
3. **Gateway is registered & healthy** — the standalone gateway container shows up under
the gateway group (dashboard UI or `GET /api/gateway_groups/{id}/instances`). This only
needs to be healthy; we are not testing traffic through it.

## Build

```bash
cd <path-to-a7-repo>
make build # -> ./bin/a7
./bin/a7 version # record the version string in the report
```

All manual steps below use `./bin/a7`. The automated suite (Phase A) builds its own binary.
Comment thread
shreemaan-abhishek marked this conversation as resolved.

---

## Phase A — Run the existing automated E2E suite

Establishes a known-good baseline against the local 3.9.12 instance before manual work.

```bash
cd <path-to-a7-repo>
export A7_ADMIN_URL="https://localhost:7443"
export A7_TOKEN="a7ee.xxxxxxxx"
export A7_GATEWAY_GROUP="default"
# A7_GATEWAY_URL and HTTPBIN_URL intentionally left UNSET — data-plane tests are skipped.
make test-e2e
```

**Expected**: suite passes, or fails only in ways explained by known gaps. For every
failure, decide: real bug, missing/incorrect test, or environment issue. Record each one.

> The standard `test/e2e/docker-compose.yml` is **not** used here — you already have a
> local deployment. Only the env vars above are needed.

---

## Phase B — Manual CRUD smoke tests (per resource)

For **each** resource below, run the handoff's round-trip pattern with `./bin/a7`:

1. `create` (both flag-based and `-f file` where supported)
2. `get` — assert the returned config matches what was sent
3. `list` — assert the new resource appears
4. `update` — change a field
5. `get` again — assert the update took effect
6. `export` (where supported) — assert valid YAML/JSON
7. `delete` (try with and without `--force`)
8. `get` / `list` — assert it is gone

Also exercise, at least once per resource: `-o json`, `-o yaml`, and default table output;
plus one deliberate error case (bad ID, missing `--gateway-group`, malformed `-f` file) and
confirm the error message is clear and actionable.

**Use unique resource IDs** (`a7-ga-<resource>-<timestamp>`) and clean up everything, even
on a local instance — it keeps reruns deterministic.

### Resource checklist

| Resource | Command | create/get/list/update/delete | export | Notes |
|---|---|:--:|:--:|---|
| context | `a7 context` | ☐ | — | create/use/list/current/delete; no gateway group needed |
| gateway-group | `a7 gateway-group` | ☐ | — | control-plane `/api/*` |
| service | `a7 service` | ☐ | ☐ | inline upstream — **node array format only** |
| route | `a7 route` | ☐ | ☐ | requires `--service-id`; route is service-centered |
| consumer | `a7 consumer` | ☐ | ☐ | |
| credential | `a7 credential` | ☐ | — | nested under a consumer |
| ssl | `a7 ssl` | ☐ | ☐ | **shared/global** — unique IDs, careful cleanup |
| secret | `a7 secret` | ☐ | — | **shared/global** — unique IDs, careful cleanup |
| global-rule | `a7 global-rule` | ☐ | ☐ | **shared/global** — affects whole gateway group |
| plugin | `a7 plugin` | ☐ (list/get only) | — | read-only: list plugins, get schema |
| plugin-metadata | `a7 plugin-metadata` | ☐ (no list) | — | **shared/global**; keyed by plugin name |
| proto | `a7 proto` | ☐ | ☐ | protobuf definitions |
| stream-route | `a7 stream-route` | ☐ | ☐ | **confirmed exposed** by the CP — must work |

> **Shared/global resources** (ssl, secret, global-rule, plugin-metadata) are visible
> gateway-group-wide. On a shared environment they collide between CI runs; even locally,
> assert you only touch IDs you created and never delete pre-existing resources.

### Inline upstream format (API7 EE)

`nodes` must be an **array of objects**. The APISIX map form is rejected.

```yaml
upstream:
type: roundrobin
nodes:
- host: 127.0.0.1
port: 8080
weight: 1
```

---

## Phase C — Verify unsupported resources are blocked

Confirm the resources removed in PR #21 (and the one pending in Task #2) are gone or clearly
rejected — not present as broken commands.

| Resource | Expected behavior |
|---|---|
| `a7 upstream ...` | command does not exist (unknown command error) |
| `a7 consumer-group ...` | command does not exist |
| `a7 service-template ...` | command does not exist |
| `a7 plugin-config ...` | **after Task #2**: command does not exist |
| `a7 stream-route ...` | **works** — full CRUD (verified exposed by the control plane) |

Declarative config — these top-level sections must be **rejected with a clear error**:

```yaml
upstreams: [...] # -> validation error
consumer_groups: [...] # -> validation error
service_templates: [...] # -> validation error
plugin_configs: [...] # -> validation error (after Task #2)
```

> Caveat: the control plane still accepts `plugin_configs` *inside batch config-validation
> payloads*. The rejection applies to `a7`'s standalone command and top-level declarative
> section, not to that internal validation path.

---

## Phase D — Declarative config

Run the full declarative workflow end to end:

1. `a7 config dump` — export the live gateway-group config to a YAML file. Assert it is
valid YAML and contains only supported resource sections.
2. `a7 config validate -f dump.yaml` — assert it passes for a known-good file, and fails
with a clear message for a file containing an unsupported section (see Phase C).
3. `a7 config diff -f dump.yaml` — assert "no diff" against the just-dumped state; then
edit a field and assert the diff is reported correctly.
4. `a7 config sync -f dump.yaml` — apply changes. Verify `--dry-run` first if available,
then a real sync, then `diff` again to confirm convergence.

Confirm all four work with **service-centered** API7 EE resources (no standalone upstreams).

---

## Bug-handling protocol

Per the handoff: **when a bug is found, add or update E2E coverage before fixing it.**

1. Reproduce with the real binary; capture exact command, output, and HTTP status.
2. Write or update a `test/e2e/<resource>_test.go` case that fails for the same reason.
3. Fix the code.
4. Re-run that test plus `make test-e2e` to confirm green.
5. Do **not** change code purely to satisfy a test if it contradicts real API7 EE behavior.

---

## Reporting

Record results in a table and attach it to the GA tracking issue / PR:

| Phase | Resource / area | Result | Bug? | E2E test added | Notes |
|---|---|---|---|---|---|
| A | automated suite | | | | |
| B | service | | | | |
| B | route | | | | |
| ... | ... | | | | |
| C | unsupported resources | | | | |
| D | config dump/validate/diff/sync | | | | |

Also capture: exact API7 EE version tested, `a7 version` string, date, and any environment
deviations from "API7 EE 3.9.12".

## Exit criteria

- Phase A suite green against the local instance.
- Phase B round-trip passes for every resource in the checklist.
- Phase C confirms all unsupported resources are absent/blocked and `stream-route` works.
- Phase D confirms declarative config works with service-centered resources.
- Every bug found has a corresponding E2E test and a fix (or a tracked follow-up).
89 changes: 89 additions & 0 deletions docs/ga-test-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# a7 GA Test Report

Execution of [`docs/ga-test-plan.md`](./ga-test-plan.md) against a locally deployed API7 EE.

## Environment

| Item | Value |
|---|---|
| Date | 2026-05-14 |
| API7 EE version | **3.9.12** (image `api7/api7-ee-3-integrated:v3.9.12`; `/api/version` → `v3.9.12`) |
| `a7 version` | `7b67da5` (then `7b67da5-dirty` after the fixes in this report) |
| Admin URL | `https://localhost:7443` |
| Gateway group | `default` |
| Gateway instance | `api7-ee-gateway-1` — `status: Healthy`, `compatibility: Compatible` |
| Deviations | (1) `a7 plugin-config` command still present — Task #2 not yet done. (2) Data-plane traffic tests not run (out of scope per plan; `A7_GATEWAY_URL`/`HTTPBIN_URL` unset). |

## Summary

All four phases executed. **5 real bugs found, all fixed with E2E + unit coverage.** Final automated suite: **128 passed / 10 skipped / 0 failed** (skips are the intentionally-unset data-plane and gateway-group-CRUD tests).

## Results

| Phase | Resource / area | Result | Bug? | E2E test added | Notes |
|---|---|---|---|---|---|
| A | automated suite | PASS | — | — | 122→128 pass, 10 skip, 0 fail. Skips = data-plane traffic + gateway-group CRUD (intentionally disabled). |
| B | context | PASS | — | — | create/list/use/current/delete, json output, error case. |
| B | gateway-group | PASS | — | — | full CRUD; non-interactive `delete` needs `--force`. |
| B | service | PASS | — | — | create (file+flags), get, list, update, export. Map-form upstream nodes correctly rejected (400). |
| B | route | PASS | — | — | create (file+flags), get, list, update, export, delete. `get`/`delete` take id only; `update` uses `--uri`. |
| B | consumer | PASS | — | — | CRUD + export. MINOR: file `description:` key silently dropped (accepted key is `desc`). |
| B | credential | PASS | — | — | CRUD via server-returned id. MINOR: `create [id]` positional is treated as `name` (id is server-generated). |
| B | ssl | **FIXED** | **BUG-1** | `TestSSL_UpdateFlagsRequireCertAndKey` | flag-based `ssl update` silently lost partial updates. |
| B | secret | PASS | — | — | CRUD (file+flags); id format is `provider/id`. |
| B | global-rule | PASS | — | — | CRUD (file+flags), export; id must equal the plugin name. MINOR: flag `--id` is required but its value is ignored by EE. |
| B | plugin | **FIXED** | **BUG-3** | `TestPlugin_GetYAML` | `plugin get -o yaml` emitted JSON. list/get otherwise fine. |
| B | plugin-metadata | **FIXED** | **BUG-2** | `TestPluginMetadata_GetYAML` | `plugin-metadata get -o yaml` emitted a byte-array. CRUD otherwise fine. |
| B | proto | PASS | — | — | CRUD (file+flags), export. MINOR: `--desc` / `desc:` silently dropped. |
| B | stream-route | **FIXED** | **BUG-4** | `TestStreamRoute_CreateWithFlags`, `TestStreamRoute_CreateRequiresName` | `create` had no `--name` flag; EE requires `name`. CRUD via `-f` worked. |
| C | unsupported commands | PASS | — | — | `upstream` / `consumer-group` / `service-template` removed. `plugin-config` still present (Task #2 pending) — gives a clear runtime error. `stream-route` works. |
| C | declarative unsupported sections | **FIXED** | **BUG-5** | `TestConfigValidate_RejectsUnsupportedServiceTemplates` | `upstreams`/`consumer_groups` rejected; `service_templates` was silently accepted. `plugin_configs` rejection is expected-pending per plan. |
| D | config dump/validate/diff/sync | PASS | — | — | dump → valid YAML, supported sections only. validate/diff/sync round-trip converges; `--dry-run` applies nothing; post-sync diff clean; route update verified persisted. |

## Bugs found & fixed

Per the bug-handling protocol, each bug was reproduced with the real binary, given failing E2E coverage, fixed, and re-verified.

### BUG-1 — `ssl update` silently lost partial flag-based updates
`a7 ssl update <id> --sni new` (without `--cert`/`--key`) exited 0 and echoed the new SNI, but the server was never updated. Root cause: the command does a GET-merge-PUT, but API7 EE never returns `cert`/`key` on GET, so the merged PUT dropped the certificate material and the EE ignored it.
**Fix:** `pkg/cmd/ssl/update/update.go` now requires `--cert` and `--key` for flag-based updates (consistent with `ssl create`), with a clear error pointing to the `-f` path. `-f` updates with a full definition are unaffected.

### BUG-2 — `plugin-metadata get -o yaml` emitted a byte-array
The YAML output was a list of integers. Root cause: `pkg/cmd/plugin-metadata/get/get.go` passed `json.RawMessage` (a `[]byte`) to the YAML encoder.
**Fix:** pass the decoded `map` to the exporter.

### BUG-3 — `plugin get -o yaml` emitted JSON
Root cause: `pkg/cmd/plugin/get/get.go` only special-cased `-o json`; every other value fell through to a hardcoded JSON encoder.
**Fix:** honor any explicit `--output` value via the exporter.

### BUG-4 — `stream-route create` had no `--name` flag
API7 EE 3.9.12 requires `name` on stream routes, but the create command exposed no `--name` flag, making flag-based creation impossible.
**Fix:** added a `Name` field to `api.StreamRoute`, a `--name` flag, and required-field validation on both the flag and `-f` paths.

### BUG-5 — `config validate` silently accepted `service_templates`
A declarative file with a top-level `service_templates:` section validated as "Config is valid". Root cause: `api.ConfigFile` had no field for it, so the section was dropped on unmarshal.
**Fix:** added a `ServiceTemplates` field and an explicit rejection in both `config validate` and `configutil.ValidateSupportedSections` (used by `diff`/`sync`), mirroring `upstreams`/`consumer_groups`.

## Minor observations (not fixed — low severity / by-design)

- **consumer**: `-f` file with a `description:` key is silently ignored; the accepted key is `desc`.
- **credential**: `create [id]` help text is misleading — the positional arg becomes the `name`; the `id` is server-generated. This is codified by `TestCredential_CreateWithPositionalID`, so it is current intended behavior.
- **global-rule**: flag-based `create` requires `--id`, but API7 EE forces the id to equal the single plugin key, so the `--id` value is effectively ignored. File-based create errors clearly on a mismatch.
- **proto**: `--desc` and a `desc:` file field are silently dropped.
- **stream-route / EE behavior**: API7 EE rejects stream routes bound to a service it has classified as HTTP. Binding to a `type: stream` service works reliably. `a7` surfaces the EE error cleanly; this is EE-side behavior, not an `a7` bug.
- **service**: the EE's schema-mismatch error (e.g. map-form upstream nodes) is correct and actionable but very verbose (dumps the full JSON schema).
- **tooling**: `golangci-lint run ./...` reports spurious `undefined: yaml (typecheck)` on files that correctly import `gopkg.in/yaml.v3` (including untouched files). `go build`, `go vet`, and `go test` are all clean — this is a pre-existing lint-environment issue, unrelated to these changes.

## Exit criteria

| Criterion | Status |
|---|---|
| Phase A suite green against the local instance | ✅ 128 pass / 10 skip / 0 fail |
| Phase B round-trip passes for every resource | ✅ all 13 resources pass (ssl/plugin/plugin-metadata/stream-route after fixes) |
| Phase C confirms unsupported resources absent/blocked and `stream-route` works | ✅ — except `plugin-config` command still present (Task #2 pending, tracked) |
| Phase D declarative config works with service-centered resources | ✅ dump/validate/diff/sync all verified |
| Every bug has a corresponding E2E test and a fix | ✅ 5/5 fixed with E2E + unit coverage |

## Follow-ups

- **Task #2** (remove the `plugin-config` standalone command) is still outstanding. Once done, Phase C also expects the declarative `plugin_configs` top-level section to be rejected — the `service_templates` rejection added here is the template for that change.
29 changes: 16 additions & 13 deletions pkg/api/types_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ package api
// ConfigFile is the declarative configuration file format for a7.
// It holds all runtime resources that can be dumped/synced.
type ConfigFile struct {
Version string `json:"version" yaml:"version"`
Routes []Route `json:"routes,omitempty" yaml:"routes,omitempty"`
Services []Service `json:"services,omitempty" yaml:"services,omitempty"`
Upstreams []Upstream `json:"upstreams,omitempty" yaml:"upstreams,omitempty"`
Consumers []Consumer `json:"consumers,omitempty" yaml:"consumers,omitempty"`
SSL []SSL `json:"ssl,omitempty" yaml:"ssl,omitempty"`
GlobalRules []GlobalRule `json:"global_rules,omitempty" yaml:"global_rules,omitempty"`
PluginConfigs []PluginConfig `json:"plugin_configs,omitempty" yaml:"plugin_configs,omitempty"`
ConsumerGroups []ConsumerGroup `json:"consumer_groups,omitempty" yaml:"consumer_groups,omitempty"`
StreamRoutes []StreamRoute `json:"stream_routes,omitempty" yaml:"stream_routes,omitempty"`
Protos []Proto `json:"protos,omitempty" yaml:"protos,omitempty"`
Secrets []Secret `json:"secrets,omitempty" yaml:"secrets,omitempty"`
PluginMetadata []PluginMetadataEntry `json:"plugin_metadata,omitempty" yaml:"plugin_metadata,omitempty"`
Version string `json:"version" yaml:"version"`
Routes []Route `json:"routes,omitempty" yaml:"routes,omitempty"`
Services []Service `json:"services,omitempty" yaml:"services,omitempty"`
Upstreams []Upstream `json:"upstreams,omitempty" yaml:"upstreams,omitempty"`
Consumers []Consumer `json:"consumers,omitempty" yaml:"consumers,omitempty"`
SSL []SSL `json:"ssl,omitempty" yaml:"ssl,omitempty"`
GlobalRules []GlobalRule `json:"global_rules,omitempty" yaml:"global_rules,omitempty"`
PluginConfigs []PluginConfig `json:"plugin_configs,omitempty" yaml:"plugin_configs,omitempty"`
ConsumerGroups []ConsumerGroup `json:"consumer_groups,omitempty" yaml:"consumer_groups,omitempty"`
// ServiceTemplates is captured only so the section can be explicitly
// rejected; API7 EE does not support it as a top-level resource.
ServiceTemplates []interface{} `json:"service_templates,omitempty" yaml:"service_templates,omitempty"`
StreamRoutes []StreamRoute `json:"stream_routes,omitempty" yaml:"stream_routes,omitempty"`
Protos []Proto `json:"protos,omitempty" yaml:"protos,omitempty"`
Secrets []Secret `json:"secrets,omitempty" yaml:"secrets,omitempty"`
PluginMetadata []PluginMetadataEntry `json:"plugin_metadata,omitempty" yaml:"plugin_metadata,omitempty"`
}

// PluginMetadataEntry is a freeform map representing a plugin's metadata.
Expand Down
Loading
Loading