Changed exception handling in search service. Reworking tests

This commit is contained in:
danny-mhlv 2022-08-17 20:32:10 +03:00
parent 569ef08f1f
commit 5eb20c4727
6 changed files with 443 additions and 213 deletions

View File

@ -35,8 +35,8 @@ export class PapersController {
(response: SearchResultDto) => { (response: SearchResultDto) => {
return response.data; return response.data;
}, },
(error: SearchResultDto) => { (error: HttpException) => {
throw new HttpException(error.data, error.statusCode); throw error;
} }
); );
} }
@ -58,11 +58,11 @@ export class PapersController {
@HttpCode(200) @HttpCode(200)
getByID(@Param('uuid', ParseUUIDPipe) uuid: string): object { getByID(@Param('uuid', ParseUUIDPipe) uuid: string): object {
return this.searchService.findByID(uuid).then( return this.searchService.findByID(uuid).then(
(response) => { (response: SearchResultDto) => {
return response.data; return response.data;
}, },
(error) => { (error: HttpException) => {
throw new HttpException(error.data, error.status); throw error;
} }
); );
} }

View File

@ -1,7 +1,7 @@
import { HttpService } from "@nestjs/axios"; import { HttpService } from "@nestjs/axios";
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { Observable, map, take } from "rxjs"; import { Observable, map, take } from "rxjs";
import { PageDto } from "../domain/dtos"; import { EsResponseDto, PageDto } from "../domain/dtos";
import { EsHitDto } from "../domain/dtos/es-hit.dto"; import { EsHitDto } from "../domain/dtos/es-hit.dto";
import { EsQueryDto } from "../domain/dtos/es-query.dto"; import { EsQueryDto } from "../domain/dtos/es-query.dto";
import { RequestDto } from "../domain/dtos/request.dto"; import { RequestDto } from "../domain/dtos/request.dto";

View File

@ -1,9 +1,10 @@
import { HttpService } from "@nestjs/axios"; import { HttpService } from "@nestjs/axios";
import { GatewayTimeoutException, Injectable } from "@nestjs/common"; import { GatewayTimeoutException, HttpException, Injectable } from "@nestjs/common";
import { map, take } from "rxjs"; import { map, take } from "rxjs";
import { EsResponseDto } from "src/core/domain/dtos"; import { EsResponseDto } from "src/core/domain/dtos";
import { EsQueryDto } from "src/core/domain/dtos/es-query.dto"; import { EsQueryDto } from "src/core/domain/dtos/es-query.dto";
import { SearchResultDto } from "src/core/domain/dtos/search-result.dto"; import { SearchResultDto } from "src/core/domain/dtos/search-result.dto";
import { HttpResponseException } from "src/core/exceptions";
/** /**
* Search service provider * Search service provider
@ -43,17 +44,16 @@ export class SearchService {
data: ESQ, data: ESQ,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
})) }))
.pipe(take(1), map(axiosRes => axiosRes.data)) ?.pipe(take(1), map(axiosRes => axiosRes.data))
.subscribe((res: EsResponseDto) => { .subscribe((res: EsResponseDto) => {
if (res.timed_out) { if (res.timed_out) {
throw new GatewayTimeoutException; reject(new GatewayTimeoutException('Elasticsearch Timed Out'));
// reject(new SearchResultDto(504, {message: 'Timed Out'}));
} }
resolve(new SearchResultDto(200, res)); resolve(new SearchResultDto(200, res));
}); });
} catch (error) { } catch (error) {
reject(new SearchResultDto(700, error)); reject(error);
} }
}); });
} }
@ -64,24 +64,22 @@ export class SearchService {
* @returns Elasticsearch hits or an error object * @returns Elasticsearch hits or an error object
*/ */
async findByContext(es_query: EsQueryDto): Promise<SearchResultDto> { async findByContext(es_query: EsQueryDto): Promise<SearchResultDto> {
console.log(`SEARCH|SERVICE: ${JSON.stringify(es_query, null, 2)}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
(this.httpService.get<EsResponseDto>(`http://localhost:${this.ES_PORT}/_search`, { (this.httpService.get<EsResponseDto>(`http://localhost:${this.ES_PORT}/_search`, {
data: es_query, data: es_query,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
})) }))
.pipe(take(1), map(axiosRes => axiosRes.data)) ?.pipe(take(1), map(axiosRes => axiosRes.data))
.subscribe((res: EsResponseDto) => { .subscribe((res: EsResponseDto) => {
if (res.timed_out) { if (res.timed_out) {
throw new GatewayTimeoutException; reject(new GatewayTimeoutException('Elasticsearch Timed Out'));
// reject(new SearchResultDto(504, {status: 504, message: 'Timed Out'}));
} }
resolve(new SearchResultDto(200, res)); resolve(new SearchResultDto(200, res));
}); });
} catch (error) { } catch (error) {
reject(new SearchResultDto(700, error)); reject(error);
} }
}); });
} }

