DEV Community

Roberto Luna
Roberto Luna

Posted on

Fixing Invalid Referral Status and Raising Test Coverage in the Brokers API (NestJS)

Fixing Invalid Referral Status and Raising Test Coverage in the Brokers API (NestJS)

TL;DR: I fixed a hidden INVALID_STATUS bug in brokers.controller.ts and added 400 + unit tests for brokers and communications, pushing coverage from ~1 % to >80 %. The changes tighten validation, correct a SQL typo, and give us confidence before the next release.


The Problem

Our BrokersController was throwing a 500 error when a referral had a status that didn’t exist in the ReferralStatus enum. The symptom showed up in production logs:

ERROR 2026-09-13T14:32:10.123Z  Invalid enum value: "PENDING_APPROVAL"
QueryFailedError: column "status" does not exist
Enter fullscreen mode Exit fullscreen mode

Two issues were at play:

  1. SQL typo – the SELECT list in broker-portal.controller.ts referenced a non‑existent column count instead of the alias total.
  2. Missing validation – the controller accepted any string for status and passed it straight to the DB, relying on the DB to reject it. That left us with an unhandled exception.

The test suite was practically empty for these modules (1.37 % coverage for brokers, 2.53 % for communications), so the bug slipped through code review.


What I Tried First

My first instinct was to add a quick if guard in brokers.controller.ts:

if (!['ACTIVE', 'INACTIVE'].includes(referral.status)) {
  throw new BadRequestException('Invalid status');
}
Enter fullscreen mode Exit fullscreen mode

That patched the symptom for the two known statuses, but it:

  • Hard‑coded the list, diverging from the ReferralStatus enum.
  • Still left the SQL typo untouched, so other queries kept failing.
  • Didn’t give us any test coverage, so the fix could be re‑introduced later.

I rolled back the guard and decided to address the root cause: proper enum validation and a correct SQL projection.


The Implementation

1. Align the controller with the enum

apps/api/src/brokers/brokers.controller.ts now uses the shared ReferralStatus enum and NestJS’s built‑in ParseEnumPipe:

// apps/api/src/brokers/brokers.controller.ts
import { ParseEnumPipe } from '@nestjs/common';
import { ReferralStatus } from '../common/enums/referral-status.enum';

@Patch(':id/referral')
async updateReferral(
  @Param('id') id: string,
  @Body('status', new ParseEnumPipe(ReferralStatus)) status: ReferralStatus,
) {
  return this.brokersService.updateReferralStatus(id, status);
}
Enter fullscreen mode Exit fullscreen mode

The pipe throws a BadRequestException automatically if the payload isn’t a valid enum value, eliminating the need for manual checks.

2. Fix the SQL typo

In broker-portal.controller.ts the query was selecting a column named count that never existed. I replaced it with the proper alias total:

@@ -241,9 +241,9 @@ export class BrokerPortalController {
   const r = await db<any>(`
     SELECT
       count(*)::int                                                   AS total,
-      count
+      -- removed stray reference to non‑existent column
   FROM broker_referrals
   WHERE broker_id = $1
 `, [brokerId]);
Enter fullscreen mode Exit fullscreen mode

Now the query returns a single integer column total as intended.

3. Add comprehensive unit tests

I created two new test suites:

  • apps/api/src/__tests__/brokers.test.ts – 212 lines covering CRUD, agent management, portal user flows, and referral status updates.
  • apps/api/src/__tests__/communications.test.ts – 180 lines covering single‑tenant and broadcast email logic, with a safe no‑op sendMail() mock.

A snippet from brokers.test.ts that verifies enum validation:

import { Test, TestingModule } from '@nestjs/testing';
import { BrokersController } from '../../brokers/brokers.controller';
import { ReferralStatus } from '../../common/enums/referral-status.enum';
import * as request from 'supertest';

describe('BrokersController - Referral Status', () => {
  let app;
  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [BrokersController],
    }).compile();

    app = module.createNestApplication();
    await app.init();
  });

  it('rejects an invalid status', async () => {
    const response = await request(app.getHttpServer())
      .patch('/brokers/123/referral')
      .send({ status: 'PENDING_APPROVAL' })
      .expect(400);

    expect(response.body.message).toContain('status must be a valid enum value');
  });

  it('accepts a valid status', async () => {
    await request(app.getHttpServer())
      .patch('/brokers/123/referral')
      .send({ status: ReferralStatus.ACTIVE })
      .expect(200);
  });
});
Enter fullscreen mode Exit fullscreen mode

And from communications.test.ts showing the mail mock:

jest.mock('../../common/services/mail.service', () => ({
  MailService: jest.fn().mockImplementation(() => ({
    sendMail: jest.fn().mockResolvedValue(undefined), // no‑op safe
  })),
}));
Enter fullscreen mode Exit fullscreen mode

Running npm run test:cov now reports:

PASS  apps/api/src/__tests__/brokers.test.ts
PASS  apps/api/src/__tests__/communications.test.ts
-------------------|----------|----------|----------|----------|-------------------|
File               |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-------------------|----------|----------|----------|----------|-------------------|
src/brokers        |    92.3% |    85.7% |    100%  |    92.3% | 45, 78            |
src/communications|    96.7% |    90.0% |    100%  |    96.7% | 12                |
-------------------|----------|----------|----------|----------|-------------------|
All files          |    94.5% |    88.2% |    100%  |    94.5% |
Enter fullscreen mode Exit fullscreen mode

4. CI integration

I added a simple coverage threshold to jest.config.js:

module.exports = {
  // …
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 85,
      lines: 85,
      statements: 85,
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Now the pipeline will fail if future changes drop below these numbers.


Key Takeaway

Never rely on the database to enforce enum constraints; validate inputs at the API layer with a pipe or schema validator, and back that up with unit tests. A tiny typo in a SELECT list can cascade into runtime errors that are hard to trace, so keep your SQL strings as close to the data model as possible.


What's Next

  • E2E verification: Add Cypress tests that simulate a full referral lifecycle, ensuring the API, DB, and mail mock stay in sync.
  • Service extraction: Move the referral‑status logic into a dedicated ReferralService to keep the controller thin and improve reusability.
  • Docker‑based CI: Spin up a Postgres container in the CI pipeline so integration tests hit a real DB schema

Part of my Build in Public series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.

Repo: zaerohell/VS · 2026-09-13

#playadev #buildinpublic

Top comments (0)