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 all 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, HttpCode, 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 Down Expand Up @@ -30,6 +31,7 @@ export class AuthController {
}

@Post('logout')
@HttpCode(200)
@ApiOperation({ description: '로그아웃 API' })
@ApiOkResponse({ description: '로그아웃 성공' })
logout(@Req() req: Request, @Res() res: Response): void {
Expand Down
17 changes: 10 additions & 7 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,21 @@ export class AuthService {

async refreshAccessToken(req: Request) {
const userJwt = cookieExtractor(req);
if (!userJwt) {
throw new UnauthorizedException('토큰 정보가 존재하지 않습니다.');
}

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
221 changes: 221 additions & 0 deletions backend/test/auth/auth.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
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';
import { getExpiredJwtToken } from 'test/utils/testLogin';
import { REFRESH_TOKEN_EXPIRE_DATE } from 'src/auth/utils/auth.constant';
import { v4 as uuidv4 } from 'uuid';

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('/auth/login (POST)', () => {
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('/auth/refresh_token (GET)', () => {
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('만료된 토큰으로 refresh 요청 시 새 토큰 발급', async () => {
//given
const url = '/auth/refresh_token';
const user = {
id: 1,
email: mockProfile.email,
nickname: mockProfile.nickname,
socialId: mockProfile.id,
socialType: SocialType.NAVER,
profileImage: mockProfile.profile_image,
} as User;

await usersRepository.save(user);

const { accessKey, token } = await getExpiredJwtToken(user);
const refreshToken = uuidv4();
await redis.set(accessKey, refreshToken, 'EX', REFRESH_TOKEN_EXPIRE_DATE);

//when
const response = await request(app.getHttpServer())
.get(url)
.set('Cookie', [`utk=${token}`]);

//then
expect(response.status).toEqual(200);
expect(response.body.message).toEqual('access token 갱신에 성공했습니다.');
});

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('토큰 정보가 존재하지 않습니다.');
});

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('/auth/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(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(200);
expect(response.header['set-cookie']).toHaveLength(1);
expect(response.header['set-cookie'][0]).toContain('Max-Age=0;');
});
});
});
18 changes: 18 additions & 0 deletions backend/test/utils/testLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ const jwtService = new JwtService({
},
});

const expiredJwtService = new JwtService({
secret: process.env.JWT_SECRET,
signOptions: {
expiresIn: 0,
},
});

// accessToken 반환만을 위한 로그인 함수
export const testLogin = async (user: User) => {
const accessKey = uuidv4();
Expand All @@ -20,3 +27,14 @@ export const testLogin = async (user: User) => {
accessKey,
});
};

export const getExpiredJwtToken = async (user: User) => {
const accessKey = uuidv4();
const token = expiredJwtService.sign({
id: user.id,
nickname: user.nickname,
accessKey,
});

return { accessKey, token };
};
HyoJongPark marked this conversation as resolved.
Show resolved Hide resolved
Loading