From 221d01557a0a0d2c89ffb7d9201e363f0dfd86f3 Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 4 Dec 2024 17:07:57 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=85=20test=20:=20auth=20Service=20=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.service.spec.ts | 188 +++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 BE/src/auth/auth.service.spec.ts diff --git a/BE/src/auth/auth.service.spec.ts b/BE/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..3647d3e --- /dev/null +++ b/BE/src/auth/auth.service.spec.ts @@ -0,0 +1,188 @@ +import { JwtService } from '@nestjs/jwt'; +import { AuthService } from './auth.service'; +import { UserRepository } from './user.repository'; +import { ConfigService } from '@nestjs/config'; +import { AuthCredentialsDto } from './dto/auth-credentials.dto'; +import { Test } from '@nestjs/testing'; +import { User } from './user.entity'; +import * as bcrypt from 'bcrypt'; +import { config } from 'dotenv'; +import { UnauthorizedException } from '@nestjs/common'; + +config(); + +describe('auth service 테스트', () => { + let authService: AuthService; + let userRepository: UserRepository; + let jwtService: JwtService; + let configService: ConfigService; + + const mockAuthCredentials: AuthCredentialsDto = { + email: 'jindding', + password: '1234', + }; + + const mockUser: User = { + id: 1, + email: 'jindding', + password: '1234', + tutorial: false, + kakaoId: '', + currentRefreshToken: 'validRefreshToken', + currentRefreshTokenExpiresAt: null, + toAuthCredentialsDto: jest.fn().mockReturnValue({ + email: 'jindding', + password: '1234', + }), + nickname: 'testNickname', + hasId: jest.fn(), + save: jest.fn(), + remove: jest.fn(), + softRemove: jest.fn(), + recover: jest.fn(), + reload: jest.fn(), + }; + + beforeEach(async () => { + const mockUserRepository = { + registerUser: jest.fn(), + registerKakaoUser: jest.fn(), + findOne: jest.fn(), + update: jest.fn(), + }; + + const mockJwtService = { + signAsync: jest.fn(), + verify: jest.fn(), + }; + + const mockConfigService = { + get: jest.fn(), + }; + + const mockbcrypt = { + compare: jest.fn(), + }; + + const module = await Test.createTestingModule({ + providers: [ + AuthService, + { + provide: UserRepository, + useValue: mockUserRepository, + }, + { + provide: JwtService, + useValue: mockJwtService, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + authService = module.get(AuthService); + userRepository = module.get(UserRepository); + jwtService = module.get(JwtService); + configService = module.get(ConfigService); + }); + + it('회원가입 요청 시, DB에 회원 정보가 저장된다.', async () => { + const registerUserSpy = jest.spyOn(userRepository, 'registerUser'); + await authService.signUp(mockAuthCredentials); + expect(registerUserSpy).toHaveBeenCalledWith(mockAuthCredentials); + }); + + describe('로그인 테스트', () => { + it('DB에 존재하는 이메일과 비밀번호로 로그인 시, 토큰이 발급된다.', async () => { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + jest.spyOn(jwtService, 'signAsync').mockResolvedValue('token'); + jest + .spyOn(bcrypt, 'compare') + .mockImplementation(() => Promise.resolve(true)); + + jest.spyOn(configService, 'get').mockReturnValue(process.env.JWT_SECRET); + + const result = await authService.loginUser(mockAuthCredentials); + + expect(result).toHaveProperty('accessToken'); + expect(result).toHaveProperty('refreshToken'); + }); + + it('DB에 존재하지 않는 이메일로 로그인 시, 에러가 발생한다.', async () => { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(null); + + await expect( + authService.loginUser(mockAuthCredentials), + ).rejects.toThrow(); + }); + }); + + describe('카카오 로그인 테스트', () => { + it('첫 카카오 로그인 시, 회원 가입 후 토큰이 발급된다.', async () => { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(null); + jest + .spyOn(userRepository, 'registerKakaoUser') + .mockResolvedValue(undefined); + jest.spyOn(jwtService, 'signAsync').mockResolvedValue('token'); + + const result = await authService.kakaoLoginUser(mockAuthCredentials); + + expect(userRepository.registerKakaoUser).toHaveBeenCalled(); + expect(result).toHaveProperty('accessToken'); + expect(result).toHaveProperty('refreshToken'); + }); + + it('이미 가입된 카카오 계정으로 로그인 시, 토큰이 발급된다.', async () => { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + jest.spyOn(jwtService, 'signAsync').mockResolvedValue('token'); + + const result = await authService.kakaoLoginUser(mockAuthCredentials); + + expect(userRepository.registerKakaoUser).not.toHaveBeenCalled(); + expect(result).toHaveProperty('accessToken'); + expect(result).toHaveProperty('refreshToken'); + }); + }); + + describe('토큰 발급 테스트', () => { + it('유효한 리프레시 토큰으로 요청 시 , 새로운 액세스 토큰이 발급된다.', async () => { + const decodedToken = { email: mockUser.email }; + const mockRefreshToken = mockUser.currentRefreshToken; + + jest.spyOn(jwtService, 'verify').mockReturnValue(decodedToken); + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + jest + .spyOn(bcrypt, 'compare') + .mockImplementation(() => Promise.resolve(true)); + jest.spyOn(jwtService, 'signAsync').mockResolvedValue('newAccessToken'); + jest.spyOn(configService, 'get').mockReturnValue(process.env.JWT_SECRET); + + const result = await authService.refreshToken(mockRefreshToken); + + expect(result).toBe('newAccessToken'); + }); + + it('유효하지 않은 리프레시 토큰으로 요청 시, 에러가 발생한다.', async () => { + jest.spyOn(jwtService, 'verify').mockImplementation(() => { + throw new Error('Invalid token'); + }); + + await expect(authService.refreshToken('invalidToken')).rejects.toThrow(); + }); + + it('저장된 리프레시 토큰과 일치하지 않을 경우, UnauthorizedException이 발생한다.', async () => { + const decodedToken = { email: mockUser.email }; + jest.spyOn(jwtService, 'verify').mockReturnValue(decodedToken); + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + jest + .spyOn(bcrypt, 'compare') + .mockImplementation(() => Promise.resolve(false)); + + await expect(authService.refreshToken('invalidToken')).rejects.toThrow( + UnauthorizedException, + ); + }); + }); +}); From 502cb281f2c2a915107b5a46696410fb99a2d26c Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 4 Dec 2024 17:25:32 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20:=20auth=20?= =?UTF-8?q?Service=20test=20eslint=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.service.spec.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/BE/src/auth/auth.service.spec.ts b/BE/src/auth/auth.service.spec.ts index 3647d3e..f7adba0 100644 --- a/BE/src/auth/auth.service.spec.ts +++ b/BE/src/auth/auth.service.spec.ts @@ -1,13 +1,13 @@ import { JwtService } from '@nestjs/jwt'; -import { AuthService } from './auth.service'; -import { UserRepository } from './user.repository'; import { ConfigService } from '@nestjs/config'; -import { AuthCredentialsDto } from './dto/auth-credentials.dto'; import { Test } from '@nestjs/testing'; -import { User } from './user.entity'; import * as bcrypt from 'bcrypt'; import { config } from 'dotenv'; import { UnauthorizedException } from '@nestjs/common'; +import { User } from './user.entity'; +import { AuthCredentialsDto } from './dto/auth-credentials.dto'; +import { UserRepository } from './user.repository'; +import { AuthService } from './auth.service'; config(); @@ -60,10 +60,6 @@ describe('auth service 테스트', () => { get: jest.fn(), }; - const mockbcrypt = { - compare: jest.fn(), - }; - const module = await Test.createTestingModule({ providers: [ AuthService, @@ -90,18 +86,20 @@ describe('auth service 테스트', () => { it('회원가입 요청 시, DB에 회원 정보가 저장된다.', async () => { const registerUserSpy = jest.spyOn(userRepository, 'registerUser'); - await authService.signUp(mockAuthCredentials); - expect(registerUserSpy).toHaveBeenCalledWith(mockAuthCredentials); + await authService.signUp(mockAuthCredentials).then(() => { + expect(registerUserSpy).toHaveBeenCalledWith(mockAuthCredentials); + }); }); describe('로그인 테스트', () => { it('DB에 존재하는 이메일과 비밀번호로 로그인 시, 토큰이 발급된다.', async () => { jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); jest.spyOn(jwtService, 'signAsync').mockResolvedValue('token'); - jest - .spyOn(bcrypt, 'compare') - .mockImplementation(() => Promise.resolve(true)); - + /* eslint-disable @typescript-eslint/no-misused-promises */ + /* eslint-disable func-names */ + jest.spyOn(bcrypt, 'compare').mockImplementation(function () { + return Promise.resolve(true); + }); jest.spyOn(configService, 'get').mockReturnValue(process.env.JWT_SECRET); const result = await authService.loginUser(mockAuthCredentials); @@ -129,7 +127,9 @@ describe('auth service 테스트', () => { const result = await authService.kakaoLoginUser(mockAuthCredentials); - expect(userRepository.registerKakaoUser).toHaveBeenCalled(); + expect( + jest.spyOn(userRepository, 'registerKakaoUser'), + ).toHaveBeenCalled(); expect(result).toHaveProperty('accessToken'); expect(result).toHaveProperty('refreshToken'); }); @@ -140,7 +140,7 @@ describe('auth service 테스트', () => { const result = await authService.kakaoLoginUser(mockAuthCredentials); - expect(userRepository.registerKakaoUser).not.toHaveBeenCalled(); + expect(jest.spyOn(userRepository, 'registerUser')).not.toHaveBeenCalled(); expect(result).toHaveProperty('accessToken'); expect(result).toHaveProperty('refreshToken'); }); From bd4b374198318d0065c6830167b6030aa2786cd5 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 4 Dec 2024 17:26:19 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8A=A4=20=EC=B6=94=EA=B0=80=20#238?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.entity.ts | 3 ++- BE/src/stock/bookmark/stock-bookmark.entity.ts | 3 ++- BE/src/stock/order/stock-order.entity.ts | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/BE/src/asset/asset.entity.ts b/BE/src/asset/asset.entity.ts index 2e010f4..eab36b9 100644 --- a/BE/src/asset/asset.entity.ts +++ b/BE/src/asset/asset.entity.ts @@ -1,8 +1,9 @@ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm'; const INIT_ASSET = 10000000; @Entity('assets') +@Unique(['user_id']) export class Asset { @PrimaryGeneratedColumn() id: number; diff --git a/BE/src/stock/bookmark/stock-bookmark.entity.ts b/BE/src/stock/bookmark/stock-bookmark.entity.ts index 107a957..9e04e5c 100644 --- a/BE/src/stock/bookmark/stock-bookmark.entity.ts +++ b/BE/src/stock/bookmark/stock-bookmark.entity.ts @@ -1,6 +1,7 @@ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm'; @Entity('bookmarks') +@Unique(['user_id', 'stock_code']) export class Bookmark { @PrimaryGeneratedColumn() id: number; diff --git a/BE/src/stock/order/stock-order.entity.ts b/BE/src/stock/order/stock-order.entity.ts index 7498b72..66d3450 100644 --- a/BE/src/stock/order/stock-order.entity.ts +++ b/BE/src/stock/order/stock-order.entity.ts @@ -2,6 +2,7 @@ import { Column, CreateDateColumn, Entity, + Index, PrimaryGeneratedColumn, } from 'typeorm'; import { TradeType } from './enum/trade-type'; @@ -12,9 +13,11 @@ export class Order { @PrimaryGeneratedColumn() id: number; + @Index() @Column({ nullable: false }) user_id: number; + @Index() @Column({ nullable: false }) stock_code: string; From f1ddf56798518df9c1f6c3944d942f0c3c042d5e Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 4 Dec 2024 18:11:07 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=85=20test=20:=20ranking=20service=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.spec.ts | 259 +++++++++++++++++++++++++ BE/src/ranking/ranking.service.ts | 4 +- 2 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 BE/src/ranking/ranking.service.spec.ts diff --git a/BE/src/ranking/ranking.service.spec.ts b/BE/src/ranking/ranking.service.spec.ts new file mode 100644 index 0000000..afeff73 --- /dev/null +++ b/BE/src/ranking/ranking.service.spec.ts @@ -0,0 +1,259 @@ +import { RankingService } from './ranking.service'; +import { RedisDomainService } from '../common/redis/redis.domain-service'; +import { Test } from '@nestjs/testing'; +import { AssetRepository } from '../asset/asset.repository'; +import { SortType } from './enum/sort-type.enum'; + +describe('Ranking Service 테스트', () => { + let rankingService: RankingService; + let assetRepository: AssetRepository; + let redisDomainService: RedisDomainService; + + const mockAssets = [ + { + id: 1, + user_id: '1', + nickname: 'user1', + total_asset: 15000000, + prev_total_asset: 10000000, + }, + { + id: 2, + user_id: '2', + nickname: 'user2', + total_asset: 12000000, + prev_total_asset: 10000000, + }, + { + id: 3, + user_id: '3', + nickname: 'user3', + total_asset: 8000000, + prev_total_asset: 10000000, + }, + ]; + + beforeEach(async () => { + const mockAssetRepository = { + getAssets: jest.fn(), + }; + + const mockRedisDomainService = { + exists: jest.fn(), + zadd: jest.fn(), + zrange: jest.fn(), + zrevrange: jest.fn(), + zrevrank: jest.fn(), + del: jest.fn(), + }; + + const module = await Test.createTestingModule({ + providers: [ + RankingService, + { provide: AssetRepository, useValue: mockAssetRepository }, + { provide: RedisDomainService, useValue: mockRedisDomainService }, + ], + }).compile(); + + rankingService = module.get(RankingService); + assetRepository = module.get(AssetRepository); + redisDomainService = module.get(RedisDomainService); + }); + + describe('로그인 되지 않은 유저 전체 랭킹 조회 테스트', () => { + it('랭킹 조회시, 수익률과 총자산 랭킹이 모두 반환된다.', async () => { + jest + .spyOn(redisDomainService, 'exists') + .mockResolvedValue(Promise.resolve(1)); + + jest.spyOn(redisDomainService, 'zrevrange').mockResolvedValue([ + JSON.stringify({ + userId: '1', + nickname: 'user1', + value: 50, + }), + JSON.stringify({ + userId: '2', + nickname: 'user2', + value: 20, + }), + JSON.stringify({ + userId: '3', + nickname: 'user3', + value: -20, + }), + ]); + + const result = await rankingService.getRanking(); + + expect(result).toHaveProperty('profitRateRanking'); + expect(result).toHaveProperty('assetRanking'); + expect(result.profitRateRanking.topRank).toHaveLength(3); + expect(result.assetRanking.topRank).toHaveLength(3); + }); + + it('Redis에 랭킹 데이터가 없을 경우, 새로 계산하여 저장한다.', async () => { + jest + .spyOn(redisDomainService, 'exists') + .mockResolvedValue(Promise.resolve(0)); + + jest.spyOn(assetRepository, 'getAssets').mockResolvedValue(mockAssets); + const zaddSpy = jest + .spyOn(redisDomainService, 'zadd') + .mockResolvedValue(1); + + jest.spyOn(redisDomainService, 'zrevrange').mockResolvedValue([ + JSON.stringify({ + userId: '1', + nickname: 'user1', + value: 50, + }), + JSON.stringify({ + userId: '2', + nickname: 'user2', + value: 20, + }), + JSON.stringify({ + userId: '3', + nickname: 'user3', + value: -20, + }), + ]); + + await rankingService.getRanking(); + expect(zaddSpy).toHaveBeenCalled(); + }); + + describe('로그인 된 사용자 랭킹 조회 테스트', () => { + it('인증된 사용자의 랭킹 조회 시, 본인의 랭킹 정보도 함께 반환된다.', async () => { + const userId = '1'; + const mockProfitRateData = [ + JSON.stringify({ + userId: '1', + nickname: 'user1', + value: 50, + }), + JSON.stringify({ + userId: '2', + nickname: 'user2', + value: 20, + }), + JSON.stringify({ + userId: '3', + nickname: 'user3', + value: -20, + }), + ]; + + const mockAssetData = [ + JSON.stringify({ + userId: '1', + nickname: 'user1', + value: 15000000, + }), + JSON.stringify({ + userId: '2', + nickname: 'user2', + value: 12000000, + }), + JSON.stringify({ + userId: '3', + nickname: 'user3', + value: 8000000, + }), + ]; + + jest.spyOn(redisDomainService, 'exists').mockResolvedValue(1); + jest.spyOn(redisDomainService, 'zrevrange').mockResolvedValue([ + JSON.stringify({ + userId: '1', + nickname: 'user1', + value: 50, + }), + JSON.stringify({ + userId: '2', + nickname: 'user2', + value: 20, + }), + JSON.stringify({ + userId: '3', + nickname: 'user3', + value: -20, + }), + ]); + + const zrangeSpy = jest.spyOn(redisDomainService, 'zrange'); + zrangeSpy.mockImplementation((key) => { + if (key.includes('profitRate')) { + return Promise.resolve(mockProfitRateData); + } else { + return Promise.resolve(mockAssetData); + } + }); + jest.spyOn(redisDomainService, 'zrevrank').mockResolvedValue(0); + jest.spyOn(redisDomainService, 'zrevrange').mockResolvedValue([ + JSON.stringify({ + userId: '1', + nickname: 'user1', + value: 50, + }), + ]); + + const result = await rankingService.getRankingAuthUser(userId); + + expect(result).toHaveProperty('profitRateRanking'); + expect(result).toHaveProperty('assetRanking'); + expect(result.profitRateRanking.userRank.userId).toBe(userId); + expect(result.profitRateRanking.userRank.value).toBe(50); + expect(result.profitRateRanking.userRank.rank).toBe(1); + expect(result.assetRanking.userRank.userId).toBe(userId); + expect(result.assetRanking.userRank.rank).toBe(1); + expect(result.assetRanking.userRank.value).toBe(15000000); + }); + }); + + describe('랭킹 계산 테스트', () => { + it('수익률 기준으로 랭킹이 정렬된다.', async () => { + jest.spyOn(assetRepository, 'getAssets').mockResolvedValue(mockAssets); + + const result = await rankingService.calculateRanking( + SortType.PROFIT_RATE, + ); + + expect(result).toHaveLength(3); + expect(result[0].userId).toBe('1'); + expect(result[1].userId).toBe('2'); + expect(result[2].userId).toBe('3'); + }); + + it('총자산 기준으로 랭킹이 정렬된다', async () => { + jest.spyOn(assetRepository, 'getAssets').mockResolvedValue(mockAssets); + + const result = await rankingService.calculateRanking(SortType.ASSET); + + expect(result).toHaveLength(3); + expect(result[0].userId).toBe('1'); + expect(result[1].userId).toBe('2'); + expect(result[2].userId).toBe('3'); + }); + }); + + describe('랭킹 업데이트 테스트', () => { + it('랭킹 업데이트 시, Redis의 기존 데이터를 삭제하고 새로운 데이터를 저장한다.', async () => { + jest.spyOn(assetRepository, 'getAssets').mockResolvedValue(mockAssets); + + const delSpy = jest + .spyOn(redisDomainService, 'del') + .mockResolvedValue(1); + const zaddSpy = jest + .spyOn(redisDomainService, 'zadd') + .mockResolvedValue(1); + + await rankingService.updateRanking(); + + expect(delSpy).toHaveBeenCalledTimes(2); + expect(zaddSpy).toHaveBeenCalledTimes(6); + }); + }); + }); +}); diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index aa13a8b..e501aa4 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { RedisDomainService } from 'src/common/redis/redis.domain-service'; -import { AssetRepository } from 'src/asset/asset.repository'; +import { RedisDomainService } from '../common/redis/redis.domain-service'; +import { AssetRepository } from '../asset/asset.repository'; import { Cron } from '@nestjs/schedule'; import { SortType } from './enum/sort-type.enum'; import { Ranking } from './interface/ranking.interface'; From 9e136c2f731c1b77beab46e1bb9f1a37ccb2194c Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 4 Dec 2024 18:12:17 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=85=20test=20:=20ranking=20service=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.spec.ts | 5 ++--- BE/src/ranking/ranking.service.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/BE/src/ranking/ranking.service.spec.ts b/BE/src/ranking/ranking.service.spec.ts index afeff73..95eecb0 100644 --- a/BE/src/ranking/ranking.service.spec.ts +++ b/BE/src/ranking/ranking.service.spec.ts @@ -1,6 +1,6 @@ +import { Test } from '@nestjs/testing'; import { RankingService } from './ranking.service'; import { RedisDomainService } from '../common/redis/redis.domain-service'; -import { Test } from '@nestjs/testing'; import { AssetRepository } from '../asset/asset.repository'; import { SortType } from './enum/sort-type.enum'; @@ -186,9 +186,8 @@ describe('Ranking Service 테스트', () => { zrangeSpy.mockImplementation((key) => { if (key.includes('profitRate')) { return Promise.resolve(mockProfitRateData); - } else { - return Promise.resolve(mockAssetData); } + return Promise.resolve(mockAssetData); }); jest.spyOn(redisDomainService, 'zrevrank').mockResolvedValue(0); jest.spyOn(redisDomainService, 'zrevrange').mockResolvedValue([ diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index e501aa4..d5eeda6 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; import { RedisDomainService } from '../common/redis/redis.domain-service'; import { AssetRepository } from '../asset/asset.repository'; -import { Cron } from '@nestjs/schedule'; import { SortType } from './enum/sort-type.enum'; import { Ranking } from './interface/ranking.interface'; import { RankingResponseDto } from './dto/ranking-response.dto'; From 0e98e02d7ce47c24f7d64f5f5cd0860a3dfba0d6 Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 4 Dec 2024 18:37:01 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=85=20test=20:=20=20stock-list=20serv?= =?UTF-8?q?ice=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/list/stock-list.service.spec.ts | 159 +++++++++++++++++++ BE/src/stock/list/stock-list.service.ts | 2 +- 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 BE/src/stock/list/stock-list.service.spec.ts diff --git a/BE/src/stock/list/stock-list.service.spec.ts b/BE/src/stock/list/stock-list.service.spec.ts new file mode 100644 index 0000000..5d8abe6 --- /dev/null +++ b/BE/src/stock/list/stock-list.service.spec.ts @@ -0,0 +1,159 @@ +import { Test } from '@nestjs/testing'; +import { NotFoundException } from '@nestjs/common'; +import { RedisDomainService } from '../../common/redis/redis.domain-service'; +import { StockListRepository } from './stock-list.repostiory'; +import { StockListService } from './stock-list.service'; +import { Stocks } from './stock-list.entity'; + +describe('주식 목록 조회 테스트', () => { + let stockListService: StockListService; + let stockListRepository: StockListRepository; + let redisDomainService: RedisDomainService; + + const mockStocks: Stocks[] = [ + { + code: '005930', + name: '삼성전자', + market: 'KOSPI', + hasId: () => true, + save: jest.fn(), + remove: jest.fn(), + softRemove: jest.fn(), + recover: jest.fn(), + reload: () => Promise.resolve(), + }, + { + code: '373220', + name: 'LG에너지솔루션', + market: 'KOSPI', + hasId: () => true, + save: jest.fn(), + remove: jest.fn(), + softRemove: jest.fn(), + recover: jest.fn(), + reload: () => Promise.resolve(), + }, + ]; + + beforeAll(async () => { + const mockStockListRepository = { + findAllStocks: jest.fn(), + findOneStock: jest.fn(), + search: jest.fn(), + }; + + const mockRedisDomainService = { + zadd: jest.fn(), + zcard: jest.fn(), + zremrangebyrank: jest.fn(), + zrevrange: jest.fn(), + }; + + const module = await Test.createTestingModule({ + providers: [ + StockListService, + { provide: StockListRepository, useValue: mockStockListRepository }, + { provide: RedisDomainService, useValue: mockRedisDomainService }, + ], + }).compile(); + + stockListService = module.get(StockListService); + stockListRepository = module.get(StockListRepository); + redisDomainService = module.get(RedisDomainService); + }); + + describe('주식 목록 조회 테스트', () => { + it('주식 목록을 조회하면, 주식 목록이 반환된다.', async () => { + jest + .spyOn(stockListRepository, 'findAllStocks') + .mockResolvedValue(mockStocks); + + const result = await stockListService.findAll(); + expect(result[0].code).toBe('005930'); + expect(result[0].name).toBe('삼성전자'); + expect(result[0].market).toBe('KOSPI'); + }); + + it('주식 코드로 조회 시, 해당 주식 정보가 반환된다.', async () => { + jest + .spyOn(stockListRepository, 'findOneStock') + .mockResolvedValue(mockStocks[0]); + + const result = await stockListService.findOne('005930'); + + expect(result.code).toBe('005930'); + expect(result.name).toBe('삼성전자'); + expect(result.market).toBe('KOSPI'); + }); + + it('존재하지 않는 주식 코드로 조회 시, NotFoundException이 발생한다.', async () => { + jest.spyOn(stockListRepository, 'findOneStock').mockResolvedValue(null); + await expect(stockListService.findOne('000000')).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('주식 검색 테스트', () => { + it('검색 조건으로 조회 시, 조건에 맞는 주식 목록이 반환된다', async () => { + const searchParams = { name: '삼성' }; + jest + .spyOn(stockListRepository, 'search') + .mockResolvedValue([mockStocks[0]]); + + const result = await stockListService.search(searchParams); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe('삼성전자'); + }); + }); + + describe('검색 기록 관리 테스트', () => { + it('로그인 된 유저가 검색 시, Redis에 검색 기록이 저장된다.', async () => { + const searchInfo = { userId: 1, searchTerm: '삼성' }; + + const zaddSpy = jest + .spyOn(redisDomainService, 'zadd') + .mockResolvedValue(1); + + const zrevrangeSpy = jest.spyOn(redisDomainService, 'zrevrange'); + + await stockListService.addSearchTermToRedis(searchInfo); + expect(zaddSpy).toHaveBeenCalled(); + expect(zrevrangeSpy).not.toHaveBeenCalled(); + }); + + it('검색 기록이 제한을 초과하면, 가장 오래된 기록이 삭제된다', async () => { + const searchInfo = { + userId: 1, + searchTerm: '삼성', + }; + + const zaddSpy = jest + .spyOn(redisDomainService, 'zadd') + .mockResolvedValue(1); + const zcardSpy = jest + .spyOn(redisDomainService, 'zcard') + .mockResolvedValue(11); + + await stockListService.addSearchTermToRedis(searchInfo); + + expect(zaddSpy).toHaveBeenCalled(); + expect(zcardSpy).toHaveBeenCalled(); + }); + + it('검색 기록 조회 시, 최근 검색어 목록이 반환된다', async () => { + const userId = '1'; + const mockSearchHistory = ['삼성', 'LG', 'SK']; + + jest + .spyOn(redisDomainService, 'zrevrange') + .mockResolvedValue(mockSearchHistory); + + const result = await stockListService.getSearchTermFromRedis(userId); + + expect(result).toEqual(mockSearchHistory); + expect(result).toHaveLength(3); + }); + }); +}); diff --git a/BE/src/stock/list/stock-list.service.ts b/BE/src/stock/list/stock-list.service.ts index 7e4c495..b421379 100644 --- a/BE/src/stock/list/stock-list.service.ts +++ b/BE/src/stock/list/stock-list.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { RedisDomainService } from 'src/common/redis/redis.domain-service'; +import { RedisDomainService } from '../../common/redis/redis.domain-service'; import { StockListRepository } from './stock-list.repostiory'; import { Stocks } from './stock-list.entity'; import { StockListResponseDto } from './dto/stock-list-response.dto'; From 13bec0b4628cc16d838f8344db07e8afae0cdefb Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 4 Dec 2024 18:47:31 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20github=20a?= =?UTF-8?q?ction=20=EC=8B=A4=ED=96=89=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=82=AC=EC=86=8C=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/ranking.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/ranking/ranking.service.spec.ts b/BE/src/ranking/ranking.service.spec.ts index 95eecb0..4bde819 100644 --- a/BE/src/ranking/ranking.service.spec.ts +++ b/BE/src/ranking/ranking.service.spec.ts @@ -4,7 +4,7 @@ import { RedisDomainService } from '../common/redis/redis.domain-service'; import { AssetRepository } from '../asset/asset.repository'; import { SortType } from './enum/sort-type.enum'; -describe('Ranking Service 테스트', () => { +describe('랭킹 서비스 테스트', () => { let rankingService: RankingService; let assetRepository: AssetRepository; let redisDomainService: RedisDomainService; From e518dfa86c516e989342fd4fa235443f38b3d20c Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 5 Dec 2024 01:03:26 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20orders=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=B3=B5=ED=95=A9=ED=82=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#238?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.entity.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/BE/src/stock/order/stock-order.entity.ts b/BE/src/stock/order/stock-order.entity.ts index 66d3450..1c84d7b 100644 --- a/BE/src/stock/order/stock-order.entity.ts +++ b/BE/src/stock/order/stock-order.entity.ts @@ -9,6 +9,7 @@ import { TradeType } from './enum/trade-type'; import { StatusType } from './enum/status-type'; @Entity('orders') +@Index(['user_id', 'stock_code']) export class Order { @PrimaryGeneratedColumn() id: number;