REST: Testing Plan and Risks
1.11 Testing Plan
Tests are organized by category. Each test describes input and expected result.
Schema Validation Tests
| Test Case | Input | Expected |
|---|---|---|
| Missing required field | POST without name field |
400 or 422 with field error |
| Unknown field (strict mode) | POST with foo_bar extra field |
400 with "unknown field" error |
| Type mismatch | "age": "not_a_number" |
400 with type error on age |
| Null vs missing | "name": null vs no name key |
Different behavior per spec |
| Invalid nested object | "address": {"zip": 123} (zip should be string) |
422 with nested field error |
Test both strict and lenient modes. Strict mode rejects unknown fields. Lenient mode ignores them. Choose one and be consistent.
Input Validation Tests
Query parameters:
- Invalid type: ?page=abc -> 400.
- Missing required: ?search= without type param -> 400.
- Unknown param: ?foo=bar -> ignore (lenient) or 400 (strict).
Path parameters:
- Wrong type: GET /users/not-a-uuid -> 400.
- Non-existent resource: GET /users/valid-but-missing-uuid -> 404.
Request body:
- Malformed JSON: {broken -> 400 Bad Request.
- Empty body on POST: -> 400.
- Oversized body: > configured limit -> 413 Payload Too Large.
Content-Type:
- Wrong header: Content-Type: text/plain on JSON endpoint -> 415.
- Missing header: no Content-Type on POST -> 415.
Query Logic Tests
Filtering:
- Filter returns correct subset of results.
- Filter with no matches returns empty array [], not 404.
- Invalid filter field returns 400.
- Multiple filters combine with AND logic.
Sorting:
- ?sort=name returns alphabetical order.
- ?sort=-created_at returns newest first (descending).
- Multiple sort: ?sort=status,-created_at -> primary + secondary sort.
- Invalid sort field returns 400.
Pagination: - First page returns correct count and next cursor. - Last page has no next cursor. - Beyond last page returns empty array. - Invalid cursor returns 400. - Page size limits are enforced (max 100 items, for example).
Idempotency Tests
| Test Case | Action | Expected |
|---|---|---|
| Repeated GET | GET same resource twice | Same response both times |
| Repeated PUT | PUT same data twice | Same result, no side effects |
| POST with key | Same POST + same Idempotency-Key | Stored response returned |
| POST different body | Same key + different body | 422 error (key mismatch) |
| Concurrent POST | Two identical POSTs at same time | Only one executes |
HTTP Semantics Tests
GET /users-> 200 with array.POST /users-> 201 with created resource andLocationheader.PUT /users/1-> 200 with updated resource.DELETE /users/1-> 204 No Content.DELETE /users/1again -> 204 or 404 (both are acceptable).PATCH /users(unsupported on collection) -> 405 Method Not Allowed.OPTIONS /users-> 200 withAllow: GET, POST, OPTIONSheader.
Error and Auth Tests
- All error responses match the standard error schema.
- Expired JWT returns 401 with
UNAUTHORIZEDcode. - Valid token but wrong role returns 403 with
FORBIDDENcode. - Rate limit exceeded returns 429 with
Retry-Afterheader. - Internal error returns 500 with generic message (no stack trace).
- Invalid API key returns 401.
Caching Tests
- Response includes
ETagheader. If-None-Matchwith current ETag returns 304 (no body).If-None-Matchwith old ETag returns 200 with new data.- After resource update, ETag value changes.
Cache-Controlheader is present with correct directives.Last-Modifiedheader is present and accurate.
Concurrency Tests
- Client A and Client B read resource (same ETag).
- Client A updates with
If-Match-> 200 success, new ETag in response. - Client B updates with old
If-Match-> 412 Precondition Failed (RFC 7232). - 412 response includes current resource state and current ETag.
- Client B retries with new ETag -> 200 success.
Rate Limiting Tests
| Test Case | Action | Expected |
|---|---|---|
| Below threshold | Send requests within limit | All succeed (200) |
| Exceed threshold | Send requests above limit | 429 Too Many Requests |
| Retry-After header | Trigger rate limit | Response includes Retry-After header |
| Retry after wait | Wait for Retry-After duration, retry | Request succeeds |
| Per-client isolation | Client A hits limit | Client B is not affected |
| Different endpoints | Separate limits per endpoint | Each endpoint has its own counter |
Security Tests
| Test Case | Action | Expected |
|---|---|---|
| SQL injection | Send '; DROP TABLE users;-- in param |
No SQL execution, 400 error |
| NoSQL injection | Send {"$gt": ""} in filter |
Rejected, no data leak |
| XSS in input | Send <script>alert(1)</script> in field |
Stored escaped, not executable |
| Mass assignment | POST with {"role": "admin"} extra field |
Field ignored or 422 error |
| Overposting | PATCH with {"created_at": "..."} |
Internal field not changed |
| Data exposure | GET response for regular user | No password_hash or internal fields |
| CORS violation | Request from unauthorized origin | Blocked by CORS headers |
Performance Tests
| Test | Target | Measure |
|---|---|---|
| Response time (p95) | < 200ms for simple reads | Under normal load |
| Response time (p99) | < 500ms for complex queries | Under normal load |
| Throughput | > 1000 RPS per instance | Sustained load |
| Large list pagination | < 300ms for 100 items | With cursor pagination |
| Connection pool | No connection exhaustion | Under sustained load |
| Concurrent writes | No data corruption | 50+ parallel updates |
Advanced Policy: Conditional Writes
For mutating methods (PUT, PATCH, DELETE), require If-Match header with current ETag.
| Case | Expected response |
|---|---|
Missing If-Match |
428 Precondition Required |
Stale If-Match value |
412 Precondition Failed |
Valid If-Match |
200 or 204 success |
Operational checklist:
- Return clear machine code (PRECONDITION_REQUIRED, PRECONDITION_FAILED).
- Add current ETag in 412 response to support client retry flow.
- Do not cache 428 responses.
- Log provided and current ETag with request_id for conflict analysis.
1.12 Risks and Limitations
| Risk | Impact | Solution |
|---|---|---|
| Over-fetching | Client gets more data than needed, wasted bandwidth | Sparse fieldsets ?fields=id,name or GraphQL |
| Under-fetching | Multiple requests needed, higher latency | Include related resources ?include=author |
| N+1 requests | 51 HTTP calls instead of 1-2, slow on mobile | Bulk endpoints, embed related data in list |
| Multiple round-trips | High total latency to build one screen | Compound documents, batch endpoints |
| Weak typing (JSON) | No native date/decimal types, precision issues | Document formats (ISO 8601), validate on server |
| Versioning complexity | Multiple versions to maintain and test | Strict backward compatibility, minimize versions |
| Cache invalidation | Stale data or no caching benefit | Event-based invalidation, short TTLs |
| Pagination inconsistency | Duplicates/missing items with offset pagination | Cursor-based pagination for stable results |
| No real-time support | Client must poll, delayed updates | WebSocket for real-time, webhooks for events |
| Large payloads | Slow mobile transfers, high bandwidth cost | Accept-Encoding: gzip, CDN compression |
| Inconsistent API design | Developer confusion, longer integration time | API style guide, linters (Spectral), code reviews |
| Security risks (OWASP) | Data breaches, unauthorized access | Security testing in CI/CD, input validation, auth |