The Testing Academy · Class Notes Friday, 4 September (IST)
Live class · study guide

CRUD end to end, and why the failure is 403 and not 401

From a Postman collection to one automated end-to-end flow. Token, create, update, threaded through three test.step blocks with shared state, every payload and response given a TypeScript interface. Plus the concept the whole thing rests on: authentication is not authorization, and the status code proves it.

By Pramod Dutta, The Testing Academy. Study notes from the live Playwright 2x class, rebuilt from the session recording. The Eraser deck was not reachable while these notes were written, so the code was reconstructed from the session and then executed before publishing, as a real Playwright spec against the live RESTful Booker API. The full flow passes, and every status code quoted was measured.

01

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.

02

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.

POST /auth step 0, get a token POST /booking step 1, get a bookingid PUT /booking/{id} step 2, needs both DELETE /booking/{id} optional token bookingid Two values thread through the journey, which is exactly what shared state is for. Neither exists until the step before it has run, so the order is not optional.
The token comes from step 0, the booking id from step 1, and the update needs both. That dependency is the whole reason the steps share state.
03

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.

04

Authentication is not authorization

The concept the entire flow depends on, and a reliable interview question.

Authentication with a "c" Who are you? username and password, a token, an API key. You prove identity. fails with 401 Authorization with a "z" What may you do? permission. Identity already known, the question is whether you are allowed. fails with 403 Everyone with a login to the course portal is authenticated. Only an admin is authorized to delete a video.
Two different questions, two different failures. Knowing which one you are looking at tells you where to go and fix it.

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.

05

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:

Code
const res = await request.put(`/booking/${bookingId}`, { headers, data: payload });
console.log(res.status(), res.statusText());
Code
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.

06

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.

Code
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.

07

Three steps, one shared state

Code
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');
  });
});
Code
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.

08

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:

Code
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:

Code
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.

09

A second base URL, without a second framework

A question from the room: what if the framework has to test two different APIs?

Code
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();
Code
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.

10

Postman or Playwright

Asked directly in class, and the answer given was a threshold rather than a principle:

ScaleReach forWhy
Up to roughly 500 to 1,000 casesPostman, by handFast to write, fine to maintain, no framework needed
Beyond thatPlaywrightRegression 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.

11

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.

Today, all in the test Arrange base URL, headers, payload Act the request itself Assert Monday, three homes api.fixture.ts the setup every API test needs, handed in automatically APIHelper.ts builds URLs, every HTTP method, retries, one place the spec file only the assertions, which is all a reader wants to see This is how a suite reaches three or five thousand API tests without becoming unreadable.
Arrange moves to a fixture, act moves to a helper class, assert stays put. The test file ends up saying what it checks and nothing else.

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.

12

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 APIHelper class 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.