diff --git a/e2e/helper/bootstrap-module.ts b/e2e/helper/bootstrap-module.ts index 49d77e528..13e9afe84 100644 --- a/e2e/helper/bootstrap-module.ts +++ b/e2e/helper/bootstrap-module.ts @@ -41,7 +41,10 @@ class ApplicationModule { useMongoose: boolean, useSequelize: boolean, ): DynamicModule { - const imports = [TerminusModule.forRootAsync(options)]; + const imports = [ + TerminusModule.forRootAsync(options), + ...(options.imports || []), + ]; if (useDb) { imports.push(DbModule); diff --git a/lib/health-indicator/http/http.health.spec.ts b/lib/health-indicator/http/http.health.spec.ts index dc4a096f3..c76391084 100644 --- a/lib/health-indicator/http/http.health.spec.ts +++ b/lib/health-indicator/http/http.health.spec.ts @@ -1,7 +1,7 @@ import { Test } from '@nestjs/testing'; import { HealthCheckError } from '../../health-check/health-check.error'; import { HttpService } from '@nestjs/common'; -import { of, throwError } from 'rxjs'; +import { EMPTY, of, throwError } from 'rxjs'; import { AxiosResponse, AxiosRequestConfig, AxiosError } from 'axios'; import { HttpHealthIndicator } from './http.health'; @@ -28,11 +28,222 @@ describe('Http Response Health Indicator', () => { httpService = moduleRef.get(HttpService); }); - describe('success conditions', () => { - describe('async callback', () => { - it("async callback returning true should return key with status='up'", async () => { + describe('#pingCheck', () => { + it('should make use of a custom httpClient', async () => { + const httpServiceMock = ({ + request: jest.fn().mockReturnValue(EMPTY), + } as any) as HttpService; + await httpHealthIndicator.pingCheck('key', 'url', { + httpClient: httpServiceMock, + }); + + expect(httpServiceMock.request).toHaveBeenCalledWith({ url: 'url' }); + }); + }); + + describe('#responseCheck', () => { + describe('success conditions', () => { + describe('async callback', () => { + it("async callback returning true should return key with status='up'", async () => { + const key = 'key value'; + const url = 'url value'; + + interface IHttpResponse { + a: number; + b: string; + c: boolean; + } + + const mockResponse = { + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + data: { + a: 17, + b: 'some string value', + c: true, + }, + }; + + const httpServiceRequestSpy = jest + .spyOn(httpService, 'request') + .mockImplementation(() => of(mockResponse)); + + const f = async (response: AxiosResponse) => { + expect(response).toStrictEqual(mockResponse); + + return true; + }; + + const indicatorResponse = await httpHealthIndicator.responseCheck( + key, + url, + f, + ); + + expect(indicatorResponse).toStrictEqual({ + [key]: { + status: 'up', + }, + }); + + expect(httpServiceRequestSpy).toHaveBeenCalledWith({ + url, + }); + }); + + it("async callback returning false should return key with status='down'", async () => { + const key = 'key value'; + const url = 'url value'; + + interface IHttpResponse { + a: number; + b: string; + c: boolean; + } + + const mockResponse = { + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + data: { + a: 17, + b: 'some string value', + c: true, + }, + }; + + const httpServiceRequestSpy = jest + .spyOn(httpService, 'request') + .mockImplementation(() => of(mockResponse)); + + const callback = async (response: AxiosResponse) => { + expect(response).toStrictEqual(mockResponse); + + return false; + }; + + try { + await httpHealthIndicator.responseCheck(key, url, callback); + fail('a HealthCheckError should have been thrown'); + } catch (err) { + expect(err instanceof HealthCheckError).toBeTruthy(); + expect(err.message).toEqual(`${key} is not available`); + expect(err.causes).toStrictEqual({ + 'key value': { + status: 'down', + }, + }); + } + }); + }); + + describe('non-async callback', () => { + it("callback returning true should return key with status='up'", async () => { + const key = 'key value'; + const url = 'url value'; + + interface IHttpResponse { + a: number; + b: string; + c: boolean; + } + + const mockResponse = { + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + data: { + a: 17, + b: 'some string value', + c: true, + }, + }; + + const httpServiceRequestSpy = jest + .spyOn(httpService, 'request') + .mockImplementation(() => of(mockResponse)); + + const callback = (response: AxiosResponse) => { + expect(response).toStrictEqual(mockResponse); + + return true; + }; + + const indicatorResponse = await httpHealthIndicator.responseCheck( + key, + url, + callback, + ); + + expect(indicatorResponse).toStrictEqual({ + [key]: { + status: 'up', + }, + }); + + expect(httpServiceRequestSpy).toHaveBeenCalledWith({ + url, + }); + }); + + it("callback returning false should return key with status='down'", async () => { + const key = 'key value'; + const url = 'url value'; + + interface IHttpResponse { + a: number; + b: string; + c: boolean; + } + + const mockResponse = { + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + data: { + a: 17, + b: 'some string value', + c: true, + }, + }; + + const httpServiceRequestSpy = jest + .spyOn(httpService, 'request') + .mockImplementation(() => of(mockResponse)); + + const callback = (response: AxiosResponse) => { + expect(response).toStrictEqual(mockResponse); + + return false; + }; + + try { + await httpHealthIndicator.responseCheck(key, url, callback); + fail('a HealthCheckError should have been thrown'); + } catch (err) { + expect(err instanceof HealthCheckError).toBeTruthy(); + expect(err.message).toEqual(`${key} is not available`); + expect(err.causes).toStrictEqual({ + 'key value': { + status: 'down', + }, + }); + } + + expect(httpServiceRequestSpy).toHaveBeenCalledWith({ + url, + }); + }); + }); + + it('url being supplied as URL instance should be accepted', async () => { const key = 'key value'; - const url = 'url value'; + const url = new URL('http://127.0.0.1'); interface IHttpResponse { a: number; @@ -56,7 +267,7 @@ describe('Http Response Health Indicator', () => { .spyOn(httpService, 'request') .mockImplementation(() => of(mockResponse)); - const f = async (response: AxiosResponse) => { + const callback = (response: AxiosResponse) => { expect(response).toStrictEqual(mockResponse); return true; @@ -65,7 +276,7 @@ describe('Http Response Health Indicator', () => { const indicatorResponse = await httpHealthIndicator.responseCheck( key, url, - f, + callback, ); expect(indicatorResponse).toStrictEqual({ @@ -75,11 +286,11 @@ describe('Http Response Health Indicator', () => { }); expect(httpServiceRequestSpy).toHaveBeenCalledWith({ - url, + url: url.toString(), }); }); - it("async callback returning false should return key with status='down'", async () => { + it('additional options should be passed to httpService.request', async () => { const key = 'key value'; const url = 'url value'; @@ -105,64 +316,25 @@ describe('Http Response Health Indicator', () => { .spyOn(httpService, 'request') .mockImplementation(() => of(mockResponse)); - const callback = async (response: AxiosResponse) => { + const callback = (response: AxiosResponse) => { expect(response).toStrictEqual(mockResponse); - return false; + return true; }; - try { - await httpHealthIndicator.responseCheck(key, url, callback); - fail('a HealthCheckError should have been thrown'); - } catch (err) { - expect(err instanceof HealthCheckError).toBeTruthy(); - expect(err.message).toEqual(`${key} is not available`); - expect(err.causes).toStrictEqual({ - 'key value': { - status: 'down', - }, - }); - } - }); - }); - - describe('non-async callback', () => { - it("callback returning true should return key with status='up'", async () => { - const key = 'key value'; - const url = 'url value'; - - interface IHttpResponse { - a: number; - b: string; - c: boolean; - } - - const mockResponse = { - status: 200, - statusText: 'OK', - headers: {}, - config: {}, - data: { - a: 17, - b: 'some string value', - c: true, + const options: AxiosRequestConfig = { + maxRedirects: 3, + auth: { + username: 'username value', + password: 'password value', }, }; - const httpServiceRequestSpy = jest - .spyOn(httpService, 'request') - .mockImplementation(() => of(mockResponse)); - - const callback = (response: AxiosResponse) => { - expect(response).toStrictEqual(mockResponse); - - return true; - }; - const indicatorResponse = await httpHealthIndicator.responseCheck( key, url, callback, + options, ); expect(indicatorResponse).toStrictEqual({ @@ -173,10 +345,13 @@ describe('Http Response Health Indicator', () => { expect(httpServiceRequestSpy).toHaveBeenCalledWith({ url, + ...options, }); }); + }); - it("callback returning false should return key with status='down'", async () => { + describe('error conditions', () => { + it('request throwing AxiosError should throw error with message and statusCode/statusText from response', async () => { const key = 'key value'; const url = 'url value'; @@ -187,8 +362,8 @@ describe('Http Response Health Indicator', () => { } const mockResponse = { - status: 200, - statusText: 'OK', + status: 500, + statusText: 'status text value', headers: {}, config: {}, data: { @@ -198,287 +373,135 @@ describe('Http Response Health Indicator', () => { }, }; - const httpServiceRequestSpy = jest - .spyOn(httpService, 'request') - .mockImplementation(() => of(mockResponse)); - - const callback = (response: AxiosResponse) => { - expect(response).toStrictEqual(mockResponse); + jest.spyOn(httpService, 'request').mockImplementation(() => { + return throwError({ + name: 'error name value', + message: 'axios.request threw an error for some reason', + config: {}, + code: 'status code value', + request: {}, + response: mockResponse, + isAxiosError: true, + toJSON: () => ({}), + } as AxiosError); + }); - return false; - }; + const callback = fail.bind( + null, + 'callback should not have been called', + ); try { await httpHealthIndicator.responseCheck(key, url, callback); - fail('a HealthCheckError should have been thrown'); } catch (err) { expect(err instanceof HealthCheckError).toBeTruthy(); - expect(err.message).toEqual(`${key} is not available`); + expect(err.message).toEqual( + 'axios.request threw an error for some reason', + ); expect(err.causes).toStrictEqual({ 'key value': { status: 'down', + message: 'axios.request threw an error for some reason', + statusCode: 500, + statusText: 'status text value', }, }); } - - expect(httpServiceRequestSpy).toHaveBeenCalledWith({ - url, - }); }); - }); - - it('url being supplied as URL instance should be accepted', async () => { - const key = 'key value'; - const url = new URL('http://127.0.0.1'); - - interface IHttpResponse { - a: number; - b: string; - c: boolean; - } - - const mockResponse = { - status: 200, - statusText: 'OK', - headers: {}, - config: {}, - data: { - a: 17, - b: 'some string value', - c: true, - }, - }; - const httpServiceRequestSpy = jest - .spyOn(httpService, 'request') - .mockImplementation(() => of(mockResponse)); - - const callback = (response: AxiosResponse) => { - expect(response).toStrictEqual(mockResponse); + it('request throwing AxiosError w/o response should only include message', async () => { + const key = 'key value'; + const url = 'url value'; - return true; - }; + interface IHttpResponse {} - const indicatorResponse = await httpHealthIndicator.responseCheck( - key, - url, - callback, - ); + jest.spyOn(httpService, 'request').mockImplementation(() => { + return throwError({ + name: 'error name value', + message: 'axios.request threw an error for some reason', + config: {}, + code: 'status code value', + request: {}, + response: undefined, + isAxiosError: true, + toJSON: () => ({}), + } as AxiosError); + }); - expect(indicatorResponse).toStrictEqual({ - [key]: { - status: 'up', - }, - }); + const callback = fail.bind( + null, + 'callback should not have been called', + ); - expect(httpServiceRequestSpy).toHaveBeenCalledWith({ - url: url.toString(), + try { + await httpHealthIndicator.responseCheck(key, url, callback); + } catch (err) { + expect(err instanceof HealthCheckError).toBeTruthy(); + expect(err.message).toEqual( + 'axios.request threw an error for some reason', + ); + expect(err.causes).toStrictEqual({ + 'key value': { + status: 'down', + message: 'axios.request threw an error for some reason', + }, + }); + } }); - }); - - it('additional options should be passed to httpService.request', async () => { - const key = 'key value'; - const url = 'url value'; - - interface IHttpResponse { - a: number; - b: string; - c: boolean; - } - - const mockResponse = { - status: 200, - statusText: 'OK', - headers: {}, - config: {}, - data: { - a: 17, - b: 'some string value', - c: true, - }, - }; - - const httpServiceRequestSpy = jest - .spyOn(httpService, 'request') - .mockImplementation(() => of(mockResponse)); - - const callback = (response: AxiosResponse) => { - expect(response).toStrictEqual(mockResponse); - - return true; - }; - const options: AxiosRequestConfig = { - maxRedirects: 3, - auth: { - username: 'username value', - password: 'password value', - }, - }; - - const indicatorResponse = await httpHealthIndicator.responseCheck( - key, - url, - callback, - options, - ); - - expect(indicatorResponse).toStrictEqual({ - [key]: { - status: 'up', - }, - }); + it('callback throwing error should rethrow it wrapped in a HealthCheckError', async () => { + const key = 'key value'; + const url = 'url value'; - expect(httpServiceRequestSpy).toHaveBeenCalledWith({ - url, - ...options, - }); - }); - }); + interface OtherError extends Error { + property1?: string; + property2?: string; + } - describe('error conditions', () => { - it('request throwing AxiosError should throw error with message and statusCode/statusText from response', async () => { - const key = 'key value'; - const url = 'url value'; - - interface IHttpResponse { - a: number; - b: string; - c: boolean; - } - - const mockResponse = { - status: 500, - statusText: 'status text value', - headers: {}, - config: {}, - data: { - a: 17, - b: 'some string value', - c: true, - }, - }; + interface IHttpResponse { + a: number; + b: string; + c: boolean; + } - jest.spyOn(httpService, 'request').mockImplementation(() => { - return throwError({ - name: 'error name value', - message: 'axios.request threw an error for some reason', + const mockResponse = { + status: 200, + statusText: 'OK', + headers: {}, config: {}, - code: 'status code value', - request: {}, - response: mockResponse, - isAxiosError: true, - toJSON: () => ({}), - } as AxiosError); - }); - - const callback = fail.bind(null, 'callback should not have been called'); - - try { - await httpHealthIndicator.responseCheck(key, url, callback); - } catch (err) { - expect(err instanceof HealthCheckError).toBeTruthy(); - expect(err.message).toEqual( - 'axios.request threw an error for some reason', - ); - expect(err.causes).toStrictEqual({ - 'key value': { - status: 'down', - message: 'axios.request threw an error for some reason', - statusCode: 500, - statusText: 'status text value', + data: { + a: 17, + b: 'some string value', + c: true, }, - }); - } - }); + }; - it('request throwing AxiosError w/o response should only include message', async () => { - const key = 'key value'; - const url = 'url value'; + jest + .spyOn(httpService, 'request') + .mockImplementation(() => of(mockResponse)); - interface IHttpResponse {} + const callback = (response: AxiosResponse) => { + throw { + message: 'callback threw an error for some reason', + property1: 'property1 value', + property2: 'property2 value', + } as OtherError; + }; - jest.spyOn(httpService, 'request').mockImplementation(() => { - return throwError({ - name: 'error name value', - message: 'axios.request threw an error for some reason', - config: {}, - code: 'status code value', - request: {}, - response: undefined, - isAxiosError: true, - toJSON: () => ({}), - } as AxiosError); + try { + await httpHealthIndicator.responseCheck(key, url, callback); + } catch (err) { + expect(err instanceof HealthCheckError).toBeTruthy(); + expect(err.message).toEqual( + 'callback threw an error for some reason', + ); + expect(err.causes).toStrictEqual({ + 'key value': { + status: 'down', + }, + }); + } }); - - const callback = fail.bind(null, 'callback should not have been called'); - - try { - await httpHealthIndicator.responseCheck(key, url, callback); - } catch (err) { - expect(err instanceof HealthCheckError).toBeTruthy(); - expect(err.message).toEqual( - 'axios.request threw an error for some reason', - ); - expect(err.causes).toStrictEqual({ - 'key value': { - status: 'down', - message: 'axios.request threw an error for some reason', - }, - }); - } - }); - - it('callback throwing error should rethrow it wrapped in a HealthCheckError', async () => { - const key = 'key value'; - const url = 'url value'; - - interface OtherError extends Error { - property1?: string; - property2?: string; - } - - interface IHttpResponse { - a: number; - b: string; - c: boolean; - } - - const mockResponse = { - status: 200, - statusText: 'OK', - headers: {}, - config: {}, - data: { - a: 17, - b: 'some string value', - c: true, - }, - }; - - jest - .spyOn(httpService, 'request') - .mockImplementation(() => of(mockResponse)); - - const callback = (response: AxiosResponse) => { - throw { - message: 'callback threw an error for some reason', - property1: 'property1 value', - property2: 'property2 value', - } as OtherError; - }; - - try { - await httpHealthIndicator.responseCheck(key, url, callback); - } catch (err) { - expect(err instanceof HealthCheckError).toBeTruthy(); - expect(err.message).toEqual('callback threw an error for some reason'); - expect(err.causes).toStrictEqual({ - 'key value': { - status: 'down', - }, - }); - } }); }); }); diff --git a/lib/health-indicator/http/http.health.ts b/lib/health-indicator/http/http.health.ts index ea1d79537..c91983b95 100644 --- a/lib/health-indicator/http/http.health.ts +++ b/lib/health-indicator/http/http.health.ts @@ -20,18 +20,6 @@ export class HttpHealthIndicator extends HealthIndicator { super(); } - /** - * Executes a request with the given parameters - * @param url The url of the health check - * @param options The optional axios options of the request - */ - private async pingDNS( - url: string, - options: AxiosRequestConfig, - ): Promise | any> { - return await this.httpService.request({ url, ...options }).toPromise(); - } - /** * Prepares and throw a HealthCheckError * @param key The key which will be used for the result object @@ -72,12 +60,20 @@ export class HttpHealthIndicator extends HealthIndicator { async pingCheck( key: string, url: string, - options: AxiosRequestConfig = {}, + { + httpClient, + ...options + }: AxiosRequestConfig & { httpClient?: HttpService } = {}, ): Promise { let isHealthy = false; + // In case the user has a preconfigured HttpService (see `HttpModule.register`) + // we just let him/her pass in this HttpService so that he/she does not need to + // reconfigure it. + // https://github.com/nestjs/terminus/issues/1151 + const httpService = httpClient || this.httpService; try { - await this.pingDNS(url, options); + await httpService.request({ url, ...options }).toPromise(); isHealthy = true; } catch (err) { this.generateHttpError(key, err); @@ -90,10 +86,15 @@ export class HttpHealthIndicator extends HealthIndicator { key: string, url: URL | string, callback: (response: AxiosResponse) => boolean | Promise, - options: AxiosRequestConfig = {}, + { + httpClient, + ...options + }: AxiosRequestConfig & { httpClient?: HttpService } = {}, ): Promise { + const httpService = httpClient || this.httpService; + try { - const response = await this.httpService + const response = await httpService .request({ url: url.toString(), ...options }) .toPromise();