Practise API testing on a live REST API: hardcoded GET data, temporary objects you can create, update and delete, and four authentication types, with 141 ready-made test scenarios.
Free practice REST API · no sign-up
API Testing Practice
A real REST API built for automation engineers. Practise GET, POST, PUT, PATCH and DELETE, status codes, validation, pagination, headers, caching, timeouts, rate limits and four kinds of authentication: none, Basic, API key and Bearer token (JWT). Data you create is temporary and disappears on its own.
https://sqatools.in/wp-json/sqa-api/v1- 141 test scenarios
- 11 scenario groups
- 4 auth types
- 10 min default object lifetime
Practice only. Everything you send is visible to other learners until it expires. Never send real names, emails, passwords or other personal data.
Quick start
Send your first request. GET endpoints return fixed, hardcoded data, so the same request always gives the same answer and your assertions stay stable.
# 1. Read hardcoded data (no auth)
curl -i https://sqatools.in/wp-json/sqa-api/v1/users/1
# 2. Create a temporary object that lives 120 seconds
curl -i -X POST https://sqatools.in/wp-json/sqa-api/v1/objects \
-H "Content-Type: application/json" \
-d '{"name": "My test object", "data": {"qty": 2}, "ttl": 120}'
# 3. Basic auth
curl -i -u sqa_user:sqa@123 https://sqatools.in/wp-json/sqa-api/v1/basic/profile
# 4. API key
curl -i -H "X-API-Key: sqa-key-12345" https://sqatools.in/wp-json/sqa-api/v1/apikey/profile
# 5. Bearer token: log in, then send the access_token
curl -s -X POST https://sqatools.in/wp-json/sqa-api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin@123"}'
curl -i -H "Authorization: Bearer <access_token>" https://sqatools.in/wp-json/sqa-api/v1/auth/meimport requests
BASE = "https://sqatools.in/wp-json/sqa-api/v1"
# No auth: hardcoded user
r = requests.get(f"{BASE}/users/1", timeout=10)
print(r.status_code, r.json()["data"]["name"]) # 200 Asha Verma
# Create a temporary object, then read it back from the Location header
r = requests.post(f"{BASE}/objects", json={"name": "My test object", "ttl": 120}, timeout=10)
print(r.status_code, r.headers["Location"]) # 201 …/objects/obj_…
print(requests.get(r.headers["Location"], timeout=10).json()["name"])
# Basic auth, API key and Bearer token
print(requests.get(f"{BASE}/basic/profile", auth=("sqa_user", "sqa@123"), timeout=10).status_code)
print(requests.get(f"{BASE}/apikey/profile", headers={"X-API-Key": "sqa-key-12345"}, timeout=10).status_code)
token = requests.post(f"{BASE}/auth/login", json={"username": "admin", "password": "admin@123"}, timeout=10).json()["access_token"]
print(requests.get(f"{BASE}/auth/me", headers={"Authorization": f"Bearer {token}"}, timeout=10).json()["data"]["role"])const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';
// No auth: list users, page 2, 5 per page
const list = await fetch(`${BASE}/users?page=2&per_page=5`);
console.log(list.status, list.headers.get('X-Total-Count')); // 200 '12'
// Create a temporary object
const created = await fetch(`${BASE}/objects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'From fetch', data: { qty: 2 }, ttl: 120 })
});
const obj = await created.json();
console.log(created.status, obj.id, obj.version); // 201 'obj_…' 1
// Bearer token
const login = await fetch(`${BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'viewer', password: 'viewer@123' })
});
const { access_token } = await login.json();
const me = await fetch(`${BASE}/auth/me`, { headers: { Authorization: `Bearer ${access_token}` } });
console.log((await me.json()).data.can_write); // falseAuthentication
Each auth style protects its own set of endpoints, so you can practise them one at a time. All credentials below are dummy values for this practice API only.
1. No authentication
Open endpoints: /users, /products, /orders, /objects, /echo, /status/{code} and more.
GET /users/12. Basic authentication
Username sqa_user, password sqa@123. Send Authorization: Basic base64(username:password).
GET /basic/profile
Authorization: Basic c3FhX3VzZXI6c3FhQDEyMw==Wrong or missing credentials: 401 with a WWW-Authenticate: Basic header.
3. API key
Send the header X-API-Key: sqa-key-12345 (the ?api_key= query parameter also works).
GET /apikey/profile
X-API-Key: sqa-key-12345Missing key: 401. Wrong key: 403.
4. Bearer token (JWT)
Log in as admin / admin@123 (read and write) or viewer / viewer@123 (read only), then send the token.
POST /auth/login
{"username": "admin", "password": "admin@123"}
GET /auth/me
Authorization: Bearer <access_token>Tokens last 15 minutes (set expires_in from 5 to 3600 seconds). Use POST /auth/refresh with the refresh_token for new tokens; each refresh token works once. POST /auth/logout revokes the access token. The viewer gets 403 on writes.
Endpoints
All paths are relative to the base URL. GET /endpoints returns this list as JSON.
| Method | Path | Auth | What it does |
|---|---|---|---|
| GET | /health | none | Service status and server time |
| GET | /users | none | Users list: page, per_page, role, city, active, q, sort, order |
| GET | /users/{id} | none | One user (Accept: application/xml for XML) |
| GET | /users/{id}/orders | none | Orders of one user (nested resource) |
| GET | /products | none | Products: category, in_stock, min_price, max_price, sort, order |
| GET | /products/{id} | none | One product with ETag (send If-None-Match for 304) |
| GET | /orders | none | Orders: status, user_id |
| GET | /orders/{id} | none | One order with nested items, summary and shipping |
| ANY | /status/{code} | none | Responds with any status code from 200 to 599 |
| GET | /delay/{seconds} | none | Waits 0-10 seconds before responding |
| ANY | /echo | none | Echoes method, query, headers and body |
| ANY | /redirect | none | Redirects to /echo (status=301, 302, 303, 307 or 308) |
| GET | /rate-limit | none | 5 requests per minute, then 429 with Retry-After |
| POST | /validate/user | none | Validates a user payload; 422 lists every field error |
| GET | /schema/{name} | none | JSON Schema: user, product, order, object, error, token |
| GETPOST | /objects | none | Temporary objects: list, create |
| GETPUTPATCHDELETE | /objects/{id} | none | Temporary object: read, replace or create, update, delete |
| GET | /basic/profile | basic | Profile behind HTTP Basic auth |
| GETPOSTPUTPATCHDELETE | /basic/objects[/{id}] | basic | Temporary objects behind Basic auth |
| GET | /apikey/profile | apikey | Profile behind an API key |
| GETPOSTPUTPATCHDELETE | /apikey/objects[/{id}] | apikey | Temporary objects behind an API key |
| POST | /auth/login | none | Get a Bearer JWT and refresh token |
| POST | /auth/refresh | none | Swap a refresh token for new tokens |
| POST | /auth/logout | bearer | Revoke the access token |
| GET | /auth/me | bearer | Who the token belongs to |
| GETPOSTPUTPATCHDELETE | /token/objects[/{id}] | bearer | Temporary objects; viewer role may only read |
Errors always look the same, which makes them easy to assert:
{"error": {"status": 422, "code": "validation_failed", "message": "1 field(s) failed validation.",
"details": [{"field": "age", "message": "age is required: a whole number from 18 to 100 (not a string)."}]}}
Temporary objects (POST, PUT, PATCH, DELETE)
Create your own test data with POST /objects or PUT /objects/{your-id}. Each object expires automatically, after which GET returns 404.
- Fields:
name(required, 1–100 characters),data(any JSON up to 5 KB) andttl(lifetime in seconds, 10–1800, default 600). Any other field gives 422. - POST creates an object with a server id (
obj_…) and returns 201 with aLocationheader. - PUT replaces an object, or creates it with your id (3–40 letters, digits,
-or_). - PATCH changes only the fields you send. Keys inside
dataare merged, and a key set tonullis removed. - DELETE returns 204; a second DELETE returns 404.
- Every change increases
version. TheETagheader is"v<version>": send it inIf-Matchto avoid overwriting someone else’s change (412), or inIf-None-Matchto get 304 Not Modified. /objects,/basic/objects,/apikey/objectsand/token/objectsare separate collections with the same rules.
POST /objects
Content-Type: application/json
{"name": "Test laptop", "data": {"brand": "Dell", "price": 55000}, "ttl": 300}
HTTP/1.1 201 Created
Location: https://sqatools.in/wp-json/sqa-api/v1/objects/obj_k3x9q2m1za
ETag: "v1"
{"id": "obj_k3x9q2m1za", "name": "Test laptop", "data": {"brand": "Dell", "price": 55000}, "version": 1,
"created_by": "anonymous", "created_at": "…", "updated_at": "…", "expires_at": "…", "ttl_seconds": 300}
Try it console
Send any request from your browser, or press Try on a scenario below to load and run it. Add auth with the buttons; the console writes the exact headers so you can see what is sent.
The response appears here.
141 API testing scenarios
Every scenario lists the request and the expected status. Press Try to run one in the console. Setup steps (creating an object, logging in, waiting for expiry) run for you. All of them are checked automatically against this API.
- Read hardcoded data (GET)
- Pagination, filtering, sorting and search
- HTTP methods, status codes and redirects
- Headers, content types and caching
- Request body validation (negative testing)
- Temporary objects: create, read, update, delete
- Basic authentication
- API key authentication
- Token (Bearer JWT) authentication
- Authorization (roles and permissions)
- Performance, timeouts and rate limits
Read hardcoded data (GET) (17)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 1 | Health check returns ok Smoke test: the service is up. | GET /health | 200 | |
| 2 | List all users Default list with pagination metadata and headers. | GET /users | 200 | |
| 3 | Get a single user by id Verify exact field values. | GET /users/1 | 200 | |
| 4 | User that does not exist Negative test: resource not found. | GET /users/999 | 404 | |
| 5 | Non-numeric user id Path parameter type validation. | GET /users/abc | 400 | |
| 6 | Zero as user id Boundary value for an id. | GET /users/0 | 400 | |
| 7 | Nested resource: orders of a user Parent/child resources. | GET /users/1/orders | 200 | |
| 8 | Order with nested items and totals Check nested objects, arrays and calculated values. | GET /orders/5001 | 200 | |
| 9 | Product has an ETag header Response header assertions. | GET /products/101 | 200 | |
| 10 | Filter products by category Query parameter filtering. | GET /products?category=books | 200 | |
| 11 | Filter products by price range Combined range filters. | GET /products?min_price=1000&max_price=5000 | 200 | |
| 12 | Invalid price range (min > max) Cross-field validation of query parameters. | GET /products?min_price=5000&max_price=100 | 400 | |
| 13 | Unknown category value Enum validation. | GET /products?category=toys | 400 | |
| 14 | Filter orders by status Filter on a status field. | GET /orders?status=delivered | 200 | |
| 15 | JSON Schema for a user Fetch a schema for contract testing. | GET /schema/user | 200 | |
| 16 | Unknown schema name Negative test on a lookup. | GET /schema/invoice | 404 | |
| 17 | List every endpoint API discovery (JSON escapes / as \/). | GET /endpoints | 200 |
Pagination, filtering, sorting and search (17)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 18 | Second page of users Page 2 starts at the 6th record. | GET /users?page=2&per_page=5 | 200 | |
| 19 | Last page has the remaining records Last-page boundary. | GET /users?page=3&per_page=5 | 200 | |
| 20 | Page beyond the last page Out-of-range page returns an empty list, not an error. | GET /users?page=10 | 200 | |
| 21 | per_page above the maximum (51) Upper boundary of page size. | GET /users?per_page=51 | 400 | |
| 22 | per_page at the maximum (50) Boundary value that must pass. | GET /users?per_page=50 | 200 | |
| 23 | Page number zero Lower boundary of page. | GET /users?page=0 | 400 | |
| 24 | Link header points to the next page Hypermedia pagination headers. | GET /users?per_page=5 | 200 | |
| 25 | Filter users by role Single filter. | GET /users?role=admin | 200 | |
| 26 | City filter is case-insensitive Case handling in filters. | GET /users?city=pune | 200 | |
| 27 | Boolean filter active=false Boolean query parameters. | GET /users?active=false | 200 | |
| 28 | Invalid boolean value Boolean validation. | GET /users?active=maybe | 400 | |
| 29 | Combine two filters AND logic between filters. | GET /users?role=admin&city=Chennai | 200 | |
| 30 | Search by text Free-text search. | GET /users?q=gupta | 200 | |
| 31 | Search with no match Empty result set. | GET /users?q=zzz | 200 | |
| 32 | Sort by age descending Sorting order. | GET /users?sort=age&order=desc&per_page=1 | 200 | |
| 33 | Sort by an unknown field Whitelisted sort fields. | GET /users?sort=salary | 400 | |
| 34 | Invalid sort order Enum validation for order. | GET /users?order=up | 400 |
HTTP methods, status codes and redirects (18)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 35 | POST on a read-only resource 405 with an Allow header. | POST /users | 405 | |
| 36 | DELETE on a read-only resource Unsupported method. | DELETE /products/101 | 405 | |
| 37 | HEAD returns headers only HEAD has the same headers as GET and no body. | HEAD /users/1 | 200 | |
| 38 | OPTIONS lists allowed methods Pre-flight / discovery. | /users | 200 | |
| 39 | Status code 201 Client handles 201 Created. | GET /status/201 | 201 | |
| 40 | Status code 400 Client handles 400 Bad Request. | GET /status/400 | 400 | |
| 41 | Status code 401 Client handles 401 Unauthorized. | GET /status/401 | 401 | |
| 42 | Status code 403 Client handles 403 Forbidden. | GET /status/403 | 403 | |
| 43 | Status code 404 Client handles 404 Not Found. | GET /status/404 | 404 | |
| 44 | Status code 409 Client handles 409 Conflict. | GET /status/409 | 409 | |
| 45 | Status code 500 Client handles 500 Internal Server Error. | GET /status/500 | 500 | |
| 46 | Status 503 with Retry-After Back-off header on service unavailable. | GET /status/503 | 503 | |
| 47 | Status 204 has an empty body No Content. | GET /status/204 | 204 | |
| 48 | Invalid status code Validation of a path parameter. | GET /status/700 | 400 | |
| 49 | Redirect without following it Assert the redirect itself. | GET /redirect | 302 | |
| 50 | Follow a redirect Client follows to the final response. | GET /redirect | 200 | |
| 51 | 307 keeps the POST method 307/308 preserve method and body. | POST /redirect?status=307 | 200 | |
| 52 | 303 changes POST to GET 303 See Other always switches to GET. | POST /redirect?status=303 | 200 |
Headers, content types and caching (7)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 53 | Ask for XML with the Accept header Content negotiation. | GET /users/1 | 200 | |
| 54 | Unsupported Accept type 406 Not Acceptable. | GET /users/1 | 406 | |
| 55 | Custom request header is received Verify headers and query reach the server. | GET /echo?source=test | 200 | |
| 56 | Form-encoded body Non-JSON bodies. | POST /echo | 200 | |
| 57 | JSON body is echoed back Round-trip a JSON payload. | PUT /echo | 200 | |
| 58 | Conditional GET returns 304 ETag caching saves bandwidth. | GET /products/101 | 304 | |
| 59 | Responses are not cached Cache-Control header check. | GET /health | 200 |
Request body validation (negative testing) (18)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 60 | Valid payload passes Happy path. | POST /validate/user | 200 | |
| 61 | Empty object: every required field fails All errors are reported at once. | POST /validate/user | 422 | |
| 62 | Wrong data type (age as text) Type checking. | POST /validate/user | 422 | |
| 63 | Boundary: age 17 Boundary value analysis (18 to 100). | POST /validate/user | 422 | |
| 64 | Boundary: age 18 Boundary value analysis (18 to 100). | POST /validate/user | 200 | |
| 65 | Boundary: age 100 Boundary value analysis (18 to 100). | POST /validate/user | 200 | |
| 66 | Boundary: age 101 Boundary value analysis (18 to 100). | POST /validate/user | 422 | |
| 67 | Invalid email format Format validation. | POST /validate/user | 422 | |
| 68 | Value outside the enum Enum validation. | POST /validate/user | 422 | |
| 69 | Unknown extra field Strict schema rejects extra fields. | POST /validate/user | 422 | |
| 70 | Name too short (1 character) Minimum length. | POST /validate/user | 422 | |
| 71 | Name too long (51 characters) Maximum length. | POST /validate/user | 422 | |
| 72 | Invalid phone number Pattern validation. | POST /validate/user | 422 | |
| 73 | Malformed JSON Syntax errors give 400, not 500. | POST /validate/user | 400 | |
| 74 | Wrong Content-Type 415 Unsupported Media Type. | POST /validate/user | 415 | |
| 75 | Empty body Missing payload. | POST /validate/user | 400 | |
| 76 | JSON array instead of an object Top-level type check. | POST /validate/user | 400 | |
| 77 | Payload larger than 10 KB 413 Payload Too Large. | POST /objects | 413 |
Temporary objects: create, read, update, delete (24)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 78 | Create an object (POST) 201 Created with a Location header. | POST /objects | 201 | |
| 79 | Create without the required name Required field. | POST /objects | 422 | |
| 80 | ttl below the minimum (5 seconds) Boundary: minimum 10 seconds. | POST /objects | 422 | |
| 81 | ttl above the maximum (2000 seconds) Boundary: maximum 1800 seconds. | POST /objects | 422 | |
| 82 | Client cannot choose the id on POST Server-generated ids. | POST /objects | 422 | |
| 83 | Read the created object GET after POST. | GET /objects/{id} | 200 | |
| 84 | List objects The new object appears in the list. | GET /objects | 200 | |
| 85 | Replace with PUT Full update increments the version. | PUT /objects/{id} | 200 | |
| 86 | Create with PUT and your own id PUT to a new id creates it (upsert). | PUT /objects/my-{rand} | 201 | |
| 87 | PUT without the required name PUT needs the full representation. | PUT /objects/{id} | 422 | |
| 88 | Body id differs from URL id Consistency check. | PUT /objects/{id} | 422 | |
| 89 | Partial update with PATCH PATCH merges into existing data. | PATCH /objects/{id} | 200 | |
| 90 | PATCH removes a key set to null JSON merge patch semantics. | PATCH /objects/{id} | 200 | |
| 91 | PATCH with an empty object Nothing to update. | PATCH /objects/{id} | 422 | |
| 92 | PATCH an object that does not exist Update a missing resource. | PATCH /objects/missing-{rand} | 404 | |
| 93 | Delete an object 204 No Content. | DELETE /objects/{id} | 204 | |
| 94 | Read a deleted object The object is gone after DELETE. | GET /objects/{id} | 404 | |
| 95 | Delete the same object twice Second DELETE returns 404. | DELETE /objects/{id} | 404 | |
| 96 | Id too short Path parameter format. | GET /objects/ab | 400 | |
| 97 | Optimistic locking: stale If-Match Prevent lost updates. | PUT /objects/{id} | 412 | |
| 98 | Optimistic locking: current If-Match Matching version is accepted. | PUT /objects/{id} | 200 | |
| 99 | Conditional GET on an object 304 when unchanged. | GET /objects/{id} | 304 | |
| 100 | Object vanishes after its ttl Data expiry: created with ttl 10, read after 12 seconds. | GET /objects/{id} | 404 | |
| 101 | Text with SQL and HTML is stored as plain text Injection strings are data, never executed. | POST /objects | 201 |
Basic authentication (8)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 102 | Valid username and password Authorization: Basic base64(user:pass). | GET /basic/profile basic | 200 | |
| 103 | No credentials 401 with a WWW-Authenticate challenge. | GET /basic/profile | 401 | |
| 104 | Wrong password Invalid credentials. | GET /basic/profile basic_wrong | 401 | |
| 105 | Malformed Basic header Header that is not base64. | GET /basic/profile | 401 | |
| 106 | Create an object with Basic auth Write behind Basic auth. | POST /basic/objects basic | 201 | |
| 107 | Create without credentials Writes are protected too. | POST /basic/objects | 401 | |
| 108 | Read an object with Basic auth Read behind Basic auth. | GET /basic/objects/{id} basic | 200 | |
| 109 | Collections are separate An object created in /objects is not in /basic/objects. | GET /basic/objects/{id} basic | 404 |
API key authentication (6)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 110 | Valid key in the X-API-Key header Header-based API key. | GET /apikey/profile apikey | 200 | |
| 111 | Valid key as a query parameter Query-string key (less secure: it ends up in logs). | GET /apikey/profile?api_key=sqa-key-12345 | 200 | |
| 112 | Missing API key 401 when no key is sent. | GET /apikey/profile | 401 | |
| 113 | Wrong API key 403 when the key is not valid. | GET /apikey/profile apikey_wrong | 403 | |
| 114 | Key sent in the wrong header The key must be in X-API-Key. | GET /apikey/profile | 401 | |
| 115 | Create an object with an API key Write behind an API key. | POST /apikey/objects apikey | 201 |
Token (Bearer JWT) authentication (15)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 116 | Log in and get a token Token issue. | POST /auth/login | 200 | |
| 117 | Log in with a wrong password Failed login. | POST /auth/login | 401 | |
| 118 | Log in without fields Required fields. | POST /auth/login | 422 | |
| 119 | Token lifetime below 5 seconds Boundary on expires_in. | POST /auth/login | 422 | |
| 120 | Call a protected endpoint with the token Authorization: Bearer <token>. | GET /auth/me admin | 200 | |
| 121 | No token 401 without a token. | GET /auth/me | 401 | |
| 122 | Garbage token Not a JWT. | GET /auth/me | 401 | |
| 123 | Wrong scheme (Token instead of Bearer) Scheme must be Bearer. | GET /auth/me | 401 | |
| 124 | Tampered token signature Changing any character breaks the signature. | GET /auth/me tampered | 401 | |
| 125 | Expired token Token issued with expires_in 5, used after 7 seconds. | GET /auth/me expired | 401 | |
| 126 | Log out revokes the token Server-side logout. | POST /auth/logout admin | 204 | |
| 127 | Token used after logout Revoked tokens are rejected. | GET /auth/me revoked | 401 | |
| 128 | Refresh the token Get new tokens without the password. | POST /auth/refresh | 200 | |
| 129 | Reuse a refresh token Refresh tokens rotate: each works once. | POST /auth/refresh | 401 | |
| 130 | Refresh without a token Required field. | POST /auth/refresh | 422 |
Authorization (roles and permissions) (6)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 131 | Viewer can read objects Read permission. | GET /token/objects viewer | 200 | |
| 132 | Viewer cannot create objects 403 = authenticated but not allowed. | POST /token/objects viewer | 403 | |
| 133 | Admin can create objects Write permission. | POST /token/objects admin | 201 | |
| 134 | Viewer cannot delete Role check on DELETE. | DELETE /token/objects/{id} viewer | 403 | |
| 135 | Admin can delete Owner role succeeds. | DELETE /token/objects/{id} admin | 204 | |
| 136 | Viewer token says can_write false Claims in the response. | GET /auth/me viewer | 200 |
Performance, timeouts and rate limits (5)
| # | Scenario | Request | Expected | |
|---|---|---|---|---|
| 137 | Response time under 3 seconds Simple SLA assertion. | GET /users | 200 | |
| 138 | Delayed response (2 seconds) Measure latency. | GET /delay/2 | 200 | |
| 139 | Delay above the limit Validation on the delay. | GET /delay/11 | 400 | |
| 140 | Client timeout Your client should give up after 2 seconds. | GET /delay/5 | client timeout | |
| 141 | Rate limit: 6th call in a minute 429 Too Many Requests with Retry-After. | GET /rate-limit | 429 |
Automation code
Complete, runnable tests for this API. Save one as a file and run it with pytest.
import pytest
import requests
BASE = "https://sqatools.in/wp-json/sqa-api/v1"
@pytest.fixture(scope="session")
def admin_headers():
r = requests.post(f"{BASE}/auth/login", json={"username": "admin", "password": "admin@123"}, timeout=10)
assert r.status_code == 200
return {"Authorization": f"Bearer {r.json()['access_token']}"}
def test_get_user():
r = requests.get(f"{BASE}/users/1", timeout=10)
assert r.status_code == 200
assert r.headers["Content-Type"].startswith("application/json")
assert r.json()["data"]["email"] == "asha@example.com"
@pytest.mark.parametrize("user_id, status", [(1, 200), (12, 200), (13, 404), ("abc", 400)])
def test_user_ids(user_id, status):
assert requests.get(f"{BASE}/users/{user_id}", timeout=10).status_code == status
def test_pagination():
body = requests.get(f"{BASE}/users", params={"page": 3, "per_page": 5}, timeout=10).json()
assert [u["id"] for u in body["data"]] == [11, 12]
assert body["meta"]["total_pages"] == 3
assert body["links"]["next"] is None
def test_object_lifecycle():
r = requests.post(f"{BASE}/objects", json={"name": "Laptop", "data": {"ram": "16GB"}, "ttl": 120}, timeout=10)
assert r.status_code == 201
url = r.headers["Location"]
r = requests.patch(url, json={"data": {"ram": "32GB"}}, timeout=10)
assert r.status_code == 200
assert r.json()["data"] == {"ram": "32GB"}
assert r.json()["version"] == 2
assert requests.delete(url, timeout=10).status_code == 204
assert requests.get(url, timeout=10).status_code == 404
def test_validation_errors():
r = requests.post(f"{BASE}/validate/user", json={"name": "A", "email": "bad", "age": "20"}, timeout=10)
assert r.status_code == 422
fields = {d["field"] for d in r.json()["error"]["details"]}
assert fields == {"name", "email", "age"}
def test_basic_auth():
assert requests.get(f"{BASE}/basic/profile", auth=("sqa_user", "sqa@123"), timeout=10).status_code == 200
r = requests.get(f"{BASE}/basic/profile", auth=("sqa_user", "wrong"), timeout=10)
assert r.status_code == 401
assert r.headers["WWW-Authenticate"].startswith("Basic")
def test_api_key():
assert requests.get(f"{BASE}/apikey/profile", headers={"X-API-Key": "sqa-key-12345"}, timeout=10).status_code == 200
assert requests.get(f"{BASE}/apikey/profile", headers={"X-API-Key": "wrong"}, timeout=10).status_code == 403
def test_token_crud(admin_headers):
r = requests.post(f"{BASE}/token/objects", json={"name": "Secured"}, headers=admin_headers, timeout=10)
assert r.status_code == 201
assert r.json()["created_by"] == "admin"
def test_timeout_is_raised():
with pytest.raises(requests.exceptions.Timeout):
requests.get(f"{BASE}/delay/5", timeout=2)import pytest
from playwright.sync_api import APIRequestContext, Playwright
# base_url must end with "/" and paths must not start with "/", or the /wp-json/... part is dropped
BASE = "https://sqatools.in/wp-json/sqa-api/v1/"
@pytest.fixture(scope="session")
def api(playwright: Playwright):
ctx = playwright.request.new_context(base_url=BASE)
yield ctx
ctx.dispose()
@pytest.fixture(scope="session")
def viewer_token(api: APIRequestContext):
r = api.post("auth/login", data={"username": "viewer", "password": "viewer@123"})
assert r.ok
return r.json()["access_token"]
def test_users_page_2(api: APIRequestContext):
r = api.get("users", params={"page": 2, "per_page": 5})
assert r.status == 200
assert r.headers["x-total-count"] == "12"
assert [u["id"] for u in r.json()["data"]] == [6, 7, 8, 9, 10]
def test_put_creates_then_replaces(api: APIRequestContext):
r = api.put("objects/pw-demo-object", data={"name": "First", "ttl": 60})
assert r.status in (200, 201)
r = api.put("objects/pw-demo-object", data={"name": "Second", "ttl": 60})
assert r.status == 200
assert r.json()["name"] == "Second"
def test_conditional_get(api: APIRequestContext):
etag = api.get("products/101").headers["etag"]
assert api.get("products/101", headers={"If-None-Match": etag}).status == 304
def test_viewer_is_read_only(api: APIRequestContext, viewer_token):
auth = {"Authorization": f"Bearer {viewer_token}"}
assert api.get("token/objects", headers=auth).status == 200
r = api.post("token/objects", headers=auth, data={"name": "Not allowed"})
assert r.status == 403
assert r.json()["error"]["code"] == "forbidden"
def test_basic_auth_with_http_credentials(playwright: Playwright):
ctx = playwright.request.new_context(base_url=BASE, http_credentials={"username": "sqa_user", "password": "sqa@123", "send": "always"})
assert ctx.get("basic/profile").status == 200
ctx.dispose()Install with pip install pytest requests or pip install pytest-playwright.
Limits and good manners
- 600 requests per minute per IP across the whole API (then 429).
/rate-limitallows only 5 per minute so you can test throttling. - Request bodies up to 10 KB (413 above that);
dataup to 5 KB. - Up to 300 live objects per collection; when full, the oldest are removed first.
/delay/{seconds}waits at most 10 seconds.- Responses are never cached (
Cache-Control: no-store).