Find and fix weaknesses
Write security tests
Turn important abuse cases into repeatable tests for authorization, validation, secret handling, rate limits, and safe failure.
A security requirement becomes much stronger when a test proves it. “Users cannot read each other’s notes” is a sentence. A test that fails the day someone breaks it is a control.
Start with the failures that would hurt most — usually authorization — and work down. Keep these tests beside the behavior they protect, in the same suite, running on every commit.
The two-user pattern
Most authorization bugs are invisible to single-user tests. A route test signs in Alice and confirms she can read her note. Green. It never asks whether Bob can read the same note, so the missing ownership check stays invisible for as long as nobody thinks to ask.
Every ownership test needs two identities:
test('users cannot read notes they do not own', async () => {
const note = await createNote(alice, { title: 'draft' })
const res = await request(app)
.get(`/api/notes/${note.id}`)
.set('Cookie', bob.sessionCookie)
assert.equal(res.status, 403)
})
A 403 is not enough
For mutations, the status code is weak evidence on its own. A 403 alone proves little if the unauthorized update still reached a background job, or the row changed before the check fired. Assert three layers: the response, the stored state, and the emitted event.
test('denied update changes nothing', async () => {
const note = await createNote(alice, { title: 'original' })
const res = await request(app)
.patch(`/api/notes/${note.id}`)
.set('Cookie', bob.sessionCookie)
.send({ title: 'hijacked' })
assert.equal(res.status, 403)
const stored = await db.notes.find(note.id)
assert.equal(stored.title, 'original') // state unchanged
const events = await capturedEvents('note.update.denied')
assert.equal(events.length, 1) // the attempt was recorded
})
Widen the net
The same discipline covers the rest of your requirements. Send malformed and oversized input and assert clean rejection. Verify secrets never appear in public output — a test that greps API responses for token prefixes is cheap and catches painful bugs. Assert that a rate limit engages after repeated failed logins. And exercise expired and revoked credentials, not only missing ones: “signed out yesterday” is a different code path from “never signed in.”
Add the two-user test for one read and one mutation in your own project, then run the mutation test again with an expired session. Each test turns a security promise into something the build enforces.
Lesson completed