Testing Error Responses
Objectives
By the end of this lesson, you should be able to:
- Write tests confirming 401, 403, and 400 each come back exactly when they should
- Test a route with multiple middleware layers, confirming they run in the correct order
- Recognize authentication, authorization, and validation failures as three distinct, separately testable cases
💡 Why this matters: A route like
POST /api/v1/posts(Module 5) has more than one way to fail, no token, invalid input, and each needs a specific, correct status code. This lesson tests all of them on the same route, confirming the middleware chain behaves correctly as a whole.
⚠️ A note on verification: every snippet and every output in this lesson was actually run.
A Route with Two Middleware Layers
app.post('/api/v1/posts', requireAuth, validate(postSchema), (req, res) => {
res.status(201).json(req.validatedBody);
});
requireAuth (Module 3) runs first, validate(postSchema) (Module 5) runs second, only if authentication passed. This ordering matters, and is exactly what these tests confirm.
Testing the Authentication Failure
describe('POST /api/v1/posts validation errors', () => {
it('returns 401 with no token', async () => {
const res = await request(app).post('/api/v1/posts').send({ title: 'Hi', body: 'Post body' });
expect(res.status).toBe(401);
});
No token, requireAuth rejects the request before validate ever runs, even though the body itself (title: 'Hi') would also fail validation, 401 is the correct response here, not 400, confirming the middleware order.
Testing the Validation Failure, While Authenticated
it('returns 400 for a title that is too short, even when authenticated', async () => {
const token = await tokenFor(1);
const res = await request(app).post('/api/v1/posts').set('Authorization', `Bearer ${token}`).send({ title: 'Hi', body: 'Post body' });
expect(res.status).toBe(400);
expect(res.body.details[0].field).toBe('title');
});
it('returns 201 for valid, authenticated input', async () => {
const token = await tokenFor(1);
const res = await request(app).post('/api/v1/posts').set('Authorization', `Bearer ${token}`).send({ title: 'Hello World', body: 'Post body' });
expect(res.status).toBe(201);
});
});
With a valid token this time, the same too-short title now correctly reaches validate, and is rejected with 400, not 401, this pair of tests, same bad body, with and without a token, confirms both middleware layers are actually being reached and actually doing their own job, not just one masking the other.
Testing Role-Based Errors
describe('GET /api/v1/admin/users role errors', () => {
it('returns 401 with no token', async () => {
const res = await request(app).get('/api/v1/admin/users');
expect(res.status).toBe(401);
});
it('returns 403 for an authenticated non-admin', async () => {
const token = await tokenFor(1);
const res = await request(app).get('/api/v1/admin/users').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(403);
});
it('returns 200 for an authenticated admin', async () => {
const token = await tokenFor(2);
const res = await request(app).get('/api/v1/admin/users').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body).toHaveLength(2);
});
});
Three outcomes on the same route: 401 for no authentication at all, 403 for authenticated but wrong role (Module 4’s authentication-vs-authorization distinction, tested directly), 200 for the correct role. Getting a 403 instead of a 401, or vice versa, is a real, meaningful bug, these tests catch either mistake specifically.
Running Everything
PASS integrationtest2/app.test.js
POST /api/v1/posts validation errors
✓ returns 401 with no token (26 ms)
✓ returns 400 for a title that is too short, even when authenticated (11 ms)
✓ returns 201 for valid, authenticated input (6 ms)
GET /api/v1/admin/users role errors
✓ returns 401 with no token (2 ms)
✓ returns 403 for an authenticated non-admin (5 ms)
✓ returns 200 for an authenticated admin (7 ms)
Six tests, two routes, each covering its full range of outcomes, this is what a real integration test suite for a secured API looks like, not just “does it work”, but “does it fail correctly, in every way it’s supposed to”.
Try It
- Build the two routes above, with
requireAuth,requireRole, andvalidatemiddleware, and write all six tests. - Add a test for
POST /api/v1/postswith a missingbodyfield entirely, and confirm it’s400with the correct field name indetails. - Add ownership-based authorization (Module 4) to a
DELETE /api/v1/posts/:idroute, and write tests for all three outcomes: wrong owner (403), correct owner (204), admin deleting someone else’s post (204). - Explain, in one or two sentences, why testing “too-short title, no token” and “too-short title, with a token” as two separate tests is more informative than testing just one of them.
Recap
- A route can fail in more than one way, authentication, authorization, validation, each deserves its own specific status code and its own test.
- Testing the same invalid body both with and without authentication confirms middleware layers are each doing their own job, not one accidentally masking another.
- A thorough integration test suite covers a route’s full range of outcomes, not just its single success case.
Next lesson: exercises, building a complete integration test suite for a route combining authentication, roles, and validation together.