Exercises
Objectives
By the end of this lesson, you should be able to:
- Write a full integration test suite for an ownership-based route
- Use
beforeEachto reset state between tests - Cover all three outcomes of an ownership check with real requests
⚠️ A note on verification: every command and every output in this lesson was actually run, with real HTTP requests against a real Express app.
Exercise: Testing Ownership-Based Authorization
a) The route, from Module 4, unchanged:
app.delete('/api/v1/posts/:id', requireAuth, (req, res) => {
const post = posts.find(p => p.id === Number(req.params.id));
if (!post) return res.status(404).json({ error: 'NotFound' });
const isOwner = post.authorId === req.user.sub;
const isAdmin = req.user.role === 'admin';
if (!isOwner && !isAdmin) {
return res.status(403).json({ error: 'Forbidden', message: 'You do not own this post' });
}
posts = posts.filter(p => p.id !== post.id);
res.status(204).send();
});
b) A reset endpoint for tests. Deleting a post is destructive, each test needs the post to still exist beforehand, a small test-only endpoint resets it:
app.post('/api/v1/test-reset', (req, res) => {
posts = [{ id: 1, title: 'First Post', authorId: 1 }];
res.status(204).send();
});
c) The test suite, using beforeEach to reset state before every single test:
describe('DELETE /api/v1/posts/:id ownership', () => {
beforeEach(async () => {
await request(app).post('/api/v1/test-reset');
});
it('returns 403 when a non-owner, non-admin tries to delete', async () => {
const token = await tokenFor(2);
const res = await request(app).delete('/api/v1/posts/1').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(403);
});
it('returns 204 when the owner deletes their own post', async () => {
const token = await tokenFor(1);
const res = await request(app).delete('/api/v1/posts/1').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(204);
});
it("returns 204 when an admin deletes someone else's post", async () => {
const token = await tokenFor(3);
const res = await request(app).delete('/api/v1/posts/1').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(204);
});
});
beforeEach runs before every it in its describe block, without it, the second test to run would find the post already deleted by an earlier test, and fail for the wrong reason, entirely unrelated to ownership logic. This is a common integration-testing problem: tests that mutate shared state need that state reset between runs, or they end up depending on execution order, a fragile, hard-to-debug way for a suite to behave.
d) Run it:
PASS integrationtest3/app.test.js
DELETE /api/v1/posts/:id ownership
✓ returns 403 when a non-owner, non-admin tries to delete (28 ms)
✓ returns 204 when the owner deletes their own post (7 ms)
✓ returns 204 when an admin deletes someone else's post (8 ms)
All three outcomes of the ownership check, tested with real requests, real tokens for three different users, and a clean, predictable starting state for every single test.
e) Extend it. Add a fourth test, deleting a post that doesn’t exist (DELETE /api/v1/posts/999), and confirm it returns 404, not 403, add the 404 branch to the route first if it isn’t already handled correctly.
f) Reflect. In a real project, a /api/v1/test-reset endpoint would never ship to production, describe, in one or two sentences, an alternative way to reset state between tests that doesn’t involve adding test-only routes to the actual application.
Recap
This module replaced ad hoc manual verification with a real, automated integration test suite: Supertest sending real requests to a real Express app, tests covering authentication, authorization, and validation failures each with their correct status code, and beforeEach keeping tests independent of each other’s side effects. Combined with Module 7’s unit tests, this course’s authentication and authorization logic now has real, automatically-rerunnable coverage, not just a set of scripts that were correct the one time they were run by hand.
Next module: logging and error monitoring, making failures visible in production instead of silent.