Testing Security, Not Just Functionality
The gap in most test suites
Most apps have tests like: “User can sign up,” “User can log in,” “User can create a note.” These are functional tests — they verify the happy path works.
But auth security is about the unhappy path. The important questions are: “Can a user see another user’s notes?” “Does rate limiting actually kick in after 20 attempts?” “Does a revoked token actually get rejected?” “If I send user_id: 'admin-1' in the request body, does the server ignore it?”
These are security tests. They verify that defenses work, not just that features work. And they are almost always missing.
Why security tests matter more
A failing functional test means a user cannot do something they should be able to do. Annoying, but you will hear about it quickly (the user complains).
A failing security test means a user can do something they should not be able to do. Dangerous, and you will not hear about it until it is exploited. The user who benefits from the bug (the attacker) is not going to file a support ticket.
Security bugs are silent. Tests are how you find them before attackers do.
What security tests look like
A functional test for login:
it("valid credentials return a session", async () => {
const res = await login("[email protected]", "password123");
assert.strictEqual(res.status, 200);
assert.ok(res.headers.get("set-cookie"));
}); Security tests for the same endpoint:
it("wrong password returns 401", async () => {
const res = await login("[email protected]", "wrongpassword");
assert.strictEqual(res.status, 401);
});
it("non-existent email returns 401 (not 404)", async () => {
const res = await login("[email protected]", "anything");
assert.strictEqual(res.status, 401); // Same as wrong password — no enumeration
});
it("response time is similar for existing and non-existing users", async () => {
const t1 = Date.now();
await login("[email protected]", "wrong");
const d1 = Date.now() - t1;
const t2 = Date.now();
await login("[email protected]", "wrong");
const d2 = Date.now() - t2;
// Both should take roughly the same time (dummy hash)
assert.ok(Math.abs(d1 - d2) < 100, "Timing difference too large");
}); The functional test verifies one thing. The security tests verify three things the endpoint must not do: reveal whether an email exists, leak timing information, or return a session for wrong credentials.
The testing mindset
For every feature, ask:
- What should happen when the input is valid? (Functional test)
- What should happen when the input is invalid? (Security test)
- What should happen when the user is not who they claim to be? (Security test)
- What should happen when the user tries to access something they should not? (Security test)
- What should happen under abuse conditions (many requests, large payloads)? (Security test)
This course teaches you to write all five categories.
Why are security bugs harder to catch than functional bugs?