Skip to content
Back to skills

Endpoint Test Method

ASecurity

Hit a running dev endpoint via curl and verify status + response shape + required headers (auth, tenant) field-by-field against the DTO. Use AFTER any controller, DTO, guard, pipe or interceptor edit to prove the route works end-to-end, or when a frontend reports an unexpected shape and you need ground truth. NOT when no dev server is running (it refuses to auto-start), NOT against staging/prod hosts, and NOT for static contract diffing — that is api-snapshot.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentsgobashtestingapifrontendbackendsecurity

Works with

  • cursor
  • terminal
  • api

Security analysis

A100/100

Scanned October 20, 2026

npx -y skills add adnanmokhtar/refract --skill endpoint-test-method --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Endpoint Test Method?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Endpoint Test Method
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adnanmokhtar-endpoint-test-method/badge)](https://www.skillsdirectory.com/skills/adnanmokhtar-endpoint-test-method)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: endpoint-test-method
description: Hit a running dev endpoint via curl and verify status + response shape + required headers (auth, tenant) field-by-field against the DTO. Use AFTER any controller, DTO, guard, pipe or interceptor edit to prove the route works end-to-end, or when a frontend reports an unexpected shape and you need ground truth. NOT when no dev server is running (it refuses to auto-start), NOT against staging/prod hosts, and NOT for static contract diffing — that is api-snapshot.
kind: skill
pack: backend
allowed-tools: [Read, Grep, Glob, Bash]
---

# endpoint-test-method

## Premise

Find real bugs, not hand-waves. Every assertion cites the controller/DTO `<file:line>` and the actual response body produced. "Returned 200, looks fine" is not verification — phantom-success (200 with wrong shape) is the most common regression here. Field-by-field diff against the response DTO is mandatory; key-set comparison alone is insufficient. Cross-tenant 200 is a real bug, never "dev mode".

A run that skips any of the 5 mandatory cases (golden, invalid body, no auth, wrong tenant, idempotency) is incomplete.

Make an HTTP request to a local endpoint, then verify the status, headers, and response body match what the controller + DTO declared.

**Ownership inside the endpoint-test-method triad.** This skill owns the *mechanism*: the curl invocations, the assertions, and the field-by-field diff. The `endpoint-tester` agent owns *case selection and the verdict*. The `/endpoint-test` command owns *argument resolution and escalation routing*. Each of the three states its cases once; when you need to know what a call actually asserts, it is here.

## When to use

- After editing a controller method (handler, decorators, route).
- After changing an input or response DTO.
- After a guard / interceptor / pipe change that affects the route.
- When suspecting a regression — quick sanity check before blaming the frontend.

## Prerequisites

- Dev server running on a known port. **Resolve the port from the project, never from a convention** — read the dev script in `package.json` / `Procfile` / compose file, or `CLAUDE.md`, and confirm with `lsof -i :<port>` or the `start:dev` log. A repo with several deployable apps has one port per app; enumerate them from the same source rather than assuming a pairing. Testing the right route against the wrong app's port produces a `404` that reads like a missing endpoint and a `200` that reads like a pass.
- Auth credentials in `.env.local` / `.env.dev` — NEVER use `*PROD*` credentials or `*.prod.env`.
- `curl` and `jq` available.

## Safety invariants

- ONLY target `localhost`, `127.0.0.1`, `::1`, or a tunnel the user explicitly named in this session (`*.ngrok.io`, `*.trycloudflare.com`, `*.loca.lt`).
- Refuse on any other host unless the user confirms in writing with the URL.
- Print every curl so the user can replay.

## Procedure

1. Read the controller — method, path, required headers, body shape, response shape.
2. Read the input DTO — derive the **minimal valid payload** (required fields + valid types).
3. Read the response DTO — note every field that should appear.
4. Resolve base URL from `CLAUDE.md` / `ai/stack.md`. If absent, ask the user; do not guess.
5. Source credentials from `.env.local`, `.env.dev`, or `.env.example`.
6. Execute 5 cases minimum:

   **1. Golden path** (valid auth, valid body)
   ```bash
   curl -sS -X POST "http://localhost:4000/api/v1/orders" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer $DEV_JWT" \
     -H "X-Tenant-Id: dev-tenant" \
     -d '{"customerId":"u_1","items":[{"sku":"A1","qty":2}]}' \
     -w "\n---\nstatus=%{http_code}\ntime=%{time_total}s\n" | tee /tmp/r1.json
   ```
   Assert: status matches documented success (201/200/204), body field-by-field matches response DTO, correlation-id header (if declared), `time_total` within SLO.

   **2. Invalid body** — drop a required field; expect the project's declared validation-failure status (`422 Unprocessable Content` for a well-formed body that fails semantic validation; `400` only for a body that could not be parsed at all — see `ai/patterns/error-handling.md` § Status mapping) carrying the project's field-error rows, each naming the offending field with a stable machine code.
   **3. No auth** — omit `Authorization`; expect 401 (or 403 for public-but-rate-limited).
   **4. Wrong tenant** — header that doesn't own the resource; expect 404 (preferred) or 403. A 200 here is a cross-tenant leak (file as security finding).
   **5. Idempotency** (mutating endpoints with `Idempotency-Key`) — two POSTs with the same key; second returns same body, no duplicate side effects.

7. Field-by-field shape diff vs response DTO:
   ```bash
   jq -r 'keys_unsorted|join(",")' /tmp/r1.json
   ```
   Compare the key set against the DTO file.

## Conditional cases (run when the endpoint's contract declares the capability)

These are not optional in the sense of "nice to have" — each one runs **iff** the endpoint claims the corresponding capability, and skipping a case for a capability the endpoint does claim is the same incompleteness as skipping one of the mandatory five. Cite the declaration (`<file:line>` of the decorator, the OpenAPI row, the middleware) that put the case in scope.

**Rate limit** — if a limiter is bound to the route.
```bash
for i in $(seq 1 "$((LIMIT + 1))"); do
  curl -sS -o /dev/null -D - -X GET "$BASE/api/v1/orders" \
    -H "Authorization: Bearer $DEV_JWT" \
    -w "status=%{http_code}\n" | grep -iE '^(status|retry-after|ratelimit)'
done
```
Assert on the `(LIMIT+1)`-th call: status `429`, **`Retry-After` present** (this is the one field with an RFC behind it — RFC 9110 §10.2.3), and a quota field present. The quota field family is the project's choice per `ai/patterns/rate-limiting.md` — the draft's `RateLimit` / `RateLimit-Policy`, the draft-05 `RateLimit-Limit`/`-Remaining`/`-Reset` triple, or both during migration. Assert the family the project declared; do **not** assert a family the project never adopted, and do not treat the absence of the legacy triple as a failure on a project that emits the two-field form.

**Conditional requests** — if the read emits `ETag` or `Last-Modified`.
```bash
ETAG=$(curl -sS -D - -o /dev/null "$BASE/api/v1/orders/$ID" -H "Authorization: Bearer $DEV_JWT" \
       | awk 'tolower($1)=="etag:"{print $2}' | tr -d '\r')
curl -sS -o /dev/null -w "revalidate=%{http_code}\n" "$BASE/api/v1/orders/$ID" \
     -H "Authorization: Bearer $DEV_JWT" -H "If-None-Match: $ETAG"
```
Assert `304` with an empty body. An endpoint that sets `ETag` and still returns `200` on a matching `If-None-Match` has a header that costs bytes and buys nothing. If the resource carries a version column and the write path declares `If-Match`, repeat with a stale validator and assert `412`.

**Async / `202` hand-off** — if the write offloads to a job.
```bash
LOC=$(curl -sS -D - -o /dev/null -X POST "$BASE/api/v1/exports" -H "Authorization: Bearer $DEV_JWT" \
      -d '{}' | awk 'tolower($1)=="location:"{print $2}' | tr -d '\r')
curl -sS "$BASE$LOC" -H "Authorization: Bearer $DEV_JWT"
```
Assert `202`, a `Location` header, and that following it returns a job-status document whose state is one of the declared values. A `202` with no `Location` is a promise with no way to collect on it.

**Streaming** — if the response is NDJSON / SSE / chunked. Assert the `Content-Type` matches the declared transport and that the stream carries a terminal marker (success sentinel or error record) rather than just ending. See `ai/patterns/response-streaming.md`; note that a streaming route is exempt from the envelope diff, so the terminal-marker assertion is the only completeness check it gets.

**Pagination** — `?limit=10&cursor=...` returns at most 10 items plus a next cursor; the next cursor actually advances (page 2 ≠ page 1).

**Filters / sorts** — each declared param demonstrably changes the result set. A param that is accepted and ignored is worse than a `400`.

**Soft-delete** — a deleted record returns `404` on read while the row still carries `deleted_at`.

## Output

```
## /api/v1/orders [POST]   base=http://localhost:4000 (SAFE — localhost)

| # | Case                | Status | Assertion                       | Result |
|---|---------------------|--------|---------------------------------|--------|
| 1 | Golden path         | 201    | DTO shape match                 | PASS   |
| 2 | Missing `items`     | 422    | fieldErrors[].field == 'items'  | PASS   |
| 3 | No auth             | 401    | WWW-Authenticate header         | PASS   |
| 4 | Wrong tenant        | 404    | no resource in body             | PASS   |
| 5 | Idempotency replay  | 201    | same body both calls            | FAIL — different createdAt timestamps

Bugs surfaced:
  - case 5: idempotency not honored; missing IdempotencyService.getOrSet wrap.
  - case 1: response DTO declares correlationId but response omits it.

Curl commands:
  <each executed curl preserved for replay>
```

## False positives / gotchas

- **Phantom success** — 200 with wrong shape. Always diff response keys against DTO field-by-field.
- **Too-permissive dev auth** — local server may skip tenant guards that prod enforces. Cross-tenant 200 is a real bug, not "dev mode".
- **Stale server** — code edited but server not restarted. Look for the new line of code in the log; absent = restart needed.
- **Dynamic fields** (`createdAt`, `id`, `correlationId`) differ between calls — exclude when comparing shapes, not values.
- **A capability asserted that the project never adopted.** The mirror of phantom success: failing an endpoint for not emitting a header family, a cursor style, or a validator the project's conventions never declared. Assert the declared contract, not the one you would have designed.
- Don't start the server yourself (side effects). Print the dev command and stop if it isn't running.

## Related

- **Runnable primitive.** endpoint-test-method is the executable curl primitive that the `@endpoint-tester` agent orchestrates — the agent adds case selection, reporting, and verdict discipline; this skill is the hit-and-assert mechanism it drives.
- `.claude/skills/log-tail-method/SKILL.md` — follow a failing case (500, phantom success) into the structured logs by its correlation id.
- `.claude/skills/debug-tenant/SKILL.md` — escalate a cross-tenant 200 (case 4) into the full tenant-leak playbook.
- `.claude/skills/api-snapshot/SKILL.md` — the static-contract counterpart: snapshot guards the *declared* OpenAPI shape, endpoint-test-method guards the *running* response.
- `ai/patterns/api-contract.md` — the response-DTO / envelope shape each field-by-field assertion diffs against.
- `ai/patterns/error-handling.md` — the error-envelope + status mapping cases 2–4 expect.

## Halt conditions

- Halt on hand-waves: every PASS must cite the case number + status code + DTO `<file:line>` it diffed against.
- Halt if any of the 5 mandatory cases (golden / invalid body / no auth / wrong tenant / idempotency) was silently skipped.
- Halt if a conditional case was skipped for a capability the endpoint **does** declare — cite the declaration that put it in scope, or state why it is out of scope. "Probably fine" is not a scope decision.
- Halt if a 200 response was accepted without field-by-field diff against the response DTO — phantom success is the classic miss.
- Halt if the target host is not localhost / 127.0.0.1 / a session-named tunnel — refuse to fire requests at unverified hosts.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.