From 836902893d88c815f649c3156a3b58265299bda1 Mon Sep 17 00:00:00 2001 From: eldpswp99 Date: Tue, 31 Oct 2023 20:54:51 +0900 Subject: [PATCH 1/2] add backend unit test --- backend/package-lock.json | 9 ++ backend/package.json | 1 + backend/src/review/review.controller.ts | 9 +- backend/src/review/review.service.ts | 7 +- ...pec.ts => adjacent-restaurant-e2e.spec.ts} | 1 - .../adjacent-restaurant.service.spec.ts | 127 ++++++++++++++++++ backend/src/test/user/token.spec.ts | 55 ++++++++ 7 files changed, 205 insertions(+), 4 deletions(-) rename backend/src/test/review/{adjacent-restaurant.spec.ts => adjacent-restaurant-e2e.spec.ts} (99%) create mode 100644 backend/src/test/review/adjacent-restaurant.service.spec.ts create mode 100644 backend/src/test/user/token.spec.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 0da5c5e..ba50972 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -36,6 +36,7 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "string-similarity-js": "^2.1.4", + "ts-mockito": "^2.6.1", "typeorm": "^0.3.17", "typeorm-naming-strategies": "^4.1.0" }, @@ -10385,6 +10386,14 @@ "webpack": "^5.0.0" } }, + "node_modules/ts-mockito": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", + "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", + "dependencies": { + "lodash": "^4.17.5" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", diff --git a/backend/package.json b/backend/package.json index 01d6123..86d8587 100644 --- a/backend/package.json +++ b/backend/package.json @@ -47,6 +47,7 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "string-similarity-js": "^2.1.4", + "ts-mockito": "^2.6.1", "typeorm": "^0.3.17", "typeorm-naming-strategies": "^4.1.0" }, diff --git a/backend/src/review/review.controller.ts b/backend/src/review/review.controller.ts index 3f0fc9c..a99ddf1 100644 --- a/backend/src/review/review.controller.ts +++ b/backend/src/review/review.controller.ts @@ -29,6 +29,7 @@ import { UserEntity } from '../user/models/user.entity'; import { UserRepository } from '../user/repostiories/user.repository'; import { ReviewAdjacentQueryDto } from './dtos/in-dtos/review-adjacent-query.dto'; import { RestaurantListDto } from './dtos/out-dtos/restaurantList.dto'; +import { RestaurantRepository } from './repositories/restaurant.repository'; @ApiTags('reviews') @Controller('reviews') @@ -37,6 +38,7 @@ export class ReviewController { private s3ImageService: S3ImageService, private reviewService: ReviewService, private reviewRepository: ReviewRepository, + private restaurantRepository: RestaurantRepository, ) {} @UseGuards(JwtAccessGuard) @@ -81,7 +83,12 @@ export class ReviewController { @Req() { user }: UserRequest, @Query() data: ReviewAdjacentQueryDto, ) { - const restaurants = await this.reviewService.getAdjacentRestaurant(data); + const allRestaurants = await this.restaurantRepository.find({}); + + const restaurants = this.reviewService.getAdjacentRestaurant( + data, + allRestaurants, + ); return new RestaurantListDto(restaurants); } diff --git a/backend/src/review/review.service.ts b/backend/src/review/review.service.ts index 38695c3..6ce2d3f 100644 --- a/backend/src/review/review.service.ts +++ b/backend/src/review/review.service.ts @@ -7,6 +7,7 @@ import { In } from 'typeorm'; import { ReviewEntity } from './models/review.entity'; import { ReviewAdjacentQueryDto } from './dtos/in-dtos/review-adjacent-query.dto'; import { getDistance } from 'geolib'; +import { RestaurantEntity } from './models/restaurant.entity'; @Injectable() export class ReviewService { @@ -40,9 +41,11 @@ export class ReviewService { }).save(); } - async getAdjacentRestaurant(data: ReviewAdjacentQueryDto) { + getAdjacentRestaurant( + data: ReviewAdjacentQueryDto, + restaurants: RestaurantEntity[], + ) { const { longitude, latitude, distance } = data; - const restaurants = await this.restaurantRepository.find({}); const KILOMETER = 1000; return restaurants.filter( diff --git a/backend/src/test/review/adjacent-restaurant.spec.ts b/backend/src/test/review/adjacent-restaurant-e2e.spec.ts similarity index 99% rename from backend/src/test/review/adjacent-restaurant.spec.ts rename to backend/src/test/review/adjacent-restaurant-e2e.spec.ts index 7a44cb8..a997010 100644 --- a/backend/src/test/review/adjacent-restaurant.spec.ts +++ b/backend/src/test/review/adjacent-restaurant-e2e.spec.ts @@ -99,7 +99,6 @@ describe('Get adjacent restaurant test', () => { validateRestaurantList(body); }); - // TODO : unittest it('멀리 떨어지면 안잡한다', async () => { restaurant.longitude = 127.0; restaurant.latitude = 37.018018; diff --git a/backend/src/test/review/adjacent-restaurant.service.spec.ts b/backend/src/test/review/adjacent-restaurant.service.spec.ts new file mode 100644 index 0000000..654f47d --- /dev/null +++ b/backend/src/test/review/adjacent-restaurant.service.spec.ts @@ -0,0 +1,127 @@ +import { NestExpressApplication } from '@nestjs/platform-express'; +import { AppModule } from '../../app.module'; +import { Test } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; +import { UserEntity } from '../../user/models/user.entity'; +import { RestaurantEntity } from '../../review/models/restaurant.entity'; +import { ReviewService } from '../../review/review.service'; +import { plainToInstance } from 'class-transformer'; +import { ReviewAdjacentQueryDto } from '../../review/dtos/in-dtos/review-adjacent-query.dto'; + +describe('Get adjacent restaurant test', () => { + let testServer: NestExpressApplication; + let reviewService: ReviewService; + let restaurant: RestaurantEntity; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + restaurant = RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 127.018018, + latitude: 37.018018, + }); + restaurant = RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 127.018018, + latitude: 50.018018, + }); + reviewService = module.get(ReviewService); + }); + + beforeEach(async () => {}); + + it('멀리 떨어지면 안잡한다', async () => { + const restaurants = [ + RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 127.018018, + latitude: 37.018018, + }), + RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 127.018018, + latitude: 50.018018, + }), + ]; + + const adjacentRestaurants = reviewService.getAdjacentRestaurant( + plainToInstance(ReviewAdjacentQueryDto, { + longitude: 127.018018, + latitude: 37.018018, + distance: 1, + }), + restaurants, + ); + + expect(adjacentRestaurants.length).toBe(1); + }); + + it('멀리 떨어지면 안잡한다', async () => { + const restaurants = [ + RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 127.018018, + latitude: 37.018018, + }), + RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 127.018018, + latitude: 50.018018, + }), + ]; + + const adjacentRestaurants = reviewService.getAdjacentRestaurant( + plainToInstance(ReviewAdjacentQueryDto, { + longitude: 127.018018, + latitude: 37.018018, + distance: 1, + }), + restaurants, + ); + + expect(adjacentRestaurants.length).toBe(1); + }); + + it('거리 세밀한 체크 2.5km내', async () => { + const restaurants = [ + RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 127.046107, + latitude: 37.018018, + }), + RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 127.018018, + latitude: 36.995496, + }), + RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 126.989929, + latitude: 37.018018, + }), + RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 127.057343, + latitude: 37.018018, + }), + RestaurantEntity.create({ + googleMapPlaceId: '123', + longitude: 126.978693, + latitude: 37.018018, + }), + ]; + + const adjacentRestaurants = reviewService.getAdjacentRestaurant( + plainToInstance(ReviewAdjacentQueryDto, { + longitude: 127.018018, + latitude: 37.018018, + distance: 3, + }), + restaurants, + ); + + expect(adjacentRestaurants.length).toBe(3); + }); +}); diff --git a/backend/src/test/user/token.spec.ts b/backend/src/test/user/token.spec.ts new file mode 100644 index 0000000..aeac9c0 --- /dev/null +++ b/backend/src/test/user/token.spec.ts @@ -0,0 +1,55 @@ +import { NestExpressApplication } from '@nestjs/platform-express'; +import { AppModule } from '../../app.module'; +import { Test } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; +import { appSetting } from '../../main'; +import * as supertest from 'supertest'; +import { UserEntity } from '../../user/models/user.entity'; +import { UserFixture } from '../fixture/user.fixture'; +import { HttpStatus } from '@nestjs/common'; +import { FollowEntity } from '../../user/models/follow.entity'; +import { validateDtoKeys } from '../utils'; +import { decode } from 'jsonwebtoken'; + +describe('getUserMe test', () => { + let testServer: NestExpressApplication; + let user: UserEntity; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + }); + + beforeEach(async () => { + user = UserEntity.create({ + id: 8, + username: 'hello', + password: 'world', + name: 'hi', + }); + }); + + it('token creation', async () => { + const token = user.createToken(); + expect(token).toHaveProperty('accessToken'); + expect(token).toHaveProperty('refreshToken'); + }); + + it('token payload', async () => { + const payload = user.tokenPayload; + + expect(payload).toHaveProperty('id'); + expect(payload).toHaveProperty('username'); + expect(payload).toHaveProperty('iat'); + }); + + it('decode access token', () => { + const { accessToken } = user.createToken(); + const payload = decode(accessToken); + + expect(payload).toHaveProperty('id'); + expect(payload).toHaveProperty('username'); + expect(payload).toHaveProperty('iat'); + }); +}); From 7e49d008d04cdc58a3c07e07d64bb7100f6e4093 Mon Sep 17 00:00:00 2001 From: eldpswp99 Date: Thu, 2 Nov 2023 16:00:20 +0900 Subject: [PATCH 2/2] get restaurants where friends posted reviews --- backend/package.json | 2 +- .../review/repositories/review.repository.ts | 24 ++- backend/src/review/review.controller.ts | 15 ++ .../friend-review-restaurant-e2e.spec.ts | 171 ++++++++++++++++++ backend/src/test/user/token.spec.ts | 4 +- 5 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 backend/src/test/review/friend-review-restaurant-e2e.spec.ts diff --git a/backend/package.json b/backend/package.json index 86d8587..9f80cdf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,7 +13,7 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest --runInBand --forceExit", + "test": "jest --runInBand --forceExit --coverage", "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", diff --git a/backend/src/review/repositories/review.repository.ts b/backend/src/review/repositories/review.repository.ts index e06fd36..10a45d6 100644 --- a/backend/src/review/repositories/review.repository.ts +++ b/backend/src/review/repositories/review.repository.ts @@ -1,9 +1,10 @@ import { CustomRepository } from '../../typeorm-ex/typeorm-ex.decorator'; import { ReviewEntity } from '../models/review.entity'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { FindManyOptions } from 'typeorm/find-options/FindManyOptions'; import { FindOptionsWhere } from 'typeorm/find-options/FindOptionsWhere'; import { UserEntity } from '../../user/models/user.entity'; +import { FollowEntity } from '../../user/models/follow.entity'; @CustomRepository(ReviewEntity) export class ReviewRepository extends Repository { @@ -58,6 +59,27 @@ export class ReviewRepository extends Repository { }); } + async findReviewOfFriends(user: UserEntity) { + const friendsIds = ( + await FollowEntity.find({ + where: { + user: { + id: user.id, + }, + }, + relations: { + follower: true, + }, + }) + ).map((follow) => follow.follower.id); + + return this.findFull({ + user: { + id: In(friendsIds), + }, + }); + } + findRandomReviews(limit: number) { return this.createQueryBuilder('review') .leftJoinAndSelect('review.images', 'image') diff --git a/backend/src/review/review.controller.ts b/backend/src/review/review.controller.ts index a99ddf1..1a4ba7e 100644 --- a/backend/src/review/review.controller.ts +++ b/backend/src/review/review.controller.ts @@ -30,6 +30,7 @@ import { UserRepository } from '../user/repostiories/user.repository'; import { ReviewAdjacentQueryDto } from './dtos/in-dtos/review-adjacent-query.dto'; import { RestaurantListDto } from './dtos/out-dtos/restaurantList.dto'; import { RestaurantRepository } from './repositories/restaurant.repository'; +import { In } from 'typeorm'; @ApiTags('reviews') @Controller('reviews') @@ -93,6 +94,20 @@ export class ReviewController { return new RestaurantListDto(restaurants); } + @UseGuards(JwtAccessGuard) + @Get('/friends/restaurants') + async getReviewOfFriends(@Req() { user }: UserRequest) { + const reviewEntities = + await this.reviewRepository.findReviewOfFriends(user); + + const restaurantIds = reviewEntities.map((review) => review.restaurant.id); + const restaurants = await this.restaurantRepository.findBy({ + id: In(restaurantIds), + }); + + return new RestaurantListDto(restaurants); + } + @UseGuards(JwtAccessGuard) @Get('/:reviewId') async getReviewDetail( diff --git a/backend/src/test/review/friend-review-restaurant-e2e.spec.ts b/backend/src/test/review/friend-review-restaurant-e2e.spec.ts new file mode 100644 index 0000000..50efbe9 --- /dev/null +++ b/backend/src/test/review/friend-review-restaurant-e2e.spec.ts @@ -0,0 +1,171 @@ +import { NestExpressApplication } from '@nestjs/platform-express'; +import { AppModule } from '../../app.module'; +import { Test } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; +import { appSetting } from '../../main'; +import * as supertest from 'supertest'; +import { UserEntity } from '../../user/models/user.entity'; +import { UserFixture } from '../fixture/user.fixture'; +import { HttpStatus } from '@nestjs/common'; +import { RestaurantEntity } from '../../review/models/restaurant.entity'; +import { RestaurantFixture } from '../fixture/restaurant.fixture'; +import { ReviewEntity } from '../../review/models/review.entity'; +import { ReviewFixture } from '../fixture/review.fixture'; +import { ImageFixture } from '../fixture/image.fixture'; +import { + validateRestaurantList, + validateReview, + validateReviewList, +} from './validateReviewList'; + +describe('Get adjacent restaurant test', () => { + let testServer: NestExpressApplication; + let dataSource: DataSource; + let user: UserEntity; + let anotherUser: UserEntity; + let accessToken: string; + let restaurant: RestaurantEntity; + let review: ReviewEntity; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + testServer = module.createNestApplication(); + dataSource = testServer.get(DataSource); + await dataSource.synchronize(true); + appSetting(testServer); + + await testServer.init(); + }); + + beforeEach(async () => { + await dataSource.synchronize(true); + + user = await UserFixture.create({ + name: 'hi', + username: 'hello', + password: 'world', + }); + + anotherUser = await UserFixture.create({ + name: 'hi', + username: 'hello', + password: 'world', + }); + + const { body } = await supertest(testServer.getHttpServer()) + .post('/auth/login') + .send({ + username: 'hello', + password: 'world', + }) + .expect(HttpStatus.CREATED); + + accessToken = body.accessToken; + + restaurant = await RestaurantFixture.create({}); + const image = await ImageFixture.create({}); + review = await ReviewFixture.create({ + restaurant, + images: [image], + user: anotherUser, + }); + + const anotherRestaurant = await RestaurantFixture.create({}); + review = await ReviewFixture.create({ + restaurant: anotherRestaurant, + images: [image], + user, + }); + + await user.follow(anotherUser); + }); + + it('unauthorized', async () => { + await supertest(testServer.getHttpServer()) + .get(`/reviews/friends/restaurants`) + .expect(HttpStatus.UNAUTHORIZED); + }); + + it('OK', async () => { + await supertest(testServer.getHttpServer()) + .get(`/reviews/friends/restaurants`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + }); + + it('DTO check', async () => { + const { body } = await supertest(testServer.getHttpServer()) + .get(`/reviews/friends/restaurants`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + validateRestaurantList(body); + expect(body.restaurantList.length).toBe(1); + }); + + it('친구 끊으면 나오지 않는다', async () => { + const follow = await user.findFollow(anotherUser); + await user.unfollow(follow!); + const { body } = await supertest(testServer.getHttpServer()) + .get(`/reviews/friends/restaurants`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + + expect(body.restaurantList.length).toBe(0); + }); + + it('제 3의 유저의 리뷰 레스토랑은 나오지 않는다', async () => { + const newUser = await UserFixture.create({ + name: 'hi', + username: 'hello', + password: 'world', + }); + + const newRestaurant = await RestaurantFixture.create({}); + const image = await ImageFixture.create({}); + review = await ReviewFixture.create({ + restaurant: newRestaurant, + images: [image], + user: newUser, + }); + + const { body } = await supertest(testServer.getHttpServer()) + .get(`/reviews/friends/restaurants`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + expect(body.restaurantList.length).toBe(1); + }); + + it('제 3의 유저의 리뷰 레스토랑은 나오지 않는다 - 친구하면 나온다', async () => { + const newUser = await UserFixture.create({ + name: 'hi', + username: 'hello', + password: 'world', + }); + + const newRestaurant = await RestaurantFixture.create({}); + const image = await ImageFixture.create({}); + review = await ReviewFixture.create({ + restaurant: newRestaurant, + images: [image], + user: newUser, + }); + + const { body } = await supertest(testServer.getHttpServer()) + .get(`/reviews/friends/restaurants`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + expect(body.restaurantList.length).toBe(1); + + await user.follow(newUser); + + const { body: newBody } = await supertest(testServer.getHttpServer()) + .get(`/reviews/friends/restaurants`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(HttpStatus.OK); + expect(newBody.restaurantList.length).toBe(2); + }); +}); diff --git a/backend/src/test/user/token.spec.ts b/backend/src/test/user/token.spec.ts index aeac9c0..c0e9078 100644 --- a/backend/src/test/user/token.spec.ts +++ b/backend/src/test/user/token.spec.ts @@ -11,7 +11,7 @@ import { FollowEntity } from '../../user/models/follow.entity'; import { validateDtoKeys } from '../utils'; import { decode } from 'jsonwebtoken'; -describe('getUserMe test', () => { +describe('token test', () => { let testServer: NestExpressApplication; let user: UserEntity; @@ -41,7 +41,6 @@ describe('getUserMe test', () => { expect(payload).toHaveProperty('id'); expect(payload).toHaveProperty('username'); - expect(payload).toHaveProperty('iat'); }); it('decode access token', () => { @@ -50,6 +49,5 @@ describe('getUserMe test', () => { expect(payload).toHaveProperty('id'); expect(payload).toHaveProperty('username'); - expect(payload).toHaveProperty('iat'); }); });