Skip to content

Commit

Permalink
Feat(#520): OAuth Signin 테스트 코드 추가 완료
Browse files Browse the repository at this point in the history
  • Loading branch information
Kimsoo0119 committed Jun 3, 2024
1 parent 58698d1 commit 778094c
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 106 deletions.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --testPathPattern=test/e2e"
},
"dependencies": {
"@elastic/elasticsearch": "^8.10.0",
Expand Down Expand Up @@ -110,7 +110,9 @@
"testEnvironment": "node",
"moduleNameMapper": {
"^@src/(.*)$": "<rootDir>/../src/$1",
"^src/(.*)$": "<rootDir>/../src/$1"
"^src/(.*)$": "<rootDir>/../src/$1",
"^@test/(.*)$": "<rootDir>/../test/$1",
"^test/(.*)$": "<rootDir>/../test/$1"
}
}
}
7 changes: 7 additions & 0 deletions src/auth/constants/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const OAuthProvider = {
KAKAO: 'KAKAO',
GOOGLE: 'GOOGLE',
NAVER: 'NAVER',
} as const;

export type OAuthProvider = (typeof OAuthProvider)[keyof typeof OAuthProvider];
74 changes: 8 additions & 66 deletions src/auth/controllers/auth-oauth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, HttpStatus, Query, Res } from '@nestjs/common';
import { Controller, Get, HttpStatus, Param, Query, Res } from '@nestjs/common';
import { AuthOAuthService } from '@src/auth/services/auth-oauth.service';
import { AuthTokenService } from '@src/auth/services/auth-token.service';
import { Token } from '@src/common/interface/common-interface';
Expand All @@ -9,6 +9,8 @@ import { GetUserResponse } from '@src/auth/interface/interface';
import { ApiTags } from '@nestjs/swagger';
import { ApiSignInGoogle } from '@src/auth/swagger-decorators/oauth/sign-in-google-decorator';
import { ApiSignInNaver } from '../swagger-decorators/oauth/sign-in-naver-decorator';
import { OAuthProvider } from '../constants/const';
import { ProviderValidator } from '../validators/auth-provider.validator';

@ApiTags('OAuth')
@Controller('auth/oauth')
Expand All @@ -19,82 +21,22 @@ export class AuthOAuthController {
) {}

