본문 바로가기

Dot Programming/Spring Clone

[스프링 웹앱 프로젝트 #27 #28] 패스워드 수정 및 테스트

27. 패스워드 수정

1. 패스워드 탭 활성화
2. 새 패스워드와 새 패스워드 확인 값 일치
3. 패스워드 인코딩
4. 둘다 최소 8~50자 사이
5. 사용자 정보를 변경하는 작업
   > 서비스로 위임해서 트랜잭션 안에서 처리해야 하낟.
   > 또는 Detached 상태의 객체를 변경한 다음 Repository의 save를 호출해서 상태 변경 내역을 적용할 것(Merge)

 

백엔드 로직 처리하기

1. 첫 번째로는 PasswordForm을 생성 (variable : newPassowrd, newPasswordConfirm)

@Data
public class PasswordForm {

    @Length(min = 8, max =50)
    private String newPassword;

    @Length(min = 8, max =50)
    private String newPasswordConfirm;
}

 

 

2. Password와 PasswordConfirm값이 일치하는지 확인해주는 PasswordFormValidator를 생성

public class PasswordFormValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return PasswordForm.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        PasswordForm passwordForm = (PasswordForm) target;
        if(!passwordForm.getNewPassword().equals(passwordForm.getNewPasswordConfirm())){
            errors.rejectValue("newPassword", "wrong value", "입력한 새 패스워드가 일치하지 않습니다.");
        }
    }
}

 

 

Controller

3. SettingsController에서 비밀번호 변경 로직 생성

Front단에서 설정한 비밀번호 변경 url

<a class="list-group-item list-group-item-action" th:classappend="${currentMenu == 'password'}? active" href="#" th:href="@{/settings/password}">패스워드</a>

 

SettingsController.java

@Controller
@RequiredArgsConstructor
public class SettingsController {

    /**
     * AccountController - SignUpForm - SignUpFormValidator와 같은 구조로 동작
     * passwordForm 데이터를 받을때 바인더를 설정
     * passwordFormValidator는 Bean등록을 안했기 때문에 new로 객체 생성
     **/
    @InitBinder("passwordForm")
    public void initBinder(WebDataBinder webDataBinder){
        webDataBinder.addValidators(new PasswordFormValidator());
    }

    static final String SETTINGS_PROFILE_VIEW_NAME = "settings/profile";
    static final String SETTINGS_PROFILE_URL = "/settings/profile";

   // ...

    // Password 변경 페이지
    @GetMapping(SETTINGS_PASSWORD_URL)
    public String updatePasswordForm(@CurrentUser Account account, Model model) {
        model.addAttribute(account);
        model.addAttribute(new PasswordForm());

        return SETTINGS_PASSWORD_VIEW_NAME;
    }

    @PostMapping(SETTINGS_PASSWORD_URL)
    public String updatePassword(@CurrentUser Account account, @Valid PasswordForm passwordForm, Errors errors,
                                 Model model, RedirectAttributes attributes){

        // 에러
        if(errors.hasErrors()){
            model.addAttribute(account);
            return SETTINGS_PASSWORD_VIEW_NAME;
        }

        // 비밀번호 변경 성공
        accountService.updatePassword(account, passwordForm.getNewPassword());
        attributes.addFlashAttribute("message", "패스워드를 변경했습니다.");
        return "redirect:"  + SETTINGS_PASSWORD_URL;

    }

}

 

 

4. Service단에서 updatePassword로직 생성

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class AccountService implements UserDetailsService {

    private final PasswordEncoder passwordEncoder;

   // ...

    public void updatePassword(Account account, String newPassword) {
        account.setPassword(passwordEncoder.encode(newPassword));
        accountRepository.save(account); // detached인 상태인 객체(Account)를 명시적으로 merge
    }
}

 

 

Front 개발

 

