The Testing Academy · Class Notes Thursday, 5 November (IST)
Live class · study guide

Playwright API tests: ping, POST, new context, PUT, negative

The first Playwright API tests against restful-booker, written raw: a ping with the request fixture, how the base URL and the reporter get chosen, POST /booking with a payload and headers, a new request context, a PUT built from a token, a booking and test.step, and AI-written negative tests trimmed with a Ponytail review.

By Pramod Dutta, The Testing Academy. Study notes from the Playwright 3x class, built from the session recording, the batch repo and the 3x Eraser notes deck. The code is AdvancePlaywrightFramework3x at commit "feat: add raw restful-booker API specs and docs", pushed as the class ended and still the newest when checked at 12:06 IST on 5 November. The deck's 5 November section holds one heading, CRUD with the request fixture, and no code or links. For these notes the ping, POST and new-context specs and the two negative tests that send no login were run against the live APIs, headless, with the list reporter and one worker; their outputs are shown with the dates masked. 04_put_operation and the two negative tests that call POST /auth were not run, because these notes never send a login to any site: what the page says about them comes from the recording and the repo, and their logic was checked only against a stand-in server on the local machine. The login values in the code are masked, and the login details are in the task post. The object Object output comes from a version rebuilt from the recording, since the pushed code already has the fix.

Hides every output until you tap Reveal, so you can predict it first.
01

What this class covered

  • Ready first, then three levels of API tests
  • The ping test and the request fixture
  • Where the base URL comes from
  • Choosing a reporter, and screenshots
  • POST /booking: payload and headers
  • Postman or Playwright, and other questions
  • A new request context
  • PUT: a token, a booking, then the update
  • Negative tests from AI, and the Ponytail review
  • Which requests need a token
  • Tasks, and what comes next
  • Have the test plan, the test cases and the Postman collection ready first. Then the API tests grow in three levels: raw, an API helper, a fixture. You keep only the last.
  • An API test takes the built-in request fixture instead of page, and gives only the last part of the path, such as /ping. The base URL comes from outside, on the command line.
  • In this framework BASE_URL is checked first, so TTA_ENV=api works only when BASE_URL is empty in .env.
  • Keep all the reporters in the config, and let the user pick one with --reporter.
  • To log an object, use JSON.stringify, or you get [object Object].
  • In restful-booker, PUT, PATCH and DELETE need a token. Create the token and the booking first, then send Cookie: token=.... Never print the token in a report.
  • AI agents add extra code: give their tests a Ponytail review before you push.
02

Ready first, then three levels

READY FIRST, THEN THREE LEVELS OF API TESTS Test plan Test cases Postman collection Now we can automate LEVEL 1 Raw tests basic test cases, every call written out in the spec LEVEL 2 API helper still restful-booker, now with an API helper LEVEL 3 Fixture the final boss: the proper end-to-end API tests The same path as the e2e specs A basic login test, then checkout, then fixtures, then a Ponytail review, then visual steps. The class teaches all three levels. You keep only the last one.
The plan the instructor laid out. Today's class stays on level 1, in the folder 01_restfulbooker_raw.

Last class ended with a task: a test plan, test cases and a Postman collection for restful-booker. This class starts from there. The instructor called it the endgame: the APIs go into the Playwright framework, and then comes the last part, the AI part.

First, a walk through the Postman collection. You can generate a token, get all the bookings, create a booking with a JSON payload, and get one booking. You can update a booking or partially update it, but for an update you need a token: set it first, or the answer is forbidden. Deleting needs a token too. So, at this level, you need three things: a base URL, a token and a booking ID. The endpoints to automate are the eight on last class's page: ping, auth, get bookings, get one booking, POST, PUT, PATCH and DELETE.

The most important thing right now, the instructor said, is that three things are ready for restful-booker: the test plan, the test cases and the Postman collection. Until they are, it will be very difficult to proceed. A few learners had done it, and a few had not. With all three ready, you can select a test case and automate it, and this is the real testing starting.

Three levels. The specs go in a new folder, apiTests, beside the end-to-end tests. Its first folder is 01_restfulbooker_raw, and raw means basic test cases. Level 2 stays on restful-booker and uses an API helper. Level 3 uses a fixture, which the instructor called the final boss, with the proper end-to-end API tests.

The instructor keeps all three folders in order to teach them. You will keep only the last one. It is how the end-to-end tests grew too: first a basic login test, then checkout, then fixtures, then a Ponytail review, and last the visual step. Taught directly with a fixture, the instructor said, you would be a little confused, so the code gets better slowly.

In the repo, this class's code is the folder 01_restfulbooker_raw: five specs numbered 01 to 06, with no 05.

03

The ping test and the request fixture

Two panels. Left, titled Before class, with the sublabel Audio and video good?: a laptop on a desk with a small camera on its lid and a microphone on a small stand, a sound-wave squiggle leaving the microphone towards a blank chat bubble, and a second blank chat bubble coming back with a terracotta tick. Right, titled Before tests, with the sublabel Server up?: the same laptop sending a small envelope along a dashed line to a server rack with a row of lights and a pulse line on its panel, and a second envelope with a terracotta tick coming back.
The instructor's ping: the first thing typed in every class checks the audio and the video, before anything is taught. A ping request does the same for a server.

