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
Two issues were at play:
-
SQL typo – the
SELECTlist inbroker-portal.controller.tsreferenced a non‑existent columncountinstead of the aliastotal. -
Missing validation – the controller accepted any string for
statusand 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');
}
That patched the symptom for the two known statuses, but it:
- Hard‑coded the list, diverging from the
ReferralStatusenum. - 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);
}
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]);
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‑opsendMail()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);
});
});
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
})),
}));
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% |
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,
},
},
};
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
ReferralServiceto 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)