Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] Auth E2E 테스트 작성 #389

Merged
merged 14 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common';
import { Body, Controller, Get, Post, Req, Res, UseGuards } from '@nestjs/common';
import { ApiOperation, ApiTags, ApiOkResponse } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { Request, Response } from 'express';
import { OAuthLoginDto, OAuthLoginResponseDto } from './dto/auth.dto';
import { JwtAuthGuard } from './guards/jwtAuth.guard';

@ApiTags('Authentication API')
@Controller('auth')
Expand All @@ -20,6 +21,7 @@ export class AuthController {
}

@Get('refresh_token')
@UseGuards(JwtAuthGuard)
@ApiOperation({ description: 'access token 갱신 API' })
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존에 refresh 토큰 발급 기능에서 토큰 없이 요청이 오는 경우 500 에러가 발생해 controller 진입 전 jwt토큰 유효성 검증을 하도록 변경했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 하면 이 API를 사용하는 일반적인 경우인,
jwt 토큰이 있지만 유효기한이 지난 경우에 가드에서 계속 막히지 않나요..?

서비스 로직에서 jwt가 없는 경우에 로그인 하도록 UnauthorizedException을 보내도록 추가해도 좋을 것 같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가드는 삭제하고, 서비스 로직에서 체크하도록 변경했습니다