A ping request helps you test if the server is up: a communication between client and server, to check that all is well. It is like a health check of an API.

The instructor's own ping: every time the class starts, the first thing typed is "Is my audio video is good?" Before teaching anything at all, that is the health check.

In Playwright, the instructor said, API testing is super easy. The class opened the Playwright API Testing page from The Testing Academy's Playwright notes. Its diagram, the instructor said, has everything, and the 29 October page draws it. The request fixture is already created, like the page fixture. It uses something called APIRequestContext, which makes the HTTP request and gives you the response automatically. Expectations you already know. So where a test used to take page, it now takes request, and that is the only thing you need to do.

01_basic_ping.spec.tsTypeScript
import { test, expect} from '@playwright/test';
import { request } from 'node:http';

//  Ping Request.
// Ping request help you to test if the server is up.
// Client -> Server to check if all well.

// HTTP GET -> https://restful-booker.herokuapp.com/ping

test('ping request - GET', async({ request})=>{
        const responseData = await request.get("/ping");
        console.log(responseData);
        expect(responseData.status()).toBe(201);
});

When you type request., you can see all the HTTP functions it can make. On the installed Playwright, a run for these notes lists them: get, post, put, patch, delete, head, fetch, and also dispose, storageState, cookies, addCookies and clearCookies.

The path is only /ping. Where the rest of the URL comes from is the next section. And status is a function you call directly, the instructor pointed out, with no await. The test expects 201.

Run for these notes:

Terminal
BASE_URL=https://restful-booker.herokuapp.com npx playwright test src/tests/apiTests/01_restfulbooker_raw/01_basic_ping.spec.ts --reporter=list
Output
Running 1 test using 1 worker

APIResponse: 201 Created
  Content-Length: 7
  Content-Type: text/plain; charset=utf-8
  Date: Thu, 05 Nov YYYY 06:29:05 GMT
  Etag: W/"7-rM9AyJuqT6iOan/xHh+AW+7K/T8"
  Server: Heroku
  Via: 1.1 heroku-router
  X-Powered-By: Express
  ✓  1 [chromium] › src/tests/apiTests/01_restfulbooker_raw/01_basic_ping.spec.ts:10:5 › ping request - GET (711ms)

  1 passed (992ms)

console.log(responseData) prints the status and its text, then the headers. Three long Heroku reporting headers and the year in the date are cut above. In class the instructor found the same two things in the output: 201 and created. The repo's README counts this among the API's unusual status codes: /ping returns 201.

Line 2 is never used. The request inside the test is the fixture from the test's own parameters, so the import from node:http does nothing. With that line removed, the spec passed the same way in a run for these notes. In the new-context spec below, request is imported from @playwright/test, and there the import is what gets used.

04

Where the base URL comes from

HOW THE BATCH CONFIG PICKS THE BASE URL FIRST CHECK Is BASE_URL set? inline on the command, or in the .env file IF YES That value is used TTA_ENV is not read at all yes no IF NO Read TTA_ENV lowercased; qa when it is not set api API_BASE_URL, or restful-booker dev, local DEV_BASE_URL, or localhost:3000 stg, stage, staging STG_BASE_URL, or the stage site prod, production PROD_BASE_URL, or the TTA site qa, or anything else QA_BASE_URL, or the TTA site An inline value beats .env: dotenv does not overwrite a variable that is already set.
The logic of resolveBaseURL() in playwright.config.ts, drawn as a flow. The class's specs only ever give the last part of the path, such as /ping.

Do we use the full URL, or something else? This is very controversial, the instructor said, and the mystery is solved by the Playwright configuration:

playwright.config.tsTypeScript
function resolveBaseURL(): string {
  if (process.env.BASE_URL) return process.env.BASE_URL;
  const env = (process.env.TTA_ENV || 'qa').toLowerCase();
  switch (env) {
    case 'api':
      return process.env.API_BASE_URL || 'https://restful-booker.herokuapp.com';
    case 'dev':
    case 'local':
      return process.env.DEV_BASE_URL || 'http://localhost:3000';
    case 'stg':
    case 'stage':
    case 'staging':
      return process.env.STG_BASE_URL || 'https://stage.thetestingacademy.com';
    case 'prod':
    case 'production':
      return process.env.PROD_BASE_URL || 'https://app.thetestingacademy.com';
    case 'qa':
    default:
      return process.env.QA_BASE_URL || 'https://app.thetestingacademy.com';
  }
}

Line 56 of the same file sets baseURL: resolveBaseURL(). The condition, in the instructor's reading: if BASE_URL is set, use it. Else check TTA_ENV, and if that is api, the API base URL is used. The advantage is that you do not have to type the base URL: it comes from the configuration, and you add only the last part, ping.

The class also tried exporting TTA_ENV as api, then running the npx command. In that run, the instructor thought, the environment had not been set. Then came the AI's explanation, read out: TTA_ENV will not work, because the URL resolver checks BASE_URL first, so it is better to set the base URL. The command it gave set BASE_URL first, then the test, then the custom reporter. An inline value overrides, the instructor said. There are multiple options, so use whichever works for you, and the command the AI gave, the instructor thought, is actually good: it contains everything. The repo's README gives the same form for the whole folder:

