Prompt: Achieve 100% Locust Coverage of All REST API Endpoints
Systematically extend tests/loadtest/locustfile.py to cover every REST API endpoint exposed by the platform. The goal is 100% endpoint coverage — every method+path in the OpenAPI spec has a corresponding @task in at least one Locust User class.
Prerequisite: All existing tests must pass with 0 failures first. See llms/prompts/locust-fix-failures.md.
Running Services
- Platform:
localhost:8080 (nginx → gateway:4444, via docker-compose.yml)
- Locust UI:
localhost:8089 (make load-test-ui)
Key Files
tests/loadtest/locustfile.py — Main locust test file (~3660 lines, 30+ User classes)
mcpgateway/routers/*.py — 19 router files defining all REST endpoints
infra/nginx/nginx.conf — nginx reverse proxy (timeouts, routing)
docker-compose.yml — service topology, env vars
- OpenAPI spec:
GET http://localhost:8080/openapi.json
Step 1: Generate the Coverage Map
Extract every endpoint from the OpenAPI spec and cross-reference with existing locust tasks:
# Get all API endpoints (method + path)
curl -s http://localhost:8080/openapi.json | python3 -c "
import sys, json
spec = json.load(sys.stdin)
for path in sorted(spec.get('paths', {}).keys()):
methods = [m.upper() for m in spec['paths'][path].keys() if m in ('get','post','put','delete','patch')]
for m in methods:
print(f'{m:7s} {path}')
"
# Get all endpoints currently tested in locustfile (grep for URL patterns)
grep -oP '(?:self\.client\.(get|post|put|delete|patch))\(\s*[f"]([^"]+)"' \
tests/loadtest/locustfile.py | sort -u
Compare the two lists. Any endpoint in the OpenAPI spec but not in the locustfile is a coverage gap.
Step 2: Categorize Endpoints by Router Group
The API has 399 endpoints across these groups (by count):
| Group |
Endpoints |
Router File |
/admin/* |
~200 |
Various (HTMX admin UI) |
/teams/* |
18 |
routers/teams.py |
/servers/* |
15 |
main.py (core) |
/llm/* |
14 |
routers/llm_admin_router.py, llm_proxy_router.py |
/rbac/* |
13 |
routers/rbac.py |
/auth/* |
12 |
routers/auth.py, routers/email_auth.py |
/resources/* |
12 |
main.py (core) |
/a2a/* |
10 |
main.py (core) |
/gateways/* |
10 |
main.py (core) |
/prompts/* |
10 |
main.py (core) |
/tokens/* |
10 |
routers/tokens.py |
/api/logs/* |
5 |
routers/log_search.py |
/api/metrics/* |
4 |
routers/metrics_maintenance.py |
/tools/* |
9 |
main.py (core) |
/oauth/* |
7 |
routers/oauth_router.py |
/llmchat/* |
6 |
routers/llmchat_router.py |
/roots/* |
6 |
main.py (core) |
/protocol/* |
5 |
main.py (core) |
/import/* |
4 |
main.py (core) |
/reverse-proxy/* |
4 |
routers/reverse_proxy.py |
/metrics/* |
3 |
main.py (core) |
/tags/* |
3 |
main.py (core) |
/cancellation/* |
2 |
routers/cancellation_router.py |
/export/* |
2 |
main.py (core) |
/v1/* |
2 |
routers/llm_proxy_router.py |
| Other singletons |
~10 |
/health, /ready, /version, /sse, etc. |
Step 3: Manually Test New Endpoints Before Writing Locust Tasks
For every uncovered endpoint, manually test with curl first to understand the expected request/response:
# Generate admin JWT
export JWT=$(python3 -c "
import jwt, datetime, uuid
payload = {'sub':'admin@example.com',
'exp':datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(hours=1),
'iat':datetime.datetime.now(datetime.timezone.utc),
'aud':'mcpgateway-api','iss':'mcpgateway',
'jti':str(uuid.uuid4()),'teams':None,'is_admin':True}
print(jwt.encode(payload, 'my-test-key', algorithm='HS256'))")
AUTH="Authorization: Bearer $JWT"
# Example: test an uncovered endpoint
curl -s -w "\nHTTP %{http_code}\n" -H "$AUTH" http://localhost:8080/version
curl -s -w "\nHTTP %{http_code}\n" -H "$AUTH" http://localhost:8080/tags
curl -s -w "\nHTTP %{http_code}\n" -H "$AUTH" -X POST http://localhost:8080/logging/setLevel \
-H "Content-Type: application/json" -d '{"level":"INFO"}'
# For admin HTMX endpoints, use Accept: text/html
curl -s -w "\nHTTP %{http_code}\n" -H "$AUTH" -H "Accept: text/html" http://localhost:8080/admin/plugins
Document the required headers, payload shape, and expected status codes for each endpoint before adding it to the locustfile.
Step 4: Add Missing Coverage Systematically
Follow the existing patterns in locustfile.py:
- One User class per logical group (e.g.,
LLMProviderUser, ServerWellKnownUser)
- Use
@tag decorators for filtering (e.g., @tag("llm", "read"))
- Use
catch_response=True with _validate_json_response() or _validate_html_response()
- Include realistic
allowed_codes — don't just allow 200; include 401, 403, 404, 409, 422 where appropriate
- CRUD operations should create → read → update → delete in a single task method to avoid orphaned test data
- Weight classes appropriately — read-heavy users get higher weight, write/admin users get weight=1
- Clean up test data in
on_stop() method
Endpoint categories to handle differently:
| Type |
Pattern |
Approach |
| Read-only GET |
/version, /tags, /health/security |
Simple GET, validate JSON/status |
| Admin HTMX pages |
/admin/* |
GET with Accept: text/html, use _validate_html_response() |
| CRUD lifecycle |
POST→GET→PUT→DELETE |
Single task does full lifecycle, cleans up |
| JSON-RPC |
POST /rpc |
Use _json_rpc_request() helper, validate with _validate_jsonrpc_response() |
| SSE/streaming |
/sse, /servers/{id}/sse |
Skip or test connection-only (no streaming load test) |
| OAuth flows |
/oauth/authorize/*, /oauth/callback |
May require browser redirects — test what's possible via API |
| File operations |
/admin/logs/export, /admin/support-bundle/* |
GET with appropriate timeout |
Step 5: Verify Coverage
After adding tasks, re-run the coverage map from Step 1 and confirm every endpoint has a corresponding task. Then:
# Light test to verify no failures
make load-test-light # 10 users, 30s
# Check failures
curl -s http://localhost:8089/stats/requests/csv | awk -F',' '$4 > 0'
# Full test
make load-test-ui # Start UI, configure desired load, run
Step 6: Create Coverage Tracking Document
Create todo/locust-coverage.md with:
- Full endpoint list grouped by router
- Coverage status for each (covered / not covered / skipped with reason)
- Which User class covers each endpoint
- Any endpoints intentionally skipped (SSE streaming, OAuth browser flows, etc.) with justification
Priority Order for Adding Coverage
- Core REST API —
/tools, /servers, /gateways, /resources, /prompts, /roots (CRUD + state + toggle)
- Auth & RBAC —
/auth/*, /rbac/*, /tokens/*, /teams/*
- Observability —
/api/logs/*, /api/metrics/*, /metrics/*
- LLM & Chat —
/llm/*, /llmchat/*, /v1/*
- Admin UI pages —
/admin/* (HTMX, lower priority but high endpoint count)
- Specialized —
/oauth/*, /reverse-proxy/*, /cancellation/*, /protocol/*
- Singletons —
/version, /tags, /export, /import, /sse, /.well-known/*
1---2name: 2174-locust-improve-coverage-a0dbe3533description: Prompt: Achieve 100% Locust Coverage of All REST API Endpoints4---5# Prompt: Achieve 100% Locust Coverage of All REST API Endpoints67Systematically extend `tests/loadtest/locustfile.py` to cover every REST API endpoint exposed by the platform. The goal is 100% endpoint coverage — every method+path in the OpenAPI spec has a corresponding `@task` in at least one Locust User class.89**Prerequisite:** All existing tests must pass with 0 failures first. See `llms/prompts/locust-fix-failures.md`.1011## Running Services1213- Platform: `localhost:8080` (nginx → gateway:4444, via `docker-compose.yml`)14- Locust UI: `localhost:8089` (`make load-test-ui`)1516## Key Files1718- `tests/loadtest/locustfile.py` — Main locust test file (~3660 lines, 30+ User classes)19- `mcpgateway/routers/*.py` — 19 router files defining all REST endpoints20- `infra/nginx/nginx.conf` — nginx reverse proxy (timeouts, routing)21- `docker-compose.yml` — service topology, env vars22- OpenAPI spec: `GET http://localhost:8080/openapi.json`2324## Step 1: Generate the Coverage Map2526Extract every endpoint from the OpenAPI spec and cross-reference with existing locust tasks:2728```bash29# Get all API endpoints (method + path)30curl -s http://localhost:8080/openapi.json | python3 -c "31import sys, json32spec = json.load(sys.stdin)33for path in sorted(spec.get('paths', {}).keys()):34 methods = [m.upper() for m in spec['paths'][path].keys() if m in ('get','post','put','delete','patch')]35 for m in methods:36 print(f'{m:7s} {path}')37"3839# Get all endpoints currently tested in locustfile (grep for URL patterns)40grep -oP '(?:self\.client\.(get|post|put|delete|patch))\(\s*[f"]([^"]+)"' \41 tests/loadtest/locustfile.py | sort -u42```4344Compare the two lists. Any endpoint in the OpenAPI spec but not in the locustfile is a coverage gap.4546## Step 2: Categorize Endpoints by Router Group4748The API has 399 endpoints across these groups (by count):4950| Group | Endpoints | Router File |51|---|---|---|52| `/admin/*` | ~200 | Various (HTMX admin UI) |53| `/teams/*` | 18 | `routers/teams.py` |54| `/servers/*` | 15 | `main.py` (core) |55| `/llm/*` | 14 | `routers/llm_admin_router.py`, `llm_proxy_router.py` |56| `/rbac/*` | 13 | `routers/rbac.py` |57| `/auth/*` | 12 | `routers/auth.py`, `routers/email_auth.py` |58| `/resources/*` | 12 | `main.py` (core) |59| `/a2a/*` | 10 | `main.py` (core) |60| `/gateways/*` | 10 | `main.py` (core) |61| `/prompts/*` | 10 | `main.py` (core) |62| `/tokens/*` | 10 | `routers/tokens.py` |63| `/api/logs/*` | 5 | `routers/log_search.py` |64| `/api/metrics/*` | 4 | `routers/metrics_maintenance.py` |65| `/tools/*` | 9 | `main.py` (core) |66| `/oauth/*` | 7 | `routers/oauth_router.py` |67| `/llmchat/*` | 6 | `routers/llmchat_router.py` |68| `/roots/*` | 6 | `main.py` (core) |69| `/protocol/*` | 5 | `main.py` (core) |70| `/import/*` | 4 | `main.py` (core) |71| `/reverse-proxy/*` | 4 | `routers/reverse_proxy.py` |72| `/metrics/*` | 3 | `main.py` (core) |73| `/tags/*` | 3 | `main.py` (core) |74| `/cancellation/*` | 2 | `routers/cancellation_router.py` |75| `/export/*` | 2 | `main.py` (core) |76| `/v1/*` | 2 | `routers/llm_proxy_router.py` |77| Other singletons | ~10 | `/health`, `/ready`, `/version`, `/sse`, etc. |7879## Step 3: Manually Test New Endpoints Before Writing Locust Tasks8081For every uncovered endpoint, manually test with curl first to understand the expected request/response:8283```bash84# Generate admin JWT85export JWT=$(python3 -c "86import jwt, datetime, uuid87payload = {'sub':'admin@example.com',88 'exp':datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(hours=1),89 'iat':datetime.datetime.now(datetime.timezone.utc),90 'aud':'mcpgateway-api','iss':'mcpgateway',91 'jti':str(uuid.uuid4()),'teams':None,'is_admin':True}92print(jwt.encode(payload, 'my-test-key', algorithm='HS256'))")93AUTH="Authorization: Bearer $JWT"9495# Example: test an uncovered endpoint96curl -s -w "\nHTTP %{http_code}\n" -H "$AUTH" http://localhost:8080/version97curl -s -w "\nHTTP %{http_code}\n" -H "$AUTH" http://localhost:8080/tags98curl -s -w "\nHTTP %{http_code}\n" -H "$AUTH" -X POST http://localhost:8080/logging/setLevel \99 -H "Content-Type: application/json" -d '{"level":"INFO"}'100101# For admin HTMX endpoints, use Accept: text/html102curl -s -w "\nHTTP %{http_code}\n" -H "$AUTH" -H "Accept: text/html" http://localhost:8080/admin/plugins103```104105Document the required headers, payload shape, and expected status codes for each endpoint before adding it to the locustfile.106107## Step 4: Add Missing Coverage Systematically108109Follow the existing patterns in `locustfile.py`:110111- **One User class per logical group** (e.g., `LLMProviderUser`, `ServerWellKnownUser`)112- **Use `@tag` decorators** for filtering (e.g., `@tag("llm", "read")`)113- **Use `catch_response=True`** with `_validate_json_response()` or `_validate_html_response()`114- **Include realistic `allowed_codes`** — don't just allow 200; include 401, 403, 404, 409, 422 where appropriate115- **CRUD operations** should create → read → update → delete in a single task method to avoid orphaned test data116- **Weight classes appropriately** — read-heavy users get higher weight, write/admin users get weight=1117- **Clean up test data** in `on_stop()` method118119### Endpoint categories to handle differently:120121| Type | Pattern | Approach |122|---|---|---|123| **Read-only GET** | `/version`, `/tags`, `/health/security` | Simple GET, validate JSON/status |124| **Admin HTMX pages** | `/admin/*` | GET with `Accept: text/html`, use `_validate_html_response()` |125| **CRUD lifecycle** | POST→GET→PUT→DELETE | Single task does full lifecycle, cleans up |126| **JSON-RPC** | `POST /rpc` | Use `_json_rpc_request()` helper, validate with `_validate_jsonrpc_response()` |127| **SSE/streaming** | `/sse`, `/servers/{id}/sse` | Skip or test connection-only (no streaming load test) |128| **OAuth flows** | `/oauth/authorize/*`, `/oauth/callback` | May require browser redirects — test what's possible via API |129| **File operations** | `/admin/logs/export`, `/admin/support-bundle/*` | GET with appropriate timeout |130131## Step 5: Verify Coverage132133After adding tasks, re-run the coverage map from Step 1 and confirm every endpoint has a corresponding task. Then:134135```bash136# Light test to verify no failures137make load-test-light # 10 users, 30s138139# Check failures140curl -s http://localhost:8089/stats/requests/csv | awk -F',' '$4 > 0'141142# Full test143make load-test-ui # Start UI, configure desired load, run144```145146## Step 6: Create Coverage Tracking Document147148Create `todo/locust-coverage.md` with:1491501. Full endpoint list grouped by router1512. Coverage status for each (covered / not covered / skipped with reason)1523. Which User class covers each endpoint1534. Any endpoints intentionally skipped (SSE streaming, OAuth browser flows, etc.) with justification154155## Priority Order for Adding Coverage1561571. **Core REST API** — `/tools`, `/servers`, `/gateways`, `/resources`, `/prompts`, `/roots` (CRUD + state + toggle)1582. **Auth & RBAC** — `/auth/*`, `/rbac/*`, `/tokens/*`, `/teams/*`1593. **Observability** — `/api/logs/*`, `/api/metrics/*`, `/metrics/*`1604. **LLM & Chat** — `/llm/*`, `/llmchat/*`, `/v1/*`1615. **Admin UI pages** — `/admin/*` (HTMX, lower priority but high endpoint count)1626. **Specialized** — `/oauth/*`, `/reverse-proxy/*`, `/cancellation/*`, `/protocol/*`1637. **Singletons** — `/version`, `/tags`, `/export`, `/import`, `/sse`, `/.well-known/*`