This is the worked walkthrough through each of the nine deliverables from the brief. Read it after you've made a first attempt — the goal is to confirm or correct your instincts, not substitute for them. The examples below are illustrative; treat them as a quality bar, not a copy-paste template. If yours look better, use yours. If yours feel thinner, the gap tells you what to fix.
How the deliverables hang together
Step 1 of 6
Strategy
One page setting scope, approach, environments, tools, risks. Everything below flows from it.
Six logical phases. The order matters — strategy informs scope, the matrix surfaces authorisation gaps before they multiply across test cases, schemas can wait until you know which responses you actually want to validate.
Step 1 — Test strategy
The shortest useful strategy fits on one page. Mine for BookItNow:
Scope. All endpoints listed in the brief, with deeper coverage on bookings (highest risk) and pagination on
GET /hotels(most likely to be misused). Out of scope for this pass: admin-only hotel creation (low traffic), localisation, internationalisation.Approach. Functional first — I want a baseline of "every endpoint works for the main happy path." Then auth: every endpoint tested under three roles (guest, user, admin). Then edge cases focused on bookings. Pagination and schemas layered in. Performance and contract testing deferred (stretch goals).
Environments. All tests run against the staging URL. Smoke runs against production read-only, never against any endpoint that mutates data.
Tools. Postman for exploration; Python
requests+pytestfor automation;jsonschemafor response validation. One tool per job.Risks. (1) No reset between test runs in staging — bookings accumulate; mitigated by per-test cleanup. (2) Auth tokens expire; mitigated by a fixture that fetches a fresh token per session. (3) Rate limits on
/auth/loginmay break parallel runs; mitigated by serialising the auth tests.
That's about 200 words. It's enough. The point isn't to be comprehensive — it's to make defensible choices and document them so the team can challenge or accept each.
Step 2 — Permission matrix
Build it as a table. Rows are roles, columns are endpoints, cells are expected status codes:
| Endpoint | guest | user | admin |
|---|---|---|---|
POST /auth/login | 200 | 200 | 200 |
GET /hotels | 200 | 200 | 200 |
GET /hotels/:id | 200 | 200 | 200 |
POST /hotels | 401 | 403 | 201 |
GET /hotels/:id/rooms | 200 | 200 | 200 |
POST /bookings | 401 | 201 | 201 |
GET /bookings/:id (own) | 401 | 200 | 200 |
GET /bookings/:id (other user's) | 401 | 403 | 200 |
PUT /bookings/:id (own) | 401 | 200 | 403 |
PUT /bookings/:id (other user's) | 401 | 403 | 403 |
DELETE /bookings/:id (own) | 401 | 204 | 204 |
GET /users/me | 401 | 200 | 200 |
PUT /users/me | 401 | 200 | 200 |
A few things this table makes obvious that prose hides:
- A guest never sees 403 — they can't even authenticate, so it's always 401.
PUT /bookings/:idby an admin returns 403 in this design — admins can read and cancel any booking, but can't modify one on the user's behalf. That's a deliberate scoping decision worth documenting.- "Other user's" rows are easy to miss without a matrix; they catch IDOR-style bugs.
Build this first. Every cell becomes a test, and you'll spot ambiguities you'd otherwise leave to the engineer's interpretation.
Step 3 — Sample test cases
Fifteen examples, three per category, illustrating the shape every entry in your inventory should take.
Booking — positive cases
TC-B-001: User books an available room
POST /bookingswith{ roomId: 42, from: "2026-08-01", to: "2026-08-05", guests: 2 }, valid user token.- Expected:
201 Created, body matches booking schema,totalmatchesnights × pricePerNight.
TC-B-002: User reads their own booking
GET /bookings/{id}with the user's token.- Expected:
200 OK, body matches booking schema,userIdmatches the caller.
TC-B-003: User cancels their own booking
DELETE /bookings/{id}with the user's token.- Expected:
204 No Content. Follow-upGETreturns the booking withstatus: cancelled, or404if hard-deleted.
Booking — negative cases
TC-B-101: Missing roomId
POST /bookingswith noroomId, valid token.- Expected:
400 Bad Request, body identifies the missing field.
TC-B-102: from after to
POST /bookingswithfrom: "2026-08-05", to: "2026-08-01".- Expected:
400or422, body explains the date inversion.
TC-B-103: Anonymous booking attempt
POST /bookingswith no Authorization header.- Expected:
401 Unauthorized.
Authentication
TC-A-001: Successful login
POST /auth/loginwith valid credentials.- Expected:
200, body hasaccessToken(JWT format),refreshToken,expiresIn.
TC-A-002: Expired token
- Any protected endpoint with a JWT whose
expis in the past. - Expected:
401, error codetoken_expired(or equivalent).
TC-A-003: Wrong role
POST /hotelswith a user-role (not admin) token.- Expected:
403 Forbidden.
Edge cases
TC-E-001: Double booking
- Two users attempt
POST /bookingsfor the same room and dates. - First call:
201. Second call:409 Conflict.
TC-E-002: Booking starting in the past
POST /bookingswithfrom: "2020-01-01".- Expected:
400or422, error explains "start date must be in the future."
TC-E-003: Overlapping dates
- Existing booking:
from: 2026-08-01, to: 2026-08-05. New attempt:from: 2026-08-04, to: 2026-08-07. - Expected:
409 Conflict.
Pagination
TC-P-001: Default first page
GET /hotelswith no params.- Expected:
200, default page (1) with default limit (e.g. 20), metadata reflects total count.
TC-P-002: Page beyond total
GET /hotels?page=999.- Expected:
200with empty array,hasMore: false. Not 404.
TC-P-003: Invalid limit
GET /hotels?limit=99999.- Expected:
400(over max) or capped to documented max — confirm and assert.
Fifteen cases, three per category. Your inventory of fifty extends this same shape across every endpoint family.
Step 4 — Auth deep dive
The auth tests deserve their own focused mini-suite. The brief listed six; here's what each looks like in practice:
1. Register → returns 201 with accessToken and refreshToken
2. Register with existing email → 409 Conflict
3. Login with correct credentials → 200 + token
4. Login with wrong password → 401 (NOT 400 — preserves user enumeration safety)
5. Login with non-existent email → also 401 (same response as wrong password)
6. Refresh with valid refresh token → 200 + new accessToken
7. Refresh with expired refresh token → 401
8. Refresh with reused (already-used) refresh token → 401 (rotation enforced)
9. Protected endpoint with no token → 401
10. Protected endpoint with malformed JWT → 401
11. Protected endpoint with tampered JWT (changed payload) → 401
12. Protected endpoint with expired JWT → 401, error code `token_expired`
That set covers the whole authentication surface and a chunk of the authorisation surface. Wire it up as the first test suite to run; if any of these fail, the rest of the suite is meaningless.
Step 5 — Schemas
Two JSON Schemas, one per critical response.
Booking response (POST /bookings 201)
{
"type": "object",
"required": ["id", "roomId", "userId", "from", "to", "status", "total", "createdAt"],
"properties": {
"id": { "type": "integer", "minimum": 1 },
"roomId": { "type": "integer", "minimum": 1 },
"userId": { "type": "integer", "minimum": 1 },
"from": { "type": "string", "format": "date" },
"to": { "type": "string", "format": "date" },
"guests": { "type": "integer", "minimum": 1 },
"status": { "type": "string", "enum": ["confirmed", "pending", "cancelled"] },
"total": { "type": "number", "minimum": 0 },
"createdAt": { "type": "string", "format": "date-time" }
},
"additionalProperties": false
}Hotel response (GET /hotels/:id 200)
{
"type": "object",
"required": ["id", "name", "city", "rating"],
"properties": {
"id": { "type": "integer", "minimum": 1 },
"name": { "type": "string", "minLength": 1, "maxLength": 200 },
"city": { "type": "string" },
"rating": { "type": "number", "minimum": 0, "maximum": 5 },
"address": { "type": "object" },
"rooms": {
"type": "array",
"items": { "type": "object", "required": ["id", "type", "pricePerNight"] }
}
},
"additionalProperties": false
}The additionalProperties: false line is a deliberate choice. It means "if you add a field, the schema fails until I update it." That's friction by design — you want to be reminded.
Step 6 — Smoke test list
Ten curl commands, each with an expected status:
# 1. API is up
curl -sf -o /dev/null -w "%{http_code}\n" $BASE/health # 200
# 2. Anonymous hotel list works
curl -sf -o /dev/null -w "%{http_code}\n" $BASE/hotels # 200
# 3. Anonymous hotel detail works
curl -sf -o /dev/null -w "%{http_code}\n" $BASE/hotels/1 # 200
# 4. Login works
curl -X POST -H "Content-Type: application/json" \
-d "{\"email\":\"$SMOKE_USER\",\"password\":\"$SMOKE_PASS\"}" \
-sf -o /dev/null -w "%{http_code}\n" $BASE/auth/login # 200
# 5. /users/me works with a token
curl -H "Authorization: Bearer $TOKEN" \
-sf -o /dev/null -w "%{http_code}\n" $BASE/users/me # 200
# 6. Anonymous protected call refused
curl -o /dev/null -w "%{http_code}\n" $BASE/users/me # 401
# 7. Pagination works
curl -sf -o /dev/null -w "%{http_code}\n" "$BASE/hotels?page=1&limit=5" # 200
# 8. Hotel rooms endpoint works
curl -sf -o /dev/null -w "%{http_code}\n" $BASE/hotels/1/rooms # 200
# 9. Token refresh works
curl -X POST -H "Content-Type: application/json" \
-d "{\"refreshToken\":\"$REFRESH\"}" \
-sf -o /dev/null -w "%{http_code}\n" $BASE/auth/refresh # 200
# 10. Non-existent resource returns 404 (not 500)
curl -o /dev/null -w "%{http_code}\n" $BASE/hotels/999999 # 404Wrap them in a bash script with a final "all 10 expected codes matched" check. That single script is now your post-deploy gate.
How the deliverables interlock
Read top to bottom and notice the dependencies:
- The strategy sets scope and tool choices that the rest assumes.
- The permission matrix generates roughly half the test cases — each non-trivial cell becomes a test.
- Schemas plug into every test that reads booking or hotel responses, providing free structural validation.
- Auth tests are the prerequisite for everything else; if login is broken, nothing in the user-scoped suite runs.
- The smoke list is the trip-wire that runs in production after every deploy — three minutes of value for thirty seconds of runtime.
If you've done this work — even at a rough first-pass quality — you have something a real team would adopt. The next lesson helps you grade your own attempt and points at where to take this work next.
📝 Capstone task
Compare your draft from lesson 1 against this walkthrough.
- Open your test strategy. Does it cover scope, approach, environments, tools, and risks? Add anything you missed.
- Compare your permission matrix against the example above. Did you remember "other user's resource" rows? IDOR coverage?
- Pick three test cases from each of your category lists. Verify each has an ID, request shape, expected status, and expected body assertion. Tighten loose ones.
- Run a sanity check on your auth deep-dive — does it cover all twelve scenarios listed here, or are some missing?
- If you skipped the schemas or smoke list, add them now. Both are short and high-value.
- Save the v2 of your work alongside v1. The diff between them is your learning artefact.
Onward to the review lesson.