Terminal
BASE_URL=https://restful-booker.herokuapp.com npx playwright test src/tests/apiTests   # API specs

Why TTA_ENV lost: the deck's .env, from the framework build, sets BASE_URL to the TTA site, and the README and .env.example both say TTA_ENV works only when BASE_URL is empty. Run for these notes, with a spec that only prints its baseURL:

In the file On the command The spec's base URL
no .env nothing the TTA site
no .env TTA_ENV=api restful-booker
BASE_URL is the TTA site, as in the deck nothing the TTA site
BASE_URL is the TTA site, as in the deck TTA_ENV=api the TTA site
BASE_URL is the TTA site, as in the deck BASE_URL= restful-booker restful-booker
BASE_URL is empty, as in .env.example TTA_ENV=api restful-booker

The fourth row is the one to remember. With that .env, the class's ping goes to the wrong host, gets a 200 with an HTML page, and fails (the other headers are cut here):

Terminal
TTA_ENV=api npx playwright test src/tests/apiTests/01_restfulbooker_raw/01_basic_ping.spec.ts --reporter=list
Output
APIResponse: 200 OK
  Content-Type: text/html; charset=utf-8
  ✘  1 [chromium] › src/tests/apiTests/01_restfulbooker_raw/01_basic_ping.spec.ts:10:5 › ping request - GET (507ms)


  1) [chromium] › src/tests/apiTests/01_restfulbooker_raw/01_basic_ping.spec.ts:10:5 › ping request - GET 

    Error: expect(received).toBe(expected) // Object.is equality

    Expected: 201
    Received: 200

      11 |         const responseData = await request.get("/ping");
      12 |         console.log(responseData);
    > 13 |         expect(responseData.status()).toBe(201);
         |                                       ^
      14 | });
        at src/tests/apiTests/01_restfulbooker_raw/01_basic_ping.spec.ts:13:39

  1 failed

The README warns about this too: a plain npx playwright test now picks up these specs, and most of them fail unless BASE_URL points at restful-booker.

The order of preference. Asked whether the order is command line, test file and configuration: yes, you can say that, the instructor said. First the command line, then the test file, and then the configuration file.

One exception, from a run for these notes: with BASE_URL inline and a test.use({ baseURL }) in the spec file, the spec got the test file's value. The variable only reaches a test through the config, so a test file's own setting still beats it.

Jenkins. Where does this command go when the automation runs? Into Jenkins, the instructor said. In Jenkins you change the command whenever you run for the API, and there is an option to select: run the API test cases, run both, or run only the UI test cases.

Two short answers. Asked whether the URL can be stored in a global variable: you can. And asked whether a different host URL can be added as a full path: you just add it here, the instructor said.

05

Choosing a reporter, and screenshots

The batch config lists three reporters. Line means the command line, HTML is Playwright's default, and the third is the TTA custom reporter:

playwright.config.tsTypeScript
  reporter: [
    ['line'],
    ['html', { open: 'never' }],
    /* TTA custom report, written to tta-report/ (index.html always points to the latest run). */
    ['./src/utils/CustomReporter.ts'],
  ],

Which one do you want? With only one in the list you do not have to type anything, the instructor said, but with three you can choose: sometimes only HTML, sometimes line. For a web test you may want the custom reporter, while for an API test the custom reporter does not make sense, and a normal HTML reporter does. So always keep all of them, and through the --reporter argument, ask the user. Give this ability to the user. The base URL, the reporter and the project: we will ask everything from the user.

Asked whether this could be configured in the framework instead of passed on the command: we can, the instructor said, but give the ability to the user also.

--reporter replaces what the config lists. Each of these ran and passed for these notes, under the batch config; the second writes playwright-report/, the third tta-report/:

Terminal
BASE_URL=https://restful-booker.herokuapp.com npx playwright test src/tests/apiTests/01_restfulbooker_raw/01_basic_ping.spec.ts --reporter=line --project=chromium
BASE_URL=https://restful-booker.herokuapp.com npx playwright test src/tests/apiTests/01_restfulbooker_raw/01_basic_ping.spec.ts --reporter=html --project=chromium
BASE_URL=https://restful-booker.herokuapp.com npx playwright test src/tests/apiTests/01_restfulbooker_raw/01_basic_ping.spec.ts --reporter=line,./src/utils/CustomReporter.ts --project=chromium

--project=chromium, the instructor said, is because we want to use Chromium as the browser.

An API test still opens no browser: it asks for request, not page, and the README says these specs run with no browser. Run for these notes under the batch config, with no browser installed at all, the ping spec still passed.

Screenshots. Asked how to take a screenshot of a passed or failed test: Playwright does this automatically, the instructor said, with no code to write. In the configuration, screenshot: 'on' turns it on, and there is also an option for only on failure. The class uses on, every time, because the custom reporter has this option. Asked where they are stored: in the report, in the Playwright report data, with the videos.

playwright.config.tsTypeScript
    /* Media for the custom report. 'on' captures for every test, pass or fail.
       For a real suite prefer 'only-on-failure' / 'retain-on-failure'. */
    screenshot: 'on',
    video: 'on',
    trace: 'on',

An API test has no screenshot and no video, the instructor said. Under this config, with screenshot, video and trace all on, a run for these notes left a trace.zip and no image or video in test-results/.

