Postman Tests and Scripts: Assertions, Chaining and Newman CLI

Postman Tests and Scripts: Assertions, Chaining and Newman CLI

Clicking Send and reading the response is manual testing. The moment you add a script that checks the status, the body and the headers on its own, your Postman collection becomes an automated test suite. Add Newman and it runs every night in CI without anyone opening Postman. This lesson covers the pm scripting API, the assertions you’ll use daily, JSON Schema validation, chaining, data-driven runs and Newman with HTML reports, all on the live practice API.

Quick answerPostman tests are JavaScript snippets in the Post-response script tab, written as pm.test('name', () => { pm.expect(...)... }) using the Chai assertion library. Pre-request scripts prepare data before sending. Values are passed between requests with pm.environment.set() / pm.collectionVariables.set(). The Collection Runner runs a collection with CSV/JSON data, and Newman (newman run collection.json -e env.json) runs it from the command line or a CI pipeline with CLI, JUnit and HTML reports.
Advertisement

Pre-request vs Post-response Scripts

Pre-request scriptPost-response script (tests)
RunsBefore the request is sentAfter the response arrives
Typical useGenerate data, timestamps, signatures; fetch a tokenAssertions; save values for later requests
LevelsCollection → folder → request (all run, in that order)Request → folder → collection
Real-life example: signed payment requests Payment partner APIs often require an HMAC signature header calculated from the body and a secret. A pre-request script computes it with CryptoJS.HmacSHA256 before every call, so testers never calculate signatures by hand.

The Assertions You’ll Use Every Day

// Status
pm.test('status is 200', () => pm.response.to.have.status(200));
pm.test('status is 2xx', () => pm.response.to.be.success);
pm.test('status is one of 200/201', () => pm.expect(pm.response.code).to.be.oneOf([200, 201]));

// Response time
pm.test('responds under 1000 ms', () => pm.expect(pm.response.responseTime).to.be.below(1000));

// Headers
pm.test('JSON content type', () => {
  pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');
});
pm.test('has ETag', () => pm.response.to.have.header('ETag'));

// Body values
const json = pm.response.json();
pm.test('user is Asha Verma', () => {
  pm.expect(json.data.name).to.eql('Asha Verma');
  pm.expect(json.data.age).to.be.a('number').and.to.be.above(17);
  pm.expect(json.data.role).to.be.oneOf(['admin', 'editor', 'viewer']);
  pm.expect(json.data.email).to.match(/^[^@\s]+@[^@\s]+$/);
});

// Arrays
pm.test('every product is a book', () => {
  json.data.forEach(p => pm.expect(p.category).to.eql('books'));
});

// Negative / error format
pm.test('error has code and message', () => {
  pm.expect(json.error).to.have.all.keys('status', 'code', 'message');
});

Try the last pattern on a 404 response:

GEThttps://sqatools.in/wp-json/sqa-api/v1/users/999
curl -i \
  "https://sqatools.in/wp-json/sqa-api/v1/users/999"
to.eql vs to.equal to.equal checks strict identity (fine for strings and numbers); to.eql compares objects and arrays deeply. Use eql for {...} and [...].
Advertisement

JSON Schema Validation in Postman

Postman includes a JSON Schema validator. Validate the whole structure in one assertion:

const userSchema = {
  type: 'object',
  required: ['id', 'name', 'email', 'role', 'age', 'active'],
  properties: {
    id:     { type: 'integer', minimum: 1 },
    name:   { type: 'string' },
    email:  { type: 'string' },
    role:   { enum: ['admin', 'editor', 'viewer'] },
    age:    { type: 'integer', minimum: 18 },
    active: { type: 'boolean' },
  },
};

// Built-in assertion: wrap the user schema because the API returns { "data": user }
pm.test('user matches schema', () => {
  pm.response.to.have.jsonSchema({ type: 'object', required: ['data'], properties: { data: userSchema } });
});

// Same check with the bundled Ajv library (shows detailed errors):
pm.test('user matches schema (Ajv)', () => {
  const Ajv = require('ajv');
  const ajv = new Ajv({ allErrors: true });
  pm.expect(ajv.validate(userSchema, pm.response.json().data), JSON.stringify(ajv.errors)).to.be.true;
});

Tip: the practice API publishes its real contract at GET /schema/user. Fetch it once, store it in a collection variable, and validate every user response against it.

Chaining Requests: A Full CRUD Flow

Create a folder “CRUD flow” with five requests and these scripts:

#RequestPost-response script
1POST {{baseUrl}}/objectspm.test('201', () => pm.response.to.have.status(201));
pm.collectionVariables.set('objId', pm.response.json().id);
pm.collectionVariables.set('etag', pm.response.headers.get('ETag'));
2GET {{baseUrl}}/objects/{{objId}}Assert 200 and name
3PATCH {{baseUrl}}/objects/{{objId}} with header If-Match: {{etag}}Assert 200 and version === 2
4DELETE {{baseUrl}}/objects/{{objId}}Assert 204
5GET {{baseUrl}}/objects/{{objId}}Assert 404

Controlling the flow

// Skip the rest of the run if login failed
if (pm.response.code !== 200) {
  pm.execution.setNextRequest(null);          // older versions: postman.setNextRequest(null)
}

// Jump to a specific request by name
pm.execution.setNextRequest('Cleanup - delete object');
Advertisement

Pre-request Scripts: Get a Token Automatically

Put this at collection level so every request has a valid token, fetched only when the old one is missing or expired:

const expiresAt = pm.collectionVariables.get('token_expires_at');
if (!expiresAt || Date.now() > Number(expiresAt) - 30000) {
  pm.sendRequest({
    url: pm.environment.get('baseUrl') + '/auth/login',
    method: 'POST',
    header: { 'Content-Type': 'application/json' },
    body: { mode: 'raw', raw: JSON.stringify({
      username: pm.environment.get('username'),
      password: pm.environment.get('password'),
    }) },
  }, (err, res) => {
    const body = res.json();
    pm.collectionVariables.set('access_token', body.access_token);
    pm.collectionVariables.set('token_expires_at', Date.now() + body.expires_in * 1000);
  });
}

Then set the collection’s Authorization to Bearer Token = {{access_token}}.

Advertisement

Data-Driven Testing with the Collection Runner

Run one request many times with different inputs from a CSV or JSON file. Example age-boundaries.csv for POST /validate/user:

name,email,age,expectedStatus
Asha,asha@example.com,17,422
Asha,asha@example.com,18,200
Asha,asha@example.com,100,200
Asha,asha@example.com,101,422

Request body uses data variables:

{ "name": "{{name}}", "email": "{{email}}", "age": {{age}} }

Test script reads the expected value from the same row:

pm.test(`age ${pm.iterationData.get('age')} -> ${pm.iterationData.get('expectedStatus')}`, () => {
  pm.response.to.have.status(Number(pm.iterationData.get('expectedStatus')));
});

In the Runner, select the request or folder, choose the data file, check the preview and click Run. Each CSV row is one iteration.

POSThttps://sqatools.in/wp-json/sqa-api/v1/validate/user
curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"name":"Asha","email":"asha@example.com","age":17}' \
  "https://sqatools.in/wp-json/sqa-api/v1/validate/user"
Advertisement

Newman: Run Postman Collections from the Command Line

Newman is Postman’s command-line runner. It’s how collections run in Jenkins, GitHub Actions, GitLab CI or Azure DevOps.

# 1. Install (needs Node.js)
npm install -g newman newman-reporter-htmlextra

# 2. Export from Postman: collection (v2.1) and environment as JSON files

# 3. Run
newman run sqa-practice.postman_collection.json \
  -e sqa-practice.postman_environment.json \
  -d age-boundaries.csv \
  -r cli,junit,htmlextra \
  --reporter-junit-export results/junit.xml \
  --reporter-htmlextra-export results/report.html

# Useful options
#   --folder "CRUD flow"     run one folder only
#   -n 3                     run 3 iterations
#   --delay-request 200      wait 200 ms between requests
#   --bail                   stop on first failure
#   --env-var "password=$QA_PASSWORD"   inject secrets from CI

Newman exits with code 1 when any test fails, which fails the CI build: exactly what you want.

Newman in GitHub Actions

name: api-tests
on: [push, workflow_dispatch]
jobs:
  newman:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm install -g newman newman-reporter-htmlextra
      - run: |
          newman run postman/sqa-practice.postman_collection.json \
            -e postman/sqa-practice.postman_environment.json \
            --env-var "password=${{ secrets.QA_PASSWORD }}" \
            -r cli,htmlextra --reporter-htmlextra-export report.html
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: newman-report, path: report.html }
Advertisement

Organising a Maintainable Postman Test Suite

  • Folders per resource, plus “Smoke”, “Negative” and “Flows”.
  • Reusable checks at collection level, e.g. a post-response script asserting every response is JSON and under 2 seconds.
  • Names that state the expectation: “PATCH object with stale If-Match – 412”.
  • No hardcoded ids or URLs; everything via variables.
  • Each flow creates and cleans up its own data.
  • Collections stored in Git (exported JSON) so changes are reviewed.

Reusable code across requests can be saved as a function in a collection variable or, in recent Postman versions, as package library scripts.

Postman vs Code-Based Automation

Postman + NewmanPlaywright (TypeScript)
Start-up speedVery fast, low codeNeeds project setup
Version control and reviewJSON exports (hard to diff)Normal code reviews
Complex logic, reuseLimitedFull language, fixtures, helpers
UI + API in one frameworkNoYes
Best forExploration, smaller suites, mixed-skill teamsLarge long-lived regression suites

Many teams use both: Postman for exploration and contract smoke tests, Playwright for the main regression suite (Module 6).

Advertisement

Interview Questions: Postman Scripts and Newman

What is the difference between pre-request and post-response scripts?

Pre-request scripts run before a request is sent to prepare data, headers, signatures or tokens. Post-response scripts run after the response arrives to make assertions and store values for later requests.

How do you write an assertion in Postman?

Use pm.test with a name and a function containing pm.expect or pm.response.to assertions, for example pm.test(‘status 200’, () => pm.response.to.have.status(200)).

Which assertion library does Postman use?

Chai’s BDD style (expect), exposed as pm.expect, plus Postman’s own pm.response.to helpers.

How do you validate JSON Schema in Postman?

Use pm.response.to.have.jsonSchema(schema) in recent versions, or the bundled Ajv library: create a validator, validate the parsed response and assert the result is true, printing ajv.errors on failure.

What is Newman?

The command-line collection runner for Postman. It runs exported collections with environments and data files, produces CLI, JUnit and HTML reports and returns a non-zero exit code on failures, which makes it suitable for CI/CD pipelines.

How do you do data-driven testing in Postman?

Use {{variables}} in the request, then run it in the Collection Runner or Newman with a CSV or JSON data file. Each row is an iteration, accessible in scripts through pm.iterationData.get().

How do you control the order of requests in a run?

Requests run in collection order by default; pm.execution.setNextRequest(‘name’) jumps to a specific request and setNextRequest(null) stops the run.

How do you send a request from inside a script?

Use pm.sendRequest with a URL or request object and a callback. It’s commonly used in pre-request scripts to fetch auth tokens.

How do you keep secrets out of shared Postman collections?

Store them as secret-type environment variables or in Postman Vault, keep real values only in “current value”, and inject them in CI with newman –env-var from the CI secret store.

How does Newman report failures to CI?

It exits with code 1 when any assertion fails or a request errors, causing the CI step to fail. JUnit XML reports can be published by the CI tool and HTML reports uploaded as build artifacts.

Advertisement
View the full API testing syllabus

Postman Tutorial for Beginners: API Testing Step by Step

Postman Tutorial for Beginners: API Testing Step by Step

Postman is the tool most testers open first when a developer says “the API is ready”. It lets you send any request in seconds, save it, share it with the team and later turn it into automated tests. In this Postman tutorial you’ll go from installation to a complete, reusable collection for a real API, with environments, variables, authentication and request chaining, using the free SQA Tools practice API at every step.

Quick answerPostman is an API platform for sending HTTP requests, inspecting responses and organising requests into collections. Key features for testers: environments (switch dev/staging/prod with one click), variables ({{baseUrl}}, {{token}}), an Authorization tab (Basic, API key, Bearer, OAuth 2.0), scripts for assertions and chaining, the Collection Runner for data-driven runs, and Newman for running collections in CI.
Advertisement

Why Testers Use Postman

  • No code needed to start: fill in method, URL and body, click Send.
  • Organised: collections and folders mirror the API structure; everyone uses the same requests.
  • Reusable: variables and environments mean one collection works on dev, QA and staging.
  • Testable: JavaScript test scripts turn requests into automated checks.
  • Shareable: export collections as JSON, share workspaces, generate documentation.
  • CI-ready: Newman runs collections from the command line in Jenkins or GitHub Actions.
Real-life example In a typical sprint, a developer finishes “apply coupon” on Monday. The tester imports the updated OpenAPI file into Postman, explores the endpoint, writes 25 requests with test scripts on Tuesday, and adds the folder to the nightly Newman run. The feature is regression-tested every night from then on, before the UI is even ready.

Install Postman and Understand the Interface

  1. Download the desktop app from postman.com (Windows, macOS, Linux) or use the web version. A free account syncs your work.
  2. Create a workspace (personal or team) for your project.
AreaWhat it’s for
SidebarCollections, Environments, History, APIs, Flows
Request tabsMethod dropdown, URL bar, Send button
Request sub-tabsParams, Authorization, Headers, Body, Scripts (Pre-request / Post-response), Settings
Response panelBody (Pretty/Raw/Preview), Cookies, Headers, Test Results, status, time and size
Environment selectorTop right: choose the active environment
Console (bottom)Raw request/response log: your best debugging friend
Advertisement

Send Your First Request

  1. Click New → HTTP (or the + tab).
  2. Method: GET. URL: https://sqatools.in/wp-json/sqa-api/v1/users
  3. Click Send. Check status 200 OK, time and size in the response panel.
  4. Open the Headers tab of the response: find X-Total-Count: 12 and the Link header.

Here’s the same request in your browser to compare:

GEThttps://sqatools.in/wp-json/sqa-api/v1/users?role=admin
curl -i \
  "https://sqatools.in/wp-json/sqa-api/v1/users?role=admin"

Query parameters with the Params tab

Instead of typing ?role=admin&sort=age, add rows in Params: key role value admin, key sort value age. Postman builds and encodes the URL. Untick a row to disable it temporarily: handy for negative tests.

POST with a JSON Body

  1. New request, method POST, URL https://sqatools.in/wp-json/sqa-api/v1/objects
  2. Body → raw → JSON, then paste:
    {
      "name": "Postman test object",
      "data": { "color": "blue", "qty": 3 },
      "ttl": 300
    }
  3. Send. Expect 201 Created. Postman added Content-Type: application/json automatically (see the hidden headers in the Headers tab).
  4. Copy the id from the response; you’ll use it next with PUT, PATCH and DELETE.
