Boosting Test Coverage & Securing the Construction Controller in a NestJS Monorepo
TL;DR: I added a full‑stack Jest suite for the public Meta webhook, tightened request validation, and bumped the construction.controller.ts coverage from 1 % to >85 %. The changes live in apps/api/src/__tests__/whatsapp‑ai.test.ts and a few guards/helpers, and they illustrate how to lock down external webhooks without inflating build times.
The Problem
Our monorepo (content‑automation) ships a NestJS API that exposes a public webhook (/whatsapp/ai) used by Meta’s WhatsApp Business Cloud API. The controller (construction.controller.ts) was barely exercised – the badge on the README showed 1 % coverage. Worse, the endpoint accepted any payload, making us vulnerable to malformed requests and replay attacks. The CI pipeline kept failing on the coverage threshold (npm run test:cov expects ≥80 %).
Typical symptom in the logs:
FAIL src/construction.controller.ts
1) should return 200 on valid webhook payload
0) should reject request with missing X-Hub-Signature header
0) should reject request with invalid JSON schema
We needed a reliable test harness and a security layer that could be verified automatically.
What I Tried First
My first attempt was to sprinkle a couple of unit tests directly in the controller file, using jest.spyOn to mock the service layer. I wrote a single test in apps/api/src/__tests__/construction.controller.spec.ts that called the handler with a hard‑coded object.
it('returns 200 for a valid payload', async () => {
const resp = await request(app.getHttpServer())
.post('/whatsapp/ai')
.send({ entry: [{ changes: [{ value: { messages: [] } }] }] })
.set('X-Hub-Signature', 'sha1=validsignature');
expect(resp.status).toBe(200);
});
What went wrong:
- No request validation – the test passed even when the payload missed required fields, because Nest’s built‑in validation pipe wasn’t enabled for this route.
- Signature verification was bypassed – I had hard‑coded the signature header, but the verification logic lived in a separate guard that never executed in the test context.
- Coverage stayed at 1 % – the test only touched the controller’s entry point; the internal service methods and guard remained unexecuted.
The result was a false sense of security and still a failing CI job.
The Implementation
1. Enable Global Validation Pipe
In apps/api/src/main.ts I added:
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.listen(3000);
}
bootstrap();
Now any request that does not conform to the DTO schema throws a BadRequestException, which is automatically caught by Nest’s exception filter.
2. Create a Signature Guard
File: apps/api/src/guards/meta-signature.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import * as crypto from 'crypto';
@Injectable()
export class MetaSignatureGuard implements CanActivate {
private readonly secret = process.env.META_APP_SECRET;
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const signature = req.headers['x-hub-signature'] as string;
if (!signature) {
throw new UnauthorizedException('Missing X-Hub-Signature header');
}
const hash = crypto
.createHmac('sha1', this.secret)
.update(JSON.stringify(req.body))
.digest('hex');
const expected = `sha1=${hash}`;
if (signature !== expected) {
throw new UnauthorizedException('Invalid signature');
}
return true;
}
}
The guard is applied in the controller:
import { UseGuards } from '@nestjs/common';
import { MetaSignatureGuard } from '../guards/meta-signature.guard';
@UseGuards(MetaSignatureGuard)
@Post('whatsapp/ai')
async handleWebhook(@Body() payload: WhatsAppDto) {
return this.constructionService.process(payload);
}
3. Add a Full‑stack Jest Suite
File: apps/api/src/__tests__/whatsapp-ai.test.ts
ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import * as crypto from 'crypto';
import { AppModule } from '../../app.module';
describe('Meta WhatsApp Webhook (e2e)', () => {
let app: INestApplication;
const secret = 'test_secret';
// Helper to compute a valid signature
const sign = (payload: any) => {
const hash = crypto.createHmac('sha1', secret).update(JSON.stringify(payload)).digest('hex');
return `sha1=${hash}`;
};
const validPayload = {
entry: [
{
changes: [
{
value: {
messages: [{ from: '12345', text: { body: 'Hello' } }],
},
},
],
},
],
};
beforeAll(async () => {
process.env.META_APP_SECRET = secret;
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.init();
});
afterAll(async () => {
await app.close();
});
it('accepts a valid signed payload', async () => {
const res = await request(app.getHttpServer())
.post('/whatsapp/ai')
.send(validPayload)
.set('X-Hub-Signature', sign(validPayload));
expect(res.status).toBe(200);
expect(res.body).toEqual({ success: true });
});
it('rejects missing signature', async () => {
const res = await request(app.getHttpServer())
.post('/whatsapp/ai')
.send(validPayload);
expect(res.status).toBe(401);
expect(res.body.message).toContain('Missing X-Hub-Signature');
});
it('rejects malformed payload', async () => {
const badPayload = { foo: 'bar' };
const res = await request(app.getHttpServer())
.post('/whatsapp/ai')
.send(bad
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*
*Repo: `zaerohell/content-automation` · 2026-09-12*
\#playadev #buildinpublic
Top comments (0)