06

POST /booking: payload and headers

Two panels. Left, titled A contract, with the sublabel JSON in, JSON out: a laptop and a server rack on either side of one large paper contract on an easel, with two squiggle signatures and a terracotta seal, and below it two blush parcels with arrows going each way. Right, titled Anything else, with the sublabel Will not accept: the same server rack behind a low barrier with a terracotta cross on it, and three different parcels stopped in front of it: a sheet of angled marks, a sheet of plain lines and a small framed picture.
The instructor's contract: the header says what the client sends and what it accepts. Do not send me XML, a text file or an image, I will not accept.

The next test case is create booking. Before writing it, the class listed what changes. The URL is the base URL plus /booking, and the HTTP method is POST. There is a payload, which can be a simple JSON file or a JS object: some people call it the request body, the body or the data. Headers need to be set. And authentication? For create booking, nothing is required, and no token. The @p0 in the title is just a tag.

02_post_operation.spec.tsTypeScript
import { test, expect } from '@playwright/test';
import { logger } from '@utils/logger';

//  Ping Request.
// Ping request help you to test if the server is up.
// Client -> Server to check if all well.

// HTTP GET -> https://restful-booker.herokuapp.com/ping

test('TC#1 @p0 - POST : Verify that create booking is working fine.', async ({ request }) => {

        // HTTP -> POST 
        // URL => https://restful-booker.herokuapp.com/booking
        // PAYLOAD / request body /. Body / data -> json file or js object
        // headers  - content-type json
        // auth ? 

        const payload = {
                firstname: 'Jim',
                lastname: 'Brown',
                totalprice: 111,
                depositpaid: true,
                bookingdates: {
                        checkin: '2018-01-01',
                        checkout: '2019-01-01',
                },
                additionalneeds: 'Breakfast',
        }
        logger.info(`Payload is set: ${JSON.stringify(payload)}`);


        const headers = {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
        };
        logger.info(`Header is set: ${JSON.stringify(headers)}`);


        const responseData = await request.post('/booking', {
                headers,
                data: payload,
        });

        expect(responseData.status()).toBe(200);
        const data = await responseData.json();
        logger.info(`Response is set: ${JSON.stringify(data)}`);
        expect(data.bookingid).toBeTruthy();
        expect(data.booking.firstname).toBe(payload.firstname);
        expect(data.booking.lastname).toBe(payload.lastname);
        expect(data.booking.bookingdates.checkin).toBe(payload.bookingdates.checkin);
        logger.info(`Created booking id: ${data.bookingid}`);

});

Lines 4 to 8 are the ping spec's comments: the file began as a copy of it.

Headers. Content-Type says what the content is, and Accept what comes back. Here both say JSON.

A header, the instructor said, basically means a contract between client and server. The content we are sending is JSON, and we accept JSON. Do not send me XML, I will not accept. Do not send me a text file or an image, I will not accept.

Asked whether this header is the same for most applications: 90% this is the same header, the instructor said, and the developer will tell you which one to use.

The call. request.post takes the URL first, then headers and data as the second argument. Someone had said the base URL has to be set again and again, so the instructor showed a way round it: a template literal around process.env.API_BASE_URL, using that variable if it is set and a default if not. Then the verdict: ideally, this is not a good approach. The best approach is the plain /booking, because the base URL can then be set from outside, on the command line. The pushed spec has the plain path.

The checks. The status should be 200. Then responseData.json() gives the data, and everything can be verified. toBeTruthy on data.bookingid means it should exist: if it is null or zero or something, the booking was not created. In Playwright's own types, truthy means anything but false, 0, '', null, undefined or NaN. First name and last name should equal the payload's. A learner asked whether check-in and check-out can be checked too. Absolutely: keep adding dots, data.booking.bookingdates.checkin. The pushed spec has check-in. Asked whether each field needs its own step: we can verify in one go also.

Run for these notes:

Terminal
BASE_URL=https://restful-booker.herokuapp.com npx playwright test src/tests/apiTests/01_restfulbooker_raw/02_post_operation.spec.ts --reporter=list
Output
Running 1 test using 1 worker

YYYY-MM-DD 11:59:06 [info] Payload is set: {"firstname":"Jim","lastname":"Brown","totalprice":111,"depositpaid":true,"bookingdates":{"checkin":"2018-01-01","checkout":"2019-01-01"},"additionalneeds":"Breakfast"}
YYYY-MM-DD 11:59:06 [info] Header is set: {"Accept":"application/json","Content-Type":"application/json"}
YYYY-MM-DD 11:59:07 [info] Response is set: {"bookingid":4284,"booking":{"firstname":"Jim","lastname":"Brown","totalprice":111,"depositpaid":true,"bookingdates":{"checkin":"2018-01-01","checkout":"2019-01-01"},"additionalneeds":"Breakfast"}}
YYYY-MM-DD 11:59:07 [info] Created booking id: 4284
  ✓  1 [chromium] › src/tests/apiTests/01_restfulbooker_raw/02_post_operation.spec.ts:10:5 › TC#1 @p0 - POST : Verify that create booking is working fine. (679ms)

  1 passed (994ms)

It will be super fast, the instructor said, and in the logs you can see everything. Here it took 679ms.