@ApiOkResponse({ description: 'access token 갱신 성공' })
async refreshAccessToken(@Req() req: Request, @Res() res: Response): Promise<void> {
Expand Down
13 changes: 6 additions & 7 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,14 @@ export class AuthService {
const payload = this.jwtService.decode(userJwt);
const refreshToken = await this.authRepository.getRefreshToken(payload.accessKey);

if (refreshToken) {
return this.jwtService.sign({
id: payload.id,
nickname: payload.nickname,
accessKey: payload.accessKey,
});
} else {
if (!refreshToken) {
throw new UnauthorizedException('로그인이 필요합니다.');
}
return this.jwtService.sign({
id: payload.id,
nickname: payload.nickname,
accessKey: payload.accessKey,
});
HyoJongPark marked this conversation as resolved.
Show resolved Hide resolved
}

removeRefreshToken(req: Request) {
Expand Down
190 changes: 190 additions & 0 deletions backend/test/auth/auth.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { AppModule } from 'src/app.module';
import { AuthService } from 'src/auth/auth.service';
import { testRedisConfig } from 'src/configs/redis.config';
import Redis from 'ioredis';
import { DataSource, QueryRunner } from 'typeorm';
import { UsersRepository } from 'src/users/users.repository';
import { SocialType } from 'src/users/entity/socialType';
import { User } from 'src/users/entity/user.entity';
import * as cookieParser from 'cookie-parser';

describe('AuthController (e2e)', () => {
let app: INestApplication;
let authService: AuthService;
let usersRepository: UsersRepository;
let queryRunner: QueryRunner;
const redis = new Redis(testRedisConfig);

const mockProfile = {
id: '1',
email: 'test@test.com',
nickname: 'testUser',
profile_image: 'testImage',
};
const mockAccessToken = 'mock token';
const body = { code: 'code', state: 'state', socialType: 'naver' };

beforeAll(async () => {
await redis.flushall();
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();

const dataSource = module.get<DataSource>(DataSource);
queryRunner = dataSource.createQueryRunner();
dataSource.createQueryRunner = jest.fn();
queryRunner.release = jest.fn();
(dataSource.createQueryRunner as jest.Mock).mockReturnValue(queryRunner);

authService = module.get<AuthService>(AuthService);
usersRepository = module.get<UsersRepository>(UsersRepository);
app = module.createNestApplication();
app.use(cookieParser());
await app.init();

jest.spyOn(authService, <keyof AuthService>'getToken').mockResolvedValue(mockAccessToken);
jest
.spyOn(authService, <keyof AuthService>'getUserProfile')
.mockResolvedValue(mockProfile as any);
HyoJongPark marked this conversation as resolved.
Show resolved Hide resolved
});

afterAll(async () => {
await redis.quit();
await app.close();
});

beforeEach(async () => {
await queryRunner.startTransaction();
});

afterEach(async () => {
await redis.flushall();
await queryRunner.rollbackTransaction();
});

describe('/login (POST)', () => {
HyoJongPark marked this conversation as resolved.
Show resolved Hide resolved
beforeAll(() => {
jest.spyOn(usersRepository, 'createUser');
});

afterEach(() => {
(usersRepository.createUser as jest.Mock).mockClear();
});

it('DB에 존재하지 않는 socialId로 로그인 요청이 오면, 회원가입 후 토큰 반환', async () => {
shunny822 marked this conversation as resolved.
Show resolved Hide resolved
//given
const url = '/auth/login';

//when
const response = await request(app.getHttpServer()).post(url).send(body);

//then
expect(response.status).toEqual(201);
expect(response.header['set-cookie']).toBeTruthy();
expect(usersRepository.createUser).toHaveBeenCalled();
});

it('DB에 존재하는 않는 socialId로 로그인 요청이 오면, 회원가입 후 토큰 반환', async () => {
//given
const user = {
id: 1,
email: mockProfile.email,
nickname: mockProfile.nickname,
socialId: mockProfile.id,
socialType: SocialType.NAVER,
profileImage: mockProfile.profile_image,
} as User;
const url = '/auth/login';

await usersRepository.save(user);

//when
const response = await request(app.getHttpServer()).post(url).send(body);

//then
expect(response.status).toEqual(201);
expect(response.header['set-cookie']).toBeTruthy();
expect(usersRepository.createUser).not.toHaveBeenCalled();
});
});

describe('/refresh_token (GET)', () => {
HyoJongPark marked this conversation as resolved.
Show resolved Hide resolved
it('유효한 jwt 토큰으로 refresh 요청 시 새 토큰 발급', async () => {
//given
const url = '/auth/refresh_token';
const agent = request.agent(app.getHttpServer());
await agent.post('/auth/login').send(body);

//when
const response = await agent.get(url);

//then
expect(response.status).toEqual(200);
expect(response.header['set-cookie']).toBeTruthy();
});

it('jwt 없이 refresh 요청 시 401 에러 발생', async () => {
//given
const url = '/auth/refresh_token';
const agent = request.agent(app.getHttpServer());
await agent.post('/auth/login').send(body);

//when
const response = await request(app.getHttpServer()).get(url);

//then
expect(response.status).toEqual(401);
expect(response.body.message).toEqual('Unauthorized');
});

it('redis에 저장되지 않은 토큰으로 요청 시 401 에러 발생', async () => {
//given
const url = '/auth/refresh_token';
const agent = request.agent(app.getHttpServer());

await agent.post('/auth/login').send(body);
redis.flushall();

//when
const response = await agent.get(url);

//then
expect(response.status).toEqual(401);
expect(response.body.message).toEqual('로그인이 필요합니다.');
});
});

describe('/logout (POST)', () => {
it('유효한 jwt 토큰으로 logout 요청 시 201 반환', async () => {
//given
const url = '/auth/logout';
const agent = request.agent(app.getHttpServer());

await agent.post('/auth/login').send(body);

//when
const response = await agent.post(url);

//then
expect(response.status).toEqual(201);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logout의 response status가 201로 오던데, 의도하신건가요??

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

호오.. 아니욥ㅋㅋㅋ
공식문서 찾아보니까 POST 요청인 경우에 응답코드가 기본으로 201이 되나 봅니다!
image

status code를 바꾸려면 @HttpCode를 사용하면 되는 것 같은데 어떻게 할까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋네요!! 이걸로 200 코드를 반환하도록 변경했습니다.

expect(response.header['set-cookie']).toHaveLength(1);
expect(response.header['set-cookie'][0]).toContain('Max-Age=0;');
});

it('비로그인 사용자가 로그아웃 요청을 보내면 201 반환', async () => {
//given
const url = '/auth/logout';

//when
const response = await request(app.getHttpServer()).post(url);

//then
expect(response.status).toEqual(201);
expect(response.header['set-cookie']).toHaveLength(1);
expect(response.header['set-cookie'][0]).toContain('Max-Age=0;');
});
HyoJongPark marked this conversation as resolved.
Show resolved Hide resolved
});
});
49 changes: 0 additions & 49 deletions backend/test/tags/app.e2e-spec.ts

This file was deleted.

2 changes: 1 addition & 1 deletion backend/test/tags/tags.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe('TagsController (e2e)', () => {

//then
expect(response.status).toEqual(200);
expect(body).toHaveLength(0);
expect(body.keywords).toHaveLength(0);
});

it('일치하는 키워드가 있으면 모든 유사 문자열 리스트 반환', async () => {
Expand Down
Loading