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

View File

@ -1,7 +1,7 @@
import { HttpService } from "@nestjs/axios";
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
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 { EsQueryDto } from "../domain/dtos/es-query.dto";
import { RequestDto } from "../domain/dtos/request.dto";

View File

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

View File

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

View File

@ -1,9 +1,6 @@
import { Test, TestingModule } from "@nestjs/testing";
import { INestApplication } from "@nestjs/common";
import { SearchModule } from "src/infrastructure/modules/search.module";
import request from 'supertest'
import { assert } from "console";
import { resolve } from "path";
import { AppModule } from "src/infrastructure/modules";
describe('E2E Testing of /papers', () => {
@ -18,7 +15,7 @@ describe('E2E Testing of /papers', () => {
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())
.get('/papers/eeeb2d01-8315-454e-b33f-3d6caa25db42')
.expect(200)

View File

@ -1,112 +1,306 @@
// import { HttpService } from "@nestjs/axios";
// import { ConfigModule } from "@nestjs/config";
// import { Test } from "@nestjs/testing";
// import exp from "constants";
// import { of } from "rxjs";
// import { EsTime } from "src/core/domain/enums/es-time.enum";
// import { HttpResponseException } from "src/core/exceptions";
// import { SearchService } from "src/core/services/common/search.service";
import { HttpService } from "@nestjs/axios";
import { GatewayTimeoutException, HttpException } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { Test } from "@nestjs/testing";
import { of } from "rxjs";
import { EsQueryDto, SearchResultDto } from "src/core/domain";
import { SearchService } from "src/core/services/common/search.service";
// describe('Unit tests for SearchService', () => {
// let searchService: SearchService;
// let httpService: HttpService;
describe('Unit tests for SearchService', () => {
let searchService: SearchService;
let httpService: HttpService;
// beforeAll(async () => {
// const moduleRef = await Test.createTestingModule({
// providers: [
// SearchService,
// {
// provide: HttpService,
// useValue: {
// post: jest.fn(),
// },
// },
// ],
// imports: [
// ConfigModule.forRoot({
// isGlobal: true,
// cache: true,
// expandVariables: true,
// })
// ],
// }).compile();
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
SearchService,
{
provide: HttpService,
useValue: {
get: jest.fn(),
},
},
],
imports: [
ConfigModule.forRoot({
isGlobal: true,
cache: true,
expandVariables: true,
})
],
}).compile();
// searchService = moduleRef.get(SearchService);
// httpService = moduleRef.get(HttpService);
// });
searchService = moduleRef.get(SearchService);
httpService = moduleRef.get(HttpService);
});
// describe('getPIT()', () => {
// it('Should touch HttpService.post() method', () => {
// let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({
// data: {id: '2567'},
// status: 0,
// statusText: '',
// headers: {},
// config: {},
// }));
describe('findByID()', () => {
it('Should touch HttpService.get() method', () => {
let httpGetSpy = jest.spyOn(httpService, 'get');
// searchService.getPIT(1);
// expect(postMock).toHaveBeenCalled();
// });
searchService.findByID('');
expect(httpGetSpy).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: {},
// }));
it('Should send correct data via HttpService.get() body parameter', () => {
let httpGetSpy = jest.spyOn(httpService, 'get');
// searchService.getPIT(1);
// expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`);
// });
const uuid = 'thisIsUUID_Provided';
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', () => {
// let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({
// data: {id: '2567'},
// status: 0,
// statusText: '',
// headers: {},
// config: {},
// }));
it('Should call HttpService.get() with correct URI and port number', () => {
let httpGetSpy = jest.spyOn(httpService, 'get');
// let time = 2;
// let unit = EsTime.sec;
searchService.findByID('');
expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(
`http://localhost:${process.env.ES_PORT}/_search`,
expect.anything()
);
});
// searchService.getPIT(time, unit);
// expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`);
// });
it('Should return a Promise', () => {
expect(searchService.findByID('')).toBeInstanceOf(Promise);
});
// it('Should return error exeception when HttpService fails', () => {
// jest.spyOn(httpService, 'post').mockImplementation(() => {
// throw HttpResponseException;
// });
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'
}
})
);
// 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', () => {
// jest.spyOn(httpService, 'post').mockReturnValue(of({
// data: {id: '2567', keep_alive: '1m'},
// status: 0,
// statusText: '',
// headers: {},
// config: {},
// }));
// 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'
}
})
);
// expect(searchService.getPIT(1)).resolves.toEqual({
// id: '2567',
// keep_alive: '1m',
// });
// });
searchService.findByID('').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.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()', () => {
// it.todo('Should fail to delete, because the requested PIT ID is invalid');
// it.todo('Should call HttpService.delete() method with correct body');
// });
// });
describe('deletePIT()', () => {
it.todo('Should fail to delete, because the requested PIT ID is invalid');
it.todo('Should call HttpService.delete() method with correct body');
});
*/