Body types in Postman raw JSON for REST APIs; form-data for file uploads (multipart); x-www-form-urlencoded for classic HTML forms and OAuth token endpoints; raw XML for SOAP; binary for raw files; GraphQL for GraphQL queries with variables.
Advertisement

Collections: Organise Your Requests

A collection is a folder of saved requests, with shared variables, auth and scripts.

  1. Click Collections → + and name it “SQA Practice API”.
  2. Add folders: Users, Products, Objects CRUD, Auth, Negative tests.
  3. Save each request (Ctrl/Cmd + S) into a folder with a clear name, e.g. “POST create object – 201”.

Shortcut: the API Testing Practice page has a Postman collection button that downloads a ready-made collection for this API. In Postman click Import and drop the file in.

Variables and Environments

Hardcoding https://sqatools.in/wp-json/sqa-api/v1 into 50 requests is painful when the team moves to a staging server. Use variables.

Create an environment

  1. Environments → +, name it “SQA Practice”.
  2. Add variable baseUrl = https://sqatools.in/wp-json/sqa-api/v1.
  3. Add username = admin and password = admin@123; set the password type to secret.
  4. Select the environment in the top-right dropdown.
  5. Change your request URLs to {{baseUrl}}/users, {{baseUrl}}/objects…
Real-life example: three environments A banking team keeps DEV, QA and UAT environments with different baseUrl, client ids and test users. The same collection runs against any of them by switching the dropdown, with no edits to requests.

Variable scopes (narrowest wins)

ScopeLives inUse for
GlobalWhole workspaceRarely; quick experiments
CollectionOne collectionValues fixed for this API (api version, page size)
EnvironmentSelected environmentPer-server values: baseUrl, users, keys
DataCSV/JSON file in a runData-driven testing
LocalOne request executionTemporary values from scripts

If the same name exists in several scopes, the narrower one (local, then data, then environment, then collection, then global) wins.

Secrets Use secret type or Postman Vault for passwords and keys, keep the real values in “current value” (not synced/shared), and never export environments containing production secrets.
Advertisement

Authorization Tab

Set auth once at the collection level and let requests Inherit auth from parent.

Practice endpointPostman Authorization typeValues
/basic/profileBasic Authsqa_user / sqa@123
/apikey/profileAPI KeyKey X-API-Key, value sqa-key-12345, add to Header
/auth/meBearer Token{{access_token}}
OAuth 2.0 APIsOAuth 2.0Grant type, token URL, client id/secret, scope; “Get New Access Token”

Postman shows the header it generates (e.g. Authorization: Basic c3FhX3VzZXI6c3FhQDEyMw==) under the Headers tab’s hidden headers.

Request Chaining: Use Data from One Response in the Next

Real flows need values from earlier responses: a token from login, an id from create. In the Scripts → Post-response tab of the login request:

const body = pm.response.json();
pm.environment.set('access_token', body.access_token);

And in “POST create object”:

pm.collectionVariables.set('objectId', pm.response.json().id);

Then GET {{baseUrl}}/objects/{{objectId}}, PATCH {{baseUrl}}/objects/{{objectId}} and DELETE {{baseUrl}}/objects/{{objectId}} just work, in order. Run the folder with the Collection Runner and you have an end-to-end CRUD flow.

Dynamic variables

Postman has built-in random data: {{$randomFirstName}}, {{$randomEmail}}, {{$randomInt}}, {{$guid}}, {{$timestamp}}. Use them so every run creates unique data:

{ "name": "{{$randomProductName}}", "data": { "ref": "{{$guid}}" }, "ttl": 120 }
Advertisement

A Quick Look at Tests (Assertions)

Add checks in Scripts → Post-response:

pm.test('status is 201', () => pm.response.to.have.status(201));
pm.test('has Location header', () => pm.response.to.have.header('Location'));
pm.test('name echoed back', () => {
  pm.expect(pm.response.json().name).to.eql('Postman test object');
});

Results appear in the Test Results tab. Writing powerful scripts, schema validation, data-driven runs and Newman are covered in the next lesson: Postman Tests, Scripts and Newman.

Useful Postman Features Testers Forget

  • Console (View → Show Postman Console): the exact request sent, after variables were resolved. Check here first when something “doesn’t work”.
  • Code snippet (</> icon): converts a request into cURL, Python, JavaScript and more. Paste cURL into bug reports.
  • Import cURL: paste a developer’s cURL command into Import to get a ready request.
  • Import OpenAPI: generates a whole collection from a Swagger file.
  • Settings per request: disable “Automatically follow redirects” to test 3xx codes; set request timeout.
  • Mock servers: fake an API from saved examples so front-end and tests can start early.
  • Postbot (AI assistant): generates test scripts and documentation from responses. Review what it writes.
Advertisement

Postman Practice Exercises

  1. Build a collection with folders Users, Products, Objects, Auth using {{baseUrl}}.
  2. GET page 2 of users with 5 per page; confirm ids 6 to 10.
  3. Filter products by category=books; then try category=toys (expect 400).
  4. Create an object, save its id with a script, then PATCH, GET and DELETE it in a chained run.
  5. Call /basic/profile, /apikey/profile and /auth/me with each auth type; then each with wrong credentials.
  6. Turn off “follow redirects” and GET /redirect: confirm 302 and the Location header.
  7. Create a second environment “SQA Practice – broken” with a wrong baseUrl and see how errors look.
Advertisement

Interview Questions: Postman

What is Postman used for in API testing?

Sending HTTP requests and inspecting responses, organising requests into collections, managing environments and variables, writing JavaScript test scripts, chaining requests, running data-driven collections, mocking APIs, generating documentation and running collections in CI with Newman.

What is a collection in Postman?

A group of saved requests organised in folders, with shared variables, authorization and scripts. It can be run with the Collection Runner or Newman and exported as JSON.

What is the difference between environment and global variables?

Environment variables belong to a selected environment such as DEV or QA and change when you switch environments. Global variables are available across the whole workspace regardless of environment. Environment variables are preferred for server-specific values.

What are the variable scopes in Postman and their precedence?

Global, collection, environment, data and local. When names clash, the narrowest scope wins: local, then data, then environment, then collection, then global.

How do you pass data from one request to another?

In the first request’s post-response script, read the value with pm.response.json() and store it with pm.environment.set or pm.collectionVariables.set, then reference it as {{variableName}} in later requests.

How do you handle authentication in Postman?

Use the Authorization tab with the right type (Basic, API Key, Bearer Token, OAuth 2.0), usually set at collection level and inherited by requests. Tokens are often obtained by a login request whose script stores the token in a variable.

How do you test redirects in Postman?

Disable “Automatically follow redirects” in the request settings, then assert the 3xx status and the Location header.

How do you share a Postman collection with your team?

Use a shared team workspace, or export the collection and environment as JSON files and store them in the project’s Git repository. Never share environments with real secrets.

What is the Postman Console used for?

It shows the exact raw requests and responses, including resolved variables, headers and script logs. It’s the first place to debug unexpected results.

What are Postman dynamic variables?

Built-in random data generators such as {{$guid}}, {{$timestamp}}, {{$randomEmail}} and {{$randomInt}}, used to create unique test data on every run.

Advertisement
View the full API testing syllabus

How to Write API Test Cases: Template, Examples and 50+ Scenarios

How to Write API Test Cases: Template, Examples and 50+ Scenarios

“Write test cases for this API” is both a daily task and a favourite interview exercise. Most beginners write five happy-path cases and stop. Experienced testers use a repeatable method that produces 30 to 50 meaningful cases for a single endpoint in under an hour. In this lesson you’ll get a ready-to-use API test case template, the five test design techniques that generate cases systematically, and a complete worked example on a real endpoint you can execute right away.

Quick answerA good API test case contains: ID, title, preconditions, method and endpoint, headers, request body/parameters, expected status code, expected response body and headers, post-conditions (e.g. data saved), priority and type. Generate cases with equivalence partitioning, boundary value analysis, decision tables, state transitions and an error guessing checklist, covering positive, negative, auth, header, performance and security scenarios for every endpoint.
Advertisement

Where API Test Cases Come From

Before writing a single case, collect the inputs:

  • API specification (OpenAPI/Swagger, WSDL, Postman docs): endpoints, fields, types, required flags, status codes.
  • Business requirements / user stories: rules like “age must be 18 to 100”, “only admins can delete”.
  • Acceptance criteria and conversations with developers and product owners.
  • Existing bugs and production incidents: past failures tell you where to look.
Real-life example A user story says “As a customer I can save up to 5 delivery addresses.” The Swagger shows POST /addresses with pincode (6 digits). From one sentence and one field you already get boundary cases (5th and 6th address), format cases (5- and 7-digit pincodes, letters) and auth cases (another customer’s address).

The API Test Case Template

FieldDescriptionExample
Test case IDUnique idTC_OBJ_POST_012
TitleWhat is verified, in one lineCreate object fails when ttl is below minimum
TypePositive / Negative / Boundary / Auth / Security / PerformanceBoundary
PriorityP1 (critical) to P3P2
PreconditionsState and data neededAPI reachable; no auth required
Method + EndpointVerb and pathPOST /objects
HeadersRequired headersContent-Type: application/json
Request body / paramsExact test data{"name":"Bag","ttl":9}
Expected statusExact code422
Expected responseBody, headers, error formaterror.code = "validation_failed", details mention ttl
Post-conditionsWhat must (not) changeNo object created
Actual result / StatusFilled during executionPass / Fail + bug id

Keep this in a spreadsheet, a test management tool (Jira Xray, Zephyr, TestRail) or directly as a Postman collection where each request is a case and its scripts are the expected results.

Advertisement

5 Techniques That Generate API Test Cases

1. Equivalence Partitioning (EP)

Group inputs that should behave the same and test one value per group. For role with allowed values admin/editor/viewer: valid partition {admin}, invalid partitions {unknown value “manager”}, {wrong type 5}, {empty “”}, {null}.

2. Boundary Value Analysis (BVA)

Bugs cluster at the edges. For a range min..max test min-1, min, max, max+1 (and optionally min+1, max-1).

Field (practice API rule)Boundary valuesExpected
ttl (10 to 1800 s)9, 10, 1800, 1801422, 201, 201, 422
age in /validate/user (18 to 100)17, 18, 100, 101422, 200, 200, 422
name in /validate/user (2 to 50 chars)1, 2, 50, 51 chars422, 200, 200, 422
per_page (1 to 50)0, 1, 50, 51400, 200, 200, 400

3. Decision Tables

For rules that combine conditions. Example: delete an object on /token/objects.

Token presentToken validRoleObject existsExpected
No–––401
YesNo––401
YesYesviewerYes403
YesYesadminYes204
YesYesadminNo404

4. State Transition

Resources have lifecycles. An object: does not exist → created (v1) → updated (v2, v3…) → deleted or expired. Test valid transitions and invalid ones: update after delete (404), delete twice (404), read after TTL expiry (404), update with a stale version (412).

Real-life example: order states An order moves placed → paid → shipped → delivered, or placed → cancelled. Invalid transitions to test: cancel after delivery, ship before payment, pay a cancelled order. These often slip through because each endpoint works fine on its own.

5. Error Guessing (Experience Checklist)

Empty body, malformed JSON, wrong Content-Type, very long strings, Unicode and emojis, leading/trailing spaces, duplicate requests, special characters, SQL/HTML strings, numbers as strings, nulls, extra fields, huge numbers, negative numbers, decimals where integers expected.

Advertisement

Worked Example: Test Cases for POST /objects

Specification (from the practice API docs): name required, 1 to 100 characters; data any JSON up to 5 KB; ttl 10 to 1800 seconds, default 600; other fields → 422; success → 201 with Location and ETag; body over 10 KB → 413.

POSThttps://sqatools.in/wp-json/sqa-api/v1/objects
curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"name":"Test case demo","data":{"qty":2},"ttl":60}' \
  "https://sqatools.in/wp-json/sqa-api/v1/objects"
