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.
Showing 30 of 30 cases
| ID | Test case | Type | Priority | Preconditions | Steps | Test data | Expected result |
|---|---|---|---|---|---|---|---|
| API-01 | Create a resource returns 201 with a Location header | Functional | High | Authenticated as a user who may create |
| a valid payload | 201, a Location header pointing at the new resource, and the body of the created record. |
| API-02 | Read a resource returns 200 with the stored values | Functional | High | The resource exists and belongs to the caller |
| — | 200, and every field matches what was written. |
| API-03 | Update applies only the fields sent | Functional | High | The resource exists |
| one field | 200, that field changed, and no other field was reset to a default. |
| API-04 | Delete returns 204 and the resource is gone | Functional | High | The resource exists |
| — | 204 with an empty body, then 404 on the read. |
| API-05 | Deleting twice is safe | Functional | Medium | The resource was already deleted |
| — | 404 or 204 consistently, per the documented contract. Never a 500. |
| API-06 | The list endpoint paginates | Functional | High | More records exist than one page holds |
| — | A bounded page, a total or a next cursor, and no duplicate or skipped record across pages. |
| API-07 | Filtering and sorting work together | Functional | Medium | Mixed records exist |
| status and created date | Only matching records, in the requested order, and the total reflects the filter. |
| API-08 | A conditional GET returns 304 | Functional | Low | The resource has an ETag |
| — | 304 with no body. |
| API-09 | Malformed JSON returns 400 | Negative | High | — |
| broken JSON | 400 with a machine-readable error. Not a 500 and not a parser stack trace. |
| API-10 | An unknown id returns 404 | Negative | High | — |
| a valid but unused id | 404 with the same shape of error body as every other error. |
| API-11 | The wrong method returns 405 | Negative | Medium | — |
| — | 405 with an Allow header listing what is accepted. |
| API-12 | The wrong content type returns 415 | Negative | Medium | — |
| — | 415. The body is not parsed on a guess. |
| API-13 | A validation failure names the field | Negative | High | — |
| missing name | 422 or 400 with the offending field named, not a prose sentence a client cannot parse. |
| API-14 | A duplicate unique value returns 409 | Negative | Medium | A record with that unique value exists |
| an existing slug | 409, and no second record is created. |
| API-15 | No token returns 401 | Security | High | — |
| — | 401 with a WWW-Authenticate header. Never 403, and never data. |
| API-16 | An expired token returns 401 | Security | High | A token past its expiry |
| — | 401. The expiry is checked server-side, not trusted from the payload. |
| API-17 | Another tenant's record returns 404 or 403 | Security | High | Two accounts, each with a record |
| B's resource id | Access refused. This is broken object level authorization and it is the single most exploited API flaw. |
| API-18 | Guessing sequential ids reveals nothing | Security | High | Records use sequential ids |
| id minus one, id plus one | Every id outside the caller's scope is refused identically, so the response cannot be used to count records. |
| API-19 | Extra fields in the body are ignored | Security | High | — |
| role=admin | 200 and the role is unchanged, or 400 for the unknown field. It is never bound. |
| API-20 | A read-only field cannot be written | Security | Medium | — |
| a new owner id | Refused or silently ignored per the contract, and the stored value is unchanged. |
| API-21 | Rate limiting returns 429 with Retry-After | Security | High | The documented limit is known |
| — | 429, a Retry-After header, and normal service once the window passes. |
| API-22 | Errors never leak internals | Security | High | — |
| an oversized integer | No stack trace, SQL fragment, file path, framework name or version in the response. |
| API-23 | Page size is capped | Boundary | Medium | Many records exist |
| limit=100000 | The cap is applied and stated in the response, or 400. The database is not asked for everything. |
| API-24 | An oversized payload returns 413 | Boundary | Medium | — |
| 20MB body | 413, and the connection is not held open while the whole body is read. |
| API-25 | Unicode survives a round trip | Boundary | Medium | — |
| an emoji and Japanese text | The value returns byte for byte, with the correct content type and charset. |
| API-26 | A retried request with an idempotency key creates one record | Data integrity | High | The endpoint documents idempotency keys |
| the same key twice | One record exists, and the second call returns the first result rather than a duplicate. |
| API-27 | A concurrent update is detected | Data integrity | High | Two clients hold the same ETag |
| — | B receives 412 rather than silently overwriting A. Last write does not blindly win. |
| API-28 | A failed write leaves nothing behind | Data integrity | High | A request that fails partway |
| — | No partial record and no orphaned child rows. The transaction rolled back. |
| API-29 | The list endpoint does not degrade with size | Performance | Medium | Ten thousand records exist |
| — | Both are within the documented budget, and the query count does not grow with the row count. |
| API-30 | A webhook is delivered once, signed, and retried on failure | Integration | Medium | A subscribed endpoint that can be made to fail |
| — | 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.