diff --git a/backend/package.json b/backend/package.json index 819e97d..63a6dc5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,7 +15,7 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest --detectOpenHandles", + "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", @@ -70,7 +70,15 @@ "json", "ts" ], - "rootDir": "src", + "roots": [ + "" + ], + "modulePaths": [ + "" + ], + "moduleDirectories": [ + "node_modules" + ], "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" diff --git a/backend/src/search/entities/crossRef.entity.ts b/backend/src/search/entities/crossRef.entity.ts index 05707fb..919b2dd 100644 --- a/backend/src/search/entities/crossRef.entity.ts +++ b/backend/src/search/entities/crossRef.entity.ts @@ -2,14 +2,32 @@ export class PaperInfo { title?: string; authors?: string[]; doi?: string; + + constructor(body: PaperInfo) { + this.title = body.title; + this.authors = body.authors; + this.doi = body.doi; + } } export class PaperInfoExtended extends PaperInfo { publishedAt?: string; citations?: number; references?: number; + + constructor(body: PaperInfoExtended) { + super(body); + this.publishedAt = body.publishedAt; + this.citations = body.citations; + this.references = body.references; + } } export class PaperInfoDetail extends PaperInfoExtended { referenceList?: ReferenceInfo[]; + + constructor(body: PaperInfoDetail) { + super(body); + this.referenceList = body.referenceList; + } } export interface ReferenceInfo { issn?: string; @@ -21,7 +39,7 @@ export interface ReferenceInfo { 'doi-asserted-by'?: string; 'first-page'?: string; isbn?: string; - doi?: string; + DOI?: string; component?: string; 'article-title'?: string; 'volume-title'?: string; diff --git a/backend/src/search/search.controller.ts b/backend/src/search/search.controller.ts index 39d17b4..567e871 100644 --- a/backend/src/search/search.controller.ts +++ b/backend/src/search/search.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query, UsePipes, ValidationPipe } from '@nestjs/common'; +import { Controller, Get, NotFoundException, Query, UsePipes, ValidationPipe } from '@nestjs/common'; import { SearchService } from './search.service'; import { AutoCompleteDto, GetPaperDto, SearchDto } from './entities/search.dto'; import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; @@ -17,7 +17,8 @@ export class SearchController { if (elasticDataCount > 0) { return elastic.hits.hits.map((paper) => paper._source); } - const { items, totalItems } = await this.searchService.getCrossRefAutoCompleteData(keyword); + const selects = ['title', 'author', 'DOI']; + const { items, totalItems } = await this.searchService.getCrossRefData(keyword, 5, 1, selects); const papers = this.searchService.parseCrossRefData(items, this.searchService.parsePaperInfo); this.searchService.crawlAllCrossRefData(keyword, totalItems, 1000); return papers; @@ -27,14 +28,20 @@ export class SearchController { @UsePipes(new ValidationPipe({ transform: true })) async getPapers(@Query() query: SearchDto) { const { keyword, rows, page } = query; - const { items, totalItems } = await this.searchService.getCrossRefData(keyword, rows, page); + const selects = ['title', 'author', 'created', 'is-referenced-by-count', 'references-count', 'DOI']; + const { items, totalItems } = await this.searchService.getCrossRefData(keyword, rows, page, selects); + const totalPages = Math.ceil(totalItems / rows); + if (page > totalPages) { + throw new NotFoundException(`page(${page})는 ${totalPages} 보다 클 수 없습니다.`); + } const papers = this.searchService.parseCrossRefData(items, this.searchService.parsePaperInfoExtended); this.rankingService.insertRedis(keyword); + return { papers, pageInfo: { totalItems, - totalPages: Math.ceil(totalItems / rows), + totalPages, }, }; } diff --git a/backend/src/search/search.module.ts b/backend/src/search/search.module.ts index c15dce3..3361bcb 100644 --- a/backend/src/search/search.module.ts +++ b/backend/src/search/search.module.ts @@ -8,7 +8,12 @@ import { ScheduleModule } from '@nestjs/schedule'; import { RankingService } from 'src/ranking/ranking.service'; @Module({ imports: [ - HttpModule, + HttpModule.register({ + timeout: 20000, + headers: { + 'User-Agent': `Axios/1.1.3(mailto:${process.env.MAIL_TO})`, + }, + }), ElasticsearchModule.registerAsync({ useFactory: () => ({ node: process.env.ELASTIC_HOST, diff --git a/backend/src/search/search.service.ts b/backend/src/search/search.service.ts index effc444..c7c3847 100644 --- a/backend/src/search/search.service.ts +++ b/backend/src/search/search.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, RequestTimeoutException } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { CrossRefResponse, @@ -15,22 +15,12 @@ import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; @Injectable() export class SearchService { constructor(private readonly httpService: HttpService, private readonly esService: ElasticsearchService) {} - async getCrossRefAutoCompleteData(keyword: string) { - const crossRefdata = await this.httpService.axiosRef.get(CROSSREF_API_URL(keyword)); - const items = crossRefdata.data.message.items; - const totalItems = crossRefdata.data.message['total-results']; - return { items, totalItems }; - } - - async getCrossRefData(keyword: string, rows: number, page: number) { - const crossRefdata = await this.httpService.axiosRef.get( - CROSSREF_API_URL( - keyword, - rows, - ['title', 'author', 'created', 'is-referenced-by-count', 'references-count', 'DOI'], - page, - ), - ); + async getCrossRefData(keyword: string, rows: number, page: number, selects?: string[]) { + const crossRefdata = await this.httpService.axiosRef + .get(CROSSREF_API_URL(keyword, rows, page, selects)) + .catch((err) => { + throw new RequestTimeoutException(err.message); + }); const items = crossRefdata.data.message.items; const totalItems = crossRefdata.data.message['total-results']; return { items, totalItems }; @@ -42,21 +32,23 @@ export class SearchService { .fill(0) .map((v, i) => { CROSSREF_CACHE_QUEUE.push( - CROSSREF_API_URL( - keyword, - rows, - ['title', 'author', 'created', 'is-referenced-by-count', 'references-count', 'DOI'], - i + 1, - ), + CROSSREF_API_URL(keyword, rows, i + 1, [ + 'title', + 'author', + 'created', + 'is-referenced-by-count', + 'references-count', + 'DOI', + ]), ); }), ); } parseCrossRefData(items: CrossRefItem[], parser: (item: CrossRefItem) => T) { - return items.map(parser).filter((info) => info.title || info.authors?.length > 0); + return items.map(parser).filter((info) => info.title); } parsePaperInfo = (item: CrossRefItem) => { - const paperInfo = { + const data = { title: item.title?.[0], authors: item.author?.reduce((acc, cur) => { const authorName = `${cur.name ? cur.name : cur.given ? cur.given + ' ' : ''}${cur.family || ''}`; @@ -64,42 +56,45 @@ export class SearchService { return acc; }, []), doi: item.DOI, - } as PaperInfo; + }; - return paperInfo; + return new PaperInfo(data); }; parsePaperInfoExtended = (item: CrossRefItem) => { - const paperInfo = { + const data = { ...this.parsePaperInfo(item), publishedAt: item.created?.['date-time'], citations: item['is-referenced-by-count'], references: item['references-count'], - } as PaperInfoExtended; + }; - return paperInfo; + return new PaperInfoExtended(data); }; parsePaperInfoDetail = (item: CrossRefItem) => { - const referenceList = item['reference'].map((reference) => { - return { - title: - reference['article-title'] || - reference['journal-title'] || - reference['series-title'] || - reference['volume-title'], - doi: reference['doi'], - // TODO: 현재 원하는 정보를 얻기 위해서는 해당 reference에 대한 정보를 crossref에 다시 요청해야함 - author: reference['author'], - publishedAt: reference['year'], - citations: 0, - references: 0, - }; - }); - const paperInfo = { + const referenceList = + item['reference']?.map((reference) => { + return { + key: reference['DOI'] || reference.key || reference.unstructured, + title: + reference['article-title'] || + reference['journal-title'] || + reference['series-title'] || + reference['volume-title'] || + reference.unstructured, + doi: reference['DOI'], + // TODO: 현재 원하는 정보를 얻기 위해서는 해당 reference에 대한 정보를 crossref에 다시 요청해야함 + author: reference['author'], + publishedAt: reference['year'], + citations: 0, + references: 0, + }; + }) || []; + const data = { ...this.parsePaperInfoExtended(item), referenceList, - } as PaperInfoDetail; + }; - return paperInfo; + return new PaperInfoDetail(data); }; async getPaper(doi: string) { diff --git a/backend/src/search/tests/search.controller.spec.ts b/backend/src/search/tests/search.controller.spec.ts index 8796ac6..d08f477 100644 --- a/backend/src/search/tests/search.controller.spec.ts +++ b/backend/src/search/tests/search.controller.spec.ts @@ -1,64 +1,133 @@ import { SearchController } from '../search.controller'; import { SearchService } from '../search.service'; import { HttpService } from '@nestjs/axios'; -import { CrossRefResponse, PaperInfo, PaperInfoExtended } from '../entities/crossRef.entity'; -import mockData from './crossref.mock'; +import { PaperInfo, PaperInfoDetail, PaperInfoExtended } from '../entities/crossRef.entity'; import { Test, TestingModule } from '@nestjs/testing'; +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { mockElasticService, mockHttpService, mockRankingService } from './search.service.mock'; +import { RankingService } from '../../ranking/ranking.service'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import * as request from 'supertest'; + describe('SearchController', () => { let controller: SearchController; + let service: SearchService; + let app: INestApplication; - beforeEach(async () => { - const httpService = new HttpService(); - jest.spyOn(httpService['axiosRef'], 'get').mockImplementation((url: string) => { - const params = new URLSearchParams(new URL(url).search); - const keyword = params.get('query'); - const rows = parseInt(params.get('rows')); - const selects = params.get('select').split(','); - const items = mockData.message.items.slice(0, rows).map((item, i) => { - return selects.reduce((acc, select) => { - if (select === 'title') item[select] = [`${keyword}-${i}`]; - acc[select] = item[select]; - return acc; - }, {}); - }); + let spyGetElasticSearch: jest.SpyInstance; + let spyGetCrossRefData: jest.SpyInstance; + const keyword = 'coffee'; - return Promise.resolve<{ data: CrossRefResponse }>({ - data: { message: { 'total-results': mockData.message['total-results'], items } }, - }); - }); + beforeEach(async () => { + const httpService = mockHttpService(); + const rankingService = mockRankingService(); + const elasticService = mockElasticService(); const module: TestingModule = await Test.createTestingModule({ controllers: [SearchController], providers: [ SearchService, + { + provide: RankingService, + useValue: rankingService, + }, { provide: HttpService, useValue: httpService, }, + { + provide: ElasticsearchService, + useValue: elasticService, + }, ], }).compile(); controller = module.get(SearchController); - }); + service = module.get(SearchService); + spyGetElasticSearch = jest.spyOn(service, 'getElasticSearch'); + spyGetCrossRefData = jest.spyOn(service, 'getCrossRefData'); - it('search controller 정의', () => { - expect(controller).toBeDefined(); + app = module.createNestApplication(); + app.useGlobalPipes(new ValidationPipe()); + await app.init(); }); + afterEach(() => app.close()); + + describe('/search/auto-complete', () => { + it('getAutoCompletePapers - keyword=coffee 일 때 PaperInfo[]를 return', async () => { + // Case 1. elasticsearch에 data가 없을 경우 + const itemsByCrossRef = await controller.getAutoCompletePapers({ keyword }); + expect(itemsByCrossRef.length).toBe(5); + itemsByCrossRef.forEach((item) => { + expect(item instanceof PaperInfo).toBe(true); + }); - it('getAutoCompletePapers - keyword=coffee 일 때 PaperInfo[]를 return', async () => { - const keyword = 'coffee'; - const items = await controller.getAutoCompletePapers(keyword); - expect(items.length).toBe(5); - items.forEach((item) => { - expect(item).toBeInstanceOf(PaperInfo); + // Case 2. elasticsearch에 data가 있는 경우 + const itemsByElasticsearch = await controller.getAutoCompletePapers({ keyword }); + expect(itemsByElasticsearch.length).toBe(5); + itemsByElasticsearch.forEach((item) => { + expect(item instanceof PaperInfo).toBe(true); + }); + + expect(spyGetElasticSearch).toBeCalledTimes(2); + expect(spyGetCrossRefData).toBeCalledTimes(1); + }); + it('keyword 미포함시 error - GET /search/auto-complete?keyword=', () => { + const url = (keyword: string) => `/search/auto-complete?keyword=${keyword}`; + request(app.getHttpServer()).get(url('')).expect(400); + }); + }); + describe('/search', () => { + const DEFAULT_ROWS = 20; + const TOTAL_ITEMS = 28810; + it(`getPapers - keyword='coffee' 일 때 PaperInfoExtended[]를 return`, async () => { + const keyword = 'coffee'; + const { papers: items, pageInfo } = await controller.getPapers({ keyword, rows: 20, page: 1 }); + expect(items.length).toBe(DEFAULT_ROWS); + expect(pageInfo.totalItems).toBe(TOTAL_ITEMS); + items.forEach((item) => { + expect(item).toBeInstanceOf(PaperInfoExtended); + }); + // TODO: elasticsearch로 검색? + expect(spyGetElasticSearch).toBeCalledTimes(0); + expect(spyGetCrossRefData).toBeCalledTimes(1); + }); + it('keyword 미포함시 error - GET /search?keyword=', () => { + const url = (keyword: string) => `/search?keyword=${keyword}`; + request(app.getHttpServer()).get(url('')).expect(400); + }); + it('rows<=0 이거나, rows 값이 integer가 아닐 경우 error - GET /search?keyword=coffee&rows=', () => { + const url = (rows: string | number) => `/search?keyword=${keyword}&rows=${rows}`; + const rowsNotAvailables = [-1, -0.1, '0', 'value']; + rowsNotAvailables.forEach((value) => { + request(app.getHttpServer()).get(url(value)).expect(400); + }); + }); + it('page<=0 이거나, page 값이 integer가 아닐 경우 error - GET /search?keyword=coffee&page=', () => { + const url = (page: string | number) => `/search?keyword=${keyword}&page=${page}`; + const pageNotAvailables = [-1, -0.1, '0', 'value']; + pageNotAvailables.forEach((value) => { + request(app.getHttpServer()).get(url(value)).expect(400); + }); + }); + it('page>max 이면 error - GET /search?keyword=coffee&page=', () => { + const url = (page: string | number) => `/search?keyword=${keyword}&page=${page}`; + const maxPage = Math.ceil(TOTAL_ITEMS / DEFAULT_ROWS); + request(app.getHttpServer()) + .get(url(maxPage + 1)) + .expect(404); }); }); - it(`getAutoCompletePapers - keyword='' 일 때 PaperInfoExtended[]를 return`, async () => { - const keyword = 'coffee'; - const { papers: items, pageInfo } = await controller.getPapers(keyword); - expect(items.length).toBe(20); - expect(pageInfo.totalItems).toBe(28810); - items.forEach((item) => { - expect(item).toBeInstanceOf(PaperInfoExtended); + describe('/search/paper', () => { + it(`getPaper - doi=10.1234/some_doi 일 때 PaperInfoDetail을 return`, async () => { + const doi = '10.1234/some_doi'; + const paper = await controller.getPaper({ doi }); + expect(paper.references).toBe(5); + expect(paper.referenceList.length).toBe(5); + expect(paper).toBeInstanceOf(PaperInfoDetail); + }); + it('doi가 입력되지 않을 경우 error - GET /search/paper?doi=', () => { + const url = (keyword: string) => `/search/paper?doi=${keyword}`; + request(app.getHttpServer()).get(url('')).expect(400); }); }); }); diff --git a/backend/src/search/tests/search.service.mock.ts b/backend/src/search/tests/search.service.mock.ts new file mode 100644 index 0000000..356617b --- /dev/null +++ b/backend/src/search/tests/search.service.mock.ts @@ -0,0 +1,72 @@ +import mockCrossRefData from './crossref.mock'; +import mockSearchData from './searchdata.mock'; +import { HttpService } from '@nestjs/axios'; +import { CrossRefPaperResponse, CrossRefResponse, PaperInfo } from '../entities/crossRef.entity'; + +export function mockHttpService() { + const httpService = new HttpService(); + jest.spyOn(httpService['axiosRef'], 'get').mockImplementation((url: string) => { + const params = new URLSearchParams(new URL(url).search); + const keyword = params.get('query'); + if (keyword === null) { + const item = mockCrossRefData.message.items[0]; + return Promise.resolve<{ data: CrossRefPaperResponse }>({ + data: { message: item }, + }); + } + const rows = parseInt(params.get('rows')); + const selects = params.get('select').split(','); + const items = mockCrossRefData.message.items.slice(0, rows).map((item, i) => { + return selects.reduce((acc, select) => { + if (select === 'title') item[select] = [`${keyword}-${i}`]; + acc[select] = item[select]; + return acc; + }, {}); + }); + + return Promise.resolve<{ data: CrossRefResponse }>({ + data: { message: { 'total-results': mockCrossRefData.message['total-results'], items } }, + }); + }); + return httpService; +} + +export function mockElasticService() { + // TODO: should mockup index? + const index = jest.fn().mockResolvedValue(() => { + return true; + }); + const search = jest.fn(); + search + .mockResolvedValueOnce({ + hits: { + total: { + value: 0, + }, + }, + }) + .mockResolvedValue({ + hits: { + total: { + value: 222, + }, + hits: mockSearchData + .map((data) => { + return { + _source: new PaperInfo(data), + }; + }) + .slice(0, 5), + }, + }); + const elasticService = { index, search }; + return elasticService; +} + +export function mockRankingService() { + const insertRedis = jest.fn().mockResolvedValue(() => { + return true; + }); + const rankingService = { insertRedis }; + return rankingService; +} diff --git a/backend/src/search/tests/searchdata.mock.ts b/backend/src/search/tests/searchdata.mock.ts new file mode 100644 index 0000000..8a38a6c --- /dev/null +++ b/backend/src/search/tests/searchdata.mock.ts @@ -0,0 +1,172 @@ +import { PaperInfoExtended } from '../entities/crossRef.entity'; + +export default [ + { + title: + 'Thermodynamic Properties of Systems Comprising Esters: Experimental Data and Modeling with PC-SAFT and SAFT Mie', + doi: '10.1021/acs.iecr.9b00714.s001', + publishedAt: '2020-04-09T15:47:35Z', + citations: 0, + references: 0, + }, + { + title: + 'Comparison of CP-PC-SAFT and PC-SAFT with k12 = 0 and PPR78 in Predicting Binary Systems of Hydrocarbons with Squalane, ndodecylbenzene, cis-decalin, Tetralin, and Naphthalene at High Pressures', + doi: '10.1021/acs.iecr.1c03486.s001', + publishedAt: '2021-10-25T13:26:30Z', + citations: 0, + references: 0, + }, + { + title: 'Boyle temperature from SAFT, PC-SAFT and SAFT-VR equations of state', + authors: ['Samane Zarei', 'Farzaneh Feyzi'], + doi: '10.1016/j.molliq.2013.06.010', + publishedAt: '2013-07-04T20:46:55Z', + citations: 15, + references: 44, + }, + { + title: 'Comparison of SAFT-VR-Mie and CP-PC-SAFT in Estimating the Phase Behavior of Acetone + nAlkane Systems', + doi: '10.1021/acs.iecr.0c04435.s001', + publishedAt: '2020-11-25T06:50:17Z', + citations: 0, + references: 0, + }, + { + title: 'Modeling of carbon dioxide and water sorption in glassy polymers through PC-SAFT and NET PC-SAFT', + authors: ['Liang Liu', 'Sandra E. Kentish'], + doi: '10.1016/j.polymer.2016.10.002', + publishedAt: '2016-10-09T04:06:52Z', + citations: 10, + references: 60, + }, + { + title: 'Thermodynamic Modeling of Triglycerides using PC-SAFT', + doi: '10.1021/acs.jced.8b01046.s001', + publishedAt: '2020-04-09T21:21:54Z', + citations: 0, + references: 0, + }, + { + title: 'Vle Property Measurements and Pc-Saft/ Cp- Pc-Saft/ E-Ppr78 Modeling of the Co2 + N-Tetradecane Mixture', + authors: [ + 'Vener Khairutdinov', + 'Farid Gumerov', + 'Ilnar Khabriev', + 'Talgat Akhmetzyanov', + 'Ilfat Salikhov', + 'Ilya Polishuk', + 'ilmutdin abdulagatov', + ], + doi: '10.2139/ssrn.4188488', + publishedAt: '2022-08-13T13:32:29Z', + citations: 0, + references: 0, + }, + { + title: 'Application of PC-SAFT to glycol containing systems – PC-SAFT towards a predictive approach', + authors: ['Andreas Grenner', 'Georgios M. Kontogeorgis', 'Nicolas von Solms', 'Michael L. Michelsen'], + doi: '10.1016/j.fluid.2007.04.025', + publishedAt: '2007-04-30T14:49:55Z', + citations: 37, + references: 37, + }, + { + title: 'PC-SAFT Modeling of CO2 Solubilities in Deep Eutectic Solvents', + doi: '10.1021/acs.jpcb.5b07888.s001', + publishedAt: '2020-04-08T16:19:35Z', + citations: 0, + references: 0, + }, + { + title: + 'IPC-SAFT: An Industrialized Version of the Volume-Translated PC-SAFT Equation of State for Pure Components, Resulting from Experience Acquired All through the Years on the Parameterization of SAFT-Type and Cubic Models', + doi: '10.1021/acs.iecr.9b04660.s001', + publishedAt: '2020-04-07T21:03:41Z', + citations: 0, + references: 0, + }, + { + title: + 'Parameterization of SAFT Models: Analysis of Different Parameter Estimation Strategies and Application to the Development of a Comprehensive Database of PC-SAFT Molecular Parameters', + doi: '10.1021/acs.jced.0c00792.s001', + publishedAt: '2020-11-30T23:27:16Z', + citations: 0, + references: 0, + }, + { + title: + 'Implementation of CP-PC-SAFT and CS-SAFT-VR-Mie for Predicting Thermodynamic Properties of C1C3 Halocarbon Systems. I. Pure Compounds and Mixtures with Nonassociating Compounds', + doi: '10.1021/acs.iecr.1c01700.s001', + publishedAt: '2021-06-28T13:45:30Z', + citations: 0, + references: 0, + }, + { + title: 'Predicting the Solubility of CO2 in Toluene + Ionic Liquid Mixtures with PC-SAFT', + doi: '10.1021/acs.iecr.7b01497.s001', + publishedAt: '2020-04-06T18:59:00Z', + citations: 0, + references: 0, + }, + { + title: + 'Evaluation of SAFT and PC-SAFT models for the description of homo- and co-polymer solution phase equilibria', + authors: ['Theodora Spyriouni', 'Ioannis G. Economou'], + doi: '10.1016/j.polymer.2005.09.001', + publishedAt: '2005-09-23T13:48:14Z', + citations: 19, + references: 27, + }, + { + title: 'VLE property measurements and PC-SAFT/ CP- PC-SAFT/ E-PPR78 modeling of the CO2 + n-tetradecane mixture', + authors: [ + 'Vener F. Khairutdinov', + 'Farid M. Gumerov', + 'Ilnar Sh. Khabriev', + 'Talgat R. Akhmetzyanov', + 'Ilfat Z. Salikhov', + 'Ilya Polishuk', + 'Ilmutdin M. Abdulagatov', + ], + doi: '10.1016/j.fluid.2022.113615', + publishedAt: '2022-09-20T01:54:20Z', + citations: 0, + references: 79, + }, + { + title: 'Thermodynamic Properties of 1Hexyl-3-methylimidazolium Nitrate and 1Alkanols Mixtures: PC-SAFT Model', + doi: '10.1021/acs.jced.9b00507.s001', + publishedAt: '2020-04-08T11:44:12Z', + citations: 0, + references: 0, + }, + { + title: 'Integrated Working Fluids and Process Optimization for Refrigeration Systems Using Polar PC-SAFT', + doi: '10.1021/acs.iecr.1c03624.s001', + publishedAt: '2021-11-29T13:45:54Z', + citations: 0, + references: 0, + }, + { + title: 'Thermodynamic and Transport Properties of Formic Acid and 2Alkanol Mixtures: PC-SAFT Model', + doi: '10.1021/acs.jced.2c00496.s001', + publishedAt: '2022-11-09T21:00:10Z', + citations: 0, + references: 0, + }, + { + title: '5Hydroxymethylfurfural Synthesis in Nonaqueous Two-Phase Systems (NTPS)PC-SAFT Predictions and Validation', + doi: '10.1021/acs.oprd.0c00072.s001', + publishedAt: '2020-05-26T16:46:36Z', + citations: 0, + references: 0, + }, + { + title: 'Investigating Various Parametrization Strategies for Pharmaceuticals within the PC-SAFT Equation of State', + doi: '10.1021/acs.jced.0c00707.s001', + publishedAt: '2020-09-30T13:48:49Z', + citations: 0, + references: 0, + }, +] as PaperInfoExtended[]; diff --git a/backend/src/tests/search.controller.e2e.spec.ts b/backend/src/tests/search.controller.e2e.spec.ts deleted file mode 100644 index e349122..0000000 --- a/backend/src/tests/search.controller.e2e.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { SearchController } from '../search/search.controller'; -import { SearchService } from '../search/search.service'; -import { PaperInfo, PaperInfoExtended } from '../search/entities/crossRef.entity'; -import { HttpModule } from '@nestjs/axios'; -import { INestApplication, ValidationPipe } from '@nestjs/common'; -import * as request from 'supertest'; - -describe('(e2e) SearchController - /search/auto-complete', () => { - let app: INestApplication; - const url = (keyword: string) => `/search/auto-complete?keyword=${keyword}`; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [HttpModule], - controllers: [SearchController], - providers: [SearchService], - }).compile(); - app = module.createNestApplication(); - app.useGlobalPipes(new ValidationPipe()); - await app.init(); - }); - - it('keyword 포함 검색 - GET /search/auto-complete?keyword=coffee', async () => { - const keyword = 'coffee'; - const res = await request(app.getHttpServer()).get(url(keyword)); - const items: PaperInfo[] = JSON.parse(res.text); - expect(items.length).toBe(5); - // TODO: type check - // items.forEach((item) => { - // expect(item).toBeInstanceOf(PaperInfo); - // }); - }); - it('keyword 미포함시 error - GET /search/auto-complete?keyword=', () => { - return request(app.getHttpServer()).get(url('')).expect(400); - }); -}); - -describe('(e2e) SearchController - /search', () => { - let app: INestApplication; - const url = (keyword: string) => `/search?keyword=${keyword}`; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [HttpModule], - controllers: [SearchController], - providers: [SearchService], - }).compile(); - app = module.createNestApplication(); - app.useGlobalPipes(new ValidationPipe()); - await app.init(); - }); - - it('keyword 포함 검색 - GET /search?keyword=coffee', async () => { - const keyword = 'coffee'; - const res = await request(app.getHttpServer()).get(url(keyword)); - const info: { papers: PaperInfoExtended[] } = JSON.parse(res.text); - expect(info.papers.length).toBe(20); - // TODO: type check - // items.forEach((item) => { - // expect(item).toBeInstanceOf(PaperInfoExtended); - // }); - }); - it('keyword 미포함시 error - GET /search?keyword=', () => { - return request(app.getHttpServer()).get(url('')).expect(400); - }); -}); diff --git a/backend/src/util.ts b/backend/src/util.ts index 247bb9a..9c16960 100644 --- a/backend/src/util.ts +++ b/backend/src/util.ts @@ -1,5 +1,5 @@ const BASE_URL = 'https://api.crossref.org/works'; -export const CROSSREF_API_URL = (keyword: string, rows = 5, selects: string[] = ['author', 'title', 'DOI'], page = 1) => +export const CROSSREF_API_URL = (keyword: string, rows = 5, page = 1, selects: string[] = ['author', 'title', 'DOI']) => `${BASE_URL}?query=${keyword}&rows=${rows}&select=${selects.join(',')}&offset=${rows * (page - 1)}`; export const CROSSREF_API_PAPER_URL = (doi: string) => `${BASE_URL}/${doi}`;