5. /settings/password.html 생성

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments.html :: head"></head>
<body class="bg-light">
<div th:replace="fragments.html :: main-nav"></div>
<div class="container">
    <div class="row mt-5 justify-content-center">
        <div class="col-2">
            <div th:replace="fragments.html :: settings-menu(currentMenu='password')"></div>
        </div>
        <div class="col-8">
            <div th:if="${message}" class="alert alert-info alert-dismissible fade show mt-3" role="alert">
                <span th:text="${message}">메시지</span>
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="row">
                <h2 class="col-sm-12" >패스워드 변경</h2>
            </div>
            <div class="row mt-3">
                <form class="needs-validation col-12" action="#"
                      th:action="@{/settings/password}" th:object="${passwordForm}" method="post" novalidate>
                    <div class="form-group">
                        <label for="newPassword">새 패스워드</label>
                        <input id="newPassword" type="password" th:field="*{newPassword}" class="form-control"
                               aria-describedby="newPasswordHelp" required min="8" max="50">
                        <small id="newPasswordHelp" class="form-text text-muted">
                            새 패스워드를 입력하세요.
                        </small>
                        <small class="invalid-feedback">패스워드를 입력하세요.</small>
                        <small class="form-text text-danger" th:if="${#fields.hasErrors('newPassword')}" th:errors="*{newPassword}">New Password Error</small>
                    </div>

                    <div class="form-group">
                        <label for="newPasswordConfirm">새 패스워드 확인</label>
                        <input id="newPasswordConfirm" type="password" th:field="*{newPasswordConfirm}" class="form-control"
                               aria-describedby="newPasswordConfirmHelp" required min="8" max="50">
                        <small id="newPasswordConfirmHelp" class="form-text text-muted">
                            새 패스워드를 다시 한번 입력하세요.
                        </small>
                        <small class="invalid-feedback">새 패스워드를 다시 입력하세요.</small>
                        <small class="form-text text-danger" th:if="${#fields.hasErrors('newPasswordConfirm')}" th:errors="*{newPasswordConfirm}">New Password Confirm Error</small>
                    </div>

                    <div class="form-group">
                        <button class="btn btn-outline-primary" type="submit" aria-describedby="submitHelp">패스워드 변경하기</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
<script th:replace="fragments.html :: form-validation"></script>
</body>
</html>

 

 

그러면 아래와 같이 Form이 생성된다

 

 

 

결과화면 테스트

마지막으로 로직이 잘 먹히는지 테스트를 해보자

 

1. newPassword != newPasswordConfirm

 

 

 

2. 비밀번호 길이 8~50자 사이로 입력

 

 

 

3. 에러없이 비밀번호 변경 완료

 

 

 

테스트 코드 작성

테스트코드는 3가지 케이스로 나눠서 테스트를 하였다.

  1. 패스워드 수정 폼이 제대로 나오는지
  2. 패스워드가 정상적으로 수정이 되는지
  3. 패스워드 불일치 시 에러가 정상적으로 발생하는지

 

SettingsControllerTest.java

 // 패스워드 수정 테스트
    @WithAccount("loosie")
    @DisplayName("패스워드 수정 폼")
    @Test
    void updatePassword_form() throws Exception {
        mockMvc.perform(get(SettingsController.SETTINGS_PASSWORD_URL))
                .andExpect(status().isOk())
                .andExpect(model().attributeExists("account"))
                .andExpect(model().attributeExists("passwordForm"));
    }

    @WithAccount("loosie")
    @DisplayName("패스워드 수정 - 입력값 정상")
    @Test
    void updatePassword_success() throws Exception {
        mockMvc.perform(post(SettingsController.SETTINGS_PASSWORD_URL)
                .param("newPassword", "12345678")
                .param("newPasswordConfirm", "12345678")
                .with(csrf()))
                .andExpect(status().is3xxRedirection())
                .andExpect(redirectedUrl(SettingsController.SETTINGS_PASSWORD_URL))
                .andExpect(flash().attributeExists("message"));

        Account loosie = accountRepository.findByNickname("loosie");
        assertTrue(passwordEncoder.matches("12345678", loosie.getPassword()));
    }

    @WithAccount("loosie")
    @DisplayName("패스워드 수정 - 입력값 에러 - 패스워드 불일치")
    @Test
    void updatePassword_fail() throws Exception {
        mockMvc.perform(post(SettingsController.SETTINGS_PASSWORD_URL)
                .param("newPassword", "12345678")
                .param("newPasswordConfirm", "11111111")
                .with(csrf()))
                .andExpect(status().isOk())
                .andExpect(view().name(SettingsController.SETTINGS_PASSWORD_VIEW_NAME))
                .andExpect(model().hasErrors())
                .andExpect(model().attributeExists("passwordForm"))
                .andExpect(model().attributeExists("account"));
    }

 

테스트케이스 정상 작동

 


참고

인프런 강의 - 스프링과 JPA 기반 웹 애플리케이션 개발