How I test an API, shown through tests you can open.
Ten risks I check on any API. For each one: the tests in my public repositories that cover it,
whether they run in CI, the exact lines at a pinned commit, and where the coverage is weak or missing.
The evidence comes from three independent public projects, not from employer work: a TypeScript
suite for the RESTful Booker practice API, a JavaScript suite for the Automation Exercise demo shop
(API and web pages), and the API and security tests of a Razorpay Test Mode checkout app that I built
as the test target. The targets are practice APIs and a payment sandbox. None of them is a client system.
I gave every API test in those repositories one risk category: all 47 tests in RESTful Booker,
32 of the 37 in Razorpay (the other five check the checkout page) and 16 of the 26 in Automation
Exercise (the other ten drive the shop's web pages). The table shows where they sit. It is my tagging,
and a count says how many tests touch a risk, not how well they test it.
API tests by risk category and repository, as I tagged them
Risk
RESTful Booker
Razorpay
Automation Exercise
All
authentication
4
0
2
6
authorization
5
0
0
5
status codes
6
0
2
8
response structure
7
5
3
15
invalid input
9
7
6
22
CRUD lifecycle
9
0
1
10
read-back after write
6
0
2
8
idempotency/replay
1
4
0
5
signature/HMAC
0
10
0
10
error handling
0
6
0
6
API tests tagged
47
32
16
95
Every code link opens the file at a pinned commit: RESTful Booker a385240, Razorpay
da9e7a2, Automation Exercise 789d413. RESTful Booker's newest commit,
1e7f137, changed only its CI workflow file, so the tests are the same as at
a385240.
What I check on any API
Each item links to its block in the evidence map below.
Do failures return a controlled message without internal details, and does the service keep working afterwards?
Evidence map
One block per risk. Each names the tests that cover it, links every test to its lines at the pinned
commit, shows a short excerpt copied from the repository, and says what is weak or missing. Razorpay
tests are named by their IDs; the other two repositories have no IDs, so their tests are named by file
and line range. A badge describes one test or one gap:
Runs in CI The repository's own GitHub Actions workflow runs it. That says nothing about how strong it is.
Live gateway only It needs my original Razorpay Test Mode account, so CI never runs it.
Partly tested Some of the risk is covered; the gap is stated next to it.
Not tested No test in these repositories covers it. I say what I would test instead.
Assertion cannot fail The test passes whatever the API returns, so it proves nothing.
Authentication
Partly testedWhat I verify: a valid login returns a usable credential, and a failed login is reported as a failure, not as a success.
Runs in CIRESTful Booker · auth.spec.ts:28–35 (opens in a new tab): wrong credentials return HTTP 200 with a reason field and no token. The test pins that behaviour; the article says why I would raise it.
Not tested Expired and revoked tokens, lockout or rate limiting after repeated failures, and malformed credential headers. On a real API I would test each of those.
Not tested Roles. RESTful Booker has one admin account and a token that is either present or not, so nothing here shows that one user cannot change another user's booking, or that a lower role cannot do what an admin can.
What I would test: two accounts, each trying to read and change the other's data; every role against every endpoint; a token issued for one resource used on another.
test('PUT /booking/{id} without token should return 403', async () => {
const response = await bookingClient.updateBookingResponse(bookingId, updatedBooking, '');
expect(response.status()).toBe(403);
});
test('PUT /booking/{id} with invalid token should fail', async () => {
const response = await bookingClient.updateBookingResponse(bookingId, updatedBooking, 'invalid_token');
expect(response.status()).toBe(403);
});
Not tested Headers that belong with a status, such as Allow on a 405 or Retry-After on a 429, and the codes for rate limiting and server errors. I would assert the header together with the code, and on an API that rate-limits I would trigger the limit on purpose.
// Automation Exercise returns HTTP 200// and places the actual API error code// inside the response body.expect(response.status()).toBe(200);
// ...expect(body).toHaveProperty('responseCode');
expect(body.responseCode).toBe(405);
Not tested Schema or contract validation. Nothing checks a response against a schema, so outside the two toEqual checks a renamed, retyped or extra field would pass. I would validate each response against the API's published schema, including required fields and types.
test('POST /booking with numeric fields should maintain data types', async () => {
const response = await bookingClient.createBooking(validBooking);
expect(typeof response.booking.totalprice).toBe('number');
expect(typeof response.booking.depositpaid).toBe('boolean');
expect(typeof response.bookingid).toBe('number');
});
Assertion cannot failRazorpay · ORDER-003 (orders.spec.js:26–37) (opens in a new tab): a client-supplied amount must not change the order amount. Its assertions run only when order creation returns 200, which it does not in CI (excerpt below).
Not tested Boundary values, wrong types (a string where a number belongs), unknown fields, very large bodies and injection-style strings, each with an assertion on the rejection and its error message.
test('POST /booking with checkout before checkin is accepted by API', async () => {
const bookingWithBackwardDates = {
...validBooking,
bookingdates: {
checkin: '2025-03-20',
checkout: '2025-03-15',
},
};
const response = await bookingClient.postResponse('/booking', bookingWithBackwardDates);
expect(response.ok).toBeTruthy();
});
if (response.status() === 200) {
const body = await response.json();
// Backend ignores whatever the client sent and always uses config.js's amount/currencyexpect(body.amount).toBe(50000);
expect(body.currency).toBe('INR');
}
Runs in CIRESTful Booker · workflow.spec.ts:49–84 (opens in a new tab): create two bookings, find both in the list, update and delete one, and confirm the other still exists.
Runs in CIAutomation Exercise · account.api.spec.js:17–62 (opens in a new tab): a fixture creates a fresh account, the test reads it back by email, and the fixture deletes it afterwards.
Partly testedRESTful Booker · cleanup. The workflow tests delete their bookings only after their other assertions, so a failed assertion earlier in the test skips the delete. Only six of the 47 tests delete a booking at all.
Not tested Two clients updating the same record at once, and filtering or paging the list. I would update one record from two sessions and check which write wins and what each response says, and request a filtered, paged list and check its contents against what I created.
// Delete first bookingawait bookingClient.deleteBooking(bookingId1, token);
// Verify first is deleted, second still existsconst deleteCheck = await bookingClient.getBookingResponse(bookingId1);
expect(deleteCheck.status()).toBe(404);
const stillExists = await bookingClient.getBooking(bookingId2);
expect(stillExists.firstname).toBe(validBooking.firstname);
// Cleanup: delete second bookingawait bookingClient.deleteBooking(bookingId2, token);
Partly testedRESTful Booker · booking-delete.spec.ts:20–26 (opens in a new tab): the delete test asserts only that the response body is truthy. A rejected delete returns 403 with the body “Forbidden”, which is also truthy; I checked that body on 22 October 2026. The read-back test above would still catch a delete that did nothing.
Not tested Two identical requests in flight at the same moment, and idempotency keys on create requests. I would send concurrent duplicates and check that exactly one record or charge results.
Live gateway onlyRazorpay · SIG-001 (signature-verification.spec.js:37–56) (opens in a new tab): payment verification accepts a correct signature for a real captured payment. It needs the original Test Mode account and that payment, so in CI payment verification is only tested for rejections, never for acceptance.
Not tested A signature made with an old or rotated secret, and a signature header of the wrong length or encoding. I would send each and expect a 401, not a server error.
Not tested A slow or failing dependency, 5xx bodies, and the error bodies of RESTful Booker and Automation Exercise. I would simulate the dependency failing and assert the status, the message, and that nothing was half-written.
// Should fail at signature validationexpect(response.status()).toBe(401);
const body = await response.json();
const responseString = JSON.stringify(body);
// Verify no stack trace or internal detailsexpect(responseString).not.toContain('at ');
expect(responseString).not.toContain('Error');
expect(responseString).not.toContain('stack');
expect(responseString).not.toContain('/routes/');
Requests go through client classes, not inline calls. In RESTful Booker a base class builds the URL
and headers, and one class per resource gives each call a name, so a test reads
bookingClient.createBooking(validBooking) and a header change is made in one place.
Automation Exercise uses the same idea in plain JavaScript, one class per area
(AccountApi.js (opens in a new tab)).
Each HTTP verb has two helpers. The parsed ones return the body; the …Response
ones return the raw response, so a test can assert the status. The weakness is that the parsed
helpers never look at the status. The delete helper below returns the text of any reply,
so a 403 comes back as the string “Forbidden”, and that is why the truthy check in the delete
test proves nothing. The article walks through the same
split for get.
Each test creates the data it needs instead of sharing records. In Automation Exercise a fixture
generates a unique account, creates it through the API, hands it to the test and deletes it in its
teardown, which Playwright runs even when the test fails. RESTful Booker creates a booking per test,
but its read, update and partial-update files do it in beforeEach and never delete it.
The Razorpay webhook tests that expect an event to be accepted build a unique event ID with Date.now(),
because processed IDs are stored and a fixed ID would be “already processed” on the next run
(webhook-validation.spec.js, lines 43–47 (opens in a new tab)).
A write's own response says what the server claims to have done; a separate GET says what it stored.
I treat the second as the evidence. The read-back block above lists where
these repositories do it, and the delete test shows what happens when a test trusts the write alone.
Retries, workers and traces
Each setting changes how much a green run means. RESTful Booker allows one retry in CI and runs one
worker there (playwright.config.ts, lines 7–8 (opens in a new tab)).
Razorpay allows two retries in CI with one worker, and its CI command runs Chromium only and leaves
out the live tests (playwright.config.js, lines 7–8 (opens in a new tab);
package.json, line 13 (opens in a new tab)).
Automation Exercise allows one retry everywhere, not only in CI. All three keep a trace only on the
first retry, so a run that needed no retry leaves no trace to open. A test that fails and then passes
on retry still counts as passed, so I read a green run together with its retry setting.
All of this is true of the published code at the commits above.
No schema or contract validation. Responses are checked field by field, and four assertions in RESTful Booker compare a whole booking with toEqual. Nothing validates a response against a schema.
No performance or load testing. None of the three repositories asserts a response time or sends load.
No role-based authorization. The only access control tested is a token being present, absent or invalid. No test uses more than one role, or one user's token on another user's data.
Cleanup is missing in most RESTful Booker tests. Only six of the 47 delete what they create, so the suite leaves bookings on a shared public service.
Some tests cannot fail. Three RESTful Booker negative tests (lines 35, 48 and 61 of negative-testing.spec.ts) pass whatever the API returns, and Razorpay's ORDER-001 and ORDER-003 run no assertions in CI. The counts on this page include them.
Payment verification is never accepted in CI. SIG-001 and REPLAY-001 need my original Razorpay Test Mode account, so CI only tests that payment verification rejects bad input.
Practice APIs and a sandbox only. RESTful Booker and Automation Exercise are public services that other people also use, and their behaviour and uptime are outside my control. The Razorpay tests run against my own app in Test Mode, so a defect found there is a bug in my code, not in Razorpay. None of it is a production system.
CI results are historical. The last runs were on 22 October 2026 for RESTful Booker (run #3 (opens in a new tab); by my reading of the job log, no test was retried), 14 September 2026 for Razorpay (run #3 (opens in a new tab)) and 29 August 2026 for Automation Exercise (run #17 (opens in a new tab), with retries on). Each passed when it ran; that says nothing about today. Job logs need a GitHub sign-in.
The categories are mine. Another tester could file some of these tests under a different risk.
Professional work
In my job I test six client projects covering donation and payment platforms, CRM and role-based
access, and logistics, with API checks in Postman. I describe that work only in general terms, on
the About page and the home page, and nothing on this page comes
from it. It includes role-based access control on a CRM platform, which is the kind of authorization
testing these public repositories cannot show.
Next steps
Razorpay case studyPayment scenarios, the two defects in my own app, and how CI is kept away from real credentials.