Skip to content

Commit

Permalink
Merge pull request #131 from Instagram-Clone-Coding/Test/MemberAuth
Browse files Browse the repository at this point in the history
회원가입 인증코드 이메일 HTML 적용, 멤버 인증 테스트 작성
vectorch9 authored Mar 7, 2022
2 parents 8355aff + d969e29 commit a5960a2
Showing 8 changed files with 670 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -175,7 +175,7 @@ public ResponseEntity<ResultResponse> sendResetPasswordCode(

@ApiOperation(value = "코드를 통한 비밀번호 재설정")
@ApiImplicitParam(name = "Authorization", value = "불필요", required = false, example = " ")
@PostMapping(value = "/accounts/password/reset")
@PutMapping(value = "/accounts/password/reset")
public ResponseEntity<ResultResponse> resetPassword(@Validated @RequestBody ResetPasswordRequest resetPasswordRequest, HttpServletResponse response) {
JwtDto jwt = memberAuthService.resetPassword(resetPasswordRequest);

Original file line number Diff line number Diff line change
@@ -74,7 +74,10 @@ public enum ErrorCode {
CANT_CONVERT_FILE(500, "FI001", "파일을 변환할 수 없습니다."),

// Alarm
MISMATCHED_ALARM_TYPE(400, "A001", "알람 형식이 올바르지 않습니다.")
MISMATCHED_ALARM_TYPE(400, "A001", "알람 형식이 올바르지 않습니다."),

// Email
CANT_SEND_EMAIL(500, "E001", "이메일 전송 중 오류가 발생했습니다.")
;

private int status;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package cloneproject.Instagram.exception;

import cloneproject.Instagram.dto.error.ErrorCode;

public class CantSendEmailException extends BusinessException{
public CantSendEmailException(){
super(ErrorCode.CANT_SEND_EMAIL);
}

}
20 changes: 13 additions & 7 deletions src/main/java/cloneproject/Instagram/service/EmailCodeService.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package cloneproject.Instagram.service;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.Random;

import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.stereotype.Service;

import cloneproject.Instagram.entity.member.Member;
import cloneproject.Instagram.entity.redis.EmailCode;
import cloneproject.Instagram.entity.redis.ResetPasswordCode;
import cloneproject.Instagram.exception.CantResetPasswordException;
import cloneproject.Instagram.exception.CantSendEmailException;
import cloneproject.Instagram.exception.MemberDoesNotExistException;
import cloneproject.Instagram.exception.NoConfirmEmailException;
import cloneproject.Instagram.repository.EmailCodeRedisRepository;
@@ -29,21 +33,23 @@ public class EmailCodeService {
private final EmailService emailService;

public boolean sendEmailConfirmationCode(String username, String email){

String text;
String code = createConfirmationCode(6);

try{
ClassPathResource resource = new ClassPathResource("confirmEmailUI.html");
String html = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
text = String.format(html, email, code, email);
}catch(IOException e){
throw new CantSendEmailException();
}
EmailCode emailCode = EmailCode.builder()
.username(username)
.email(email)
.code(code)
.build();
emailCodeRedisRepository.save(emailCode);

SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(email);
mailMessage.setSubject("회원가입 이메일 인증코드");
mailMessage.setText(code);
emailService.sendEmail(mailMessage);
emailService.sendHtmlTextEmail(username+ ", welcome to Instagram." ,text, email);

return true;
}
18 changes: 18 additions & 0 deletions src/main/java/cloneproject/Instagram/service/EmailService.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package cloneproject.Instagram.service;

import javax.mail.internet.MimeMessage;

import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import cloneproject.Instagram.exception.CantSendEmailException;
import lombok.RequiredArgsConstructor;

@Service
@@ -18,4 +22,18 @@ public void sendEmail(SimpleMailMessage email){
javaMailSender.send(email);
}

@Async
public void sendHtmlTextEmail(String subject, String content, String email){
MimeMessage message = javaMailSender.createMimeMessage();
try {
MimeMessageHelper messageHelper = new MimeMessageHelper(message, true, "UTF-8");
messageHelper.setTo(email);
messageHelper.setSubject(subject);
messageHelper.setText(content, true);
javaMailSender.send(message);
}catch(Exception e){
throw new CantSendEmailException();
}
}

}
281 changes: 281 additions & 0 deletions src/main/resources/confirmEmailUI.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>Instagram</title>
</head>
<body>
<table
border="0"
width="100%%"
cellspacing="0"
cellpadding="0"
style="border-collapse: collapse"
>
<tbody>
<tr>
<td height="20" colspan="3" style="line-height: 20px">&nbsp;</td>
</tr>
<tr>
<td height="1" colspan="3" style="line-height: 1px"></td>
</tr>
<tr>
<td>
<table
border="0"
width="100%%"
cellspacing="0"
cellpadding="0"
style="
border-collapse: collapse;
text-align: center;
width: 100%%;
max-width: 433px;
margin-top: 0;
margin-left: auto;
margin-bottom: 0;
margin-right: auto;
"
>
<tbody>
<tr>
<td width="15"></td>
<td style="line-height: 0px; padding: 0 0 15px 0">
<table
border="0"
width="100%%"
cellspacing="0"
cellpadding="0"
style="border-collapse: collapse"
>
<tbody>
<tr>
<td
style="width: 100%%; text-align: left; height: 33px"
>
<img
src="http://drive.google.com/uc?export=view&id=1oArh9BFHw1K2Gc5dsYzs_ZQMt6JAuQeE"
height="33"
style="border: 0"
/>
</td>
</tr>
</tbody>
</table>
</td>
<td width="15"></td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>
<table
border="0"
width="430"
cellspacing="0"
cellpadding="0"
style="
border-collapse: collapse;
margin-top: 0;
margin-left: auto;
margin-bottom: 0;
margin-right: auto;
"
>
<tbody>
<tr>
<td>
<table
border="0"
width="430"
cellspacing="0"
cellpadding="0"
style="
border-collapse: collapse;
margin-top: 0;
margin-left: auto;
margin-bottom: 0;
margin-right: auto;
width: 430;
"
>
<tbody>
<tr>
<td width="15" style="display: block">
&nbsp;&nbsp;&nbsp;
</td>
</tr>
<tr>
<td width="12" style="display: block">
&nbsp;&nbsp;&nbsp;
</td>
<td>
<table
border="0"
width="100%%"
cellspacing="0"
cellpadding="0"
style="border-collapse: collapse"
>
<tbody>
<tr>
<td></td>
<td
style="
margin-top: 10;
margin-left: 0;
margin-bottom: 10;
margin-right: 0;
color: #565a5c;
font-size: 18px;
"
>
<p
style="
margin-top: 10;
margin-left: 0;
margin-bottom: 10;
margin-right: 0;
color: #565a5c;
font-size: 18px;
"
>
Hi,
</p>
<p
style="
margin-top: 10;
margin-left: 0;
margin-bottom: 10;
margin-right: 0;
color: #565a5c;
font-size: 18px;
"
>
<!-- 이메일 -->
Someone tried to sign up for an Instagram
account with %s. If it
was you, enter this confirmation code in
the app:
</p>
</td>
</tr>
<tr>
<td></td>
<td
style="
padding: 10px;
color: #565a5c;
font-size: 32px;
font-weight: 500;
text-align: center;
padding-bottom: 25px;
"
>
<!-- 코드 -->
%s
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>
<table
border="0"
cellspacing="0"
cellpadding="0"
style="
border-collapse: collapse;
margin: 0 auto 0 auto;
margin-top: 0;
margin-left: auto;
margin-bottom: 0;
margin-right: auto;
width: 100%%;
max-width: 600px;
"
>
<tbody style="text-align: center">
<tr>
<td width="15" style="display: block">
&nbsp;&nbsp;&nbsp;
</td>
</tr>
<tr>
<td>
<table style="padding-top: 10px; ">
<tbody style="margin: auto">
<tr>
<img
src="http://drive.google.com/uc?export=view&id=1Max2S-mP-GpssexCdfo2obS4OYpcSqK-"
height="26"
width="52"
alt="meta logo"
/>
</td>
</tr>
</tbody>
</table>
</tr>
<tr>
<td style="height: 10px"></td>
</tr>
<tr>
<td
style="
color: #abadae;
font-size: 11px;
padding-bottom: 5px;
"
>
© Instagram. Meta Platforms, Inc., 1601 Willow Road, Menlo
Park, CA 94025<br aria-hidden="true" />
</td>
</tr>
<tr>
<td
style="
color: #abadae;
font-size: 11px;
padding-bottom: 5px;
"
>
<!--이메일-->
This message was sent to %s.<br aria-hidden="true" />
</td>
</tr>
</td>
<td width="20" style="display: block">&nbsp;&nbsp;&nbsp;</td>
<td width="15"></td>
</tr>
<tr>
<td height="32" colspan="3" style="line-height: 32px">
&nbsp;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td height="20" colspan="3" style="line-height: 20px">&nbsp;</td>
</tr>
</tbody>
</table>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
import cloneproject.Instagram.dto.member.JwtDto;
import cloneproject.Instagram.dto.member.LoginRequest;
import cloneproject.Instagram.dto.member.RegisterRequest;
import cloneproject.Instagram.dto.member.ResetPasswordRequest;
import cloneproject.Instagram.dto.member.SendConfirmationEmailRequest;
import cloneproject.Instagram.dto.member.UpdatePasswordRequest;
import cloneproject.Instagram.dto.result.ResultCode;
@@ -50,7 +51,7 @@ public class MemberAuthControllerTest {
private ObjectMapper objectMapper;

private MockMvc mockMvc;

private String mockCode;

@BeforeEach
private void setup() {
@@ -59,6 +60,7 @@ private void setup() {
.addFilter(new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockCode = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
}


@@ -122,6 +124,25 @@ void it_return_success() throws Exception{
}
}

@Nested
@DisplayName("이메일코드 인증에 실패한경우")
class Context_eamilcode_fail{
@Test
@DisplayName("실패 ResultCode를 반환한다")
void it_return_failure() throws Exception{
RegisterRequest registerRequest = new RegisterRequest("dlwlrma", "이지금", "a12341234", "aaa@gmail.com", "ABC123");
when(memberAuthService.register(any())).thenReturn(false);

mockMvc.perform(post("/accounts")
.contentType("application/json")
.content(objectMapper.writeValueAsString(registerRequest))
)
.andExpect(status().isOk())
.andExpect(content().string(objectMapper.writeValueAsString(
ResultResponse.of(ResultCode.CONFIRM_EMAIL_FAIL, false))));
}
}

@Nested
@DisplayName("잘못된 parameters가 주어지면")
class Context_wrong_params{
@@ -284,5 +305,121 @@ void it_return_error() throws Exception{
}

}
@Nested
@DisplayName("비밀번호 변경 이메일 전송은")
class Describe_sendResetPasswordCode{

@Nested
@DisplayName("올바른 parameters가 주어지면")
class Context_correct_params{
@Test
@DisplayName("성공 ResultCode를 반환한다")
void it_return_success() throws Exception{
mockMvc.perform(post("/accounts/password/email")
.param("username", "dlwlrma")
)
.andExpect(status().isOk())
.andExpect(content().string(objectMapper.writeValueAsString(
ResultResponse.of(ResultCode.SEND_RESET_PASSWORD_EMAIL_SUCCESS, null))));
}
}

}

@Nested
@DisplayName("비밀번호 재설정은")
class Describe_resetPassword{

@Nested
@DisplayName("올바른 parameters가 주어지면")
class Context_correct_params{
@Test
@DisplayName("성공 ResultCode를 반환한다")
void it_return_success() throws Exception{
ResetPasswordRequest resetPasswordRequest =
new ResetPasswordRequest("dlwlrma", mockCode, "a12341234");
JwtDto jwtDto = JwtDto.builder()
.type("Bearer")
.accessToken("AAA.BBB.CCC")
.refreshToken("CCC.BBB.AAA")
.build();
when(memberAuthService.resetPassword(any())).thenReturn(jwtDto);


mockMvc.perform(put("/accounts/password/reset")
.contentType("application/json")
.content(objectMapper.writeValueAsString(resetPasswordRequest))
)
.andExpect(cookie().value("refreshToken", jwtDto.getRefreshToken()))
.andExpect(status().isOk());
}
}

@Nested
@DisplayName("잘못된 parameters가 주어지면")
class Context_wrong_params{
@Test
@DisplayName("400 에러가 발생한다")
void it_return_error() throws Exception{
ResetPasswordRequest resetPasswordRequest =
new ResetPasswordRequest("dlwlrma", "AAA123", "a12341234");

mockMvc.perform(put("/accounts/password/reset")
.contentType("application/json")
.content(objectMapper.writeValueAsString(resetPasswordRequest))
)
.andExpect(status().isBadRequest());
}
}

}
@Nested
@DisplayName("코드를 이용한 로그인은")
class Describe_loginWithCode{

@Nested
@DisplayName("올바른 parameters가 주어지면")
class Context_correct_params{
@Test
@DisplayName("성공 ResultCode를 반환한다")
void it_return_success() throws Exception{
JwtDto jwtDto = JwtDto.builder()
.type("Bearer")
.accessToken("AAA.BBB.CCC")
.refreshToken("CCC.BBB.AAA")
.build();
when(memberAuthService.loginWithCode(any(), any())).thenReturn(jwtDto);


mockMvc.perform(post("/accounts/login/recovery")
.param("username", "dlwlrma")
.param("code", mockCode)
)
.andExpect(cookie().value("refreshToken", jwtDto.getRefreshToken()))
.andExpect(status().isOk());
}
}

}
@Nested
@DisplayName("비밀번호 재설정 코드 만료시키기는")
class Describe_expireResetPasswordCode{

@Nested
@DisplayName("올바른 parameters가 주어지면")
class Context_correct_params{
@Test
@DisplayName("성공 ResultCode를 반환한다")
void it_return_success() throws Exception{
mockMvc.perform(delete("/accounts/login/recovery")
.param("username", "dlwlrma")
)
.andExpect(status().isOk())
.andExpect(content().string(objectMapper.writeValueAsString(
ResultResponse.of(ResultCode.EXPIRE_RESET_PASSWORD_CODE_SUCCESS, null))));
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package cloneproject.Instagram.service;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import cloneproject.Instagram.dto.member.JwtDto;
import cloneproject.Instagram.dto.member.LoginRequest;
import cloneproject.Instagram.dto.member.RegisterRequest;
import cloneproject.Instagram.entity.member.Member;
import cloneproject.Instagram.exception.UseridAlreadyExistException;
import cloneproject.Instagram.repository.MemberRepository;
import cloneproject.Instagram.util.JwtUtil;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Optional;

@ExtendWith(SpringExtension.class)
@DisplayName("Follow Service")
public class MemberAuthServiceTest {

@InjectMocks
private MemberAuthService memberAuthService;

@Mock
private JwtUtil jwtUtil;

@Mock
private EmailCodeService emailCodeService;

@Mock
private MemberRepository memberRepository;

@Mock
private BCryptPasswordEncoder bCryptPasswordEncoder;

@BeforeEach
void setRepostiory(){
Member member = Member.builder()
.username("dlwlrma")
.name("이지금")
.email("aaa@gmail.com")
.password("1234")
.build();

when(memberRepository.findById(1L)).thenReturn(Optional.of(member));
when(memberRepository.findByUsername("dlwlrma")).thenReturn(Optional.of(member));
}

@Nested
@DisplayName("username 중복조회 API는")
class Describe_checkUsername{

@Nested
@DisplayName("존재하는 경우에는")
class Context_exist{
@DisplayName("false를 반환한다")
@Test
void it_return_false(){
when(memberRepository.existsByUsername("dlwlrma")).thenReturn(true);

assertEquals(false, memberAuthService.checkUsername("dlwlrma"));
}
}
@Nested
@DisplayName("존재하지 않는 경우에는")
class Context_dont_exist{
@DisplayName("true를 반환한다")
@Test
void it_return_true(){
assertEquals(true, memberAuthService.checkUsername("dlwlrma1"));
}
}
}

@Nested
@DisplayName("register는")
class Describe_register{

@Nested
@DisplayName("username이 존재하는 경우에는")
class Context_username_already_exist{
@DisplayName("exception을 발생시킨다")
@Test
void it_throw_exception(){
RegisterRequest registerRequest = new RegisterRequest("dlwlrma", "이지금", "a12341234", "aaa@gmail.com", "ABC123");
when(memberRepository.existsByUsername("dlwlrma")).thenReturn(true);

assertThrows(UseridAlreadyExistException.class, ()->memberAuthService.register(registerRequest));
}
}

@Nested
@DisplayName("EmailCode가 올바르지 않은 경우에는")
class Context_dont_exist{
@DisplayName("false를 반환한다")
@Test
void it_return_false(){
RegisterRequest registerRequest = new RegisterRequest("dlwlrma", "이지금", "a12341234", "aaa@gmail.com", "ABC123");
assertEquals(false, memberAuthService.register(registerRequest));
}
}

@Nested
@DisplayName("정상적인 경우에는")
class Context_correct_process{
@DisplayName("true를 반환한다")
@Test
void it_return_true(){
RegisterRequest registerRequest = new RegisterRequest("dlwlrma", "이지금", "a12341234", "aaa@gmail.com", "ABC123");
when(emailCodeService.checkEmailCode(any(), any(), any())).thenReturn(true);

assertEquals(true, memberAuthService.register(registerRequest));
}
}
}

@Nested
@DisplayName("sendEmailConfirmation는")
class Describe_sendEmailConfirmation{

@Nested
@DisplayName("username이 존재하는 경우에는")
class Context_username_exist{
@DisplayName("exception을 발생시킨다")
@Test
void it_throw_exception(){
when(memberRepository.existsByUsername("dlwlrma")).thenReturn(true);

assertThrows(UseridAlreadyExistException.class, ()->memberAuthService.sendEmailConfirmation("dlwlrma", ""));
}
}
@Nested
@DisplayName("username이 존재하지 않는 경우에는")
class Context_username_dont_exist{
@DisplayName("정상적으로 실행된다")
@Test
void it_do_well(){
memberAuthService.sendEmailConfirmation("dlwlrma", "");
verify(emailCodeService, times(1)).sendEmailConfirmationCode(any(), any());
}
}
}

// TODO AuthenticationManagerBuilder NullPointerException 해결
// @Nested
// @DisplayName("login은")
// class Describe_login{

// @Nested
// @DisplayName("올바른 정보를 입력하면")
// class Context_correct_account{
// @DisplayName("JWT 토큰이 반환된다")
// @Test
// void it_return_jwt() throws Exception{
// LoginRequest loginRequest = new LoginRequest("dlwlrma", "a12341234");
// JwtDto jwtDto = JwtDto.builder()
// .type("Bearer")
// .accessToken("AAA.BBB.CCC")
// .refreshToken("CCC.BBB.AAA")
// .build();
// when(jwtUtil.generateTokenDto(any())).thenReturn(jwtDto);

// assertEquals(jwtDto, memberAuthService.login(loginRequest));
// }
// }

// @Nested
// @DisplayName("EmailCode가 올바르지 않은 경우에는")
// class Context_dont_exist{
// @DisplayName("false를 반환한다")
// @Test
// void it_return_false(){
// RegisterRequest registerRequest = new RegisterRequest("dlwlrma", "이지금", "a12341234", "aaa@gmail.com", "ABC123");
// assertEquals(false, memberAuthService.register(registerRequest));
// }
// }

// @Nested
// @DisplayName("정상적인 경우에는")
// class Context_correct_process{
// @DisplayName("true를 반환한다")
// @Test
// void it_return_true(){
// RegisterRequest registerRequest = new RegisterRequest("dlwlrma", "이지금", "a12341234", "aaa@gmail.com", "ABC123");
// when(emailCodeService.checkEmailCode(any(), any(), any())).thenReturn(true);

// assertEquals(true, memberAuthService.register(registerRequest));
// }
// }
// }

}

0 comments on commit a5960a2

Please sign in to comment.