View File

@ -1,47 +1,81 @@
// // import { CallHandler, ExecutionContext } from "@nestjs/common"; import { HttpModule } from "@nestjs/axios";
// import { HttpModule } from "@nestjs/axios"; import { Test } from "@nestjs/testing";
// import { Test } from "@nestjs/testing"; import exp from "constants";
// import { Observable, of } from "rxjs"; import { Observable, of } from "rxjs";
// import { PapersController } from "src/application"; import { PapersController } from "src/application";
// import { Order } from "src/core/domain"; import { Order } from "src/core/domain";
// import { PageDto, SearchQueryDto } from "src/core/domain/dtos"; import { PageDto, SearchQueryDto } from "src/core/domain/dtos";
// import { PageInterceptor } from "src/core/interceptors/page.interceptor"; import { PageInterceptor } from "src/core/interceptors/page.interceptor";
// import { SearchService } from "src/core/services/common/search.service"; import { SearchService } from "src/core/services/common/search.service";
// const executionContext = { const execCtxMock = {
// switchToHttp: jest.fn().mockReturnThis(), switchToHttp: jest.fn().mockReturnThis(),
// getRequest: jest.fn().mockReturnThis(), getRequest: jest.fn().mockReturnThis(),
// getHandler: jest.fn().mockReturnThis(), getHandler: jest.fn().mockReturnThis(),
// getArgs: jest.fn().mockReturnThis(), getArgs: jest.fn().mockReturnThis(),
// getArgByIndex: jest.fn().mockReturnThis(), getArgByIndex: jest.fn().mockReturnThis(),
// switchToRpc: jest.fn().mockReturnThis(), switchToRpc: jest.fn().mockReturnThis(),
// switchToWs: jest.fn().mockReturnThis(), switchToWs: jest.fn().mockReturnThis(),
// getType: jest.fn().mockReturnThis(), getType: jest.fn().mockReturnThis(),
// getClass: jest.fn().mockReturnThis(), getClass: jest.fn().mockReturnThis(),
// }; };
// const callHandler = { const callHandlerMock = {
// handle: jest.fn(), handle: jest.fn(),
// }; };
// describe('Testing PageInterceptor', () => { describe('Unit tests for PageInterceptor', () => {
// let pageInter: PageInterceptor; let pageInter: PageInterceptor;
// let moduleRef; let moduleRef;
// beforeEach(async () => { beforeAll(async () => {
// moduleRef = await Test.createTestingModule({ moduleRef = await Test.createTestingModule({
// imports: [HttpModule], imports: [HttpModule],
// controllers: [PapersController], controllers: [PapersController],
// providers: [SearchService, PageInterceptor], providers: [SearchService, PageInterceptor],
// }).compile(); }).compile();
// pageInter = moduleRef.get(PageInterceptor); pageInter = moduleRef.get(PageInterceptor);
// });
pageInter.getPIT = jest.fn().mockReturnValue({});
execCtxMock.getRequest.mockReturnValue({
query: {
query: 'thisIsMyQuery',
page: 1,
limit: 5,
order: Order.DESC
}
});
callHandlerMock.handle.mockReturnValueOnce(
of({
total: { value: 1 },
hits: { hits: [{}] }
})
);
});
it('Should be defined', () => {
expect(pageInter).toBeDefined();
});
describe('intercept()', () => {
it('Should return a Promise', () => {
expect(pageInter.intercept(execCtxMock, callHandlerMock)).toBeInstanceOf(Promise);
});
it('Should return a Promise with Observable and PageDto inside', () => {
pageInter.intercept(execCtxMock, callHandlerMock).then((res) => {
expect(res).toBeInstanceOf(Observable);
res.subscribe((data) => {
expect(data).toBeInstanceOf(PageDto);
});
});
});
it.todo('Should touch CallHandler.handle() method');
// describe('intercept()', () => {
// it('Should be defined', () => {
// expect(pageInter).toBeDefined();
// });
// it('Should return an Observable with a page of type PageDto', (done) => { // it('Should return an Observable with a page of type PageDto', (done) => {
// executionContext.getRequest.mockReturnValue( { query: new SearchQueryDto('someQuery', 1, 10, 'desc') }); // executionContext.getRequest.mockReturnValue( { query: new SearchQueryDto('someQuery', 1, 10, 'desc') });
@ -51,11 +85,11 @@
// })); // }));
// expect(pageInter.intercept(executionContext, callHandler)).toBeInstanceOf(Observable); // expect(pageInter.intercept(executionContext, callHandler)).toBeInstanceOf(Promise);
// pageInter.intercept(executionContext, callHandler).subscribe((data) => { // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => {
// expect(data).toBeInstanceOf(PageDto); // expect(data).toBeInstanceOf(PageDto);
// done(); // done();
// }); // }));
// }) // })
// it('Should hold content on the returned page', (done) => { // it('Should hold content on the returned page', (done) => {
@ -65,13 +99,13 @@
// hits: [{dummy: 'dum'}], // hits: [{dummy: 'dum'}],
// })); // }));
// pageInter.intercept(executionContext, callHandler).subscribe((data) => { // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => {
// expect(data).toEqual({ // expect(data).toEqual({
// data: expect.anything(), // data: expect.anything(),
// meta: expect.anything(), // meta: expect.anything(),
// }); // });
// done(); // done();
// }); // }));
// }); // });
// it('Should have next page', (done) => { // it('Should have next page', (done) => {
@ -81,11 +115,11 @@
// hits: Array(10).fill({dummy: 'dum'}, 0, 10), // hits: Array(10).fill({dummy: 'dum'}, 0, 10),
// })); // }));
// pageInter.intercept(executionContext, callHandler).subscribe((data) => { // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => {
// expect(data.meta.hasNext).toEqual(true); // expect(data.meta.hasNext).toEqual(true);
// expect(data.meta.hasPrev).toEqual(false); // expect(data.meta.hasPrev).toEqual(false);
// done(); // done();
// }); // }));
// }); // });
// it('Should have correct meta-data', (done) => { // it('Should have correct meta-data', (done) => {
@ -95,7 +129,7 @@
// hits: Array(15).fill({dummy: 'dum'}, 0, 15), // hits: Array(15).fill({dummy: 'dum'}, 0, 15),
// })); // }));
// pageInter.intercept(executionContext, callHandler).subscribe((data) => { // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => {
// expect(data.meta).toEqual({ // expect(data.meta).toEqual({
// total: 15, // total: 15,
// pagenum: 2, // pagenum: 2,
@ -105,8 +139,15 @@
// pagesize: 5 // pagesize: 5
// }); // });
// done(); // done();
// }));
// }); // });
// }); });
// });
// describe('getPIT()', () => {
// }); // });
// describe('deletePIT()', () => {
// });
});

