diff --git a/test/auth/post-logout.spec.ts b/test/auth/post-logout.spec.ts new file mode 100644 index 0000000..04f3947 --- /dev/null +++ b/test/auth/post-logout.spec.ts @@ -0,0 +1,176 @@ +import { CACHE_MANAGER, HttpStatus, INestApplication } from '@nestjs/common'; +import { StartedPostgreSqlContainer } from '@testcontainers/postgresql'; +import { DataSource, Repository } from 'typeorm'; +import { User } from '../../src/users/entities/user.entity'; +import { Seeder } from '../seeder/seeder.interface'; +import { UserSeeder } from '../seeder/user.seeder'; +import { getBuilder } from '../common/application-builder'; +import { cacheManagerMock } from '../mock/cache-manager.mock'; +import { JwtAuthGuard } from '../../src/auth/jwt/jwt.guard'; +import { jwtAuthGuardMock } from '../mock/jwt-auth-guard.mock'; +import { LogoutBodyDto } from '../../src/auth/dtos/login.dto'; +import { Cache } from 'cache-manager'; +import * as request from 'supertest'; + +jest.setTimeout(30_000); +describe('[POST] /api/auth/logout', () => { + // Application + let app: INestApplication; + let container: StartedPostgreSqlContainer; // TODO 결합도 낮추기 + let dataSource: DataSource; + let cacheManager: Cache; + + // Repository + let userRepository: Repository; + + // Seeder + const userSeeder: Seeder = new UserSeeder(); + + // Stub + let userStub: User = userSeeder.generateOne({ id: 1 }); + + beforeAll(async () => { + const { + builder, + container: _container, + dataSource: _dataSource, + } = await getBuilder(); + container = _container; + dataSource = _dataSource; + + const module = await builder + .overrideProvider(DataSource) + .useValue(dataSource) + .overrideProvider(CACHE_MANAGER) + .useValue(cacheManagerMock) + .overrideGuard(JwtAuthGuard) + .useValue(jwtAuthGuardMock(userStub)) + .compile(); + + app = module.createNestApplication(); + await app.init(); + + userRepository = dataSource.getRepository(User); + cacheManager = app.get(CACHE_MANAGER); + }); + + beforeEach(async () => { + await dataSource.synchronize(true); + }); + + afterAll(async () => { + await app.close(); + await container.stop(); + }); + + /** + * 1. 로그아웃에 성공한다. + * 2. 유저가 존재하지 않아 실패한다. + * 3. refresh token이 body에 존재하지 않아 실패한다. + * 4. refresh token이 캐시에 존재하지 않아 실패한다. + * 5. refresh token 캐시 값이 userId와 일치하지 않아 실패한다. + */ + + describe('로그아웃에 성공한다', () => { + beforeEach(async () => { + userStub = userSeeder.generateOne({ id: userStub.id }); + await userRepository.save(userStub); + }); + + it('빈 객체와 함께 201을 반환한다.', async () => { + const logoutDto: LogoutBodyDto = { + refresh_token: '123', + }; + cacheManager.get = jest.fn().mockImplementationOnce(() => userStub.id); + + const { status, body } = await request(app.getHttpServer()) + .post('/auth/logout') + .send(logoutDto); + + expect(status).toBe(HttpStatus.CREATED); + }); + }); + + describe('유저가 존재하지 않아 실패한다.', () => { + it('404 예외를 던진다.', async () => { + const logoutDto: LogoutBodyDto = { + refresh_token: '123', + }; + cacheManager.get = jest.fn().mockImplementationOnce(() => userStub.id); + + const { status, body } = await request(app.getHttpServer()) + .post('/auth/logout') + .send(logoutDto); + + expect(status).toBe(HttpStatus.NOT_FOUND); + expect(body.message).toBe('User not found'); + }); + }); + + describe('refresh token이 body에 존재하지 않아 실패한다.', () => { + beforeEach(async () => { + userStub = userSeeder.generateOne({ id: userStub.id }); + await userRepository.save(userStub); + }); + + it('400 예외를 던진다', async () => { + const logoutDto: LogoutBodyDto = { + refresh_token: '', + }; + cacheManager.get = jest + .fn() + .mockImplementationOnce(() => userStub.id + 1); + + const { status, body } = await request(app.getHttpServer()) + .post('/auth/logout') + .send(logoutDto); + + expect(status).toBe(HttpStatus.BAD_REQUEST); + expect(body.message).toBe('Refresh token is required'); + }); + }); + + describe('refresh token이 캐시에 존재하지 않아 실패한다.', () => { + beforeEach(async () => { + userStub = userSeeder.generateOne({ id: userStub.id }); + await userRepository.save(userStub); + }); + + it('400 예외를 던진다.', async () => { + const logoutDto: LogoutBodyDto = { + refresh_token: '123', + }; + cacheManager.get = jest.fn().mockImplementationOnce(() => undefined); + + const { status, body } = await request(app.getHttpServer()) + .post('/auth/logout') + .send(logoutDto); + + expect(status).toBe(HttpStatus.NOT_FOUND); + expect(body.message).toBe('Refresh token not found'); + }); + }); + + describe('refresh token 캐시 값이 userId와 일치하지 않아 실패한다.', () => { + beforeEach(async () => { + userStub = userSeeder.generateOne({ id: userStub.id }); + await userRepository.save(userStub); + }); + + it('401 예외를 던진다.', async () => { + const logoutDto: LogoutBodyDto = { + refresh_token: '123', + }; + cacheManager.get = jest + .fn() + .mockImplementationOnce(() => userStub.id + 1); + + const { status, body } = await request(app.getHttpServer()) + .post('/auth/logout') + .send(logoutDto); + + expect(status).toBe(HttpStatus.UNAUTHORIZED); + expect(body.message).toBe('Invalid refresh token'); + }); + }); +}); diff --git a/test/mock/cache-manager.mock.ts b/test/mock/cache-manager.mock.ts index b438dda..297a253 100644 --- a/test/mock/cache-manager.mock.ts +++ b/test/mock/cache-manager.mock.ts @@ -1,3 +1,4 @@ export const cacheManagerMock = { set: jest.fn(), + del: jest.fn(), }; diff --git a/test/mock/jwt-auth-guard.mock.ts b/test/mock/jwt-auth-guard.mock.ts new file mode 100644 index 0000000..63d5f2f --- /dev/null +++ b/test/mock/jwt-auth-guard.mock.ts @@ -0,0 +1,10 @@ +import { ExecutionContext } from '@nestjs/common'; +import { User } from '../../src/users/entities/user.entity'; + +export const jwtAuthGuardMock = (user: User) => ({ + canActivate(context: ExecutionContext) { + const request = context.switchToHttp().getRequest(); + request.user = user; + return true; + }, +}); diff --git a/test/seeder/seeder.interface.ts b/test/seeder/seeder.interface.ts index 8f00447..49af5f6 100644 --- a/test/seeder/seeder.interface.ts +++ b/test/seeder/seeder.interface.ts @@ -1,3 +1,3 @@ export interface Seeder { - generateOne(options?: { [K in keyof T]: any }): T; + generateOne(options?: Partial): T; } diff --git a/test/seeder/user.seeder.ts b/test/seeder/user.seeder.ts index d1f178f..c152b62 100644 --- a/test/seeder/user.seeder.ts +++ b/test/seeder/user.seeder.ts @@ -4,6 +4,9 @@ import { faker } from '@faker-js/faker'; export class UserSeeder implements Seeder { private readonly userStub = { + id: faker.number.int({ min: 1, max: 9999 }), + createdAt: faker.date.recent(), + updatedAt: faker.date.recent(), name: faker.person.lastName(), email: faker.internet.email(), profileImage: faker.internet.url(), @@ -17,7 +20,7 @@ export class UserSeeder implements Seeder { verified: true, }; - generateOne(options?: { [K in keyof User]: any }): User { + generateOne(options?: Partial): User { return { ...this.userStub, ...options } as User; } }