Practice: API testing with Postman, Newman, Bruno and curl
Generated by Claude (Fable 5.1) by hand from the identity and integration ops spike, Scout learning center on 2026-10-14.
Main tool: Postman collection format with Newman, and the Bruno CLI · A collection is the artifact that is at once the demo, the regression suite and the documentation of an integration, Newman runs it headless with secrets from the environment so it can gate a pipeline, and Bruno keeps the same thing as reviewable text files; the learning center ran the seven-request collection against Keycloak 26.4 with 17 assertions passing and the two-request Bruno version with 4 of 4. · Alternatives: Hoppscotch is an open-source web client with a CLI; Insomnia is another open-source client; curl with a shell script is the no-tool route and the referee for all of them; WireMock or Mockoon give a mock API to run the collection against before the real one exists.
Every step links into the tool's own documentation; links were checked on 2026-10-14 · version 1
Setup (one time)
1. Have Keycloak 26.4 running on 127.0.0.1:18080 with the realm and the confidential client m2m-batch from the OAuth practice path, and its secret in an environment variable.
The collection tests a real token endpoint; Keycloak is the local one.
Explained in: Keycloak: Getting started with Docker (www.keycloak.org), checked 2026-10-14
Done when: A curl client-credentials request answers 200.
2. Choose the authoring tool: the Postman app (export the collection as v2.1 JSON) or Bruno (plain .bru files); install Newman and the Bruno CLI with npx in a scratch folder, no global install.
Both runners work from the command line without the desktop apps; the collection format decides which you use.
Explained in: Postman: Postman overview (learning.postman.com), checked 2026-10-14
Done when: npx -y newman --version and npx -y @usebruno/cli --version print versions.
Steps
1. Create an environment with issuer, client_id, client_secret (secret type) and an empty access_token; export it and confirm the secret's value is not in the file.
Secret-typed variables are the hygiene rule in practice; checking the export once makes it a habit.
Explained in: Postman: Store values in variables (learning.postman.com), checked 2026-10-14
Done when: The exported environment holds the four names and no secret value.
2. Request 1, discovery: GET {{issuer}}/.well-known/openid-configuration with no auth; tests assert 200, issuer equals the environment's issuer and S256 is supported; store token_endpoint, introspection_endpoint, jwks_uri and userinfo_endpoint in the environment.
The first link in the chain: every later URL comes from this response, so the collection follows the provider wherever it moves its endpoints.
Explained in: Postman: Write test scripts (learning.postman.com), checked 2026-10-14
Done when: The request passes its three assertions and the four endpoint variables are populated.
3. Request 2, token: POST {{token_endpoint}} with the Basic auth helper (username {{client_id}}, password {{client_secret}}) and a form body grant_type=client_credentials, scope=openid; tests assert 200, token_type Bearer, expires_in above 0 and no refresh_token; decode the JWT header and assert alg and kid; store access_token and kid.
The Basic helper is the client authentication; the scope=openid is the lesson the first run taught (userinfo is 403 without it).
Explained in: Postman: Authorizing requests (learning.postman.com), checked 2026-10-14
Done when: The request passes and access_token and kid are set for the rest of the run.
4. Request 3, JWKS: GET {{jwks_uri}}; assert the stored kid is among the published keys.
A collection can assert the token lesson's kid rule in one line; it also catches a rotated key whose JWKS has not been updated.
Explained in: RFC 7517: JSON Web Key (JWK) (datatracker.ietf.org), checked 2026-10-14
Done when: The assertion passes, proving the token's signing key is where a verifier would look.
5. Request 4, introspection: POST {{introspection_endpoint}} with Basic auth as the client and token={{access_token}}; assert active true and client_id equals the environment's client id.
The resource-server check in the collection; it fails loudly when a token is revoked.
Explained in: RFC 7662: OAuth 2.0 Token Introspection (datatracker.ietf.org), checked 2026-10-14
Done when: The assertions pass.
6. Set the collection's auth to Bearer with {{access_token}}; request 5, protected call: GET {{userinfo_endpoint}} inheriting auth; assert 200 and that preferred_username is service-account- plus the client id.
Inheritance is how a collection stays maintainable; one place to change the token for every request.
Explained in: Postman: Authorizing requests (learning.postman.com), checked 2026-10-14
Done when: The request passes using inherited auth and no per-request header.
7. Requests 6 and 7, negative: the token request with the password not-the-secret, asserting 401 and an error of unauthorized_client or invalid_client; and the protected call with no auth, asserting 401 and a WWW-Authenticate header containing Bearer.
Negative tests that pass are the proof that the positive tests mean something.
Explained in: Postman: Postman JavaScript reference (pm API) (learning.postman.com), checked 2026-10-14
Done when: Both requests pass by failing as asserted.
8. Run the collection headless: npx newman run collection -e environment --reporters cli,json --reporter-json-export newman-run.json.
The CI gate in one command; the exit code is what a pipeline reads.
Explained in: Postman: Run and test collections from the command line using Newman (learning.postman.com), checked 2026-10-14
Done when: Newman reports 7 requests, 17 assertions and 0 failed, exits 0, and the JSON report exists; this is the learning center's result.
9. Remove client_secret's value from the environment file entirely and run again with --env-var client_secret=$CLIENT_SECRET; then run once with a wrong value.
This is how the collection runs in CI: the secret from the pipeline's store, the file in git with none.
Explained in: Newman README (postmanlabs/newman) (github.com), checked 2026-10-14
Done when: The first run passes identically; the second fails requests 2 to 5 and exits non-zero, and no secret is in any committed file.
10. Write the token and protected-call requests as two .bru files with a local environment that declares client_secret under vars:secret, and run bru run --env local --env-var client_secret=$CLIENT_SECRET.
The same scenario as reviewable plain text; a team that keeps collections in git can stop here.
Explained in: Bruno CLI overview (docs.usebruno.com), checked 2026-10-14
Done when: The Bruno CLI reports 2 requests passed and 4 of 4 tests, as in the learning center's run.
11. Reproduce request 2 with curl -v -u and --data-urlencode and compare the request headers curl sent with what the Postman console shows for the same request.
curl is the referee when a tool and a pipeline disagree; knowing the equivalent command for every helper is the skill.
Explained in: curl manual page (curl.se), checked 2026-10-14
Done when: The two Authorization headers are identical and you can say what the helper did for you.