@ApiSignInKakao()
@Get('/signin/kakao')
async signInKakao(
@Get('/signin/:provider')
async signIn(
@Query('access-token') accessToken: string,
@Param('provider', ProviderValidator) provider: OAuthProvider,
@Res({ passthrough: true }) response: Response,
) {
const user: GetUserResponse = await this.authOAuthService.signIn(
'KAKAO',
provider,
accessToken,
);

if (user.userEmail) {
return {
statusCode: HttpStatus.CREATED,
authEmail: user.userEmail,
signUpType: 'KAKAO',
};
} else {
const token: Token = await this.authTokenService.generateToken(
{ userId: user.userId },
TokenTypes.User,
);

response.cookie('refreshToken', token.refreshToken, {
httpOnly: true,
});

return { userAccessToken: token.accessToken };
}
}
@ApiSignInGoogle()
@Get('/signin/google')
async signInGoogle(
@Query('access-token') accessToken: string,
@Res({ passthrough: true }) response: Response,
) {
const user: GetUserResponse = await this.authOAuthService.signIn(
'GOOGLE',
accessToken,
);

if (user.userEmail) {
return {
statusCode: HttpStatus.CREATED,
authEmail: user.userEmail,
signUpType: 'GOOGLE',
};
} else {
const token: Token = await this.authTokenService.generateToken(
{ userId: user.userId },
TokenTypes.User,
);

response.cookie('refreshToken', token.refreshToken, {
httpOnly: true,
});

return { userAccessToken: token.accessToken };
}
}

@ApiSignInNaver()
@Get('/signin/naver')
async signInNaver(
@Query('access-token') accessToken: string,
@Res({ passthrough: true }) response: Response,
) {
const user: GetUserResponse = await this.authOAuthService.signIn(
'NAVER',
accessToken,
);

if (user.userEmail) {
return {
statusCode: HttpStatus.CREATED,
authEmail: user.userEmail,
signUpType: 'NAVER',
signUpType: provider,
};
} else {
const token: Token = await this.authTokenService.generateToken(
Expand Down
37 changes: 37 additions & 0 deletions src/auth/controllers/swagger/lecturer-payments.swagger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ApiOperator } from '@src/common/types/type';
import { OperationObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
import { HttpStatus, applyDecorators } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { ExceptionResponseDto } from '@src/common/swagger/dtos/exeption-response.dto';
import { PaginationResponseDto } from '@src/common/swagger/dtos/pagination-response.dto';
import { LecturerPaymentItemDto } from '@src/payments/dtos/response/lecturer-payment-item.dto';
import { AuthOAuthController } from '../auth-oauth.controller';

export const ApiOAuth: ApiOperator<keyof AuthOAuthController> = {
SignIn: (
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
Partial<OperationObject>,
): PropertyDecorator => {
return applyDecorators(
ApiOperation(apiOperationOptions),
ApiBearerAuth(),
PaginationResponseDto.swaggerBuilder(
HttpStatus.OK,
'lecturerPaymentList',
LecturerPaymentItemDto,
),
ExceptionResponseDto.swaggerBuilder(HttpStatus.BAD_REQUEST, [
{
error: 'differentSignUpMethod',
description: '다른 방식으로 가입된 이메일 입니다.',
},
]),
ExceptionResponseDto.swaggerBuilder(HttpStatus.INTERNAL_SERVER_ERROR, [
{
error: 'oAuthServerError',
description: 'OAuth 서버 요청 중 오류가 발생했습니다.',
},
]),
);
},
};
19 changes: 12 additions & 7 deletions src/auth/services/auth-oauth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { SignUpType } from '@src/common/config/sign-up-type.config';
import { Auth } from '@prisma/client';
import { AuthRepository } from '@src/auth/repository/auth.repository';
import { OAuthProvider } from '../constants/const';

@Injectable()
export class AuthOAuthService implements OnModuleInit {
Expand All @@ -40,17 +41,21 @@ export class AuthOAuthService implements OnModuleInit {
this.logger.log('AuthOAuthService init');
}
async signIn(
provider: string,
provider: OAuthProvider,
accessToken: string,
): Promise<GetUserResponse> {
let userEmail: string;

if (provider === 'KAKAO') {
userEmail = await this.getKakaoUserEmail(accessToken);
} else if (provider === 'GOOGLE') {
userEmail = await this.getGoogleUserEmail(accessToken);
} else if (provider === 'NAVER') {
userEmail = await this.getNaverUserEmail(accessToken);
switch (provider) {
case OAuthProvider.KAKAO:
userEmail = await this.getKakaoUserEmail(accessToken);
break;
case OAuthProvider.GOOGLE:
userEmail = await this.getGoogleUserEmail(accessToken);
break;
case OAuthProvider.NAVER:
userEmail = await this.getNaverUserEmail(accessToken);
break;
}

const userAuth: Auth = await this.authRepository.getUserAuth(
Expand Down
13 changes: 7 additions & 6 deletions src/auth/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ export class AuthService implements OnModuleInit {

async trxCreateUserAuth(
tx: PrismaTransaction,
{ userId, authEmail, signUpType }: CreateUserAuthDto,
authInfo: CreateUserAuthDto,
): Promise<any> {
const mappedSignUpType: SignUpType = this.mapSignUpType(signUpType);
await this.trxValidateUserAuth(tx, userId, authEmail);
const mappedSignUpType: SignUpType = this.mapSignUpType(
authInfo.signUpType,
);
await this.trxValidateUserAuth(tx, authInfo.userId, authInfo.authEmail);

const authData: AuthInputData = {
userId,
email: authEmail,
userId: authInfo.userId,
email: authInfo.authEmail,
signUpTypeId: mappedSignUpType,
};

Expand Down Expand Up @@ -136,7 +138,6 @@ export class AuthService implements OnModuleInit {

private mapSignUpType(signUpTypeString): SignUpType {
let mappedSignUpType: SignUpType;

switch (signUpTypeString) {
case 'KAKAO':
mappedSignUpType = SignUpType.KAKAO;
Expand Down
18 changes: 18 additions & 0 deletions src/auth/validators/auth-provider.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ValidatorConstraint } from 'class-validator';
import { BadRequestException, PipeTransform } from '@nestjs/common';
import { OAuthProvider } from '../constants/const';

@ValidatorConstraint()
export class ProviderValidator implements PipeTransform {
transform(provider: string): OAuthProvider {
const convertedProvider = OAuthProvider[provider.toUpperCase()];
if (!OAuthProvider.hasOwnProperty(convertedProvider)) {
throw new BadRequestException(
'올바르지 않은 OAuthProvider 입니다.',
'InvalidProvider',
);
}

return convertedProvider;
}
}
3 changes: 3 additions & 0 deletions src/user/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class UserService {
authEmail: user.authEmail,
signUpType: user.provider,
};

const createAuth = await this.authService.trxCreateUserAuth(
transaction,
auth,
Expand All @@ -86,6 +87,8 @@ export class UserService {
},
);
} catch (error) {
console.log(error);

throw error;
}
}
Expand Down
24 changes: 0 additions & 24 deletions test/app.e2e-spec.ts

This file was deleted.

63 changes: 63 additions & 0 deletions test/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { AppModule } from '@src/app.module';
import axios from 'axios';
import { testUserSignin } from '@test/features/auth/test-user-signin';
import { randomInt } from 'crypto';
import { v4 } from 'uuid';
import { testCreateUser } from '@test/features/user/test-create-user';
import { OAuthProvider } from '@src/auth/constants/const';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('AuthOAuthController (e2e)', () => {
let server: INestApplication;
const PORT = randomInt(20000, 50000);

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

server = moduleFixture.createNestApplication();
await server.init();
await server.listen(PORT);
});

afterAll(async () => {
await server.close();
});

describe('/auth/oauth/signin/kakao (GET)', () => {
//소셜 로그인 테스트
it('새로운 사용자일 경우 authEmail을 반환해야 한다', async () => {
const testEmail = v4() + '@example.com';
mockedAxios.post.mockResolvedValue({
data: { kakao_account: { email: testEmail } },
});

const response = await testUserSignin(PORT, OAuthProvider.KAKAO);

expect(response.authEmail).toEqual(testEmail);
});

// 기존 사용자일 경우 액세스 토큰을 반환하는 테스트
it('기존 사용자일 경우 액세스 토큰을 반환해야 한다', async () => {
const testEmail = v4() + '@example.com';

await testCreateUser(PORT, {
provider: OAuthProvider.KAKAO,
email: testEmail,
});

mockedAxios.post.mockResolvedValue({
data: { kakao_account: { email: testEmail } },
});
const response = await testUserSignin(PORT, OAuthProvider.KAKAO);

expect(response.userAccessToken).toBeDefined();
expect(typeof response.userAccessToken).toBe('string');
});
});
});
30 changes: 30 additions & 0 deletions test/features/auth/test-user-signin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { OAuthProvider } from '@src/auth/constants/const';
import { AuthOAuthController } from '@src/auth/controllers/auth-oauth.controller';
import { v4 } from 'uuid';

/**
* 유저 OAuth 로그인 테스트
* 테스트 환경에서는 OAuth 서버와 통신하지 않는다.
* Axios 요청을 모킹하여 OAuth 프로바이더에 맞게 반환값을 할당해야한다.
* OAuth Server는 성공한다는 가정하에 비즈니스 로직 성공 보장
*
* @param PORT 포트번호
* @param provider 로그인 할 OAuth provider
* @returns 로그인에 성공하면 토큰을 발급받는다.
*/
export const testUserSignin = async (
PORT: number,
provider: OAuthProvider,
): Promise<ReturnType<AuthOAuthController['signIn']>> => {
const accessToken = v4();
const url = `http://localhost:${PORT}/auth/oauth/signin/${
provider ?? 'kakao'
}?access-token=${accessToken}`;

const response = await fetch(url, {
method: 'GET',
});
const responseBody = await response.json();

return responseBody as Awaited<ReturnType<AuthOAuthController['signIn']>>;
};
Loading

0 comments on commit 778094c

Please sign in to comment.