I have a NestJS Controller: search.controller.ts
import { Body, Controller, Post, Req, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from '../exception/http-exception.filter';
import { SearchData } from './models/search-data.model';
import { SearchResults } from 'interfaces';
import { SearchService } from './search.service';
@Controller('')
@UseFilters(HttpExceptionFilter)
export class SearchController {
constructor(private searchService: SearchService) {}
@Post('api/search')
async searchDataById(
@Body() searchData: SearchData,
@Req() req
): Promise<SearchResults> {
return await this.searchService.getSearchResultsById(
searchData,
token
);
}
}
This search controller uses Filters named HttpExceptionFilter. This Filter gets triggered whenever there is an HttpException thrown. I have created ServiceException which extends HttpException. I throw new ServiceException() whenever there is an error.
HttpExceptionFilter
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException
} from '@nestjs/common';
import { ErrorDetails } from './error-details.interface';
import { HTTP_ERRORS } from './errors.constant';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();
const api = exception.getResponse() as string;
const errorDetails = this.getErrorDetails(api, status);
response.status(status).json({
status: status,
title: errorDetails.title,
message: errorDetails.message
});
}
private getErrorDetails(api: string, status: string | number): ErrorDetails {
const errorDetails: ErrorDetails = {
title: HTTP_ERRORS.GENERAL.ERROR.title,
message: HTTP_ERRORS.GENERAL.ERROR.message
};
// if rejection status is logged out or toke expired then redirect to login
if (
HTTP_ERRORS.hasOwnProperty(api) &&
HTTP_ERRORS[api].hasOwnProperty(status)
) {
errorDetails.title = HTTP_ERRORS[api][status].title;
errorDetails.message = HTTP_ERRORS[api][status].message;
}
return errorDetails;
}
}
ServiceException
import { HttpException } from '@nestjs/common';
export class ServiceException extends HttpException {
constructor(private details, private code) {
super(details, code);
}
}
search.service.ts
import { APIS } from '../app.constants';
import { HttpService, HttpStatus, Injectable } from '@nestjs/common';
import { SearchData, SearchResultSchema } from './models/search-data.model';
import { AppConfigService } from '../app-config/app-config.service';
import { AxiosResponse } from 'axios';
import { DataMappingPayload } from './models/data-mapping-payload.model';
import { SEARCH_SCHEMAS } from './search.constants';
import { SearchModelMapper } from './search-model-mapper.service';
import { SearchResults } from '@delfi-data-management/interfaces';
import { ServiceException } from '../exception/service.exception';
@Injectable()
export class SearchService {
constructor(
private searchModelMapper: SearchModelMapper,
private configService: AppConfigService,
private readonly httpService: HttpService
) {}
// eslint-disable-next-line max-lines-per-function
async getSearchResultsById(
searchData: SearchData,
stoken: string
): Promise<SearchResults> {
if (searchData.filters.collectionId && searchData.viewType) {
if (
Object.values(SEARCH_SCHEMAS).indexOf(
searchData.viewType as SEARCH_SCHEMAS
) !== -1
) {
try {
...... some code cant paste here
return this.searchModelMapper.generateSearchResults(
kinds,
mappingPayload,
searchResultsAPI.data.results
);
} catch (error) {
throw new ServiceException(
APIS.SEARCH,
HttpStatus.INTERNAL_SERVER_ERROR
);
}
} else {
throw new ServiceException(APIS.SEARCH, HttpStatus.BAD_REQUEST);
}
} else if (!searchData.filters.collectionId) {
throw new ServiceException(APIS.SEARCH, HttpStatus.BAD_REQUEST);
} else {
throw new ServiceException(APIS.SEARCH, HttpStatus.BAD_REQUEST);
}
}
Now the thing never reaches to HttpExceptionFilter file in the unit tests
search.service.spec.ts
beforeEach(async () => {
const app = await Test.createTestingModule({
imports: [AppConfigModule, HttpModule, SearchModule]
}).compile();
searchService = app.get<SearchService>(SearchService);
});
it('should throw error message if viewType not provided', () => {
const searchDataquery = {
filters: {
collectionId: 'accd'
},
viewType: ''
};
const result = searchService.getSearchResultsById(searchDataquery, 'abc');
result.catch((error) => {
expect(error.response).toEqual(
generateSearchResultsErrorResponse.viewTypeError
);
});
});
Is there a reason why throw new ServiceException, which internally triggers HttpException does not trigger HttpExceptionFilter?
Filters are not bound during unit tests because they require the request context for Nest to bind properly (it's how Nest handles the lifecycle of the request). As unit tests do not have an incoming HTTP request, the lifecycle is only seen as what you are calling explicitly, in this case: SearchSerivce
. If you are looking to test a filter, you should set up and e2e type test where you use supertest to send in HTTP requests and allow your filter to catch during the request.