[object Object]. The first run of those logger lines printed the objects directly, and the class got this. It is rebuilt here from the recording, by putting the objects straight into the three lines, and run for these notes:

TypeScript
logger.info(`Payload is set: ${payload}`);
logger.info(`Header is set: ${headers}`);
logger.info(`Response is set: ${data}`);
Output
YYYY-MM-DD 11:55:29 [info] Payload is set: [object Object]
YYYY-MM-DD 11:55:29 [info] Header is set: [object Object]
YYYY-MM-DD 11:55:30 [info] Response is set: [object Object]
YYYY-MM-DD 11:55:30 [info] Created booking id: 2370

The payload is an object, so to print its values it has to become a string. Either convert it to a string or stringify it, the instructor said, and asked the AI, which used JSON.stringify on all three lines. Those are the pushed lines 29, 36 and 46. A plain .toString() would not have helped, as a run for these notes shows:

JavaScript
const payload = { firstname: 'Jim', lastname: 'Brown' };
console.log(`${payload}`);
console.log(payload.toString());
console.log(JSON.stringify(payload));
Output
[object Object]
[object Object]
{"firstname":"Jim","lastname":"Brown"}

Why a logger at all? A logger is created to log the data, the instructor said: it keeps printing, so we know what is going on.

07

Postman or Playwright, and other questions

What is the difference between Postman and Playwright? Postman is generally more helpful for manual API testing, the instructor said. It can do automation too, but limited. Playwright helps you automate the APIs, and the advantage is that you can mix UI and API.

We can automate through Postman, right? You can, but Postman automation is not scalable. When you have something like 6000 or 5000 test cases, Postman will be very, very difficult to maintain.

Can it support REST and SOAP? It supports REST, and the class works on REST only. SOAP can also be done, but there is another way.

Do we convert XML to JSON? If it is required, yes.

How do we validate a response that has extra fields? That will also be discussed.

In a real project, do we combine API and UI test cases? Yes, the instructor said. In a real-time project we will combine them.

The workers. Asked about 8 workers: it is a default value, the instructor said. In the config, workers is undefined outside CI, and Playwright's own types give the default as half of the machine's logical CPU cores, so your number can differ.

All of this in one spec? Asked what happens to the auth, the headers and everything else sitting in a single spec: wait and watch, the instructor said. This is not optimized code, and optimized code will come with the fixture.

The extra things in the report. You will also notice AI Data, AI Verdict, flakiness and self-healing, the instructor said. These are AI agents the class will create in the upcoming classes.

08

A new request context

Suppose you have written around 5000 test cases around restful-booker, and suddenly you have to test Go REST. Do you change everything: the base URL, the API URL? No. You can write a test case normally and create a different context. This time you do not use the request fixture:

03_newContext_api.spec.tsTypeScript
import { test, expect, request } from '@playwright/test';

test('newContext for isolated headers', async () => {

    const ctx = await request.newContext({
        baseURL: 'https://gorest.in',
        extraHTTPHeaders: { 'X-Trace-Id': 'demo-123' },
        timeout: 15_000,
    });

    const ping = await ctx.get('/public/v2/users/1001?page=1&per_page=10');
    expect(ping.status()).toBe(200);

    await ctx.dispose();



});

The context has everything of its own: its own base URL, its own extra HTTP headers, its own timeout. The test makes its GET request, verifies it, and disposes the context. Create a new context, run your test case, verify it, then dispose it: this is the beauty, the instructor said.

Run for these notes. This spec brings its own base URL, so the command sets none, and it prints nothing but the result:

Terminal
npx playwright test src/tests/apiTests/01_restfulbooker_raw/03_newContext_api.spec.ts --reporter=list
Output
Running 1 test using 1 worker

  ✓  1 [chromium] › src/tests/apiTests/01_restfulbooker_raw/03_newContext_api.spec.ts:3:5 › newContext for isolated headers (893ms)

  1 passed (1.2s)

Several projects in one framework. Asked how to combine multiple products in one framework: very simple, the instructor said. You just change the base URLs, and the test cases are always separate anyway. You can also use a different context for them.

Sixteen API projects in one repo. Asked whether a company's 16 different API projects in the same repo can be combined: absolutely yes. Create a configuration file containing all the different URLs, and whichever test case you want to run, change the base URL. Base URL 1 with run 1, base URL 2 with run 2: the commands change, that's it.

09

PUT: a token, a booking, then the update

A PUT NEEDS WHAT THE TWO CALLS BEFORE IT RETURN STEP 1 Create auth token POST /auth gives the token STEP 2 Create booking POST /booking gives the booking ID STEP 3 Update booking PUT /booking/${bookingId} Cookie: token=${token} only with both values token booking ID No token, or an invalid one: 403 Forbidden you cannot make a PUT request directly Everything common, like the token and the booking ID, moves to a fixture later.
The three test.step blocks of 04_put_operation.spec.ts. The 403 was run for these notes; Steps 1 and 3 with a real token were not, because they send a login.

A PUT is a little difficult, the instructor said. You cannot make one directly: if you do, it gives you forbidden. First create a token, which is a POST. Then create a booking, also a POST, to /booking, which gives you a booking ID. Only by using this token and this booking ID can you make the PUT. In Postman it is the same: create the booking and put its ID in the variable, create the token and put it in the variable, and only when both values are there will update booking work.