IDTitleInputExpectedType
01Create with all fieldsname, data, ttl=60201, Location, ETag “v1”, version 1Positive
02Create with name only{"name":"A"}201, ttl_seconds 600 (default)Positive
03Created object is readableGET Location200, same dataPositive
04Appears in listGET /objectsContains new idPositive
05Missing name{"data":{}}422, details.field = nameNegative
06Empty name{"name":""}422Boundary
07Name 100 chars100 x “a”201Boundary
08Name 101 chars101 x “a”422Boundary
09Name wrong type{"name":123}422Negative
10ttl 9ttl=9422Boundary
11ttl 10ttl=10201Boundary
12ttl 1800ttl=1800201Boundary
13ttl 1801ttl=1801422Boundary
14ttl as stringttl=”60″422Negative
15Client sends id{"id":"x","name":"A"}422 (server-generated ids)Negative
16Unknown field{"name":"A","colour":"red"}422Negative
17Malformed JSON{"name":400Negative
18Empty body(none)400Negative
19JSON array body[{"name":"A"}]400Negative
20Wrong Content-Typetext/plain415Header
21Body over 10 KB11 KB data413Boundary
22Unicode name“आशा 😀”201, stored exactlyPositive
23Injection text' OR 1=1 <script>201, stored as plain textSecurity
24Wrong methodPATCH /objects405 + AllowNegative
25Expiryttl=10, GET after 12 s404State
26Two creates, same bodyPOST twiceTwo different ids (not idempotent)Positive
27Response timeAny valid create< 2 sPerformance
28Response schemaAny valid createid, name, data, version, created_at, expires_at, ttl_secondsContract

28 cases from one endpoint, and every one of them is concrete and runnable. The practice page has 141 more, grouped exactly like this: API Testing Practice scenarios.

Advertisement

The Generic Checklist for Any Endpoint

CategoryQuestions to turn into cases
FunctionalDoes each documented behaviour work? Are values, calculations and side effects correct?
ValidationRequired, type, format, length, range, enum, pattern, cross-field rules?
Status codesCorrect and specific for each outcome?
HeadersContent-Type, Location, caching, CORS, rate limit headers?
AuthNo/invalid/expired token, each role, other users’ data?
StateValid and invalid transitions, concurrency, idempotency?
DataPersistence (GET after write), no duplicates, Unicode, large data?
ContractMatches schema and documentation, no extra fields?
Non-functionalResponse time, rate limits, payload limits, error message quality?

Prioritising: You Can’t Run Everything Every Time

  • P1 (smoke/critical): main happy paths, auth works, money calculations. Run on every deployment.
  • P2 (regression): validation, roles, state transitions. Run on every build/nightly.
  • P3 (extended): rare edge cases, large data, Unicode. Run before releases.

Risk-based rule: anything touching money, personal data, permissions or irreversible actions gets higher priority.

Advertisement

Writing Good Test Case Titles

WeakStrong
Test create APIPOST /objects returns 201 with Location header for valid input
Negative test 3POST /objects rejects ttl=1801 with 422 and ttl error detail
Check deleteViewer token gets 403 on DELETE /token/objects/{id}; object remains

Format: [Method] [endpoint] [expected outcome] when/for [condition]. Strong titles become readable automated test names too.

From Test Cases to Automation

A well-written table converts directly into data-driven Playwright tests: each row becomes an object, and one loop runs them all.

import { test, expect } from '@playwright/test';

const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';

const cases = [
  { id: 'TC_10', title: 'ttl 9 is rejected',    body: { name: 'A', ttl: 9 },    status: 422 },
  { id: 'TC_11', title: 'ttl 10 is accepted',   body: { name: 'A', ttl: 10 },   status: 201 },
  { id: 'TC_12', title: 'ttl 1800 is accepted', body: { name: 'A', ttl: 1800 }, status: 201 },
  { id: 'TC_13', title: 'ttl 1801 is rejected', body: { name: 'A', ttl: 1801 }, status: 422 },
  { id: 'TC_05', title: 'name is required',     body: { data: {} },             status: 422 },
  { id: 'TC_15', title: 'client id is refused', body: { id: 'x', name: 'A' },   status: 422 },
];

for (const c of cases) {
  test(`${c.id} POST /objects: ${c.title}`, async ({ request }) => {
    const res = await request.post(`${BASE}/objects`, { data: c.body });
    expect(res.status()).toBe(c.status);
  });
}
Advertisement

Common Mistakes in API Test Design

  • Only positive cases. Aim for more negative than positive.
  • “Expected: error” instead of the exact status and message.
  • Not verifying side effects (was it saved? was nothing saved on failure?).
  • Test cases that depend on each other’s data and order.
  • Ignoring headers and response time.
  • Copying the developer’s unit tests instead of thinking like a user and an attacker.
Advertisement

Interview Questions: API Test Cases

How do you write test cases for an API?

Read the specification and requirements, list each endpoint and its rules, then apply equivalence partitioning, boundary values, decision tables, state transitions and error guessing. Cover positive, negative, auth, header, contract, performance and security cases, each with exact input, expected status, body, headers and side effects.

What fields should an API test case contain?

ID, title, type, priority, preconditions, method and endpoint, headers, request data, expected status, expected body and headers, post-conditions, and actual result with status.

Write test cases for a login API.

Valid login (200 + token), wrong password and unknown user (401 with the same message), missing fields (400/422), empty strings, SQL/HTML in fields, case sensitivity of username, locked account, rate limiting after repeated failures, token expiry and validity on protected endpoints, password not returned or logged, HTTPS only.

How do you apply boundary value analysis to APIs?

For every numeric range, length limit, page size or list limit, test min-1, min, max and max+1. For example, for age 18 to 100 test 17, 18, 100 and 101 and expect 422, 200, 200 and 422.

What is the difference between a test scenario and a test case?

A scenario is a high-level situation to verify, such as “create object with invalid ttl”. A test case is a concrete, executable instance with exact data and expected results, such as “POST /objects with ttl 1801 returns 422 with a ttl error”.

How many test cases would you write for one endpoint?

It depends on the rules, but a typical create endpoint has 20 to 50 meaningful cases once validation, boundaries, auth, headers and state are covered. Prioritise them so critical ones run on every build.

How do you prioritise API test cases?

By risk: business-critical flows, money, personal data, permissions and irreversible actions first (P1), then validation and regression cases (P2), then rare edge cases (P3).

How do you test that a failed request has no side effects?

After a rejected create or update, query the resource or list endpoint (or the database) to confirm nothing was created or changed, and that counters, balances or stock were not affected.

What is state transition testing for APIs?

Testing a resource’s lifecycle: valid transitions (created to updated to deleted) and invalid ones (update after delete, cancel after delivery, stale version updates), checking the API enforces the allowed state changes.

Where do you store API test cases?

In a test management tool such as Jira with Xray or Zephyr, TestRail, a spreadsheet for small projects, or as executable Postman collections and automated test code linked to requirements.

Advertisement
View the full API testing syllabus

API Security Testing: OWASP API Top 10 Checklist with Examples

API Security Testing: OWASP API Top 10 Checklist with Examples

APIs now carry most of the world’s sensitive data: payments, health records, KYC documents, location history. Attackers know it, and most large data breaches of recent years trace back to an API that trusted the wrong request. You don’t need to be a penetration tester to catch these bugs. In this lesson you’ll learn the OWASP API Security Top 10 in plain language, with a real-life example and concrete test cases for each risk that any QA engineer can run.

Quick answerAPI security testing checks that an API protects data and functions against misuse. The standard checklist is the OWASP API Security Top 10 (2023 edition): API1 Broken Object Level Authorization, API2 Broken Authentication, API3 Broken Object Property Level Authorization, API4 Unrestricted Resource Consumption, API5 Broken Function Level Authorization, API6 Unrestricted Access to Sensitive Business Flows, API7 Server-Side Request Forgery, API8 Security Misconfiguration, API9 Improper Inventory Management and API10 Unsafe Consumption of APIs. QA testers cover most of these with ordinary API tools by changing ids, tokens, fields, volumes and inputs.
Advertisement

Why QA Engineers Must Test API Security

  • Security teams run penetration tests once or twice a year; QA tests every sprint.
  • The most common API vulnerabilities are logic bugs (wrong ownership checks, missing limits), not exotic exploits. Testers who know the business rules find them best.
  • Fixing a security bug before release costs a fraction of a breach: fines under data protection laws like India’s DPDP Act and GDPR, incident response and lost customer trust.
Stay legal and ethical Only security-test systems you’re authorised to test: your company’s test environments or practice targets built for it. Never run attacks against production or third-party sites without written permission. Use test accounts and test data.

OWASP API Security Top 10 at a Glance

#RiskIn one lineQuick tester check
API1Broken Object Level AuthorizationAccess others’ records by changing an idUser A requests user B’s order id
API2Broken AuthenticationWeak login, tokens or sessionsBrute force, expired/tampered tokens
API3Broken Object Property Level AuthorizationRead or write fields you shouldn’tSend "role":"admin"; look for leaked fields
API4Unrestricted Resource ConsumptionNo limits on size, rate or costper_page=100000, 1,000 requests/min
API5Broken Function Level AuthorizationNormal users call admin functionsViewer calls DELETE or /admin endpoints
API6Unrestricted Access to Sensitive Business FlowsAutomating flows that hurt the businessBot buys all concert tickets, coupon farming
API7Server-Side Request Forgery (SSRF)API fetches an attacker-chosen URLImage URL = http://169.254.169.254/
API8Security MisconfigurationBad defaults, verbose errors, CORSStack traces, open CORS, missing headers
API9Improper Inventory ManagementForgotten old/debug APIs/v1/, /beta/, staging exposed
API10Unsafe Consumption of APIsTrusting third-party API data blindlyPartner returns malicious data or redirects
Advertisement

API1: Broken Object Level Authorization (BOLA)

Real-life example A telecom app shows your bill at /api/bills/7845123. Changing the number shows other customers’ names, addresses and call records. Data leaks of exactly this shape are among the most reported API vulnerabilities.

Tests: two users of the same role; access each other’s resources via path, query and body ids; check list endpoints don’t return other users’ items; expect 403/404. Full method in Authorization Testing.

API2: Broken Authentication

Tests: no rate limit on login or OTP; tokens without expiry; tokens accepted after logout; weak password rules; different messages for wrong user vs wrong password; credentials in URLs; alg: none JWTs. The practice API rejects tampered, expired and revoked tokens, so you can see correct behaviour; the JWT lesson has all the steps.

Real-life example: OTP brute force A 4-digit OTP has only 10,000 combinations. If the verify-OTP API allows unlimited attempts, a script can try them all in minutes and take over any account. Test: send 20 wrong OTPs; expect lockout or 429, and the OTP to expire.

API3: Broken Object Property Level Authorization

Two halves: excessive data exposure (the response contains fields the UI hides, like password_hash, pan_number, internal flags) and mass assignment (the client can set fields like role, price, is_verified).

Tests: inspect every response field, not just what the screen shows; validate with a JSON Schema that has additionalProperties: false; send privileged fields in POST/PUT/PATCH and confirm they’re rejected or ignored.

Advertisement

API4: Unrestricted Resource Consumption

Every API call costs CPU, memory, database time and sometimes money (SMS, email, AI calls). Without limits, one user can slow down everyone or run up a huge bill.

TestPractice API behaviour
Huge page sizeper_page=51 → 400 (max 50)
Large bodyOver 10 KB → 413
Rate limit/rate-limit: 6th call in a minute → 429 + Retry-After; whole API: 600/min per IP
Long-running requests/delay/11 → 400 (max 10 s)
Expensive operationsSend 1,000 “forgot password” SMS requests → should be throttled
GEThttps://sqatools.in/wp-json/sqa-api/v1/rate-limit
curl -i \
  "https://sqatools.in/wp-json/sqa-api/v1/rate-limit"

Press Send six times quickly. Watch X-RateLimit-Remaining count down, then the 429.

API5: Broken Function Level Authorization

Tests: call admin endpoints with a normal user token; try methods the UI never uses (DELETE, PUT); guess admin paths (/admin, /internal, /manage); check role changes take effect. On the practice API a viewer token on POST /token/objects correctly returns 403.

API6: Unrestricted Access to Sensitive Business Flows

Real-life example: ticket and sneaker bots When a popular concert or limited-edition sneaker sale opens, bots call the booking API thousands of times per second and resell tickets at higher prices. Every single API call is “valid”, but the flow is abused. Similar cases include coupon or referral abuse and fake account creation.

Tests: can one account or device buy beyond the limit per user? Can the same coupon or referral be redeemed repeatedly via the API? Are there controls such as device fingerprinting, CAPTCHA at the UI, per-user quotas and anomaly detection? Discuss these flows with the business team; they know what “abuse” means for them.

Advertisement

API7: Server-Side Request Forgery (SSRF)

Any feature where the API fetches a URL you supply (import from URL, profile picture URL, webhook URL, PDF generation from a link) can be abused to make the server call internal systems.

Tests (in authorised test environments only): supply http://localhost/, http://127.0.0.1:8080/admin, internal IPs (10.x, 192.168.x), the cloud metadata address http://169.254.169.254/, file:///etc/passwd and redirects to them. Expected: rejected with a validation error; only allowed domains and schemes accepted.

API8: Security Misconfiguration

  • Verbose errors with stack traces, SQL or file paths (send malformed input and read the 500 body).
  • Missing HTTPS or HSTS; old TLS versions.
  • CORS reflecting any origin with credentials.
  • Default credentials on admin consoles; debug endpoints enabled (/actuator, /debug, /swagger in production when not intended).
  • Unnecessary methods enabled (TRACE).
  • Sensitive responses cacheable (Cache-Control missing no-store).

The practice API returns structured errors without stack traces, even for malformed JSON:

POSThttps://sqatools.in/wp-json/sqa-api/v1/validate/user
curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"name": "Asha", ' \
  "https://sqatools.in/wp-json/sqa-api/v1/validate/user"

API9: Improper Inventory Management

Companies forget APIs: /v1/ still running after /v2/ launched, a staging server with real data exposed to the internet, a debug endpoint added for one release.

Tests: compare the API documentation with what’s actually deployed; try older versions of secured endpoints; check that non-production environments don’t contain real customer data and aren’t publicly reachable; keep an up-to-date API inventory (OpenAPI files per service).

API10: Unsafe Consumption of APIs

Your API trusts data from third-party APIs (address lookup, payment gateway, partner catalogue) more than user input. Attackers compromise or imitate the third party instead.

Tests: mock the third-party API to return malicious values (script tags, SQL strings, huge payloads, redirects, timeouts) and check your API validates and handles them; verify webhook signatures; ensure calls to partners use HTTPS and certificate validation.

Advertisement

Injection Testing (Still Important)

Injection isn’t a separate item in the 2023 API list, but it’s still a must-test. Send these in every text field and parameter, and expect them to be stored or rejected as plain text, never executed:

TypeSample payloadBad sign
SQL injection' OR '1'='1, 1; DROP TABLE users--SQL errors, extra records, 500
NoSQL injection{"$ne": null} in a login fieldLogin succeeds without a password
Cross-site scripting (stored)<script>alert(1)</script>Script runs when another user views the data
Command injection; ls -la, $(whoami)Command output in the response
XML external entity (XXE)<!DOCTYPE x [<!ENTITY e SYSTEM "file:///etc/passwd">]>File contents in a SOAP/XML response

The practice API’s scenario 101 stores SQL and HTML strings in an object as plain text; create one and read it back to see correct behaviour.

Tools for API Security Testing

  • Postman / Playwright: your normal tools cover most BOLA, auth, validation and rate-limit tests.
  • OWASP ZAP (free): automated API scans from an OpenAPI file; proxy to inspect traffic.
  • Burp Suite: intercept and modify requests, Intruder for fuzzing ids and payloads.
  • Schemathesis / RESTler: generate thousands of edge-case requests from OpenAPI.
  • jwt.io-style decoders and jwt_tool: inspect and attack JWTs.
  • Semgrep, Snyk, Dependabot: code and dependency scanning (developer side).
Advertisement

A Security Smoke Suite in Playwright

import { test, expect } from '@playwright/test';

const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';

test('protected endpoints refuse anonymous calls', async ({ request }) => {
  for (const path of ['/basic/profile', '/apikey/profile', '/auth/me', '/token/objects']) {
    expect((await request.get(`${BASE}${path}`)).status(), path).toBe(401);
  }
});

test('responses expose only schema fields', async ({ request }) => {
  const user = (await (await request.get(`${BASE}/users/1`)).json()).data;
  expect(Object.keys(user).sort()).toEqual(
    ['active', 'age', 'city', 'created_at', 'email', 'id', 'name', 'role', 'username']);
});

test('malformed input gives a clean 4xx without internals', async ({ request }) => {
  const res = await request.post(`${BASE}/validate/user`, {
    headers: { 'Content-Type': 'application/json' }, data: '{"name": "Asha", ',
  });
  expect(res.status()).toBe(400);
  const text = await res.text();
  for (const leak of ['Stack trace', 'Exception', '.php', 'SQL', 'wp-includes']) expect(text).not.toContain(leak);
});

test('injection strings are stored as plain text', async ({ request }) => {
  const payload = `' OR '1'='1 <script>alert(1)</script>`;
  const res = await request.post(`${BASE}/objects`, { data: { name: payload, ttl: 30 } });
  expect(res.status()).toBe(201);
  expect((await res.json()).name).toBe(payload);
});

test('rate limit kicks in with Retry-After', async ({ request }) => {
  let last;
  for (let i = 0; i < 6; i++) last = await request.get(`${BASE}/rate-limit`);
  expect(last!.status()).toBe(429);
  expect(last!.headers()['retry-after']).toBeTruthy();
});

Note the rate-limit test shares a per-IP counter, so don’t run it in parallel with itself.

Advertisement

Interview Questions: API Security Testing

What is the OWASP API Security Top 10?

A list of the most critical API security risks published by OWASP. The 2023 edition is: BOLA, Broken Authentication, Broken Object Property Level Authorization, Unrestricted Resource Consumption, Broken Function Level Authorization, Unrestricted Access to Sensitive Business Flows, SSRF, Security Misconfiguration, Improper Inventory Management and Unsafe Consumption of APIs.

What is the most common API vulnerability?

Broken Object Level Authorization (BOLA/IDOR): the API doesn’t check that the requested object belongs to the caller, so changing an id exposes or modifies other users’ data.

How do you test an API for SQL injection?

Send SQL meta-characters and payloads such as a single quote or ‘ OR ‘1’=’1 in every parameter, header and body field. Look for 500 errors, SQL error messages, extra data or behaviour changes. A secure API returns a validation error or treats the input as plain text.

What is rate limiting and how do you test it?

Rate limiting caps the number of requests a client can make in a time window. Send requests past the documented limit and verify 429 Too Many Requests with Retry-After and rate-limit headers, and that the limit resets after the window.

What is excessive data exposure?

When an API returns more data than the client needs, relying on the UI to hide it, for example password hashes or other users’ emails in a response. Testers inspect raw responses and validate them against a strict schema.

What is SSRF?

Server-Side Request Forgery: an API that fetches a user-supplied URL can be tricked into calling internal services or cloud metadata endpoints. Test by supplying internal addresses and verifying they’re rejected.

Which tools do you use for API security testing?

Postman or Playwright for functional security checks, OWASP ZAP and Burp Suite for interception and scanning, Schemathesis for fuzzing from OpenAPI, and JWT tools for token analysis, always within authorised environments.

What should an error response never contain?

Stack traces, SQL queries, file paths, framework or server versions, internal hostnames, secrets or tokens. It should contain a clear error code and safe message.

What is mass assignment?

A vulnerability where an API automatically binds client-provided fields to internal models, letting attackers set privileged fields such as role or balance. It’s part of OWASP API3.

How is API security testing different from web UI security testing?

APIs expose raw data and actions directly without UI restrictions, so testers focus on authorization per object and function, token handling, input validation, rate limits and data exposure in responses, rather than mainly on browser issues.

Advertisement
View the full API testing syllabus

API Authorization Testing: Roles, Permissions and 401 vs 403

API Authorization Testing: Roles, Permissions and 401 vs 403

Authentication proves who you are; authorization decides what you can touch. Broken authorization is the number one API security risk: a delivery partner reading every customer’s address, a viewer deleting records, a user downloading someone else’s invoice just by changing a number in the URL. In this lesson you’ll learn how to design a permission matrix, test roles and ownership, and tell 401 from 403, with live admin and viewer tokens on the practice API.

Quick answerAPI authorization testing verifies that each authenticated user can perform only the actions and access only the data their role and ownership allow. Build a permission matrix (roles x endpoints x methods), then test every cell: allowed actions return 2xx, forbidden ones return 403 Forbidden (or 404 to hide existence), and unauthenticated calls return 401. Always include object-level checks (BOLA/IDOR): user A must not read or change user B’s resources by changing an id.
Advertisement

Three Levels of Authorization

Real-life example: a hospital system A receptionist can book appointments but can’t read diagnoses (function level). A doctor can read medical records, but only of their own patients (object level). A nurse can see a patient’s medication list but not the billing field on the same record (property level). Each level needs its own tests.
LevelQuestionOWASP API Top 10Example bug
Function levelCan this role call this endpoint/method?API5: Broken Function Level AuthorizationViewer calls DELETE /users/5 and it works
Object levelCan this user access this specific record?API1: Broken Object Level Authorization (BOLA)GET /orders/1002 shows another customer’s order
Property levelCan this user read/write this field?API3: Broken Object Property Level AuthorizationUser sends "role":"admin" in a profile update and becomes admin

401 vs 403 vs 404 in Authorization

SituationCorrect code
No token / invalid token401 Unauthorized
Valid token, role not allowed (viewer creating data)403 Forbidden
Valid token, resource belongs to someone else403 or 404 (404 hides that it exists)
Valid token, allowed, resource missing404 Not Found
Remember it this way 401 = “I don’t know who you are.” 403 = “I know who you are, and the answer is no.”
Advertisement

Step 1: Build a Permission Matrix

Before testing, write down who may do what. Here is the matrix for the practice API’s token-protected objects (/token/objects):

ActionNo tokenvieweradmin
GET /token/objects401200200
GET /token/objects/{id}401200200
POST /token/objects401403201
PUT/PATCH /token/objects/{id}401403200
DELETE /token/objects/{id}401403204
GET /auth/me401200, can_write: false200, can_write: true

In real projects the matrix often has 5 to 10 roles (customer, seller, support agent, finance, admin, super-admin…). It becomes the single source of truth for both developers and testers, and every cell is a test case.

Real-life example: food delivery roles Customer: own orders only. Restaurant: orders for its own outlet, can change status to “preparing”. Delivery partner: assigned orders only, sees address only after pickup. Support agent: any order, but can’t see full payment details. Admin: everything. Each line is a set of authorization tests.

Step 2: Test Function-Level Authorization (Roles)

Log in as both users (practice credentials: admin/admin@123, viewer/viewer@123):

POSThttps://sqatools.in/wp-json/sqa-api/v1/auth/login
curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"username":"viewer","password":"viewer@123"}' \
  "https://sqatools.in/wp-json/sqa-api/v1/auth/login"
