scaffold api-test HTTP gateway test fixture (M1+M2 + docs)#1
Merged
Conversation
Sister project to mcp-test, but for HTTP API gateways instead of MCP
gateways. api-test is the upstream fixture the Plexara API gateway
calls; same role as mcp-test, opposite polarity.
M1 (HTTP fixture skeleton):
cmd/api-test, internal/server (composition root with graceful drain),
internal/ui (go:embed placeholder for the M3 SPA), pkg/build,
pkg/config (YAML loader with ${VAR:-default} interpolation),
pkg/endpoints (Endpoints interface + Registry; identity, data,
failure, echo groups), pkg/httpsrv (mux + health + CORS).
Configs (dev/live/example), Makefile (40+ targets matching
mcp-test conventions including the verify gate sentinel),
distroless Dockerfile, README.
M2 (DB + audit + non-OAuth inbound auth):
pkg/database (pgxpool + golang-migrate with embedded migrations),
pkg/audit (HTTP-shaped Event/Payload + Memory/Noop/Async loggers
+ Postgres store with Log/Query/Count/GetPayload), pkg/apikeys
(bcrypt-hashed PG keys), pkg/auth/inbound (Identity, file API
keys, static bearer, chain composer), pkg/httpmw (RequestID,
Identity, AccessLog, Audit middleware with body capture +
truncation + redaction). Integration tests under tests/ with build
tag `integration` exercising auth matrix, audit capture, audit
failure marking, healthz-not-audited, and query filters against
testcontainers Postgres.
Initial migration (0001_init) ships HTTP-shaped audit_events +
audit_payloads tables (route_name, endpoint_group, method, path,
status, bytes_in/out columns; payloads carry headers, query,
content-type, raw bodies). 7-day default retention; export-style
large bodies inflate audit_payloads fast.
OIDC/Keycloak inbound, portal SPA, OpenAPI generator, and remaining
endpoint groups (streaming, pagination, methods, security, export)
land in M3-M5.
Three pre-commit-review findings addressed:
- Registry.GroupByRoute did literal method+path lookup, breaking for
path-parameterized routes like /v1/fixed/{key}. Replaced with
RouteForRequest(method, requestPath) + a small {name}-aware segment
matcher; audit middleware now resolves both endpoint_group and
route_name correctly. Test added for /v1/fixed/abc, /v1/status/503,
segment-count mismatches, wrong method.
- AccessLog wrapped the mux from outside but Identity ran inside the
per-route chain, so r.WithContext(...) inside Identity never flowed
back up. Added a per-request *identityHolder seeded by RequestID;
Identity records into it; AccessLog reads from it via
resolvedIdentity(). Test exercises the real composition and asserts
auth_type/subject reach the access log line.
- MemoryLogger silently dropped QueryFilter.Search and .Offset while
the Postgres store honored both. Memory now applies case-insensitive
substring on path OR error_message (mirrors Postgres ILIKE) and
applies Offset before Limit-clamping. Paired-backend test covers
search hit on path, case-insensitivity, search hit on error_message,
paged offsets, offset past end, and negative-offset clamping.
84.5% testable-subset coverage (Postgres-dependent packages excluded
since they're covered by `go test -tags integration`); make verify
green; integration suite green against testcontainers Postgres.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The site lives at https://api-test.plexara.io (CNAME shipped) and is deployed by .github/workflows/docs.yml on push to main when files under docs/, mkdocs.yml, or that workflow change. The chrome (footer, hero, capabilities grid, fonts, palette, OG card layout) is copied from mcp-test verbatim where it doesn't depend on project specifics. Content: - index.md (uses overrides/home.html template) — hero + capabilities grid + why-this-exists rail. The portal-screenshots carousel from mcp-test/home.html is intentionally omitted with a comment because the api-test portal lands in M3. - getting-started/{overview, installation, quickstart, register-with-plexara}.md - configuration/{reference, environment, auth, database}.md - endpoints/{overview, identity, data, failure, echo}.md - operations/{audit, portal, deployment, gateway-testing}.md - reference/{http-api, architecture, releases}.md Brand assets: - logo.svg/png copied verbatim (Plexara mark) - og-card.svg/png adapted: same midnight + copper palette and 1200x630 layout as mcp-test, but the right panel renders an HTTP request envelope (request line, headers including [redacted] X-API-Key, status, JSON response) instead of a JSON-RPC envelope Five pre-commit-review findings addressed across three rounds: - docs/configuration/auth.md referenced non-existent oidc.redirect_path; corrected to portal.oidc_redirect_path. - docs/configuration/reference.md documented auth.require_for_api / require_for_portal defaults as true, but the Go struct fields zero to false and aren't currently consumed by the middleware. Updated to "**Reserved**" with the actual default and a note that the shipped live.yaml opts in to true. - docs/getting-started/quickstart.md described `make dev` as bringing up Postgres + Keycloak + the binary against api-test.live.yaml. Today `make dev` aliases to `make dev-anon` (anonymous, no DB, no Keycloak). Rewrote to describe today's behavior, added an "Auth-enabled iteration" section that exercises the auth chain without Keycloak, and called out the full stack as M3-future. The same stale claim was repeated in docs/llms.txt, docs/index.md, docs/getting-started/{overview,installation}.md — fixed in all five places so the cross-page surface is consistent. - docs/reference/architecture.md listed CORS as the outermost middleware, but actual order is RequestID(AccessLog(CORS(mux))). Reordered the numbered request-flow list and clarified that Identity / Audit are per-route and that AccessLog reads identity via the holder seeded by RequestID, not via inbound.FromContext. `mkdocs build --strict` clean. `make verify` clean. The docs deploy will fire on the first push to main that touches one of the workflow's path filters. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Until now the only GitHub Actions workflow was docs.yml, so PRs landed
with zero verification. Ports the rest of mcp-test's CI suite over,
adapted for api-test (different module path, no UI yet, Go 1.26.3).
CI:
- .github/workflows/ci.yml — five jobs:
lint golangci-lint v2.11.4
test race + coverage gate ≥80% + go-mod-tidy check + Codecov
upload (only when CODECOV_TOKEN secret is set)
build go build ./... + go mod verify
security gosec v2.25.0 + govulncheck + Semgrep (community + local)
integration testcontainers Postgres, build tag `integration`
Skips on docs/markdown-only PRs (paths-ignore: docs/**, **.md).
Concurrency group cancels in-flight runs on the same ref. All
third-party actions pinned to commit SHAs. Workflow-level
read-all baseline; each job grants only what it needs.
- .github/workflows/codeql.yml — security-and-quality suite, weekly
Monday cron + per-PR.
- .github/workflows/scorecard.yml — OSSF Scorecard, weekly Saturday
cron + push to main. Job-level permissions list every grant the
scorecard action needs (security-events: write, id-token: write,
contents: read, actions: read) since job-level REPLACES (does not
merge with) workflow-level read-all.
Configs:
- .github/codeql/codeql-config.yml — excludes go/clear-text-logging
for the audit pipeline (same justification as mcp-test: the
Logger.Log surface is a forensic sink by design, sanitized via
redact_keys).
- .golangci.yml — schema v2 with 17 linters: errcheck, errorlint,
govet, ineffassign, misspell, revive, staticcheck, unparam,
unused, gocritic, gosec, bodyclose, rowserrcheck, sqlclosecheck,
nilerr, prealloc, copyloopvar, nolintlint. Test files excluded
from gosec/gocritic/errcheck/bodyclose/revive/staticcheck/
unparam/unused with rationale comments.
- .semgrep/go-security.yml — two local rules for unbounded
slice/map allocations from struct fields (CWE-770).
- scripts/coverage-gate.sh — same coverage gate the CI test job
invokes, callable from the Makefile too. Excludes Postgres-
dependent packages (apikeys, audit/postgres, database,
database/migrate) and cmd/api-test from the gate; those are
exercised by the integration suite.
Modified:
- Makefile — coverage-gate target now delegates to
scripts/coverage-gate.sh so CI and local use identical logic
(no more divergent awk one-liner).
- pkg/endpoints/registry_test.go — removed obsolete `r := r`
loop-variable copy that the new copyloopvar linter flagged
(Go 1.22+ gives each iteration its own loop variable).
Local verification (mirrors what each CI job runs):
- make verify — GREEN. Lint clean, fmt clean, vet clean, all tests
pass with -race, gosec clean, govulncheck clean, coverage gate
passes at 89.1% (gate: ≥80%).
- go mod tidy — no drift.
- gosec -quiet ./... — exit 0.
- go test -tags=integration ./tests/... — GREEN against
testcontainers Postgres (4 tests + 6 sub-tests, ~7.7s).
One pre-commit-review finding addressed:
- scorecard.yml originally listed only security-events: write +
id-token: write at the job level. Job-level permissions REPLACE
(don't merge with) workflow-level read-all, so the action would
have run without contents: read or actions: read — silently
degrading the Token-Permissions, Branch-Protection, Webhooks,
and Dangerous-Workflow checks. Added both grants and a comment
block explaining the REPLACE behavior so the next reviewer
doesn't trim them again.
Frontend job intentionally omitted — no ui/ directory yet (M3 ships
the React 19 + Vite + Tailwind 4 SPA). Comment in ci.yml says to
copy mcp-test's frontend job verbatim once ui/package.json +
ui/pnpm-lock.yaml exist. release.yml not ported either (M5 scope).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
CodeQL flagged a high-severity CWE-770 alert on PR #1 at pkg/endpoints/data/data.go:181: words := make([]string, n) // n traces to ?words= query param The pre-existing `if n > 5000 { n = 5000 }` clamp on the prior line is runtime-safe — TestLorem_DefaultsAndCaps already asserts `?words=100000` clamps to 5000 — but CodeQL's go/uncontrolled-allocation-size taint-flow query doesn't recognize the if-clamp pattern as breaking the taint. It does recognize the Go 1.21+ `min(n, const)` builtin call. Behavior unchanged. The five-case trace (`?words=0|10|100000|-5|empty`) yields the same final n in every case as the pre-change code. Same shape: extracted the magic 50 and 5000 into package-level constants loremDefaultWords and loremMaxWords with comments explaining the CodeQL-shape rationale so the next reviewer doesn't unwind them thinking the clamp is the same as before. Other make([T], userN) sites in the diff were checked: every other allocation uses len(internal-slice) or constants and isn't user-tainted, so this is the only CWE-770 hit on the branch. Verified locally: - make verify GREEN (lint, tests with race, gosec, govulncheck, coverage gate at 89.0% / >=80%). - Existing TestLorem_DefaultsAndCaps still passes — confirms the 100000 -> 5000 clamp behavior is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is two things in one. The PRIMARY change is the meta-fix: local `make verify` now mirrors every check CI runs, so the verify-passed sentinel actually means "shippable". The SECONDARY change is the bug + suppressions the new pipeline caught — they land here because splitting would force an intermediate state where the gate fails its own check. Why it's structured this way: when the previous commit (e704c96) landed and CI rejected it, the failure mode was that local `make verify` silently passed a subset of CI's checks. Treating CI as a debugger (push, watch fail, patch, push) is unacceptable on a PR/CI loop measured in minutes. The fix is to make local catch what CI catches BEFORE the push, so that next time the loop is push-once-merge-once. == Pipeline strictness (PRIMARY) == Six gaps closed in `make verify`: 1. `go mod tidy` drift check (was: missing). New `mod-tidy-check` target snapshots go.mod / go.sum first so a dirty working tree doesn't false-positive, runs `go mod tidy`, diffs, and restores the originals via a `trap ... EXIT`. `set -e` ensures a failing `tidy` (network, bad module) propagates instead of being silently swallowed by `;` chaining. 2. `go build -v ./...` (was: missing). New `build-all` target. 3. `go mod verify` (was: missing). New `mod-verify` target. 4. Semgrep with `p/golang` + `.semgrep/` configs (was: missing locally; CI ran it via `semgrep/semgrep-action@v1` which exits 0 on findings without a `SEMGREP_APP_TOKEN`, silently passing the security job). Local now uses `semgrep scan --error`; CI rewritten to use the CLI directly with the same flag and a pinned `semgrep==1.110.0` install. 5. Integration tests under `-tags=integration` (was: missing). New `integration` target hard-fails when Docker isn't running (testcontainers needs the daemon). 6. CodeQL with security-and-quality + custom config exclusions (was: missing). New `codeql` target builds the database from source, runs the analysis, filters the SARIF via `scripts/codeql-gate.sh` against `.github/codeql/codeql-config.yml`. Hard-fails when codeql or jq is missing. Tool-version pins now line up between local and CI: - golangci-lint v2.11.4 (already pinned) - gosec v2.25.0 (already pinned) - semgrep 1.110.0 (NEW pin; warns on local drift) `require-docker`, `require-codeql`, `require-semgrep`, `require-jq` gates hard-fail with install hints (brew/pipx/apt commands). `vet` removed from the verify chain — golangci-lint already runs govet (`.golangci.yml`). The verify-passed sentinel `.claude/.last-verify-passed` writes ONLY after the FULL set passes. Previously it represented a subset; that was the lie that let the CodeQL alert ship. == Bug + suppressions the new pipeline caught (SECONDARY) == Running the new full `make verify` against the previous tree state caught: 1. The `go/uncontrolled-allocation-size` CodeQL high-severity alert that triggered this whole exercise. `pkg/endpoints/data/data.go` `lorem` handler changed from clamping (`if n > 5000 { n = 5000 }`, `n = min(n, 5000)`) to validate-and-reject — same shape `sized` already uses, and the form CodeQL's taint-flow query recognizes as a sanitizer. Constants `loremDefaultWords = 50` and `loremMaxWords = 5000` extracted with doc comments. Behavior change: `?words > 5000` now returns 400 with `{"error":"words N exceeds max 5000"}` instead of silently clamping. Test renamed `TestLorem_DefaultsAndCaps` → `TestLorem_DefaultsAndRejects` and rewritten to assert at-cap success, one-over → 400, way-over → 400. Doc updated in lockstep at `docs/endpoints/data.md`. 2. Two Semgrep `math-random-used` findings on the `math/rand/v2` imports in `data.go` and `failure.go`. Both are intentional — PCG generators seeded from a caller-supplied string for reproducible test fixtures; crypto/rand would defeat the determinism contract. Suppressed with `// nosemgrep:` comments bearing justifications mirroring the existing `// #nosec G404` annotations on the same use sites. CI was silently passing these because of the action's exit-0 behavior; now both sides error on findings, and these two are explicitly suppressed. 3. The `slow` endpoint at `pkg/endpoints/failure/failure.go` had the same clamp-vs-reject shape as the original lorem bug. CodeQL didn't flag it (it's a timer, not an allocation), but the principle the new pipeline enforces is consistency: if clamp is wrong for lorem, it's wrong for slow. Fixed to validate-and-reject (`?ms > 60000` → 400), matching constant `slowMaxMS = 60_000` extracted with doc comment, new `TestSlow_RejectsOverMax` covering at-cap-with-cancel, one-over, way-over. Doc updated at `docs/endpoints/failure.md`. == Verification == - `make verify` GREEN end-to-end (lint, fmt, mod-tidy-check, mod-verify, build-all, gosec, govulncheck, semgrep, coverage-gate at 89%, integration with testcontainers Postgres ~7s, codeql with security-and-quality suite ~2 min). - Re-running `make codeql` against the previous commit's tree state reproduces the EXACT same alert CI saw — confirming the new pipeline catches what was missed. - Three rounds of pre-commit adversarial review: 6 findings round 1 (all addressed), 1 substantive finding round 2 (the silent-swallow regression in mod-tidy-check, addressed), CLEAN round 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cjimti
added a commit
that referenced
this pull request
May 10, 2026
CodeQL flagged a high-severity CWE-770 alert on PR #1 at pkg/endpoints/data/data.go:181: words := make([]string, n) // n traces to ?words= query param The pre-existing `if n > 5000 { n = 5000 }` clamp on the prior line is runtime-safe — TestLorem_DefaultsAndCaps already asserts `?words=100000` clamps to 5000 — but CodeQL's go/uncontrolled-allocation-size taint-flow query doesn't recognize the if-clamp pattern as breaking the taint. It does recognize the Go 1.21+ `min(n, const)` builtin call. Behavior unchanged. The five-case trace (`?words=0|10|100000|-5|empty`) yields the same final n in every case as the pre-change code. Same shape: extracted the magic 50 and 5000 into package-level constants loremDefaultWords and loremMaxWords with comments explaining the CodeQL-shape rationale so the next reviewer doesn't unwind them thinking the clamp is the same as before. Other make([T], userN) sites in the diff were checked: every other allocation uses len(internal-slice) or constants and isn't user-tainted, so this is the only CWE-770 hit on the branch. Verified locally: - make verify GREEN (lint, tests with race, gosec, govulncheck, coverage gate at 89.0% / >=80%). - Existing TestLorem_DefaultsAndCaps still passes — confirms the 100000 -> 5000 clamp behavior is preserved.
cjimti
added a commit
that referenced
this pull request
May 10, 2026
scaffold api-test HTTP gateway test fixture (M1+M2 + docs)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Initial scaffold of
api-test, the HTTP REST fixture used to exercisePlexara's API gateway. Sister to mcp-test;
same role, opposite polarity (HTTP upstream instead of MCP server).
Two commits, one for the code (M1+M2) and one for the docs site
(api-test.plexara.io).
d32cbc8— scaffold api-test fixture: HTTP endpoint groups + auth + auditM1 (HTTP fixture skeleton):
cmd/api-test,internal/server(composition root with graceful drain),internal/ui(go:embed placeholder for the M3 SPA),pkg/buildpkg/config— YAML loader with${VAR:-default}env interpolationpkg/endpoints— Endpoints interface + Registry; identity(whoami, headers), data (fixed/sized/lorem), failure
(status/slow/flaky), echo (catch-all)
pkg/httpsrv— mux + health + CORSconventions including the verify-gate sentinel), distroless
Dockerfile, README
M2 (DB + audit + non-OAuth inbound auth):
pkg/database— pgxpool wrapper + golang-migrate (with embeddedHTTP-shaped audit_events + audit_payloads schema)
pkg/audit— Event/Payload + Memory/Noop/Async loggers + Postgresstore (Log/Query/Count/GetPayload)
pkg/apikeys— bcrypt-hashed PG keyspkg/auth/inbound— Identity, file API keys (header + queryplacement), static bearer, chain composer
pkg/httpmw— RequestID, Identity, AccessLog, Audit middleware(body capture + truncation + redaction)
tests/— integration suite under build tag `integration` againsttestcontainers Postgres (auth matrix, audit capture, audit
failure-marking, healthz-not-audited, query filters)
OIDC/Keycloak inbound, portal SPA, OpenAPI generator, and the
remaining endpoint groups (streaming, pagination, methods, security,
export) land in M3-M5 per
docs/reference/releases.md.
Three pre-commit-review findings addressed:
broke for path-parameterized routes like `/v1/fixed/{key}` —
audit rows had empty `endpoint_group` and `route_name`.
Replaced with `RouteForRequest` + a small `{name}`-aware
segment matcher; both columns now populate correctly. Test added
for `/v1/fixed/abc`, `/v1/status/503`, segment-count
mismatches, and wrong-method.
inside the per-route chain, so `r.WithContext(...)` inside
Identity never flowed back up. Added a per-request
`*identityHolder` seeded by RequestID; Identity records into it,
AccessLog reads from it via `resolvedIdentity()`. Test exercises
the real composition and asserts `auth_type`/`subject` reach
the access log line.
`.Offset` while the Postgres store honored both. Memory now
applies case-insensitive substring on `path` OR `error_message`
(mirrors Postgres `ILIKE`) and applies Offset before
Limit-clamping. Paired-backend test added.
c75b90a— add documentation site mirroring mcp-test styleSite at https://api-test.plexara.io (CNAME shipped) deployed by
`.github/workflows/docs.yml` on push to main when files under
`docs/`, `mkdocs.yml`, or that workflow change. Chrome (footer,
hero, capabilities grid, fonts, palette, OG card layout) is copied
from mcp-test verbatim where it doesn't depend on project specifics.
Content:
capabilities grid, why-this-exists rail. Portal-screenshots
carousel from mcp-test/home.html intentionally omitted with a
comment because the api-test portal lands in M3.
Brand assets:
1200x630 layout as mcp-test, but the right panel renders an HTTP
request envelope (request line, headers including `[redacted]`
X-API-Key, status, JSON response) instead of a JSON-RPC envelope
Five pre-commit-review findings addressed across three rounds:
`oidc.redirect_path`; corrected to `portal.oidc_redirect_path`.
`auth.require_for_api` / `require_for_portal` defaults as
`true`, but the Go struct fields zero to `false` and aren't
currently consumed. Updated to "Reserved" with the actual
default and a note that the shipped `live.yaml` opts in to
`true`.
bringing up Postgres+Keycloak+the binary. Today `make dev`
aliases to `make dev-anon` (anonymous, no DB, no Keycloak).
Rewrote to describe today's behavior, added an "Auth-enabled
iteration" section, and called out the full stack as M3-future.
Same stale claim was repeated in `llms.txt`, `index.md`,
`overview.md`, `installation.md` — fixed across all five
pages so the cross-page surface is consistent.
middleware, but actual order is
`RequestID(AccessLog(CORS(mux)))`. Reordered the numbered
request-flow list and clarified that Identity/Audit are per-route
and that AccessLog reads identity via the holder seeded by
RequestID, not via `inbound.FromContext`.
Layout overview
```
cmd/api-test/ # binary entry
internal/server/ # composition root
internal/ui/ # go:embed for the M3 SPA
pkg/build/ # version metadata stamped at link time
pkg/config/ # YAML loader + ${VAR:-default} interpolation
pkg/database/{,migrate/} # pgxpool + golang-migrate
pkg/audit/{,postgres/} # Event/Payload + Logger + memory/noop/async/PG
pkg/apikeys/ # bcrypt-hashed PG key store
pkg/auth/inbound/ # Identity, file/bearer auth, chain composer
pkg/httpmw/ # RequestID, AccessLog, Identity, Audit
pkg/endpoints/{registry, # Endpoints interface + Registry
identity,data,failure,echo}/ # one package per group
pkg/httpsrv/ # HTTP mux composition + health + CORS
configs/ # dev / live / example YAML
tests/ # integration suite (build tag `integration`)
docs/ # mkdocs site source
.github/workflows/docs.yml # GH Pages deploy
```
Test plan
on the testable subset — Postgres-dependent packages excluded)
testcontainers Postgres (4 tests + 6 sub-tests; ~5s)
green, https://api-test.plexara.io/ renders the new site
`configs/api-test.dev.yaml`; `curl http://localhost:8080/v1/whoami\`
returns `{"subject":"","auth_type":"anonymous"}`
missing X-API-Key returns 401 + WWW-Authenticate; valid X-API-Key
returns the resolved identity; valid Bearer returns the resolved
identity; `/healthz` is reachable without credentials
instance; `api_invoke_endpoint(connection="api-test", method="GET", path="/v1/whoami")`
returns the expected identity for each registered auth_mode