Test and operate APIs
Write an API security matrix
Test every route across identity, role, object relationship, input boundary, rate limit, and expected security event.
API security tests need more than one happy-path token. Build a matrix that changes one security condition at a time, so a failing cell points at exactly one broken control.
Choose the rows
Include anonymous, expired, revoked, low-privilege, wrong-tenant, and administrative identities. Test another user’s object, hidden properties, alternate methods, batch requests, oversized input, and repeated calls.
For one high-value route, the matrix reads like this:
route: POST /invoices/:id/refund
anonymous -> 401, no refund row
expired token -> 401, no refund row
wrong tenant -> 404, no refund row
low-privilege role -> 403, no refund row, denial event logged
admin, valid -> 200, one refund row, audit event logged
admin, key replayed -> 200 same body, still exactly one refund row
Each row is a real request with real credentials against a real database, not a mocked unit test. Authorization bugs live in the wiring between middleware, handler, and query — a mock skips exactly the layer you are trying to test.
Assert state, not just status
A 403 alone does not prove safety if the invoice changed before the response. The matrix must check identity, outcome, stored state, and the expected security event. Verify stored state and events, not only status codes:
const res = await api.post('/invoices/inv_9f3c2a/refund', body, wrongTenantToken)
assert.equal(res.status, 404)
const refunds = await db.query(
'SELECT count(*) FROM refunds WHERE invoice_id = $1', ['inv_9f3c2a'])
assert.equal(refunds.rows[0].count, '0') // no side effect happened
const events = await db.query(
"SELECT count(*) FROM audit_events WHERE type = 'authz_denied'")
assert.equal(events.rows[0].count, '1') // the denial left evidence
Prioritize by harm
Prioritize cells by potential harm instead of automating every permutation first. Cross-tenant writes and money movement deserve stronger evidence than a harmless malformed optional field. Forty high-value cells that run on every commit beat four thousand cells nobody maintains.
One extra step keeps the matrix honest: break a policy on purpose. Comment out one permission check locally and confirm the matching cell fails. A matrix that stays green through a real regression is measuring nothing.
Automate the highest-impact rows for two users, two tenants, an expired token, and an oversized body. Save assertions for response, database state, and audit event, then make one policy fail and prove the test catches it.
Lesson completed