Where this sits
Last session got the first request running and set the homework: build the RESTful Booker collection in Postman. This one takes that collection and automates one flow end to end.
The framework is otherwise finished, so everything here lands inside it rather than beside it.
The flow, in four steps
Not every request, one journey. That distinction matters, because a journey is what a user actually does and what a regression suite should protect.
Postman first, and what broke
The collection was walked through live before any automation, which is the right order: you cannot automate a request you have not made by hand.
Three things went wrong, and each is worth knowing because each will happen to you:
- PATCH returned the wrong thing because the method was still set to the previous verb. The dropdown has to be changed, not just the URL. Easy to miss, and it looks like an API fault.
- The token expired mid-session and had to be regenerated. Tokens are not permanent.
- An update failed even with a valid token and id. RESTful Booker is a shared, public API, so another person can delete the booking you just made. The fix is to create a fresh booking and use that.
That last one is the one to remember when your suite goes red for no reason. On a shared sandbox, a test that depends on data it did not create in this run is flaky by construction. Create what you need at the start of the flow, which is exactly what the automated version does.
The finished collection was exported as JSON and put in the repo under docs/, not source/. It is reference material, not code the framework runs.
Authentication is not authorization
The concept the entire flow depends on, and a reliable interview question.
Authentication proves who you are. Authorization decides what you may do. The portal analogy from class: every student is authenticated, but only an admin is authorized to delete a video.
On RESTful Booker, anyone can create a booking. Only a token holder can update or delete one.
The auth mechanisms named in passing, worth recognising: basic, digest, API key, bearer, OAuth 1.0 and 2.0, and JWT. In practice you will meet basic auth, bearer tokens and OAuth 2.0 far more than the rest.
The 403 that proves it
Here is the part worth pinning down, because it turns the theory into something you can observe.
Send a PUT with a perfectly valid booking id and no token:
const res = await request.put(`/booking/${bookingId}`, { headers, data: payload });
console.log(res.status(), res.statusText());
403 Forbidden
Not 401. The server is not asking who you are. It has decided you may not do this. That is precisely the authorization half of the distinction above, and the status code is the API telling you which half failed.
Interview shape: you get a 401 from an endpoint, and a colleague gets a 403 from the same endpoint. What is different? The 401 says the request was not authenticated, so no valid identity was presented and the fix is credentials or a token. The 403 says identity was accepted but permission was refused, so the fix is roles or scopes, not credentials. Sending better credentials will never turn a 403 into a 200.
Give every shape an interface
Before writing the test, every payload and every response gets a type. This is the interfaces material from the 3x track doing real work.
interface BookingDates {
checkin: string;
checkout: string;
}
interface BookingPayload {
firstname: string;
lastname: string;
totalprice: number;
depositpaid: boolean;
bookingdates: BookingDates;
additionalneeds?: string;
}
interface AuthResponse {
token: string;
}
interface CreateBookingResponse {
bookingid: number;
booking: BookingPayload;
}
interface BookingFlowState {
token?: string;
bookingId?: number;
}
Five interfaces, and two details are deliberate.
bookingdates is its own interface because a nested JSON object needs its own type. You cannot describe it inline and keep it readable.
BookingFlowState has both fields optional, and that is not laziness. At the start of the test neither exists. They get filled in as the steps run. The type is honest about a value that genuinely might not be there yet, which is what forces the guard further down.
Three steps, one shared state
test('end to end CRUD flow', async ({ request }) => {
const flow: BookingFlowState = {};
await test.step('create token', async () => {
const res = await request.post('/auth', {
headers,
data: { username: 'admin', password: 'password123' },
});
expect(res.status()).toBe(200);
const data = (await res.json()) as AuthResponse;
flow.token = data.token;
});
await test.step('create booking', async () => {
const res = await request.post('/booking', { headers, data: payload });
expect(res.status()).toBe(200);
const data = (await res.json()) as CreateBookingResponse;
expect(data.booking.firstname).toBe(payload.firstname);
flow.bookingId = data.bookingid;
});
await test.step('update booking', async () => {
if (!flow.token || !flow.bookingId) {
throw new Error('create token and create booking must pass before update');
}
const res = await request.put(`/booking/${flow.bookingId}`, {
headers: { ...headers, Cookie: `token=${flow.token}` },
data: { ...payload, firstname: 'Pramod', lastname: 'Dutta' },
});
expect(res.status()).toBe(200);
const data = (await res.json()) as BookingPayload;
expect(data.firstname).toBe('Pramod');
});
});
Running 1 test using 1 worker
1 passed (1.5s)
One test, three steps, not three tests. That is the right shape here because the steps are not independent: step 2 cannot run without what steps 0 and 1 produced. Three separate tests would either share hidden state or repeat the setup.
test.step gives you the named breakdown in the report while keeping the dependency honest.
No page, no screenshots, no video, and no visual step wrapper. There is no UI to photograph. The trace is still worth having, because when a response body is wrong the trace is where you read it.
The guard, and the spread
Two small pieces of that test carry more weight than they look.
The guard turns an optional type into a clear failure:
if (!flow.token || !flow.bookingId) {
throw new Error('create token and create booking must pass before update');
}
Without it, a failure in step 0 produces PUT /booking/undefined with Cookie: token=undefined, and the error you see is a confusing 404 or 403 from the server rather than the truth, which is that an earlier step failed. The guard makes the test fail where the problem is. TypeScript also needs it: the fields are optional, so it will not let you use them until you have proved they exist.
The spread adds one header without rewriting the rest:
headers: { ...headers, Cookie: `token=${flow.token}` }
...headers copies the shared Accept and Content-Type, and Cookie is added on top. One source of truth for the common headers, one line for the difference.
A second base URL, without a second framework
A question from the room: what if the framework has to test two different APIs?
const ctx = await request.newContext({ baseURL: 'https://gorest.co.in' });
const res = await ctx.get('/public/v2/users');
expect(res.ok()).toBeTruthy();
await ctx.dispose();
200
request.newContext() creates an independent context with its own baseURL, while the rest of your specs carry on against the one in the config. Two APIs, one framework.
dispose() at the end closes it. Contexts left open hold resources for the life of the run.
Postman or Playwright
Asked directly in class, and the answer given was a threshold rather than a principle:
| Scale | Reach for | Why |
|---|---|---|
| Up to roughly 500 to 1,000 cases | Postman, by hand | Fast to write, fine to maintain, no framework needed |
| Beyond that | Playwright | Regression by hand stops being viable at that size |
The second argument is the one that decides it in practice: Playwright lets you mix API and UI in one test. Log in through the API, inject the token or cookie, and go straight to the dashboard, instead of driving the login form every single time. That is dramatically faster than a pure UI flow, and it is not something Postman can do at all.
What Monday refactors
The code that runs today is deliberately unoptimised, and the session said so. The plan is to map it onto arrange, act, assert, the same pattern used for page objects.
The APIHelper class centralises URL building, retries, and every HTTP method as generic functions. The API fixture supplies the setup. What remains in the spec is the assertions, which is the only part a reader actually needs.
Also flagged as things the current code does badly on purpose: the username and password are inline and belong in environment variables, and the test data should come from the existing data generator or faker rather than being hardcoded.
Tasks and announcements
Three tasks. Watch the authorization against authentication videos being released after the session, covering basic, digest, API key, bearer and OAuth. Rewatch this session and type the code yourself rather than copying. Then add the end-to-end CRUD test to your own project.
- Manual API testing videos are being released for anyone who has not done Postman properly. The course does not otherwise cover Postman, because the target here is automation.
- Monday: the
APIHelperclass and the API fixture refactor. - Sunday 9 AM: a test, open for 12 hours, so any time until 9 PM.
- Next week: the remaining Jenkins sessions, with Deepak.
- The CRUD code is being pushed to the student repo, and the Postman collection is in
docs/for reference.