CodingNic

Integration Testing an API

Testing Authenticated Routes

Integration Testing an API 15 min read

Testing Authenticated Routes

Objectives

By the end of this lesson, you should be able to:

  • Log in within a test to obtain a real token
  • Use that token to test a protected route
  • Confirm a protected route rejects requests with no token

💡 Why this matters: A protected route (Module 3, Module 4) can’t be tested with a single request, it needs a valid token first, obtained by actually logging in, exactly the two-step flow a real client goes through, this lesson tests that flow end to end.

⚠️ A note on verification: every snippet and every output in this lesson was actually run.

A Protected Route to Test

javascript
// added to app.js
const jwt = require('jsonwebtoken');
const JWT_SECRET = 'test-secret-for-this-course';

app.post('/api/v1/auth/login', async (req, res) => {
  const { email, password } = req.body;
  const user = users.find(u => u.email === email);
  if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
    return res.status(401).json({ error: 'InvalidCredentials', message: 'Incorrect email or password' });
  }
  const token = jwt.sign({ sub: user.id, email: user.email }, JWT_SECRET, { expiresIn: '1h' });
  res.json({ token });
});

function requireAuth(req, res, next) {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Unauthorized', message: 'Missing or invalid Authorization header' });
  }
  try {
    req.user = jwt.verify(header.slice(7), JWT_SECRET);
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Unauthorized', message: 'Invalid or expired token' });
  }
}

app.get('/api/v1/profile', requireAuth, (req, res) => {
  res.json({ email: req.user.email });
});

Nothing new here, this is exactly the login route and requireAuth middleware from Module 3, unchanged.

Testing the Rejection Case First

javascript
describe('GET /api/v1/profile', () => {
  it('rejects a request with no token', async () => {
    const res = await request(app).get('/api/v1/profile');
    expect(res.status).toBe(401);
  });

No Authorization header at all, requireAuth rejects it before the route handler ever runs, worth testing first, since it needs no setup.

Logging In Within the Test, Then Using the Token

javascript
  it('returns the profile for a logged-in user', async () => {
    await request(app).post('/api/v1/auth/register').send({ email: 'maya@example.com', password: 'sunshine123' });
    const loginRes = await request(app).post('/api/v1/auth/login').send({ email: 'maya@example.com', password: 'sunshine123' });
    const token = loginRes.body.token;

    const profileRes = await request(app).get('/api/v1/profile').set('Authorization', `Bearer ${token}`);
    expect(profileRes.status).toBe(200);
    expect(profileRes.body).toEqual({ email: 'maya@example.com' });
  });

Three real requests in sequence, register, log in, then call the protected route, exactly what a real client does, loginRes.body.token is a real, freshly-signed JWT, not a hardcoded stand-in, .set('Authorization', ...) attaches it to the third request exactly as a real client would.

Testing a Garbage Token

javascript
  it('rejects a request with a garbage token', async () => {
    const res = await request(app).get('/api/v1/profile').set('Authorization', 'Bearer not-a-real-token');
    expect(res.status).toBe(401);
  });
});

jwt.verify (inside requireAuth) throws on a token it can’t parse or verify, caught and turned into a 401, this confirms that path specifically, not just the “no header at all” case from the first test.

Running All Four Together

text
PASS integrationtest/app.test.js
  GET /api/v1/profile
    ✓ rejects a request with no token (4 ms)
    ✓ returns the profile for a logged-in user (149 ms)
    ✓ rejects a request with a garbage token (4 ms)

Three distinct outcomes for the same endpoint, each backed by a real request, together they cover the endpoint’s full contract: no token rejected, valid token accepted, invalid token rejected.

Try It

  1. Add the login route and requireAuth middleware to app.js, add /api/v1/profile, and write the three tests above.
  2. Add a test for a token signed with the wrong secret (jwt.sign(payload, 'wrong-secret')), and confirm it’s also rejected with 401.
  3. Add a test confirming the token from one registered user doesn’t return a different user’s profile, using two registered users and checking each token only returns its own email.
  4. Explain, in one or two sentences, why obtaining the token by actually calling /api/v1/auth/login inside the test is more valuable than hardcoding a manually-generated token string.

Recap

  • Testing a protected route requires logging in first, within the test, to obtain a real token, then attaching it with .set('Authorization', ...).
  • A protected route’s full contract includes at least three cases: no token, valid token, invalid token, each deserves its own test.
  • Chaining real requests together, register then login then access, tests the actual flow a real client goes through, not just isolated pieces of it.

Next lesson: testing error responses more broadly, confirming 400, 401, and 403 come back exactly when they should, across an entire route.