04_put_operation.spec.tsTypeScript
import { test, expect } from '@playwright/test';
import { logger } from '@utils/logger';


test.describe('Restful Booker booking API', () => {

    // PUT
    // Create Token -> Post Auth -> token
    // Create Booking -> Post Booking -> bookng ID
    // by using this token and bookingid
    // we can make the put request. 

    test('TC#2 @p0 - PUT : Verify that update the existing booking is working fine.', async ({ request }) => {

        const headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        };

        const payload = {
            firstname: 'Pramod',
            lastname: 'Dutta',
            totalprice: 111,
            depositpaid: true,
            bookingdates: {
                checkin: '2018-01-01',
                checkout: '2019-01-01',
            },
            additionalneeds: 'Breakfast',
        };

        let token = '';
        let bookingId = 0;

        await test.step('Step 1 - Create auth token', async () => {

            const responseData = await request.post('/auth', {
                headers,
                data: {
                    username: '<username>',
                    password: '<password>',
                },
            });
            expect(responseData.status()).toBe(200);

            const data = await responseData.json();
            token = data.token;
            expect(token).toBeTruthy();
            logger.info('Created auth token for PUT booking flow');


        });
        await test.step('Step 2 - Create booking to update', async () => {

            const responseData = await request.post('/booking', {
                headers,
                data: payload,
            });
            expect(responseData.status()).toBe(200);

            const data = await responseData.json();
            bookingId = data.bookingid;
            expect(bookingId).toBeTruthy();
            logger.info(`Created booking id for update: ${bookingId}`);


        });
        await test.step('Step 3 - Update booking', async () => {

            const responseData = await request.put(`/booking/${bookingId}`, {
                headers: {
                    ...headers,
                    Cookie:`token=${token}`
                },
                data: payload,
            });
            const data = await responseData.json();
            expect(data.firstname).toBe(payload.firstname);
            expect(data.lastname).toBe(payload.lastname);
            logger.info(`Updated booking id ${bookingId}: ${data.firstname} ${data.lastname}`);



        });




    });

});

The login is masked. Lines 40 and 41 hold restful-booker's login in the class's code. They are shown here as <username> and <password>, and the login details are in the task post. Every other byte is the repo's.

Steps. test.describe is there because the file can hold more test cases. Inside the test, each part is a test.step: create a token, create a booking, update the booking. This is a more modular way, the instructor said, so that a proper report comes out: it is about reporting. It is why the instructor loves steps: it is very easy to debug.

Step 1. Yes, the username and password are hard-coded here, and it is a bad practice for now. They will be replaced later, with something like a process.env value, when this moves to the API helper and the fixture. The step stores the token.

Step 2 is the same create booking as before, and it stores the booking ID.

Step 3. The update is the same shape, with changes. It is request.put. The URL ends in the booking ID, and backticks are how you inject a variable. And there is one more change: ...headers, the spread operator, gives all the headers, plus one more, Cookie, set to token= and the token. So all three go. The payload does not change.

What the class saw. The instructor read the report step by step: create token, token is created; create booking, booking is created; update booking, booking is updated to Pramod.

Not run for these notes. Step 1 sends restful-booker's login to POST /auth, and these notes never send a login to any site. What is above is what the class saw.

Why the token is not printed. Line 49 logs a sentence, not the token. Because of security, the instructor said: never, never, never print your tokens in your HTML reports.

Two more answers from this part. Asked why the step's async function does not declare the fixture: steps do not need your fixtures, and request is already there in the test. Asked whether several PUT endpoints can use the same token: yes, but a token also expires after 1 hour, so make sure your test case runs within 1 hour.

Where this is going. Everything common, like the token and the booking ID, will move to the fixture, the instructor said. In the end a fixture gives you a token and a booking ID automatically, before your test case starts, and you can run unlimited test cases. We will come to fixtures probably next class.

Two gaps in Step 3, as pushed. It sends the same payload that Step 2 created, so data.firstname would be Pramod even if the PUT changed nothing. And it never checks the status, so a rejected PUT fails at .json(), with a message about JSON.

Here is that failure, run for these notes on the class's Steps 2 and 3 with the Cookie header left out, so no token and no login are sent:

TypeScript
const responseData = await request.put(`/booking/${bookingId}`, {
    headers,
    data: payload,
});
const data = await responseData.json();
Output
SyntaxError: Unexpected token 'F', "Forbidden" is not valid JSON

The fix is to change a field and check the status first:

TypeScript
await test.step('Step 3 - Update booking', async () => {

    const updatedPayload = { ...payload, firstname: 'Jim' };

    const responseData = await request.put(`/booking/${bookingId}`, {
        headers: {
            ...headers,
            Cookie:`token=${token}`
        },
        data: updatedPayload,
    });
    expect(responseData.status()).toBe(200);

    const data = await responseData.json();
    expect(data.firstname).toBe(updatedPayload.firstname);
    expect(data.lastname).toBe(updatedPayload.lastname);
    logger.info(`Updated booking id ${bookingId}: ${data.firstname} ${data.lastname}`);
});

This fix was checked only on a stand-in server on the local machine, because the real flow needs the login. Against a stand-in whose PUT applied nothing, the class's Step 3 still passed and this one failed, which is the point.