BASE=https://sqatools.in/wp-json/sqa-api/v1

# Viewer can read
curl -i "$BASE/token/objects" -H "Authorization: Bearer $VIEWER"            # 200

# Viewer cannot create
curl -i -X POST "$BASE/token/objects" -H "Authorization: Bearer $VIEWER" \
  -H "Content-Type: application/json" -d '{"name":"x"}'
# 403 {"error":{"code":"forbidden","message":"The viewer role can only read objects. Log in as admin to change them."}}

# Admin can create
curl -i -X POST "$BASE/token/objects" -H "Authorization: Bearer $ADMIN" \
  -H "Content-Type: application/json" -d '{"name":"admin object","ttl":120}'   # 201

Function-level test ideas

  • Every write method (POST, PUT, PATCH, DELETE) with every low-privilege role.
  • Admin-only endpoints (/admin/*, /reports, /users/export) with a normal user token. Look for them in Swagger docs, the web app’s JavaScript and mobile app traffic.
  • “Hidden” HTTP methods: the UI only uses GET, but does DELETE work?
  • Old API versions (/v1/) that may lack the checks added in /v2/.
Advertisement

Step 3: Test Object-Level Authorization (BOLA / IDOR)

BOLA (also known as IDOR, Insecure Direct Object Reference) is the most common serious API bug. The API checks that you are logged in but not whether this object is yours.

Real-world pattern A customer opens /api/invoices/50231, changes it to 50230 and downloads another customer’s invoice with name, address and phone. Security researchers report this pattern constantly in bug bounty programmes because sequential numeric ids make it trivial to scan thousands of records.

How to test BOLA

  1. Create two users of the same role: User A and User B.
  2. As B, create a resource (order, address, document). Note its id.
  3. As A, try to GET, PUT, PATCH and DELETE B’s resource.
  4. Expected: 403 or 404 for every call, and B’s data unchanged (verify with B’s token).
  5. Repeat for nested routes (/users/{B}/orders), list endpoints with filters (/orders?user_id=B), and ids inside the body ({"order_id": B's}).

Also test predictable ids: are they sequential (1001, 1002…) or random UUIDs? UUIDs don’t fix BOLA, but sequential ids make it much easier to exploit.

Advertisement

Step 4: Test Property-Level Authorization (Mass Assignment)

Send fields you shouldn’t control and check the server ignores or rejects them:

PATCH /users/me
{
  "name": "Asha",
  "role": "admin",          // privilege escalation attempt
  "wallet_balance": 99999,  // money!
  "is_verified": true
}

Expected: role, wallet_balance and is_verified are rejected (422) or silently ignored; a follow-up GET proves nothing changed. On the read side, check responses don’t include fields a role shouldn’t see (salary, internal notes, other users’ emails).

The practice API rejects unknown fields: POST /validate/user with an extra field returns 422, and POST /objects with a client-chosen id returns 422.

Advertisement

Automating a Permission Matrix with Playwright

Turn the matrix into data, and Playwright generates one test per cell:

import { test, expect, APIRequestContext } from '@playwright/test';

const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';
const USERS = { admin: 'admin@123', viewer: 'viewer@123' } as const;
type Role = keyof typeof USERS | 'anonymous';

async function tokenFor(request: APIRequestContext, role: Role) {
  if (role === 'anonymous') return {};
  const res = await request.post(`${BASE}/auth/login`, { data: { username: role, password: USERS[role] } });
  return { Authorization: `Bearer ${(await res.json()).access_token}` };
}

const matrix: { action: string; method: 'get' | 'post'; path: string; data?: object; expect: Record<Role, number> }[] = [
  { action: 'list objects',  method: 'get',  path: '/token/objects',
    expect: { anonymous: 401, viewer: 200, admin: 200 } },
  { action: 'create object', method: 'post', path: '/token/objects', data: { name: 'rbac test', ttl: 60 },
    expect: { anonymous: 401, viewer: 403, admin: 201 } },
];

for (const row of matrix) {
  for (const role of Object.keys(row.expect) as Role[]) {
    test(`${role} -> ${row.action} = ${row.expect[role]}`, async ({ request }) => {
      const headers = await tokenFor(request, role);
      const res = await request[row.method](`${BASE}${row.path}`, { headers, data: row.data });
      expect(res.status()).toBe(row.expect[role]);
    });
  }
}

test('viewer cannot delete an admin object, admin can', async ({ request }) => {
  const admin = await tokenFor(request, 'admin');
  const viewer = await tokenFor(request, 'viewer');
  const created = await (await request.post(`${BASE}/token/objects`, { headers: admin, data: { name: 'owned', ttl: 60 } })).json();

  expect((await request.delete(`${BASE}/token/objects/${created.id}`, { headers: viewer })).status()).toBe(403);
  expect((await request.get(`${BASE}/token/objects/${created.id}`, { headers: viewer })).status()).toBe(200); // still there
  expect((await request.delete(`${BASE}/token/objects/${created.id}`, { headers: admin })).status()).toBe(204);
});

Adding a new role or endpoint now means adding one line of data, not writing new tests.

Advertisement

Where Authorization Bugs Hide

  • New endpoints added in a hurry, especially exports, bulk operations and reports.
  • Mobile-only APIs nobody tests through the web UI.
  • GraphQL fields and nested resolvers (each field needs its own check).
  • Background jobs and webhooks that act “as the system”.
  • Role changes: a user demoted from admin still has an old token with role: admin.
  • Multi-tenant SaaS: company A’s admin seeing company B’s data (tenant isolation).
  • File downloads: signed URLs that never expire or are guessable.

Authorization Testing Checklist

  1. Permission matrix documented and agreed.
  2. Every endpoint and method tested with no token (401) and each role.
  3. Two users of the same role: no cross-access (BOLA) for read, update and delete.
  4. Ids in path, query, body and headers all checked.
  5. Privileged fields can’t be set by the client (mass assignment).
  6. Responses don’t leak fields beyond the role’s permissions.
  7. Role change and account deactivation take effect immediately or at next token refresh.
  8. Tenant isolation for multi-company systems.
Advertisement

Interview Questions: Authorization Testing

What is the difference between 401 and 403 in authorization testing?

401 means the request isn’t authenticated (no, invalid or expired credentials). 403 means the user is authenticated but not allowed to perform that action or access that resource.

What is BOLA or IDOR?

Broken Object Level Authorization (Insecure Direct Object Reference) is when an API returns or modifies a resource based on an id without checking that the logged-in user owns or may access it, for example changing /orders/1001 to /orders/1002 shows another customer’s order.

How do you test role-based access control in APIs?

Create a permission matrix of roles, endpoints and methods, obtain a token for each role, call every endpoint and method with each token and with no token, and assert the expected status for each cell. Automate it as data-driven tests.

How do you test BOLA?

Use two users with the same role. Create a resource as user B, then try to read, update and delete it as user A through every route that accepts its id (path, query, body). Expect 403 or 404 and verify B’s data is unchanged.

What is mass assignment?

When an API binds all client-sent fields to an internal object, letting a user set fields they shouldn’t, such as role, is_admin or balance. Test by sending such fields in create and update requests and verifying they’re rejected or ignored.

Should an API return 403 or 404 for another user’s resource?

Both are acceptable. 404 is often preferred because it doesn’t reveal that the resource exists. What matters is that no data is returned and nothing is changed.

What is a permission matrix?

A table listing each role against each endpoint and action with the expected outcome. It’s the specification for authorization and the basis for systematic test cases.

What is tenant isolation testing?

In multi-tenant SaaS systems, verifying that users of one organisation can’t access another organisation’s data, even with an admin role in their own tenant.

What happens if a user’s role changes while their token is still valid?

If the role is read only from the token, the old privileges remain until it expires. Good systems re-check roles on the server or keep tokens short and refresh them. Testers should verify how quickly role changes take effect.

Why is authorization hard to test only through the UI?

The UI hides buttons for actions a user isn’t allowed to do, but the API may still accept the request. Authorization must be tested directly at the API level with each role’s token.

Advertisement
View the full API testing syllabus

JWT Token Testing: Structure, Expiry, Tampering, Refresh and Logout

JWT Token Testing: Structure, Expiry, Tampering, Refresh and Logout

JWTs (JSON Web Tokens) keep you logged in to almost every modern app, from food delivery to banking. They also cause some of the most serious API vulnerabilities: tokens that never expire, signatures that aren’t checked, and logouts that don’t really log you out. In this lesson you’ll take a real JWT apart, then test expiry, tampering, refresh rotation and logout on the live practice API, manually and with Playwright.

Quick answerA JWT is a signed token made of three Base64URL parts separated by dots: header (algorithm), payload (claims such as sub, role, exp) and signature. To test JWT authentication, verify that a valid token works, and that a missing, malformed, wrongly-schemed, tampered, expired or revoked token returns 401; that refresh tokens rotate and can be used only once; that the payload holds no secrets; and that the server rejects the alg: none trick.
Advertisement

What Is a JWT? (The Cinema Wristband Example)

Real-life example: a festival wristband At a music festival, you show your ticket and ID once at the gate and get a wristband printed “VIP, valid 12 Nov”. Inside, every stage checks only the wristband, not your ID. The wristband is stamped with a hologram nobody can fake, so if you write “VIP” with a pen on a normal band, guards spot it. A JWT is that wristband: issued once at login, readable by anyone, trusted only because of its signature, and useless after it expires.

Because the server can verify the signature with its secret key, it doesn’t need to look up a session in a database on every request. That’s why JWTs are called stateless tokens.

Anatomy of a Real JWT

Log in to the practice API and you get an access_token like this (shortened):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczpcL1wvc3FhdG9vbHMuaW5cLyIsInN1YiI6ImFkbWluIiwi...fQ._ZDcTiyS-183vQ2eAI_zeSnPgZMdX2Z3IYBrifVfyiM
<------------- header ------------->.<-------------------- payload -------------------->.<-------- signature -------->

Decoding the first two parts (Base64URL) shows:

// Header
{ "alg": "HS256", "typ": "JWT" }

// Payload (claims)
{
  "iss": "https://sqatools.in/",   // issuer
  "sub": "admin",                  // subject: the user
  "name": "Admin User",
  "role": "admin",                 // custom claim used for authorization
  "iat": 1794482821,               // issued at (Unix seconds)
  "exp": 1794483721,               // expires at: iat + 900 s
  "jti": "2hqtst3pmbas36wm"        // unique token id (used for revocation)
}

The third part is the signature: HMACSHA256(base64url(header) + "." + base64url(payload), secret). Change one character of the header or payload and the signature no longer matches.

Encoded, not encrypted Anyone can read a JWT payload (paste it into a decoder like jwt.io). Never put passwords, card numbers, Aadhaar/PAN or other secrets in it. Testers should decode every token they see and check this.

Standard claims to check

ClaimMeaningWhat to test
expExpiry timeExists; short (minutes, not months); enforced
iat / nbfIssued at / not beforeToken used before nbf is rejected
issIssuerTokens from another issuer are rejected
audAudienceA token for API A doesn’t work on API B
subUserServer uses it, not a user id sent in the body
jtiToken idSupports revocation and replay detection
Advertisement

The Token Lifecycle on the Practice API

StepRequestResult
1. Log inPOST /auth/login {"username":"admin","password":"admin@123"}200: access_token (900 s), refresh_token, user
2. UseGET /auth/me + Authorization: Bearer <access>200: user, role, can_write
3. RefreshPOST /auth/refresh {"refresh_token":"..."}200: new access and new refresh token
4. Reuse old refreshSame refresh token again401 invalid_refresh_token (rotation)
5. Log outPOST /auth/logout + Bearer204
6. Use after logoutGET /auth/me with the old token401 token_revoked

Start by logging in here, then copy the access_token into Postman or the cURL commands below:

POSThttps://sqatools.in/wp-json/sqa-api/v1/auth/login
curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin@123"}' \
  "https://sqatools.in/wp-json/sqa-api/v1/auth/login"

You can also request a shorter lifetime for expiry testing: send "expires_in": 5 in the login body (allowed range 5 to 3600 seconds; 3 returns 422).

JWT Test Cases (Complete List)

#ScenarioHowExpected
1Valid tokenBearer + fresh token200
2No tokenNo Authorization header401
3Garbage tokenBearer abc.def401
4Wrong schemeToken <jwt>401
5Tampered payloadChange "role":"viewer" to "admin", keep signature401 (signature invalid)
6Tampered signatureChange one character of part 3401
7alg: none attackHeader {"alg":"none"}, empty signature401
8Expired tokenLogin with expires_in: 5, wait 7 s401
9Lifetime boundaryexpires_in: 4 / 5 / 3600 / 3601422 / 200 / 200 / 422
10Refresh worksPOST /auth/refresh200, new pair
11Refresh reuseSame refresh token twiceSecond: 401
12Refresh missingEmpty body422
13Logout revokesUse token after /auth/logout401
14Token from another environmentStaging token on production401 (different secret/issuer)
15No secrets in payloadDecode the tokenNo password, PII limited
16Token not in URL / logsInspect requests and logsHeader only
Advertisement

Testing Tampering Step by Step

Why it matters: the “make me admin” attack A viewer decodes their token, sees "role":"viewer", changes it to "admin", re-encodes the payload and sends it. If the server trusts the payload without verifying the signature, a read-only user just became an admin. This exact class of bug has been found in real production systems.
  1. Log in as viewer / viewer@123 and copy the token.
  2. Split it at the dots. Base64URL-decode the middle part.
  3. Change "role":"viewer" to "role":"admin", encode it again (Base64URL, no padding).
  4. Rebuild header.newPayload.originalSignature and call GET /auth/me.
  5. Pass if the response is 401 (“Token signature is not valid”). Critical bug if it returns 200 with role admin.
# Quick signature tamper with cURL (replace TOKEN). Changing the last 2 characters breaks the signature.
TAMPERED="${TOKEN%??}AA"
curl -i https://sqatools.in/wp-json/sqa-api/v1/auth/me -H "Authorization: Bearer $TAMPERED"
# HTTP/1.1 401 ... "Token signature is not valid (was the token changed?)."

Testing Expiry Without Waiting 15 Minutes

Real access tokens may live 15 to 60 minutes. Waiting is slow, so testers use one of these:

  • A test-only short lifetime, like the practice API’s expires_in field.
  • A configuration flag in the test environment that sets token lifetime to seconds.
  • An already-expired token saved from a previous run (works as long as the signing key hasn’t changed).
  • Unit tests on the token validator with a fake clock (developer level).

Also test clock skew: servers usually allow 30 to 60 seconds of tolerance; a token expired an hour ago must fail.

Advertisement

Refresh Tokens and Rotation

Short access tokens are safer, but users don’t want to log in every 15 minutes. A refresh token (long-lived, stored securely) gets new access tokens silently. With rotation, every refresh returns a new refresh token and invalidates the old one. If an attacker steals a refresh token and the real app also uses it, the reuse is detected.

Real-life example Banking apps often keep you signed in for a few days but ask for your PIN or fingerprint before payments. Behind the scenes, short access tokens are refreshed silently, and sensitive actions demand fresh authentication.

Refresh tests: refresh works once; reuse returns 401; a refresh token can’t be used as an access token (and vice versa); logout and password change revoke refresh tokens; refresh tokens have their own maximum lifetime.

Advertisement

JWT Testing in Postman

  1. Send POST /auth/login. In the Scripts → Post-response tab add:
    pm.test('login ok', () => pm.response.to.have.status(200));
    const body = pm.response.json();
    pm.environment.set('access_token', body.access_token);
    pm.environment.set('refresh_token', body.refresh_token);
  2. On GET /auth/me, set Authorization → Bearer Token to {{access_token}}.
  3. Duplicate the request, change the token to {{access_token}}x and assert 401.

Full Postman scripting is in Postman Tests and Newman.

Advertisement

JWT Tests in Playwright (TypeScript)

import { test, expect, APIRequestContext } from '@playwright/test';

const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';

async function login(request: APIRequestContext, user = 'admin', pass = 'admin@123', expires_in?: number) {
  const res = await request.post(`${BASE}/auth/login`, { data: { username: user, password: pass, expires_in } });
  expect(res.status()).toBe(200);
  return res.json() as Promise<{ access_token: string; refresh_token: string; expires_in: number }>;
}
const bearer = (t: string) => ({ Authorization: `Bearer ${t}` });
const decode = (t: string) => JSON.parse(Buffer.from(t.split('.')[1], 'base64url').toString());

test('payload has expected claims and no secrets', async ({ request }) => {
  const { access_token } = await login(request);
  const claims = decode(access_token);
  expect(claims).toMatchObject({ sub: 'admin', role: 'admin', iss: 'https://sqatools.in/' });
  expect(claims.exp - claims.iat).toBe(900);
  expect(JSON.stringify(claims)).not.toContain('admin@123');
});

test('forged role in payload is rejected', async ({ request }) => {
  const { access_token } = await login(request, 'viewer', 'viewer@123');
  const [h, p, s] = access_token.split('.');
  const forged = Buffer.from(JSON.stringify({ ...decode(access_token), role: 'admin' })).toString('base64url');
  const res = await request.get(`${BASE}/auth/me`, { headers: bearer(`${h}.${forged}.${s}`) });
  expect(res.status()).toBe(401);
});

test('expired token is rejected', async ({ request }) => {
  test.setTimeout(30_000);
  const { access_token } = await login(request, 'admin', 'admin@123', 5);
  await new Promise(r => setTimeout(r, 7_000));
  expect((await request.get(`${BASE}/auth/me`, { headers: bearer(access_token) })).status()).toBe(401);
});

test('refresh token rotates and works only once', async ({ request }) => {
  const { refresh_token } = await login(request);
  expect((await request.post(`${BASE}/auth/refresh`, { data: { refresh_token } })).status()).toBe(200);
  expect((await request.post(`${BASE}/auth/refresh`, { data: { refresh_token } })).status()).toBe(401);
});

test('logout revokes the access token', async ({ request }) => {
  const { access_token } = await login(request);
  expect((await request.post(`${BASE}/auth/logout`, { headers: bearer(access_token) })).status()).toBe(204);
  expect((await request.get(`${BASE}/auth/me`, { headers: bearer(access_token) })).status()).toBe(401);
});

JWT Security Checklist

  • Signature always verified; alg: none and algorithm switching (RS256 → HS256) rejected.
  • Strong secret; tokens from other environments rejected.
  • Short exp; expiry enforced; refresh tokens rotated.
  • Logout, password change and role change take effect.
  • No sensitive data in the payload.
  • Authorization decisions use server data or verified claims, never values sent by the client in the body.
  • Tokens stored safely on clients (HttpOnly cookies or secure mobile storage).
Advertisement

Interview Questions: JWT Testing

What is a JWT and what are its parts?

A JSON Web Token is a compact, signed token with three Base64URL-encoded parts separated by dots: the header (algorithm and type), the payload (claims such as sub, role, iat and exp) and the signature that proves the first two parts weren’t changed.

Is a JWT encrypted?

Normally no. A standard signed JWT (JWS) is only encoded, so anyone can read the payload. It’s protected against modification, not against reading. Encrypted JWTs (JWE) exist but are less common.

How do you test JWT expiry?

Obtain a token with a short lifetime (test configuration or a parameter), wait past the exp time and confirm the API returns 401. Also check that exp exists, that the lifetime is short and that a reasonable clock-skew tolerance is applied.

How do you test if a JWT signature is verified?

Modify the payload (for example change the role) while keeping the old signature, modify a character of the signature, and try a token with alg set to none. All must return 401.

What is the difference between an access token and a refresh token?

An access token is short-lived and sent with every API call. A refresh token is longer-lived, sent only to the token endpoint, and used to obtain new access tokens without the user logging in again.

What is refresh token rotation?

Each refresh returns a new refresh token and invalidates the old one, so a stolen refresh token can be used only once and reuse can be detected.

How can logout work if JWTs are stateless?

The server keeps a deny-list of revoked token ids (jti) until they expire, revokes the refresh token, or keeps access tokens very short. Testers verify the old token returns 401 after logout.

What is the alg none attack?

An attacker sets the JWT header algorithm to none and removes the signature, hoping the server accepts unsigned tokens. Secure servers reject tokens whose algorithm isn’t the expected one.

What should never be stored in a JWT payload?

Passwords, secrets, full card numbers, government ids or other sensitive personal data, because the payload can be decoded by anyone holding the token.

Where should a web app store JWTs?

Preferably in HttpOnly, Secure, SameSite cookies, which JavaScript can’t read. localStorage is exposed to any XSS vulnerability. Mobile apps should use the platform’s secure storage (Keychain or Keystore).

Advertisement
View the full API testing syllabus

API Authentication Types: Basic Auth, API Key, Bearer Token and OAuth 2.0

API Authentication Types: Basic Auth, API Key, Bearer Token and OAuth 2.0

Every serious API asks one question before it does anything: who are you? That is authentication. Get it wrong and anyone can read customer data; test it badly and you’ll miss the bug that leaks it. In this lesson you will learn the four authentication methods you’ll meet in real projects (Basic auth, API keys, Bearer tokens/JWT and OAuth 2.0), where each one is used in real life, and exactly which tests to run for each one on the live practice API.

Quick answerThe main API authentication types are: Basic authentication (username:password, Base64-encoded in the Authorization header), API key (a secret key in a header such as X-API-Key, identifying an application), Bearer token / JWT (a signed, expiring token obtained at login and sent as Authorization: Bearer <token>) and OAuth 2.0 (a framework where an authorization server issues access tokens, used for “Login with Google” and third-party access). Missing or invalid credentials should return 401; valid credentials without permission should return 403.
Advertisement

Authentication vs Authorization

Real-life example: an airport At the entrance, security checks your ID and boarding pass: that’s authentication (proving who you are). At the gate, the boarding pass decides whether you can enter the business lounge or only economy: that’s authorization (what you’re allowed to do). A passenger with a valid ID can still be refused at the lounge door.
Authentication (AuthN)Authorization (AuthZ)
QuestionWho are you?What may you do?
HappensFirstAfter authentication
Failure code401 Unauthorized403 Forbidden (or 404 to hide data)
ExampleLogin, API key, token checkViewer can’t delete; user can’t see others’ orders

This lesson covers authentication. Authorization gets its own lesson: roles and 401 vs 403.

The 4 Main Authentication Types at a Glance

TypeHow it’s sentIdentifiesExpires?Typical real use
Basic authAuthorization: Basic base64(user:pass)A user or service accountNo (password)Internal tools, Jenkins APIs, legacy systems, test environments
API keyX-API-Key: key (or query param)An application/clientUsually no (rotated manually)Google Maps, OpenWeatherMap, SMS gateways
Bearer token (JWT)Authorization: Bearer eyJ...A logged-in user sessionYes (minutes to hours)Mobile and web app backends
OAuth 2.0Bearer access token from an auth serverA user who granted an app accessYes, with refresh tokens“Login with Google”, GitHub apps, UPI/bank partner APIs

Others you may meet: Digest auth (older, rarely used), HMAC signatures (AWS, payment webhooks), mutual TLS (bank-to-bank APIs) and session cookies (covered in the cookies lesson).

Advertisement

1. Basic Authentication

The client joins username and password with a colon, encodes it in Base64 and sends it in every request:

sqa_user:sqa@123  →  Base64  →  c3FhX3VzZXI6c3FhQDEyMw==
Authorization: Basic c3FhX3VzZXI6c3FhQDEyMw==
Base64 is not encryption Anyone can decode c3FhX3VzZXI6c3FhQDEyMw== back to the password in one second. Basic auth is only acceptable over HTTPS, and never for end-user logins in modern apps.

Practice credentials (public, for this API only): sqa_user / sqa@123.

GEThttps://sqatools.in/wp-json/sqa-api/v1/basic/profile
curl -i \
  -H "Authorization: Basic c3FhX3VzZXI6c3FhQDEyMw==" \
  "https://sqatools.in/wp-json/sqa-api/v1/basic/profile"

Basic auth test cases

ScenarioExpected
Valid credentials200
No Authorization header401 + WWW-Authenticate: Basic realm="..."
Wrong password401, same message as unknown user
Malformed header (not Base64)401, not 500
Credentials over plain HTTPRefused or redirected to HTTPS
Many wrong attemptsLockout or rate limit (429)

2. API Key Authentication

An API key is a long random string issued to an application, not to a person. The server looks it up to know which client is calling, which plan it’s on and how many calls it may make.

Real-life example: weather in a travel app A travel app shows weather for your destination. Its backend calls a weather provider with appid=YOUR_KEY. The provider counts calls per key, blocks the key at its free-plan limit and bills the company on a paid plan. No end user ever logs in to the weather service.
GEThttps://sqatools.in/wp-json/sqa-api/v1/apikey/profile
curl -i \
  -H "X-API-Key: sqa-key-12345" \
  "https://sqatools.in/wp-json/sqa-api/v1/apikey/profile"

API key test cases

ScenarioPractice API result
Valid key in X-API-Key header200
Valid key as ?api_key= query param200 (works, but keys in URLs end up in logs: flag it)
Missing key401
Wrong key403
Key in the wrong header (e.g. Authorization)401
Revoked or expired key401/403
Quota exceeded429 with Retry-After
Real bug pattern: keys in mobile apps and GitHub API keys hardcoded in Android apps or pushed to public GitHub repositories are scraped by bots within minutes. Check that secret keys aren’t in front-end code, and that keys shipped to clients are restricted (by app, domain or IP).
Advertisement

3. Bearer Token Authentication (JWT)

The user logs in once with username and password. The server returns a short-lived access token, and the client sends it as a “bearer” in every request. “Bearer” means whoever holds this token gets access, so it must be protected like a password.

  1. POST /auth/login with username and password → 200 with access_token, expires_in, refresh_token.
  2. GET /auth/me with Authorization: Bearer <access_token> → 200.
  3. When the access token expires, POST /auth/refresh with the refresh token → new tokens without asking for the password again.
  4. POST /auth/logout → the token is revoked.
POSThttps://sqatools.in/wp-json/sqa-api/v1/auth/login
curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin@123"}' \
  "https://sqatools.in/wp-json/sqa-api/v1/auth/login"

The practice API’s access tokens are JWTs that last 900 seconds (15 minutes). Users: admin / admin@123 (can write) and viewer / viewer@123 (read-only). JWT internals, expiry and tampering tests are covered in depth in JWT Token Testing.

Bearer token test cases

  • Valid login returns a token; wrong password returns 401; missing fields return 422.
  • Protected endpoint without a token → 401; with a garbage token → 401.
  • Wrong scheme (Token xyz instead of Bearer xyz) → 401.
  • Expired, tampered or revoked token → 401.
  • The password is never echoed back; the token never appears in URLs.

4. OAuth 2.0

OAuth 2.0 is an authorization framework: it lets a user give an application limited access to their data on another service without sharing their password.

Real-life example: “Login with Google” and a photo-printing app A photo-printing website wants your Google Photos. Instead of asking for your Google password, it redirects you to Google. You log in at Google and approve “view your photos”. Google gives the website an access token limited to that scope. The website can read photos but can’t read your Gmail, and you can revoke access at any time.

The four roles

  • Resource owner: you (the user).
  • Client: the app that wants access (the printing site).
  • Authorization server: issues tokens (Google’s accounts server, Okta, Keycloak, Auth0).
  • Resource server: the API holding the data (Google Photos API).

Common OAuth 2.0 grant types (flows)

FlowUsed whenExample
Authorization Code + PKCEA user logs in through a browser or mobile app“Login with Google/GitHub”
Client CredentialsMachine-to-machine, no userOrder service calling the payment service; partner APIs
Refresh TokenGet a new access token silentlyApp stays logged in for weeks
Device CodeDevices without a keyboardSmart TV “enter this code on your phone”
Implicit, PasswordDeprecated (insecure)Flag them if you see them

OpenID Connect (OIDC) adds identity on top of OAuth 2.0: an ID token (a JWT) telling the app who the user is. “Login with Google” is OIDC.

Advertisement

OAuth 2.0 test cases

  • Token request with wrong client_secret → 401 invalid_client.
  • Token with scope read calling a write endpoint → 403 insufficient_scope.
  • redirect_uri not on the allowed list → rejected (prevents token theft).
  • state parameter checked (prevents CSRF in the login flow).
  • Authorization code can be used only once and expires quickly.
  • Revoked consent stops access immediately (or at token expiry).

In Postman, the Authorization → OAuth 2.0 tab can run these flows for you and store the token; you’ll use it in the Postman tutorial.

Which Authentication Should an API Use?

SituationGood choice
Your own mobile or web app’s usersBearer JWT (short-lived) + refresh token, or OIDC
Third-party apps acting for your usersOAuth 2.0 Authorization Code + PKCE
Server-to-server, partnersOAuth 2.0 Client Credentials, mTLS or HMAC-signed requests
Public data APIs with quotasAPI keys
Quick internal tools / test environmentsBasic auth over HTTPS
Advertisement

Automating All Auth Types with Playwright

import { test, expect } from '@playwright/test';

const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';

test('Basic auth: valid and missing credentials', async ({ request }) => {
  const basic = 'Basic ' + Buffer.from('sqa_user:sqa@123').toString('base64');
  expect((await request.get(`${BASE}/basic/profile`, { headers: { Authorization: basic } })).status()).toBe(200);

  const none = await request.get(`${BASE}/basic/profile`);
  expect(none.status()).toBe(401);
  expect(none.headers()['www-authenticate']).toContain('Basic');
});

test('API key: valid, wrong and missing', async ({ request }) => {
  const url = `${BASE}/apikey/profile`;
  expect((await request.get(url, { headers: { 'X-API-Key': 'sqa-key-12345' } })).status()).toBe(200);
  expect((await request.get(url, { headers: { 'X-API-Key': 'wrong-key' } })).status()).toBe(403);
  expect((await request.get(url)).status()).toBe(401);
});

test('Bearer token: login then call a protected endpoint', async ({ request }) => {
  const login = await request.post(`${BASE}/auth/login`, {
    data: { username: 'admin', password: 'admin@123' },
  });
  expect(login.status()).toBe(200);
  const { access_token, token_type, expires_in } = await login.json();
  expect(token_type).toBe('Bearer');
  expect(expires_in).toBe(900);

  const me = await request.get(`${BASE}/auth/me`, { headers: { Authorization: `Bearer ${access_token}` } });
  expect((await me.json()).data).toMatchObject({ username: 'admin', role: 'admin', can_write: true });
});

Playwright also supports Basic auth globally through httpCredentials in the config, and you’ll learn to share a login token across tests with fixtures in Module 6.

Common Authentication Bugs

  • Different error messages for “unknown user” and “wrong password” (lets attackers find valid usernames).
  • No rate limit on login (brute force), or on OTP verification.
  • Tokens that never expire, or that still work after logout or password change.
  • Tokens or keys in URLs, logs or error messages.
  • Endpoints that someone forgot to protect (often new or “internal” ones). Call every endpoint with no credentials.
  • 500 errors for malformed Authorization headers.
Advertisement

Authentication Testing Checklist

  1. Every protected endpoint returns 401 without credentials.
  2. Invalid, malformed, expired and revoked credentials return 401.
  3. 401 responses include WWW-Authenticate.
  4. Error messages don’t reveal whether the user exists.
  5. Login and key validation are rate limited.
  6. Credentials travel only over HTTPS and only in headers.
  7. Secrets aren’t logged, cached or returned.
  8. Logout and password change invalidate tokens.
Advertisement

Interview Questions: API Authentication

What is the difference between authentication and authorization?

Authentication verifies who the caller is (login, API key, token). Authorization decides what the authenticated caller may do (roles, permissions, ownership). Authentication failures return 401; authorization failures return 403 or sometimes 404.

What types of API authentication have you tested?

A strong answer lists the types with specifics: Basic auth for an internal tool, API keys for a partner integration, JWT Bearer tokens with refresh for a mobile backend, and OAuth 2.0 client credentials between services, plus the negative cases you covered.

Is Basic authentication secure?

Only over HTTPS and only for limited cases. Base64 is encoding, not encryption, so the credentials are readable by anyone who intercepts the request. It also sends the real password on every call and has no expiry.

What is the difference between an API key and a Bearer token?

An API key identifies an application, is usually long-lived and is issued once. A Bearer token represents an authenticated user session, is short-lived, is obtained by logging in and can carry claims such as role and expiry.

What is OAuth 2.0?

An authorization framework that lets a user grant an application limited, scoped access to their resources on another service through an authorization server, without sharing their password. The app receives access tokens and optionally refresh tokens.

What is the difference between OAuth 2.0 and OpenID Connect?

OAuth 2.0 is about authorization (access tokens for APIs). OpenID Connect is an identity layer on top of OAuth 2.0 that adds an ID token so the app knows who the user is; it powers “Login with Google”.

Which OAuth flow is used for machine-to-machine communication?

The Client Credentials grant: the service authenticates with its client id and secret and receives an access token, with no user involved.

Why should API keys not be sent as query parameters?

URLs are stored in server logs, proxy logs, browser history and analytics tools, so keys in query strings can leak. Headers are the safer place.

What should happen when a protected endpoint is called without a token?

It should return 401 Unauthorized with a WWW-Authenticate header indicating the expected scheme, and no data.

How do you test a login API for security?

Check the same generic error for wrong user and wrong password, rate limiting and lockout, HTTPS only, no password in responses or logs, token expiry, token invalidation on logout and password change, and that SQL injection or very long input doesn’t cause errors.

Advertisement
View the full API testing syllabus

HTTP vs HTTPS, Cookies, Sessions and CORS for API Testers

HTTP vs HTTPS, Cookies, Sessions and CORS for API Testers

Three topics confuse new API testers more than anything else: why HTTPS matters when “the API works fine on HTTP”, how a website remembers you are logged in (cookies, sessions or tokens), and the famous red “blocked by CORS policy” error that appears in the browser while the same call works perfectly in Postman. This lesson explains all three with everyday examples and gives you a practical test checklist for each.

Quick answerHTTPS is HTTP encrypted with TLS: it hides data from anyone on the network, proves the server’s identity with a certificate and prevents tampering. Cookies are small values the server asks the browser to store and send back automatically; they often hold a session id, while APIs for mobile apps more often use tokens (JWT) in the Authorization header. CORS (Cross-Origin Resource Sharing) is a browser security rule: a web page can call an API on another origin only if the API returns headers such as Access-Control-Allow-Origin allowing it. Postman and Playwright’s request client are not browsers, so CORS never blocks them.
Advertisement

HTTP vs HTTPS (The Postcard vs Sealed Envelope Example)

Real-life example Sending data over HTTP is like sending a postcard: every post office worker who handles it can read it and even change it. HTTPS is a sealed, tamper-proof envelope with the receiver’s verified ID: only the real bank can open it, and if someone tampers with it, the seal breaks. On public café Wi-Fi, an HTTP login request exposes your password to anyone running a packet sniffer.
HTTPHTTPS
EncryptionNone (plain text)TLS encryption
Default port80443
Server identityNot verifiedVerified by a certificate from a trusted authority
Data integrityCan be modified in transitTampering is detected
Browser display“Not secure” warningPadlock
Required forNothing sensitiveLogins, payments, personal data, HTTP/2 in browsers, PWAs

How HTTPS works (simplified TLS handshake)

  1. The client says hello and lists the TLS versions and ciphers it supports.
  2. The server replies with its certificate (containing its public key) signed by a Certificate Authority such as Let’s Encrypt or DigiCert.
  3. The client checks the certificate: trusted issuer, not expired, matches the domain name.
  4. Both sides agree on a shared secret session key.
  5. All HTTP data is now encrypted with that key.

The request and response inside are exactly the same as HTTP; only the transport is protected. That’s why everything you learned about methods, headers and status codes applies unchanged.

Advertisement

How to Test HTTPS for APIs

CheckHowExpected
HTTP is redirected or refusedcurl -i http://api.example.com/v1/users301/308 to HTTPS, or connection refused; never data over HTTP
Certificate validBrowser padlock, curl -v, SSL LabsTrusted CA, correct domain, not expired
Certificate expiry monitoredCheck expiry dateAlert at least 30 days before
Old protocols disabledSSL Labs / nmap --script ssl-enum-ciphersOnly TLS 1.2 and 1.3
HSTSResponse headerStrict-Transport-Security: max-age=31536000
No mixed contentResponses with linksAll URLs in responses are https://
Self-signed certs in test envsClient configTests don’t disable TLS checks in production code
Real incident pattern Many big outages have been caused by an expired TLS certificate: the API is fine, but every mobile app suddenly fails with “connection not secure”. Certificate expiry monitoring belongs in the QA and DevOps checklist.
# See the TLS handshake and certificate details
curl -v https://sqatools.in/wp-json/sqa-api/v1/health 2>&1 | grep -E "SSL connection|subject|expire|issuer"

Cookies Explained for API Testers

A cookie is a small name-value pair that the server sets with a Set-Cookie response header. The browser stores it and automatically sends it back in a Cookie request header on every later request to that site.

HTTP/1.1 200 OK
Set-Cookie: session_id=8f2a9c1e77; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800

GET /account HTTP/1.1
Cookie: session_id=8f2a9c1e77
Real-life example: cloakroom token At a wedding hall cloakroom you hand over your bag and get token #47. You don’t carry the bag around; you show #47 when you return. A session cookie is that token: the server keeps your data (cart, login state) and the cookie just says “I’m #47”.
Advertisement

Cookie attributes you must test

AttributeMeaningSecurity expectation for session cookies
SecureSent only over HTTPSMust be set
HttpOnlyJavaScript can’t read itMust be set (protects against XSS stealing sessions)
SameSiteStrict, Lax or None: sent on cross-site requests or notLax or Strict (protects against CSRF); None requires Secure
Expires / Max-AgeLifetimeShort for sessions; none = deleted when browser closes
Domain / PathWhere it is sentAs narrow as possible

Sessions vs Tokens (JWT)

Session (cookie-based)Token (e.g. JWT Bearer)
Where state livesServer stores the sessionToken itself carries the claims (stateless)
How it’s sentCookie header, automatically by the browserAuthorization: Bearer ..., added by the app
Best forClassic server-rendered websitesMobile apps, SPAs, microservices, third-party APIs
LogoutDelete the server session: instantNeeds short expiry, a deny-list or refresh-token revocation
Main riskCSRF (fixed with SameSite + CSRF tokens)Token theft from storage; long expiry

The practice API supports token login with refresh and logout, which you will test in detail in the JWT lesson of Module 3:

POSThttps://sqatools.in/wp-json/sqa-api/v1/auth/login
curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin@123"}' \
  "https://sqatools.in/wp-json/sqa-api/v1/auth/login"

These are public practice credentials published on the API practice page; never use real credentials on practice systems.

Session and token test cases

  • After logout, the old session id or token is rejected (401).
  • Session or token expires after the configured idle and absolute timeouts.
  • A new session id is issued after login (prevents session fixation).
  • Session ids are long and random, not sequential.
  • Changing the password invalidates other sessions.
  • Tokens never appear in URLs or logs.
Advertisement

What Is CORS? (And Why Postman Works When the Browser Fails)

Browsers follow the Same-Origin Policy: JavaScript on one origin (scheme + host + port) can’t read responses from another origin unless that server explicitly allows it. CORS is the mechanism a server uses to say “these other origins may read my responses”.

Page originAPI URLSame origin?
https://shop.comhttps://shop.com/api/cartYes
https://shop.comhttps://api.shop.com/cartNo (different host)
https://shop.comhttp://shop.com/apiNo (different scheme)
http://localhost:3000http://localhost:8080/apiNo (different port)
Real-life example: the apartment security desk Your friend (a web page from another origin) asks the security desk of your building (the browser) to collect a parcel from your flat (the API). The guard only hands it over if your flat has left a note saying “my friend from shop.com may collect” (Access-Control-Allow-Origin: https://shop.com). Postman is not a guard: it walks straight in. That’s why a call that works in Postman can fail in the browser.

Simple vs pre-flighted requests

For “non-simple” requests (PUT, DELETE, PATCH, custom headers like Authorization, or JSON content type), the browser first sends an automatic OPTIONS pre-flight:

OPTIONS /api/cart HTTP/1.1
Origin: https://shop.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization, content-type

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://shop.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600

Only if the pre-flight approves does the browser send the real DELETE.

Advertisement

CORS response headers

HeaderPurpose
Access-Control-Allow-OriginWhich origin may read the response (one origin or *)
Access-Control-Allow-MethodsAllowed methods for pre-flighted requests
Access-Control-Allow-HeadersAllowed custom request headers
Access-Control-Allow-CredentialsWhether cookies/auth may be included
Access-Control-Expose-HeadersWhich response headers JavaScript may read (e.g. Location, ETag)
Access-Control-Max-AgeHow long the pre-flight result can be cached

The SQA Tools practice API is CORS-enabled and exposes headers such as Location, ETag, Retry-After and X-Total-Count, which is why the “Send request” boxes in these lessons can show them.

CORS test cases

  • Allowed origin: response contains the matching Access-Control-Allow-Origin.
  • Unknown origin (e.g. https://evil.example): no allow-origin header for it.
  • Security bug: the server reflects any Origin back and sends Allow-Credentials: true. Any website can then read logged-in users’ data.
  • Access-Control-Allow-Origin: * combined with credentials is invalid and must not be used.
  • Pre-flight OPTIONS returns 2xx with the right methods and headers.
  • Headers the frontend needs (Location, ETag, pagination) are exposed.
# Simulate a browser pre-flight from another origin
curl -i -X OPTIONS "https://api.example.com/v1/cart" \
  -H "Origin: https://evil.example" \
  -H "Access-Control-Request-Method: DELETE"
# PASS if the response does NOT allow https://evil.example
Advertisement

Playwright: Checking Cookies, HTTPS and CORS Headers

import { test, expect } from '@playwright/test';

test('API is served only over HTTPS', async ({ request }) => {
  const res = await request.get('https://sqatools.in/wp-json/sqa-api/v1/health');
  expect(res.url()).toMatch(/^https:\/\//);
  expect(res.ok()).toBeTruthy();
});

test('session cookie has secure attributes', async ({ request }) => {
  // Replace with your application's login endpoint
  const res = await request.post('https://your-app.test/api/login', {
    data: { username: 'qa_user', password: process.env.QA_PASSWORD },
  });
  const setCookie = res.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie');
  const session = setCookie.find(h => h.value.startsWith('session_id='));
  expect(session?.value).toContain('Secure');
  expect(session?.value).toContain('HttpOnly');
  expect(session?.value).toMatch(/SameSite=(Lax|Strict)/i);
});

test('CORS does not trust random origins', async ({ request }) => {
  const res = await request.fetch('https://your-app.test/api/cart', {
    method: 'OPTIONS',
    headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'DELETE' },
  });
  expect(res.headers()['access-control-allow-origin']).not.toBe('https://evil.example');
});

Use headersArray() for Set-Cookie because a response can contain several cookies with the same header name. Keep real passwords in environment variables, never in test code.

Common Mistakes

  • Reporting a CORS error as an “API down” bug. Check the browser console and the response headers first; the API usually works in Postman.
  • “Fixing” CORS with Access-Control-Allow-Origin: * on an API that uses cookies.
  • Disabling certificate checks (ignoreHTTPSErrors, curl -k) in production tests.
  • Storing JWTs in localStorage without considering XSS risk.
  • Not testing what happens after logout or expiry.
Advertisement

Interview Questions: HTTPS, Cookies and CORS

What is the difference between HTTP and HTTPS?

HTTPS is HTTP over TLS. It encrypts data in transit, verifies the server’s identity with a certificate and protects integrity against tampering. HTTP sends everything in plain text. HTTPS uses port 443 by default, HTTP uses port 80.

What is a TLS certificate and what do you test about it?

A digital certificate issued by a trusted Certificate Authority that binds a public key to a domain. Testers check that it is trusted, matches the domain, is not expired or close to expiry, and that only modern TLS versions (1.2 and 1.3) are enabled.

What is CORS?

Cross-Origin Resource Sharing is a browser mechanism that lets a server allow web pages from other origins to read its responses, using headers like Access-Control-Allow-Origin. Without it, the browser’s Same-Origin Policy blocks cross-origin reads.

Why does an API call work in Postman but fail in the browser with a CORS error?

CORS is enforced only by browsers. Postman, cURL and server-side clients don’t apply the Same-Origin Policy. The fix is on the server: return the correct CORS headers for the allowed origins.

What is a pre-flight request?

An automatic OPTIONS request the browser sends before a non-simple cross-origin request (for example PUT, DELETE or one with an Authorization header) to ask whether the method, headers and origin are allowed.

What are HttpOnly, Secure and SameSite cookie attributes?

HttpOnly stops JavaScript from reading the cookie, reducing XSS session theft. Secure sends the cookie only over HTTPS. SameSite controls whether the cookie is sent on cross-site requests, protecting against CSRF.

What is the difference between session-based and token-based authentication?

Session-based auth stores state on the server and identifies the user with a session id cookie. Token-based auth gives the client a signed token (often a JWT) that carries the claims and is sent in the Authorization header; the server can validate it without storing a session.

What CORS misconfiguration is a security risk?

Reflecting any Origin header back in Access-Control-Allow-Origin while also sending Access-Control-Allow-Credentials: true. It lets any malicious website make authenticated requests and read the victim’s data.

What is HSTS?

HTTP Strict Transport Security is a response header that tells browsers to use only HTTPS for the domain for a set time, preventing downgrade and SSL-stripping attacks.

How do you test session management in APIs?

Verify the session or token is rejected after logout and expiry, a new id is issued at login, ids are random, cookies have Secure, HttpOnly and SameSite, password change invalidates other sessions and tokens are never exposed in URLs or logs.

Advertisement
View the full API testing syllabus

JSON, XML and JSON Schema Validation for API Testers

JSON, XML and JSON Schema Validation for API Testers

Almost every API response you test will be JSON, and banking or SOAP systems will hand you XML. To write good assertions you must read nested data fluently, know the exact data types, and be able to prove the whole structure is correct, not just one field. In this lesson you will learn JSON and XML from a tester’s point of view, how to extract values with JSONPath and XPath, and how JSON Schema validation catches the bugs that field-by-field checks miss.

Quick answerJSON (JavaScript Object Notation) is a lightweight text format of key-value pairs with six data types: string, number, boolean, null, object and array. XML is a tag-based format used by SOAP and older enterprise systems. JSON Schema is a JSON document that describes the expected structure of JSON data (required fields, types, formats, ranges and allowed values). Testers validate responses against a schema to catch missing fields, wrong types and unexpected extra data automatically.
Advertisement

JSON Basics in 5 Minutes

Here is a real response from the practice API: GET /orders/5001.

{
  "data": {
    "id": 5001,
    "user_id": 1,
    "status": "delivered",
    "ordered_at": "2025-09-02T11:00:00Z",
    "items": [
      { "product_id": 101, "name": "Wireless Mouse", "quantity": 2, "unit_price": 799, "line_total": 1598 },
      { "product_id": 108, "name": "Python Testing eBook", "quantity": 1, "unit_price": 349, "line_total": 349 }
    ],
    "summary": { "item_count": 2, "subtotal": 1947, "tax": 350.46, "total": 2297.46, "currency": "INR" },
    "shipping": { "city": "Pune", "pincode": "411001", "country": "IN" }
  }
}
JSON typeExample from this responseTester’s note
String"delivered", "411001"Always double quotes. Pincode is a string (keeps leading zeros)
Number799, 350.46No quotes; integer and decimal are both “number”
Booleantrue / falseLower case, no quotes. "true" is a string, a common bug
Nullnull“No value”. Different from missing key and from ""
Object"summary": { ... }Unordered key-value pairs
Array"items": [ ... ]Ordered list; may be empty []
Real bug: “1947” vs 1947 After a backend upgrade, subtotal started coming back as the string "1947". The website still displayed it fine, but the Android app added the coupon discount with string concatenation and showed a subtotal of “1947100”. Only a type check (or schema validation) catches this kind of bug.

Reading Nested JSON: Paths

To assert a value you need its path. In JavaScript/TypeScript (Playwright) and in JSONPath:

ValueTypeScriptJSONPath
Order statusbody.data.status$.data.status
First item namebody.data.items[0].name$.data.items[0].name
All item namesbody.data.items.map(i => i.name)$.data.items[*].name
Grand totalbody.data.summary.total$.data.summary.total
Items costing over 500items.filter(i => i.unit_price > 500)$.data.items[?(@.unit_price > 500)]

Postman also uses this JavaScript syntax inside its test scripts: pm.response.json().data.status.

Advertisement

Assert Business Logic, Not Only Values

Good testers recompute numbers from the response itself:

  • Each line_total = quantity x unit_price (2 x 799 = 1598 ✔).
  • subtotal = sum of line totals (1598 + 349 = 1947 ✔).
  • tax = 18% of subtotal (1947 x 0.18 = 350.46 ✔).
  • total = subtotal + tax (2297.46 ✔).
  • item_count = length of items (2 ✔).
GEThttps://sqatools.in/wp-json/sqa-api/v1/orders/5001
curl -i \
  "https://sqatools.in/wp-json/sqa-api/v1/orders/5001"
Decimal trap In JavaScript, 0.1 + 0.2 === 0.30000000000000004. Compare money with a tolerance (toBeCloseTo(2297.46, 2)) or in paise as integers, otherwise your test fails for the wrong reason.

XML Basics for API Testers

Ask the practice API for XML using the Accept header:

GEThttps://sqatools.in/wp-json/sqa-api/v1/users/1
curl -i \
  -H "Accept: application/xml" \
  "https://sqatools.in/wp-json/sqa-api/v1/users/1"
<?xml version="1.0" encoding="UTF-8"?>
<response>
  <data>
    <id>1</id>
    <name>Asha Verma</name>
    <role>admin</role>
    <active>true</active>
  </data>
</response>

XML key concepts: elements (<name>), attributes (<price currency="INR">), namespaces (xmlns:soap="...", essential in SOAP), and every value is text until a schema (XSD) gives it a type.

XPath: extracting values from XML

GoalXPath
User name/response/data/name
Name anywhere in the document//name
Text value//name/text()
Element with an attribute//price[@currency='INR']
SOAP body ignoring namespaces//*[local-name()='AddResult']

SoapUI’s XPath Match assertion uses exactly these expressions; you will use them in the SoapUI lessons.

Advertisement

JSON vs XML

JSONXML
ReadabilityCompact, easy to readVerbose (opening and closing tags)
Data typesBuilt in (number, boolean, null…)Everything is text; types come from XSD
ArraysNative [ ]Repeated elements
Attributes / namespacesNoYes
CommentsNot allowedAllowed
SchemaJSON SchemaXSD
Query languageJSONPathXPath, XQuery
Used inREST, GraphQL, most modern APIsSOAP, banking, insurance, legacy systems, RSS

What Is JSON Schema?

A JSON Schema is a contract for JSON data. Instead of writing 30 assertions for 30 fields, you validate the whole response against one schema. The practice API publishes its schema for users:

GEThttps://sqatools.in/wp-json/sqa-api/v1/schema/user
curl -i \
  "https://sqatools.in/wp-json/sqa-api/v1/schema/user"
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "user",
  "type": "object",
  "required": ["id", "name", "username", "email", "role", "city", "age", "active", "created_at"],
  "properties": {
    "id":         { "type": "integer", "minimum": 1 },
    "name":       { "type": "string" },
    "email":      { "type": "string", "format": "email" },
    "role":       { "enum": ["admin", "editor", "viewer"] },
    "age":        { "type": "integer", "minimum": 18 },
    "active":     { "type": "boolean" },
    "created_at": { "type": "string", "format": "date-time" }
  },
  "additionalProperties": false
}

Important JSON Schema keywords

KeywordMeaningBug it catches
typestring, number, integer, boolean, object, array, null“1947” instead of 1947
requiredKeys that must existField silently removed
enumAllowed valuesNew status “dispatched” the app doesn’t know
formatemail, date-time, uri, uuid…Date returned as “12/11/2026”
minimum / maximumNumber rangeNegative price, age 0
minLength / maxLength / patternString rulesPhone with letters
items, minItemsArray content and sizeOrder with zero items
additionalProperties: falseNo unknown fieldsLeaked password_hash or internal fields
nullable / "type": ["string","null"]Allow nullUnexpected null crashing the app
Advertisement

JSON Schema Validation in Playwright (with Ajv)

Install the most popular validator: npm i -D ajv ajv-formats.

import { test, expect } from '@playwright/test';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

test('every user matches the published JSON Schema', async ({ request }) => {
  // 1. Get the contract from the API itself
  const schema = await (await request.get(`${BASE}/schema/user`)).json();
  const validate = ajv.compile(schema);

  // 2. Validate all 12 users
  const users = (await (await request.get(`${BASE}/users`, { params: { per_page: 50 } })).json()).data;
  for (const user of users) {
    const ok = validate(user);
    expect(ok, JSON.stringify(validate.errors, null, 2)).toBe(true);
  }
});

test('order totals are internally consistent', async ({ request }) => {
  const { data: order } = await (await request.get(`${BASE}/orders/5001`)).json();

  for (const item of order.items) {
    expect(item.line_total).toBe(item.quantity * item.unit_price);
  }
  const subtotal = order.items.reduce((sum: number, i: any) => sum + i.line_total, 0);
  expect(order.summary.subtotal).toBe(subtotal);
  expect(order.summary.tax).toBeCloseTo(subtotal * 0.18, 2);
  expect(order.summary.total).toBeCloseTo(subtotal + order.summary.tax, 2);
  expect(typeof order.shipping.pincode).toBe('string');
});

When validation fails, Ajv tells you exactly what broke, for example /age must be integer or must NOT have additional properties: password_hash. TypeScript teams also use Zod schemas, which double as TypeScript types; you will use both in Module 6.

Advertisement

JSON Testing Checklist

  • Response body is valid JSON (parsing doesn’t throw), even for errors.
  • Correct data types: numbers aren’t strings, booleans aren’t “true”.
  • Required fields present; no unexpected or sensitive fields.
  • Null vs missing vs empty string behave as documented.
  • Dates in one consistent format (ISO 8601 with time zone, e.g. 2025-09-02T11:00:00Z).
  • Arrays: empty list returns [], not null.
  • Calculated fields are correct; money compared safely.
  • Unicode (Hindi names, emojis) survives a round trip.
  • Malformed JSON in a request returns 400, not 500.

Try the last check now. This request sends broken JSON (the body stops halfway), and a well-built API must answer 400 with a clear message:

POSThttps://sqatools.in/wp-json/sqa-api/v1/validate/user
curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"name": "Asha", "email": ' \
  "https://sqatools.in/wp-json/sqa-api/v1/validate/user"
Advertisement

Common Mistakes

  • Comparing a whole response with a saved copy: timestamps and ids change, so tests flake. Validate structure with a schema and assert only the stable values.
  • Using toEqual on objects when extra fields are allowed: use toMatchObject for partial matching.
  • Assuming key order: JSON objects are unordered.
  • Assuming array order when the API doesn’t promise one.
  • Ignoring additionalProperties: that’s how data leaks go unnoticed.
Advertisement

Interview Questions: JSON, XML and Schema

What are the data types supported by JSON?

String, number, boolean, null, object and array. There is no separate integer or date type in JSON itself; integers are numbers and dates are strings, which is why JSON Schema adds integer and format rules.

What is JSON Schema validation and why is it used?

It validates a JSON response against a schema that defines required fields, types, formats, ranges, enums and whether extra properties are allowed. It catches structural bugs such as missing fields, wrong types and leaked data in one assertion and is the basis of contract testing.

What is the difference between JSON and XML?

JSON is lighter, has native data types and arrays and is used by most modern REST APIs. XML is more verbose, supports attributes, namespaces and comments, uses XSD for types and XPath for queries, and is used by SOAP and many enterprise systems.

What is JSONPath?

A query language for JSON, similar to XPath for XML. For example, $.data.items[*].name returns all item names and $.data.items[?(@.unit_price > 500)] filters items by price.

What is the difference between null, an empty string and a missing key?

A missing key means the field isn’t sent at all, null means the field exists with no value, and an empty string is a value of length zero. Clients often handle them differently, so APIs must be consistent and tests should check the documented behaviour.

What does additionalProperties: false do?

It makes the schema reject any property not listed in properties. It helps detect unexpected fields, especially sensitive data such as password hashes or internal ids accidentally included in responses.

How do you validate an XML response?

Validate it against the XSD, use XPath assertions for specific values, check namespaces, and for SOAP verify there is no Fault element in positive scenarios. SoapUI provides Schema Compliance and XPath Match assertions for this.

How would you compare decimal values like prices in tests?

Use a tolerance-based comparison such as toBeCloseTo with 2 decimal places, or convert money to the smallest unit (paise) as integers, to avoid floating point precision errors.

Which library do you use for JSON Schema validation in TypeScript?

Ajv with ajv-formats is the most common choice. Zod is also popular because the same schema provides TypeScript types and runtime validation.

Why should you avoid comparing full responses with snapshots?

Dynamic values such as ids, timestamps and tokens change on every run, causing flaky failures. It’s better to validate structure with a schema and assert specific stable business values.

Advertisement
View the full API testing syllabus

Path Parameters vs Query Parameters, Pagination, Filtering and Sorting

Path Parameters vs Query Parameters, Pagination, Filtering and Sorting

Open any shopping app, search “headphones”, filter by price, sort by rating and scroll to page 3. Every one of those taps changes the parameters of an API call. Parameters are where a huge share of API bugs hide: off-by-one pagination, filters that ignore case, sorting that silently fails, and ids that let you read someone else’s data. This lesson explains path vs query parameters and gives you a complete test design for pagination, filtering, sorting and search, all runnable on the practice API.

Quick answerA path parameter is part of the URL path and identifies a specific resource, for example /users/{id} → /users/1. A query parameter comes after the ? as key=value pairs joined by & and is optional; it filters, sorts, searches or paginates a collection, for example /users?role=admin&page=2. Use path parameters for which resource and query parameters for how to view a collection.
Advertisement

Path vs Query Parameters (with a Library Example)

Real-life example: a library “Give me book number 4521” points to one exact book: a path parameter, /books/4521. “Show me books in the Testing section, written after 2020, sorted by title, 10 per shelf” describes how to browse many books: query parameters, /books?section=testing&year_gte=2020&sort=title&per_page=10.
Path parameterQuery parameter
PositionInside the pathAfter ?, separated by &
Example/users/1/orders/users?city=Pune&active=true
Required?Yes, part of the routeUsually optional, with defaults
PurposeIdentify a resourceFilter, sort, search, paginate, select fields
Wrong valueTypically 404 (or 400 if wrong type)Typically 400 (validation)
Order matters?YesNo

There are two more places for data: headers (metadata such as auth) and the request body (the payload for POST/PUT/PATCH). Some older APIs also use matrix parameters (;color=red), but you’ll rarely see them.

Testing Path Parameters

Path parameters look simple, but they carry some of the worst bugs, including the number one API security risk.

TestRequestExpected
Valid idGET /users/1200, user 1
Non-existent idGET /users/999404 with message
Wrong typeGET /users/abc400
Boundary zeroGET /users/0400
Negative / decimal/users/-1, /users/1.5400
Huge number/users/99999999999999999999400 or 404, never 500
Special chars / injection/users/1'%20OR%201=1400, no SQL error
Another user’s id (BOLA)User A’s token on /orders/{B's order}403 or 404
Nested resourceGET /users/1/ordersOnly orders with user_id = 1
GEThttps://sqatools.in/wp-json/sqa-api/v1/users/0
curl -i \
  "https://sqatools.in/wp-json/sqa-api/v1/users/0"
Advertisement

Pagination: Why APIs Split Data into Pages

Amazon can’t return 50 million products in one response. Pagination returns a small slice at a time. The three common styles:

StyleExamplePros / consUsed by
Page-based?page=2&per_page=5Simple; can skip or repeat items if data changesAdmin panels, the practice API
Offset/limit?offset=5&limit=5Flexible; slow for large offsetsMany SQL-backed APIs
Cursor-based?cursor=eyJpZCI6MTB9&limit=5Stable and fast; can’t jump to page 7Instagram, Twitter/X feeds, Stripe

The practice API has 12 users and uses page-based pagination. It returns the page details in both the body (meta, links) and headers (X-Total-Count, X-Total-Pages, Link).

GEThttps://sqatools.in/wp-json/sqa-api/v1/users?page=2&per_page=5
curl -i \
  "https://sqatools.in/wp-json/sqa-api/v1/users?page=2&per_page=5"

Page 2 with 5 per page must start at the 6th record (id 6) and contain ids 6 to 10.

Pagination Test Cases (Boundary Value Analysis)

#ScenarioRequestExpected
1Default page size/users5 items, page 1
2Middle page?page=2&per_page=5ids 6-10
3Last page (partial)?page=3&per_page=52 items (ids 11-12)
4Beyond last page?page=10200 with empty list (not an error)
5Page 0?page=0400
6Max page size?per_page=50200
7Max + 1?per_page=51400
8Non-numeric?page=two400
9No duplicates / no gapsWalk all pages12 unique ids in total
10Metadata correctany pagetotal=12, total_pages = ceil(12 / per_page)
11Links?per_page=5Link has next/last; prev null on page 1
Real bug: the missing order An e-commerce admin exported orders page by page. Because new orders arrived during the export and the list was sorted newest first, every new order pushed one row to the next page, and one order per page was skipped. Reconciliation found Rs 4 lakh of “missing” orders. Cursor pagination fixed it. Test: insert data while paginating.
Advertisement

Filtering

Filters narrow a collection. Test each filter alone, in combination, with invalid values and with edge cases such as case sensitivity.

ScenarioRequestExpected
Single filter/users?role=adminEvery result has role admin
Case-insensitive/users?city=puneMatches “Pune”
Boolean/users?active=falseOnly inactive users
Invalid boolean/users?active=maybe400
Combined (AND)/users?role=admin&city=ChennaiBoth conditions true
Range/products?min_price=1000&max_price=5000All prices in range (inclusive?)
Invalid range?min_price=5000&max_price=100400 “min_price cannot be greater than max_price”
Enum validation/products?category=toys400 (unknown category)
Unknown parameter/users?colour=redDocumented behaviour: ignored or 400
GEThttps://sqatools.in/wp-json/sqa-api/v1/products?min_price=5000&max_price=100
curl -i \
  "https://sqatools.in/wp-json/sqa-api/v1/products?min_price=5000&max_price=100"
Always assert every item Checking only the first result of a filter is a common mistake. Loop through all returned items: one wrong record on page 3 is still a bug.
Advertisement

Sorting

ScenarioRequestExpected
Sort descending/users?sort=age&order=descAges in non-increasing order
Default sort/usersBy id ascending (as documented in meta.sort)
Unknown sort field/users?sort=salary400 (only whitelisted fields)
Invalid order/users?order=up400
Sort + pagination?sort=age&order=desc&page=2Page 2 continues the sorted order
TiesEqual agesStable secondary order (e.g. id)

Why the whitelist matters: if sort is pasted directly into SQL (ORDER BY {sort}), it is an injection point. A 400 for unknown fields is the secure behaviour.

Advertisement
  • Match: /users?q=gupta returns Manish Gupta.
  • No match: /users?q=zzz returns 200 with an empty list, not 404.
  • Partial and case: “GUP”, “gup” behave as documented.
  • Special characters: %, _, ', emojis, Hindi text (?q=%E0%A4%86%E0%A4%B6%E0%A4%BE) don’t cause 500.
  • Long input: 5,000 characters is rejected cleanly.
  • Search + filter + pagination together.

URL Encoding: The Silent Bug Source

Query values containing spaces or special characters must be URL-encoded: a space becomes %20 (or +), & becomes %26, @ becomes %40.

Real bug A search for “Tom & Jerry” was sent unencoded as ?q=Tom & Jerry. The server read q=Tom plus an empty parameter named Jerry. Always test values containing &, =, +, # and spaces.

Postman and Playwright’s params option encode values for you; hand-built URL strings don’t.

Advertisement

Automating Parameter Tests with Playwright

import { test, expect } from '@playwright/test';

const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';

test('page 2 starts at the 6th record', async ({ request }) => {
  const res = await request.get(`${BASE}/users`, { params: { page: 2, per_page: 5 } });
  const body = await res.json();
  expect(body.data.map((u: any) => u.id)).toEqual([6, 7, 8, 9, 10]);
  expect(body.meta).toMatchObject({ page: 2, per_page: 5, total: 12, total_pages: 3 });
});

test('walking all pages returns 12 unique users', async ({ request }) => {
  const ids: number[] = [];
  for (let page = 1; ; page++) {
    const body = await (await request.get(`${BASE}/users`, { params: { page, per_page: 5 } })).json();
    if (body.data.length === 0) break;
    ids.push(...body.data.map((u: any) => u.id));
  }
  expect(ids).toHaveLength(12);
  expect(new Set(ids).size).toBe(12);   // no duplicates
});

test('every filtered user is an admin', async ({ request }) => {
  const body = await (await request.get(`${BASE}/users`, { params: { role: 'admin', per_page: 50 } })).json();
  expect(body.data.length).toBeGreaterThan(0);
  for (const u of body.data) expect(u.role).toBe('admin');
});

test('sorted by age descending', async ({ request }) => {
  const body = await (await request.get(`${BASE}/users`, { params: { sort: 'age', order: 'desc', per_page: 50 } })).json();
  const ages = body.data.map((u: any) => u.age);
  expect(ages).toEqual([...ages].sort((a, b) => b - a));
});

for (const bad of ['per_page=51', 'page=0', 'active=maybe', 'sort=salary', 'order=up']) {
  test(`invalid query ${bad} -> 400`, async ({ request }) => {
    expect((await request.get(`${BASE}/users?${bad}`)).status()).toBe(400);
  });
}
Advertisement

Interview Questions: Parameters and Pagination

What is the difference between path parameters and query parameters?

Path parameters are part of the URL path and identify a specific resource, such as /users/1; they are required. Query parameters follow the question mark as key=value pairs and are usually optional; they filter, sort, search or paginate a collection, such as /users?role=admin&page=2.

How do you test pagination?

Check the default page size, first, middle and last pages, a page beyond the last (empty list), page 0 and negative pages, maximum and maximum+1 page size, non-numeric values, metadata such as total and total_pages, navigation links, no duplicates or gaps when walking all pages, and behaviour when data changes during paging.

What is the difference between offset and cursor pagination?

Offset pagination uses a numeric offset or page number; it is simple and allows jumping to any page but can skip or repeat items when data changes and gets slow for large offsets. Cursor pagination uses an opaque pointer to the last item; it is stable and fast but only supports next/previous navigation.

What should an API return for a page beyond the last page?

Typically 200 OK with an empty data array and correct metadata. Some APIs return 404, which is acceptable only if documented. A 500 is a bug.

Why should sort fields be whitelisted?

If sort values are inserted directly into database queries, they can be used for SQL injection or to sort by sensitive hidden columns. Only documented fields should be accepted; others should get 400.

What is BOLA and how do path parameters relate to it?

Broken Object Level Authorization (also called IDOR) happens when changing an id in the path, such as /orders/1001 to /orders/1002, returns another user’s data. Testers must verify that each id is checked against the logged-in user’s permissions.

What is URL encoding and why does it matter?

URL encoding converts special characters into percent codes, such as a space into %20 and & into %26, so they are not misread as URL syntax. Unencoded values can split parameters or break routing.

Should a search with no results return 404?

No. The collection exists; it is just empty for this query. The correct response is 200 with an empty list.

How do you test filters thoroughly?

Test each filter alone and combined, valid and invalid values, case sensitivity, boundaries for ranges, cross-field validation such as min greater than max, unknown parameters, and assert every returned item, not just the first one.

Where would you send a large amount of data: query parameters or the body?

In the request body. URLs have practical length limits (often around 2,000 to 8,000 characters) and are logged by servers and proxies, so large or sensitive data should not be placed in query parameters.

Advertisement
View the full API testing syllabus