View File

@ -1,9 +1,6 @@
import { Test, TestingModule } from "@nestjs/testing"; import { Test, TestingModule } from "@nestjs/testing";
import { INestApplication } from "@nestjs/common"; import { INestApplication } from "@nestjs/common";
import { SearchModule } from "src/infrastructure/modules/search.module";
import request from 'supertest' import request from 'supertest'
import { assert } from "console";
import { resolve } from "path";
import { AppModule } from "src/infrastructure/modules"; import { AppModule } from "src/infrastructure/modules";
describe('E2E Testing of /papers', () => { describe('E2E Testing of /papers', () => {
@ -18,7 +15,7 @@ describe('E2E Testing of /papers', () => {
await app.init(); await app.init();
}); });
it('Should return one, exact item on page', async () => { it('Should return one exact item on page', async () => {
return request(app.getHttpServer()) return request(app.getHttpServer())
.get('/papers/eeeb2d01-8315-454e-b33f-3d6caa25db42') .get('/papers/eeeb2d01-8315-454e-b33f-3d6caa25db42')
.expect(200) .expect(200)

View File

@ -1,112 +1,306 @@
// import { HttpService } from "@nestjs/axios"; import { HttpService } from "@nestjs/axios";
// import { ConfigModule } from "@nestjs/config"; import { GatewayTimeoutException, HttpException } from "@nestjs/common";
// import { Test } from "@nestjs/testing"; import { ConfigModule } from "@nestjs/config";
// import exp from "constants"; import { Test } from "@nestjs/testing";
// import { of } from "rxjs"; import { of } from "rxjs";
// import { EsTime } from "src/core/domain/enums/es-time.enum"; import { EsQueryDto, SearchResultDto } from "src/core/domain";
// import { HttpResponseException } from "src/core/exceptions"; import { SearchService } from "src/core/services/common/search.service";
// import { SearchService } from "src/core/services/common/search.service";
// describe('Unit tests for SearchService', () => { describe('Unit tests for SearchService', () => {
// let searchService: SearchService; let searchService: SearchService;
// let httpService: HttpService; let httpService: HttpService;
// beforeAll(async () => { beforeAll(async () => {
// const moduleRef = await Test.createTestingModule({ const moduleRef = await Test.createTestingModule({
// providers: [ providers: [
// SearchService, SearchService,
// { {
// provide: HttpService, provide: HttpService,
// useValue: { useValue: {
// post: jest.fn(), get: jest.fn(),
// }, },
// }, },
// ], ],
// imports: [ imports: [
// ConfigModule.forRoot({ ConfigModule.forRoot({
// isGlobal: true, isGlobal: true,
// cache: true, cache: true,
// expandVariables: true, expandVariables: true,
// }) })
// ], ],
// }).compile(); }).compile();
// searchService = moduleRef.get(SearchService); searchService = moduleRef.get(SearchService);
// httpService = moduleRef.get(HttpService); httpService = moduleRef.get(HttpService);
// }); });
// describe('getPIT()', () => { describe('findByID()', () => {
// it('Should touch HttpService.post() method', () => { it('Should touch HttpService.get() method', () => {
// let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ let httpGetSpy = jest.spyOn(httpService, 'get');
// data: {id: '2567'},
// status: 0,
// statusText: '',
// headers: {},
// config: {},
// }));
// searchService.getPIT(1); searchService.findByID('');
// expect(postMock).toHaveBeenCalled(); expect(httpGetSpy).toHaveBeenCalled();
// }); });
// it('Should contain correct port in the URI from .env', () => { it('Should send correct data via HttpService.get() body parameter', () => {
// let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ let httpGetSpy = jest.spyOn(httpService, 'get');
// data: {id: '2567'},
// status: 0,
// statusText: '',
// headers: {},
// config: {},
// }));
// searchService.getPIT(1); const uuid = 'thisIsUUID_Provided';
// expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`); searchService.findByID(uuid);
// }); expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(expect.anything(), {
data: {
size: 1,
query: {
query_string: {
query: 'id:' + uuid
}
}
},
headers: { 'Content-Type': 'application/json' }
});
});
// it('Should touch HttpService with correct URI when time alive and time-unit are set', () => { it('Should call HttpService.get() with correct URI and port number', () => {
// let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ let httpGetSpy = jest.spyOn(httpService, 'get');
// data: {id: '2567'},
// status: 0,
// statusText: '',
// headers: {},
// config: {},
// }));
// let time = 2; searchService.findByID('');
// let unit = EsTime.sec; expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(
`http://localhost:${process.env.ES_PORT}/_search`,
expect.anything()
);
});
// searchService.getPIT(time, unit); it('Should return a Promise', () => {
// expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); expect(searchService.findByID('')).toBeInstanceOf(Promise);
// }); });
// it('Should return error exeception when HttpService fails', () => { it('Should return a Promise with SearchResultDto', () => {
// jest.spyOn(httpService, 'post').mockImplementation(() => { // Axios response mock
// throw HttpResponseException; httpService.get = jest.fn().mockReturnValueOnce(
// }); of({
status: undefined,
statusText: undefined,
headers: undefined,
config: undefined,
data: {
dummy: 'dum'
}
})
);
// expect(searchService.getPIT(1)).rejects.toEqual(HttpResponseException); searchService.findByID('').then((res) => {
// }); expect(res).toBeInstanceOf(SearchResultDto);
expect(res.data).toEqual({ dummy: 'dum' });
expect(res.statusCode).toBe(200);
});
});
// it('Should return a non-empty string when HttpService request succeedes', () => { // Errors
// jest.spyOn(httpService, 'post').mockReturnValue(of({ it('Should throw 504 | GatewayTimeoutException', () => {
// data: {id: '2567', keep_alive: '1m'}, // Axios response mock
// status: 0, httpService.get = jest.fn().mockReturnValueOnce(
// statusText: '', of({
// headers: {}, status: undefined,
// config: {}, statusText: undefined,
// })); headers: undefined,
config: undefined,
data: {
timed_out: true,
dummy: 'dum'
}
})
);
// expect(searchService.getPIT(1)).resolves.toEqual({ searchService.findByID('').catch((err) => {
// id: '2567', expect(err).toBeInstanceOf(GatewayTimeoutException);
// keep_alive: '1m', console.log(err)
// }); });
// }); });
it('Should throw an HttpException when HttpService.get() fails and throws', () => {
httpService.get = jest.fn().mockImplementationOnce(() => {
throw new HttpException({ oops: 'sorry' }, 999);
});
searchService.findByID('').catch((err) => {
expect(err).toBeInstanceOf(HttpException);
expect(err.response).toEqual({ oops: 'sorry' });
expect(err.status).toEqual(999);
});
});
});
describe('findByContext()', () => {
it('Should touch HttpService.get() method', () => {
let httpGetSpy = jest.spyOn(httpService, 'get');
searchService.findByContext(null);
expect(httpGetSpy).toHaveBeenCalled();
});
it('Should send correct data via HttpService.get() body parameter', () => {
let httpGetSpy = jest.spyOn(httpService, 'get');
let es_query = new EsQueryDto();
es_query = {
query: {
query_string: {
query: 'thisIsTheQuery!'
}
}
}
searchService.findByContext(es_query);
expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(expect.anything(), {
data: es_query,
headers: { 'Content-Type': 'application/json' }
});
});
it('Should call HttpService.get() with correct URI and port number', () => {
let httpGetSpy = jest.spyOn(httpService, 'get');
searchService.findByContext(null);
expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(
`http://localhost:${process.env.ES_PORT}/_search`,
expect.anything()
);
});
it('Should return a Promise', () => {
expect(searchService.findByContext(null)).toBeInstanceOf(Promise);
});
it('Should return a Promise with SearchResultDto', () => {
// Axios response mock
httpService.get = jest.fn().mockReturnValueOnce(
of({
status: undefined,
statusText: undefined,
headers: undefined,
config: undefined,
data: {
dummy: 'dum'
}
})
);
searchService.findByContext(null).then((res) => {
expect(res).toBeInstanceOf(SearchResultDto);
expect(res.data).toEqual({ dummy: 'dum' });
expect(res.statusCode).toBe(200);
});
});
// Errors
it('Should throw 504 | GatewayTimeoutException', () => {
// Axios response mock
httpService.get = jest.fn().mockReturnValueOnce(
of({
status: undefined,
statusText: undefined,
headers: undefined,
config: undefined,
data: {
timed_out: true,
dummy: 'dum'
}
})
);
searchService.findByContext(null).catch((err) => {
expect(err).toBeInstanceOf(GatewayTimeoutException);
console.log(err)
});
});
it('Should throw an HttpException when HttpService.get() fails and throws', () => {
httpService.get = jest.fn().mockImplementationOnce(() => {
throw new HttpException({ oops: 'sorry' }, 999);
});
searchService.findByContext(null).catch((err) => {
expect(err).toBeInstanceOf(HttpException);
expect(err.response).toEqual({ oops: 'sorry' });
expect(err.status).toEqual(999);
});
});
});
});
/**
* describe('getPIT()', () => {
it('Should touch HttpService.post() method', () => {
let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({
data: {id: '2567'},
status: 0,
statusText: '',
headers: {},
config: {},
}));
searchService.getPIT(1);
expect(postMock).toHaveBeenCalled();
});
it('Should contain correct port in the URI from .env', () => {
let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({
data: {id: '2567'},
status: 0,
statusText: '',
headers: {},
config: {},
}));
searchService.getPIT(1);
expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`);
});
it('Should touch HttpService with correct URI when time alive and time-unit are set', () => {
let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({
data: {id: '2567'},
status: 0,
statusText: '',
headers: {},
config: {},
}));
let time = 2;
let unit = EsTime.sec;
searchService.getPIT(time, unit);
expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`);
});
it('Should return error exeception when HttpService fails', () => {
jest.spyOn(httpService, 'post').mockImplementation(() => {
throw HttpResponseException;
});
expect(searchService.getPIT(1)).rejects.toEqual(HttpResponseException);
});
it('Should return a non-empty string when HttpService request succeedes', () => {
jest.spyOn(httpService, 'post').mockReturnValue(of({
data: {id: '2567', keep_alive: '1m'},
status: 0,
statusText: '',
headers: {},
config: {},
}));
expect(searchService.getPIT(1)).resolves.toEqual({
id: '2567',
keep_alive: '1m',
});
});
// }); });
// describe('deletePIT()', () => { describe('deletePIT()', () => {
// it.todo('Should fail to delete, because the requested PIT ID is invalid'); it.todo('Should fail to delete, because the requested PIT ID is invalid');
// it.todo('Should call HttpService.delete() method with correct body'); it.todo('Should call HttpService.delete() method with correct body');
// }); });
// }); */