DEV Community

Roberto Luna
Roberto Luna

Posted on

Boosting Test Coverage & Securing the Construction Controller in a NestJS Monorepo

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
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

What went wrong:

  1. 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.
  2. 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.
  3. 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();
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)