From 65fee6347bdffe4e8d1443ff42c0699ef78a8c4e Mon Sep 17 00:00:00 2001 From: violet-dev Date: Sat, 15 Feb 2025 11:29:17 +0900 Subject: [PATCH 1/3] Add user service tests --- violet-server/src/user/user.service.spec.ts | 64 ++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/violet-server/src/user/user.service.spec.ts b/violet-server/src/user/user.service.spec.ts index 873de8ac4..ccc84dfa6 100644 --- a/violet-server/src/user/user.service.spec.ts +++ b/violet-server/src/user/user.service.spec.ts @@ -1,18 +1,80 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UserService } from './user.service'; +import { UserRepository } from './user.repository'; +import { UnauthorizedException } from '@nestjs/common'; +import { UserRegisterDTO } from './dtos/user-register.dto'; describe('UserService', () => { let service: UserService; + let repository: UserRepository; + + const mockUserRepository = { + isUserExists: jest.fn(), + createUser: jest.fn(), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [UserService], + providers: [ + UserService, + { + provide: UserRepository, + useValue: mockUserRepository, + }, + ], }).compile(); service = module.get(UserService); + repository = module.get(UserRepository); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('registerUser', () => { + const mockUserRegisterDTO: UserRegisterDTO = { + userAppId: 'test-user-id', + }; + + it('유저 등록에 성공해야 합니다', async () => { + // Arrange + mockUserRepository.isUserExists.mockResolvedValue(false); + mockUserRepository.createUser.mockResolvedValue(undefined); + + // Act + const result = await service.registerUser(mockUserRegisterDTO); + + // Assert + expect(result.ok).toBe(true); + expect(mockUserRepository.isUserExists).toHaveBeenCalledWith(mockUserRegisterDTO.userAppId); + expect(mockUserRepository.createUser).toHaveBeenCalledWith(mockUserRegisterDTO); + }); + + it('이미 존재하는 userAppId인 경우 UnauthorizedException을 발생시켜야 합니다', async () => { + // Arrange + mockUserRepository.isUserExists.mockResolvedValue(true); + + // Act + const result = await service.registerUser(mockUserRegisterDTO); + + // Assert + expect(result.ok).toBe(false); + expect(result.error).toBeInstanceOf(UnauthorizedException); + expect(mockUserRepository.createUser).toHaveBeenCalledTimes(1); + }); + + it('예외가 발생한 경우 에러를 포함한 실패 응답을 반환해야 합니다', async () => { + // Arrange + const testError = new Error('테스트 에러'); + mockUserRepository.isUserExists.mockRejectedValue(testError); + + // Act + const result = await service.registerUser(mockUserRegisterDTO); + + // Assert + expect(result.ok).toBe(false); + expect(result.error).toBe(testError); + }); + }); }); From 933b02892153770c7a7e4c987b229a8bc3b10dde Mon Sep 17 00:00:00 2001 From: violet-dev Date: Sat, 15 Feb 2025 11:51:01 +0900 Subject: [PATCH 2/3] Add auth service tests --- violet-server/src/auth/auth.service.spec.ts | 166 ++++++++++++++++---- 1 file changed, 133 insertions(+), 33 deletions(-) diff --git a/violet-server/src/auth/auth.service.spec.ts b/violet-server/src/auth/auth.service.spec.ts index 19e00b4ff..217d4fc70 100644 --- a/violet-server/src/auth/auth.service.spec.ts +++ b/violet-server/src/auth/auth.service.spec.ts @@ -1,49 +1,149 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthService } from './auth.service'; -import { AuthController } from './auth.controller'; -import { ConfigModule } from '@nestjs/config'; -import { HmacAuthGuard } from './guards/hmac.guard'; -import { JwtModule } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; import { UserRepository } from 'src/user/user.repository'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserModule } from 'src/user/user.module'; -import { MySQLConfigModule } from 'src/config/config.module'; -import { MySQLConfigService } from 'src/config/config.service'; -import { envValidationSchema } from 'src/app.module'; +import { BadRequestException, HttpException } from '@nestjs/common'; +import { User } from 'src/user/entity/user.entity'; describe('AuthService', () => { let service: AuthService; - let controller: AuthController; + let userRepository: UserRepository; + let jwtService: JwtService; + let configService: ConfigService; + + const mockUserRepository = { + findOneBy: jest.fn(), + update: jest.fn(), + }; + + const mockJwtService = { + sign: jest.fn(), + signAsync: jest.fn(), + }; + + const mockConfigService = { + get: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [UserRepository, AuthService], - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: '.test.env', - validationSchema: envValidationSchema, - }), - JwtModule.register({}), - UserModule, - TypeOrmModule.forRootAsync({ - imports: [MySQLConfigModule], - useClass: MySQLConfigService, - inject: [MySQLConfigService], - }), + providers: [ + AuthService, + { + provide: UserRepository, + useValue: mockUserRepository, + }, + { + provide: JwtService, + useValue: mockJwtService, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, ], - controllers: [AuthController], - }) - .overrideGuard(HmacAuthGuard) - .useValue({ canActivate: jest.fn(() => true) }) - .compile(); + }).compile(); service = module.get(AuthService); - controller = module.get(AuthController); + userRepository = module.get(UserRepository); + jwtService = module.get(JwtService); + configService = module.get(ConfigService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('verifyUserAndSignJWT', () => { + const mockUserDto = { userAppId: 'test123' }; + const mockUser = { userAppId: 'test123' } as User; + const mockTokens = { + accessToken: 'access-token', + refreshToken: 'refresh-token', + }; + + it('사용자 검증 및 JWT 발급에 성공해야 합니다', async () => { + mockUserRepository.findOneBy.mockResolvedValue(mockUser); + jest.spyOn(service, 'createJWT').mockResolvedValue(mockTokens); + jest.spyOn(service, 'updateRefreshToken').mockResolvedValue(undefined); + + const result = await service.verifyUserAndSignJWT(mockUserDto); + + expect(result).toEqual({ user: mockUser, tokens: mockTokens }); + expect(mockUserRepository.findOneBy).toHaveBeenCalledWith({ + userAppId: mockUserDto.userAppId, + }); + }); + + it('존재하지 않는 사용자일 경우 BadRequestException을 발생시켜야 합니다', async () => { + mockUserRepository.findOneBy.mockResolvedValue(null); + + await expect(service.verifyUserAndSignJWT(mockUserDto)).rejects.toThrow( + BadRequestException, + ); + }); }); - it('should be defined', () => { - expect(service).toBeDefined(); - expect(controller).toBeDefined(); + describe('refreshTokens', () => { + const mockRefreshToken = 'refresh-token'; + const mockUser = { userAppId: 'test123' } as User; + const mockTokens = { + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + }; + + it('리프레시 토큰으로 새로운 토큰 발급에 성공해야 합니다', async () => { + mockUserRepository.findOneBy.mockResolvedValue(mockUser); + jest.spyOn(service, 'createJWT').mockResolvedValue(mockTokens); + jest.spyOn(service, 'updateRefreshToken').mockResolvedValue(undefined); + + const result = await service.refreshTokens(mockRefreshToken); + + expect(result).toEqual({ tokens: mockTokens, user: mockUser }); + expect(mockUserRepository.findOneBy).toHaveBeenCalledWith({ + refreshToken: mockRefreshToken, + }); + }); + + it('유효하지 않은 리프레시 토큰일 경우 HttpException을 발생시켜야 합니다', async () => { + mockUserRepository.findOneBy.mockResolvedValue(null); + + await expect(service.refreshTokens(mockRefreshToken)).rejects.toThrow( + HttpException, + ); + }); + }); + + describe('updateDiscordInfo', () => { + const mockUser = { + userAppId: 'test123', + discordId: 'discord123', + avatar: 'avatar.png', + } as User; + + it('디스코드 정보 업데이트에 성공해야 합니다', async () => { + mockUserRepository.update.mockResolvedValue({ affected: 1 }); + + const result = await service.updateDiscordInfo(mockUser); + + expect(result).toEqual({ ok: true }); + expect(mockUserRepository.update).toHaveBeenCalledWith( + { userAppId: mockUser.userAppId }, + { discordId: mockUser.discordId, avatar: mockUser.avatar }, + ); + }); + + it('디스코드 정보 업데이트 실패 시 에러를 반환해야 합니다', async () => { + mockUserRepository.update.mockRejectedValue(new Error('Update failed')); + + const result = await service.updateDiscordInfo(mockUser); + + expect(result).toEqual({ + ok: false, + error: new Error('Update failed'), + }); + }); }); }); From d35d459f1fe819708caea04ef63670d12d2f5702 Mon Sep 17 00:00:00 2001 From: violet-dev Date: Sat, 15 Feb 2025 12:04:44 +0900 Subject: [PATCH 3/3] Add view service tests --- violet-server/src/view/view.service.spec.ts | 152 +++++++++++++++++++- 1 file changed, 148 insertions(+), 4 deletions(-) diff --git a/violet-server/src/view/view.service.spec.ts b/violet-server/src/view/view.service.spec.ts index 0582856c8..d62f09ad9 100644 --- a/violet-server/src/view/view.service.spec.ts +++ b/violet-server/src/view/view.service.spec.ts @@ -1,18 +1,162 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ViewService as ViewService } from './view.service'; +import { ViewService } from './view.service'; +import { RedisService } from '../redis/redis.service'; +import { ViewRepository } from './view.repository'; +import { ViewGetRequestDto } from './dtos/view-get.dto'; +import { ViewPostRequestDto } from './dtos/view-post.dto'; +import { User } from '../user/entity/user.entity'; describe('ViewService', () => { let service: ViewService; + let redisService: RedisService; + let viewRepository: ViewRepository; + + // Redis 서비스 모킹 + const mockRedisService = { + zrevrange_by_score: jest.fn(), + zincrby: jest.fn(), + setex: jest.fn(), + }; + + // ViewRepository 모킹 + const mockViewRepository = { + postView: jest.fn(), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ViewService], + providers: [ + ViewService, + { + provide: RedisService, + useValue: mockRedisService, + }, + { + provide: ViewRepository, + useValue: mockViewRepository, + }, + ], }).compile(); service = module.get(ViewService); + redisService = module.get(RedisService); + viewRepository = module.get(ViewRepository); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getView', () => { + it('조회수 데이터를 정상적으로 가져와야 합니다', async () => { + const mockDto: ViewGetRequestDto = { + type: 'daily', + offset: 0, + count: 10, + }; + + // Redis에서 반환될 mock 데이터 + const mockRedisResponse = ['1', '100', '2', '50', '3', '25']; + mockRedisService.zrevrange_by_score.mockResolvedValue(mockRedisResponse); + + const result = await service.getView(mockDto); + + expect(result).toEqual({ + elements: [ + { articleId: 1, count: 100 }, + { articleId: 2, count: 50 }, + { articleId: 3, count: 25 }, + ], + }); + + expect(redisService.zrevrange_by_score).toHaveBeenCalledWith( + 'daily', + 0, + 10, + ); + }); + + it('type이 없을 경우 기본값으로 daily를 사용해야 합니다', async () => { + const mockDto: ViewGetRequestDto = { + offset: 0, + count: 10, + }; + + mockRedisService.zrevrange_by_score.mockResolvedValue([]); + + await service.getView(mockDto); + + expect(redisService.zrevrange_by_score).toHaveBeenCalledWith( + 'daily', + 0, + 10, + ); + }); }); - it('should be defined', () => { - expect(service).toBeDefined(); + describe('post', () => { + it('비로그인 사용자의 조회수를 정상적으로 처리해야 합니다', () => { + const mockDto: ViewPostRequestDto = { + articleId: 1, + viewSeconds: 10, + userAppId: 'testUser', + }; + + service.post(mockDto); + + expect(viewRepository.postView).toHaveBeenCalledWith(mockDto); + expect(redisService.zincrby).toHaveBeenCalled(); + }); + }); + + describe('postLogined', () => { + it('로그인 사용자의 조회수를 정상적으로 처리해야 합니다', async () => { + const mockUser = { id: 1 } as User; + const mockDto: ViewPostRequestDto = { + articleId: 1, + viewSeconds: 10, + userAppId: 'testUser', + }; + + await service.postLogined(mockUser, mockDto); + + expect(redisService.zincrby).toHaveBeenCalled(); + }); + }); + + describe('postRedis', () => { + it('Redis에 조회수 데이터를 정상적으로 저장해야 합니다', () => { + const articleId = 1; + jest.spyOn(global.Date, 'now').mockImplementation(() => 1234567890); + + service.postRedis(articleId); + + // alltime 조회수 증가 확인 + expect(redisService.zincrby).toHaveBeenCalledWith('alltime', 1, articleId); + + // daily 조회수 증가 및 만료시간 설정 확인 + expect(redisService.zincrby).toHaveBeenCalledWith('daily', 1, articleId); + expect(redisService.setex).toHaveBeenCalledWith( + expect.stringContaining('daily-'), + 24 * 60 * 60, + '1', + ); + + // weekly 조회수 증가 및 만료시간 설정 확인 + expect(redisService.zincrby).toHaveBeenCalledWith('weekly', 1, articleId); + expect(redisService.setex).toHaveBeenCalledWith( + expect.stringContaining('weekly-'), + 7 * 24 * 60 * 60, + '1', + ); + + // monthly 조회수 증가 및 만료시간 설정 확인 + expect(redisService.zincrby).toHaveBeenCalledWith('monthly', 1, articleId); + expect(redisService.setex).toHaveBeenCalledWith( + expect.stringContaining('monthly-'), + 30 * 24 * 60 * 60, + '1', + ); + }); }); });