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
// 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
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
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
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
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
- Add the login route and
requireAuthmiddleware toapp.js, add/api/v1/profile, and write the three tests above. - Add a test for a token signed with the wrong secret (
jwt.sign(payload, 'wrong-secret')), and confirm it’s also rejected with401. - 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.
- Explain, in one or two sentences, why obtaining the token by actually calling
/api/v1/auth/logininside 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.