10

Negative tests from AI, and the Ponytail review

A learner asked: how do you find a defect in the API, when the tests so far are positive scenarios? You can add negative test cases too, the instructor said, and when you have written the positive, the AI agent can add the negative by itself. So the agent was asked, and it wrote them.

Then the problem: the agent had created code we do not understand. That is why a Ponytail review is needed, the instructor said. We hate this extra stuff, and we want our test case to look like a normal one, like we used to write. So the instructor asked for a Ponytail review of the 06 file: it will start removing the extra stuff, the instructor said. After the review, the code was to be pushed, with a go command. This is the file as pushed:

06_put_operation_negative_scenario.spec.tsTypeScript
import { test, expect } from '@playwright/test';
import { logger } from '@utils/logger';


test.describe('Restful Booker booking API - PUT negative scenarios', () => {

    // PUT - Negative
    // No token / invalid token  -> 403
    // Booking id does not exist -> 405 (this API returns 405 here, not 404)
    // Required field missing    -> 400

    const headers = {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
    };

    const payload = {
        firstname: 'Pramod',
        lastname: 'Dutta',
        totalprice: 111,
        depositpaid: true,
        bookingdates: {
            checkin: '2018-01-01',
            checkout: '2019-01-01',
        },
        additionalneeds: 'Breakfast',
    };

    test('TC#3 @p1 - PUT : Verify that update without a token is rejected.', async ({ request }) => {

        let bookingId = 0;

        await test.step('Step 1 - Create booking to update', async () => {

            const responseData = await request.post('/booking', {
                headers,
                data: payload,
            });
            expect(responseData.status()).toBe(200);

            const data = await responseData.json();
            bookingId = data.bookingid;
            expect(bookingId).toBeTruthy();
            logger.info(`Created booking id for update: ${bookingId}`);
        });

        await test.step('Step 2 - Update booking without a token', async () => {

            const responseData = await request.put(`/booking/${bookingId}`, {
                headers,
                data: payload,
            });
            expect(responseData.status()).toBe(403);
        });
    });

    test('TC#4 @p1 - PUT : Verify that update with an invalid token is rejected.', async ({ request }) => {

        let bookingId = 0;

        await test.step('Step 1 - Create booking to update', async () => {

            const responseData = await request.post('/booking', {
                headers,
                data: payload,
            });
            expect(responseData.status()).toBe(200);

            const data = await responseData.json();
            bookingId = data.bookingid;
            expect(bookingId).toBeTruthy();
            logger.info(`Created booking id for update: ${bookingId}`);
        });

        await test.step('Step 2 - Update booking with an invalid token', async () => {

            const responseData = await request.put(`/booking/${bookingId}`, {
                headers: {
                    ...headers,
                    Cookie: 'token=invalidToken123',
                },
                data: payload,
            });
            expect(responseData.status()).toBe(403);
        });
    });

    test('TC#5 @p1 - PUT : Verify that update of a non-existent booking is rejected.', async ({ request }) => {

        let token = '';

        await test.step('Step 1 - Create auth token', async () => {

            const responseData = await request.post('/auth', {
                headers,
                data: {
                    username: '<username>',
                    password: '<password>',
                },
            });
            expect(responseData.status()).toBe(200);

            const data = await responseData.json();
            token = data.token;
            expect(token).toBeTruthy();
            logger.info('Created auth token for PUT booking flow');
        });

        await test.step('Step 2 - Update a booking id that does not exist', async () => {

            const responseData = await request.put('/booking/99999999', {
                headers: {
                    ...headers,
                    Cookie: `token=${token}`,
                },
                data: payload,
            });
            expect(responseData.status()).toBe(405);
        });
    });

    test('TC#6 @p1 - PUT : Verify that update with a missing required field is rejected.', async ({ request }) => {

        let token = '';
        let bookingId = 0;

        await test.step('Step 1 - Create auth token', async () => {

            const responseData = await request.post('/auth', {
                headers,
                data: {
                    username: '<username>',
                    password: '<password>',
                },
            });
            expect(responseData.status()).toBe(200);

            const data = await responseData.json();
            token = data.token;
            expect(token).toBeTruthy();
            logger.info('Created auth token for PUT booking flow');
        });

        await test.step('Step 2 - Create booking to update', async () => {

            const responseData = await request.post('/booking', {
                headers,
                data: payload,
            });
            expect(responseData.status()).toBe(200);

            const data = await responseData.json();
            bookingId = data.bookingid;
            expect(bookingId).toBeTruthy();
            logger.info(`Created booking id for update: ${bookingId}`);
        });

        await test.step('Step 3 - Update booking without firstname', async () => {

            const responseData = await request.put(`/booking/${bookingId}`, {
                headers: {
                    ...headers,
                    Cookie: `token=${token}`,
                },
                data: {
                    lastname: 'Dutta',
                    totalprice: 111,
                    depositpaid: true,
                    bookingdates: {
                        checkin: '2018-01-01',
                        checkout: '2019-01-01',
                    },
                    additionalneeds: 'Breakfast',
                },
            });
            expect(responseData.status()).toBe(400);
        });
    });

});

The login values on lines 97, 98, 132 and 133 are masked the same way. The recording does not mention a run of these four tests, and only two of them send no login:

Test What it sends The spec expects For these notes
TC#3 no token 403 run: passed
TC#4 the made-up invalidToken123 403 run: passed
TC#5 a real token, booking 99999999 405 not run: it sends the login
TC#6 a real token, no firstname 400 not run: it sends the login
Terminal
BASE_URL=https://restful-booker.herokuapp.com npx playwright test src/tests/apiTests/01_restfulbooker_raw/06_put_operation_negative_scenario.spec.ts -g "TC#3|TC#4" --workers=1 --reporter=list
Output
Running 2 tests using 1 worker

YYYY-MM-DD 11:59:11 [info] Created booking id for update: 4325
  ✓  1 [chromium] › src/tests/apiTests/01_restfulbooker_raw/06_put_operation_negative_scenario.spec.ts:29:9 › Restful Booker booking API - PUT negative scenarios › TC#3 @p1 - PUT : Verify that update without a token is rejected. (906ms)
YYYY-MM-DD 11:59:12 [info] Created booking id for update: 4334
  ✓  2 [chromium] › src/tests/apiTests/01_restfulbooker_raw/06_put_operation_negative_scenario.spec.ts:57:9 › Restful Booker booking API - PUT negative scenarios › TC#4 @p1 - PUT : Verify that update with an invalid token is rejected. (912ms)

  2 passed (2.1s)

The 405 is the spec's own comment, and the README adds that a PUT to a booking that does not exist returns 405, not 404, and that the specs assert what the API returns today.

11

Which requests need a token

WHICH RESTFUL-BOOKER REQUESTS NEED A TOKEN CLIENT The test SERVER The API No token: straight to the server GET /ping GET /booking GET /booking/{id} POST /booking POST /auth The extra layer: a token first PUT /booking/{id} PATCH /booking/{id} DELETE /booking/{id} For this project only. Different companies do it differently: in some, every request needs a token.
The instructor's closing answer, on restful-booker. Its API documentation lists a Cookie header on the same three requests, and POST /auth is the request that creates the token.

Asked to explain the token that was used: for a GET or a POST there is no token, and the client is directly allowed to talk to the server. With a PUT there is an extra layer, which asks you for a token: first get a token, then send me a token again, then only I will allow you. PUT, PATCH and DELETE, for only this project, restful-booker, are marked red, the instructor said, and red means they need a token. You cannot update without one. For the rest, you can do whatever. restful-booker's API documentation agrees: it lists a Cookie header on exactly those three.

Different companies have different ways of doing it, the instructor said: in some, every request has a token. It depends on your company.

Asked how the token gets created: by a POST request. There is a request named auth, and it creates the token.

A normal token, a bearer token, a JWT. Asked whether "token" here means a bearer token or something different: this is a normal token as of now, the instructor said. A bearer token is also a type of token, one with no meaning, and a JWT is a token which has a meaning. (Bearer also names how a token is sent, Authorization: Bearer ..., as the diagram on the guide page shows, so a JWT can be sent as a bearer token too.)

Personas. Asked about different personas needing different tokens: wait for the fixture, the instructor said. You can inject anything there.

Checking the backend. Asked how to check whether the booking ID was created in the backend: if you have access to the database, you can check it. If a developer gives you access, open the database and verify.

12

Tasks and announcements

  • Carried over from last class, if not done: the restful-booker test plan, test cases and Postman collection. This session starts from there, the instructor said. Asked about that task, whether code needs to be pushed: you can push it, but it is not required.
  • Practise up to this point. Add tests for everything, for the GET requests too.
  • Doubts go in the doubt thread, created for this. The instructor asked everyone to add them there and said they would be answered.
  • Next class: CRUD operations using interfaces. The fixture, the instructor said, probably next class too. The deck's 5 November section holds one heading so far: CRUD with the request fixture.
  • Later: the AI agents, in the upcoming classes.
  • The code: the five specs are in the batch repo.

Task. If your restful-booker test plan, test cases and Postman collection are not ready, finish those first. Then practise up to this point: write these specs yourself and add tests for everything, for the GET requests too.

What is the difference between Postman and Playwright, and can you automate through Postman?

Worth practising too: How does an API test in Playwright differ from a UI test, and where does its base URL come from? You need to update a booking through restful-booker's API: why can't you just send the PUT, and what do you send? Your suite targets one API, and one test suddenly has to call another host: what do you use?

Show the answer

Answer. Postman is generally more helpful for manual API testing. It can automate too, but that is limited, and it is not scalable: at something like 5000 or 6000 test cases it is very difficult to maintain. Playwright automates the APIs, and you can mix UI and API. An API test takes the built-in request fixture instead of page: request uses APIRequestContext to make the HTTP call and give back the response, and no browser opens. The test gives only the last part of the path, and the base URL comes from outside, here BASE_URL on the command line, which reaches the test through the config. For the PUT: with no token, or an invalid one, the answer is 403 Forbidden, so first POST /auth for a token, then POST /booking for a booking ID, then PUT /booking/{id} with the ID in the URL and Cookie: token=... among the headers, in three test.step blocks. Never print the token in a report. For another host, use a new context, request.newContext(), imported from @playwright/test. It has its own base URL, extra HTTP headers and timeout. Verify the call, then dispose the context.