APIs and integrations

30 REST API test cases, from status codes to broken object level authorization

Thirty test cases for a REST API — status codes, pagination, idempotency, concurrency and the authorization checks that are the most exploited API flaw there is.

  • 30 cases
  • 7 coverage types
  • Last verified Sep 16, 2026

Most API test suites are a list of happy paths with the status codes written down next to them. That catches the endpoint being broken. It does not catch the endpoint being wrong.

The two that matter most are both about identity. An endpoint that checks you are signed in but not that the record belongs to you is the most exploited API flaw there is, and it passes every functional test because the functional tests only ever use your own data. The other is the write endpoint that accepts any field you send it, including the one that sets your role.

Thirty cases below, written against a collection resource. Substitute your own nouns.

Type
Priority

Showing 30 of 30 cases

30 test cases for Test cases for a REST API
IDTest caseTypePriorityPreconditionsStepsTest dataExpected result
API-01Create a resource returns 201 with a Location headerFunctionalHighAuthenticated as a user who may create
  1. POST a valid body to the collection
a valid payload201, a Location header pointing at the new resource, and the body of the created record.
API-02Read a resource returns 200 with the stored valuesFunctionalHighThe resource exists and belongs to the caller
  1. GET the resource by id
200, and every field matches what was written.
API-03Update applies only the fields sentFunctionalHighThe resource exists
  1. PATCH one field
one field200, that field changed, and no other field was reset to a default.
API-04Delete returns 204 and the resource is goneFunctionalHighThe resource exists
  1. DELETE it
  2. GET it again
204 with an empty body, then 404 on the read.
API-05Deleting twice is safeFunctionalMediumThe resource was already deleted
  1. DELETE the same id again
404 or 204 consistently, per the documented contract. Never a 500.
API-06The list endpoint paginatesFunctionalHighMore records exist than one page holds
  1. GET the collection
  2. Follow the next link
A bounded page, a total or a next cursor, and no duplicate or skipped record across pages.
API-07Filtering and sorting work togetherFunctionalMediumMixed records exist
  1. GET the collection with a filter and a sort
status and created dateOnly matching records, in the requested order, and the total reflects the filter.
API-08A conditional GET returns 304FunctionalLowThe resource has an ETag
  1. GET with If-None-Match set to the current ETag
304 with no body.
API-09Malformed JSON returns 400NegativeHigh
  1. POST a body with a trailing comma
broken JSON400 with a machine-readable error. Not a 500 and not a parser stack trace.
API-10An unknown id returns 404NegativeHigh
  1. GET an id that does not exist
a valid but unused id404 with the same shape of error body as every other error.
API-11The wrong method returns 405NegativeMedium
  1. Send PUT to a collection that only accepts GET and POST
405 with an Allow header listing what is accepted.
API-12The wrong content type returns 415NegativeMedium
  1. POST a valid body as text/plain
415. The body is not parsed on a guess.
API-13A validation failure names the fieldNegativeHigh
  1. POST a body missing a required field
missing name422 or 400 with the offending field named, not a prose sentence a client cannot parse.
API-14A duplicate unique value returns 409NegativeMediumA record with that unique value exists
  1. POST the same unique value again
an existing slug409, and no second record is created.
API-15No token returns 401SecurityHigh
  1. Call a protected endpoint with no Authorization header
401 with a WWW-Authenticate header. Never 403, and never data.
API-16An expired token returns 401SecurityHighA token past its expiry
  1. Call a protected endpoint with it
401. The expiry is checked server-side, not trusted from the payload.
API-17Another tenant's record returns 404 or 403SecurityHighTwo accounts, each with a record
  1. Authenticate as account A
  2. GET account B's record by id
B's resource idAccess refused. This is broken object level authorization and it is the single most exploited API flaw.
API-18Guessing sequential ids reveals nothingSecurityHighRecords use sequential ids
  1. Iterate ids either side of your own
id minus one, id plus oneEvery id outside the caller's scope is refused identically, so the response cannot be used to count records.
API-19Extra fields in the body are ignoredSecurityHigh
  1. PATCH your own record with an added role field
role=admin200 and the role is unchanged, or 400 for the unknown field. It is never bound.
API-20A read-only field cannot be writtenSecurityMedium
  1. PATCH the created date or the owner id
a new owner idRefused or silently ignored per the contract, and the stored value is unchanged.
API-21Rate limiting returns 429 with Retry-AfterSecurityHighThe documented limit is known
  1. Exceed the limit in a burst
429, a Retry-After header, and normal service once the window passes.
API-22Errors never leak internalsSecurityHigh
  1. Force an error with a malformed value
  2. Read the body
an oversized integerNo stack trace, SQL fragment, file path, framework name or version in the response.
API-23Page size is cappedBoundaryMediumMany records exist
  1. Request a page size far above the documented maximum
limit=100000The cap is applied and stated in the response, or 400. The database is not asked for everything.
API-24An oversized payload returns 413BoundaryMedium
  1. POST a body larger than the documented limit
20MB body413, and the connection is not held open while the whole body is read.
API-25Unicode survives a round tripBoundaryMedium
  1. POST a field with emoji and non-Latin script
  2. Read it back
an emoji and Japanese textThe value returns byte for byte, with the correct content type and charset.
API-26A retried request with an idempotency key creates one recordData integrityHighThe endpoint documents idempotency keys
  1. POST with a key
  2. Repeat the identical request with the same key
the same key twiceOne record exists, and the second call returns the first result rather than a duplicate.
API-27A concurrent update is detectedData integrityHighTwo clients hold the same ETag
  1. Client A updates with If-Match
  2. Client B updates with the now-stale ETag
B receives 412 rather than silently overwriting A. Last write does not blindly win.
API-28A failed write leaves nothing behindData integrityHighA request that fails partway
  1. POST a payload that passes validation but fails at the last step
No partial record and no orphaned child rows. The transaction rolled back.
API-29The list endpoint does not degrade with sizePerformanceMediumTen thousand records exist
  1. Time the list endpoint at page 1 and at the last page
Both are within the documented budget, and the query count does not grow with the row count.
API-30A webhook is delivered once, signed, and retried on failureIntegrationMediumA subscribed endpoint that can be made to fail
  1. Trigger the event
  2. Fail the first delivery
  3. Observe retries
The payload carries a verifiable signature, retries back off, and a successful retry does not duplicate the effect.

Case IDs are positional within this set, not stable identifiers. Import the set, then let your own tool assign its IDs.

How to use this set

These are contract cases, so they belong in the same pipeline as the code rather than in a manual pass. Everything here can be automated, and most of it in the same framework you already use.

The authorization cases only work if the fixture data includes a second account that the test user does not own. That is the part teams skip, and it is why the cases pass everywhere and the bug ships anyway.

What we deliberately left out

GraphQL, gRPC and event streams have different failure modes and deserve their own sets. So does authentication itself — issuing and refreshing the token is a separate surface from spending it.

Questions about this set

Do these assume a particular framework?

No. They are written against HTTP semantics, so they apply to anything that speaks REST.

Our API uses UUIDs, so is the id-guessing case irrelevant?

It is weaker, not irrelevant. UUIDs make enumeration impractical but they are not an authorization check, and the record still has to be refused on ownership.

What about response schema validation?

Worth doing, and better handled by a contract test generated from your OpenAPI document than by a hand-written case per field.

Somewhere to keep these once you have run them

Tesbo holds the cases, the runs and the results in one place, so the next release starts from what the last one proved.

Where this coverage comes from