CodingNic

Integration Testing an API

Testing Error Responses

Integration Testing an API 20 min read

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

javascript
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

javascript
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

javascript
  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

javascript
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

text
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

  1. Build the two routes above, with requireAuth, requireRole, and validate middleware, and write all six tests.
  2. Add a test for POST /api/v1/posts with a missing body field entirely, and confirm it’s 400 with the correct field name in details.
  3. Add ownership-based authorization (Module 4) to a DELETE /api/v1/posts/:id route, and write tests for all three outcomes: wrong owner (403), correct owner (204), admin deleting someone else’s post (204).
  4. 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.