From d658b26350e587a6e04e93edd35cadf9ccf42f09 Mon Sep 17 00:00:00 2001 From: jeonghyeokSim Date: Thu, 25 Sep 2025 16:55:09 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20password=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 11 + .../auth/dto/ChangePasswordRequestDto.java | 20 + .../domain/auth/mapper/AuthMapper.java | 14 +- .../domain/auth/service/AuthService.java | 26 ++ .../global/config/Mail/MailConfig.java | 29 ++ .../exception/GlobalExceptionHandler.java | 5 + .../exception/InvalidPasswordException.java | 8 + .../exception/PasswordMismatchException.java | 7 + .../resources/mybatis/mapper/AuthMapper.xml | 12 + .../tests/auth/AuthApiIntegrationTest.java | 431 +++++++++++++----- 10 files changed, 446 insertions(+), 117 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/auth/dto/ChangePasswordRequestDto.java create mode 100644 apps/user-service/src/main/java/site/icebang/global/config/Mail/MailConfig.java create mode 100644 apps/user-service/src/main/java/site/icebang/global/handler/exception/InvalidPasswordException.java create mode 100644 apps/user-service/src/main/java/site/icebang/global/handler/exception/PasswordMismatchException.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/auth/controller/AuthController.java b/apps/user-service/src/main/java/site/icebang/domain/auth/controller/AuthController.java index 2303cf74..8de6be8f 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/auth/controller/AuthController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/auth/controller/AuthController.java @@ -15,10 +15,12 @@ import lombok.RequiredArgsConstructor; import site.icebang.common.dto.ApiResponse; +import site.icebang.domain.auth.dto.ChangePasswordRequestDto; import site.icebang.domain.auth.dto.LoginRequestDto; import site.icebang.domain.auth.dto.RegisterDto; import site.icebang.domain.auth.model.AuthCredential; import site.icebang.domain.auth.service.AuthService; +import site.icebang.global.handler.exception.PasswordMismatchException; @RestController @RequestMapping("/v0/auth") @@ -75,4 +77,13 @@ public ApiResponse logout(HttpServletRequest request) { return ApiResponse.success(null); } + + @PatchMapping("/change-password") + public ApiResponse changePassword( + @Valid @RequestBody ChangePasswordRequestDto request, + @AuthenticationPrincipal AuthCredential user) { + + authService.changePassword(user.getEmail(), request); + return ApiResponse.success(null); + } } diff --git a/apps/user-service/src/main/java/site/icebang/domain/auth/dto/ChangePasswordRequestDto.java b/apps/user-service/src/main/java/site/icebang/domain/auth/dto/ChangePasswordRequestDto.java new file mode 100644 index 00000000..99004280 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/auth/dto/ChangePasswordRequestDto.java @@ -0,0 +1,20 @@ +package site.icebang.domain.auth.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ChangePasswordRequestDto { + @NotBlank(message = "현재 비밀번호는 필수입니다") + private String currentPassword; + + @NotBlank(message = "새 비밀번호는 필수입니다") + @Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다") + private String newPassword; + + @NotBlank(message = "새 비밀번호 확인은 필수입니다") + private String confirmPassword; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/auth/mapper/AuthMapper.java b/apps/user-service/src/main/java/site/icebang/domain/auth/mapper/AuthMapper.java index ddc07ffe..1d123f50 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/auth/mapper/AuthMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/auth/mapper/AuthMapper.java @@ -7,13 +7,17 @@ @Mapper public interface AuthMapper { - AuthCredential findUserByEmail(String email); + AuthCredential findUserByEmail(String email); - boolean existsByEmail(String email); + boolean existsByEmail(String email); - int insertUser(RegisterDto dto); // users insert + int insertUser(RegisterDto dto); // users insert - int insertUserOrganization(RegisterDto dto); // user_organizations insert + int insertUserOrganization(RegisterDto dto); // user_organizations insert - int insertUserRoles(RegisterDto dto); // user_roles insert (foreach) + int insertUserRoles(RegisterDto dto); // user_roles insert (foreach) + + String findPasswordByEmail(String email); + + int updatePassword(String email, String newPassword); } diff --git a/apps/user-service/src/main/java/site/icebang/domain/auth/service/AuthService.java b/apps/user-service/src/main/java/site/icebang/domain/auth/service/AuthService.java index 25a5bd42..fd7736a3 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/auth/service/AuthService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/auth/service/AuthService.java @@ -8,10 +8,13 @@ import site.icebang.common.exception.DuplicateDataException; import site.icebang.common.utils.RandomPasswordGenerator; +import site.icebang.domain.auth.dto.ChangePasswordRequestDto; import site.icebang.domain.auth.dto.RegisterDto; import site.icebang.domain.auth.mapper.AuthMapper; import site.icebang.domain.email.dto.EmailRequest; import site.icebang.domain.email.service.EmailService; +import site.icebang.global.handler.exception.InvalidPasswordException; +import site.icebang.global.handler.exception.PasswordMismatchException; @Service @RequiredArgsConstructor @@ -51,4 +54,27 @@ public void registerUser(RegisterDto registerDto) { emailService.send(emailRequest); } + + public void changePassword(String email, ChangePasswordRequestDto request) { + // 1. 새 비밀번호와 확인 비밀번호 일치 검증 + if (!request.getNewPassword().equals(request.getConfirmPassword())) { + throw new PasswordMismatchException("새 비밀번호가 일치하지 않습니다"); + } + + + // 2. 사용자 조회 + String currentHashedPassword = authMapper.findPasswordByEmail(email); + if (currentHashedPassword == null) { + throw new IllegalArgumentException("사용자를 찾을 수 없습니다"); // 이건 그대로 + } + + // 3. 현재 비밀번호 검증 + if (!passwordEncoder.matches(request.getCurrentPassword(), currentHashedPassword)) { + throw new InvalidPasswordException("현재 비밀번호가 올바르지 않습니다"); + } + + // 4. 새 비밀번호 해싱 및 업데이트 + String hashedNewPassword = passwordEncoder.encode(request.getNewPassword()); + authMapper.updatePassword(email, hashedNewPassword); + } } diff --git a/apps/user-service/src/main/java/site/icebang/global/config/Mail/MailConfig.java b/apps/user-service/src/main/java/site/icebang/global/config/Mail/MailConfig.java new file mode 100644 index 00000000..86fc8cd0 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/config/Mail/MailConfig.java @@ -0,0 +1,29 @@ +package site.icebang.global.config.Mail; + +import java.util.Properties; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +@Configuration +public class MailConfig { + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost("smtp.gmail.com"); + mailSender.setPort(587); + mailSender.setUsername(""); + mailSender.setPassword(""); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.transport.protocol", "smtp"); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.debug", "true"); + + return mailSender; + } +} diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java index 8243acde..3167d5b2 100644 --- a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java @@ -55,4 +55,9 @@ public ApiResponse handleDuplicateData(DuplicateDataException ex) { log.warn(ex.getMessage(), ex); return ApiResponse.error("Duplicate: " + ex.getMessage(), HttpStatus.CONFLICT); } + @ExceptionHandler({PasswordMismatchException.class, InvalidPasswordException.class}) //추가 + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handlePasswordException(RuntimeException ex) { + return ApiResponse.error(ex.getMessage(), HttpStatus.BAD_REQUEST); + } } diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/InvalidPasswordException.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/InvalidPasswordException.java new file mode 100644 index 00000000..4f190ade --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/InvalidPasswordException.java @@ -0,0 +1,8 @@ +package site.icebang.global.handler.exception; + +public class InvalidPasswordException extends RuntimeException { + + public InvalidPasswordException(String message) { + super(message); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/PasswordMismatchException.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/PasswordMismatchException.java new file mode 100644 index 00000000..4ad5873b --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/PasswordMismatchException.java @@ -0,0 +1,7 @@ +package site.icebang.global.handler.exception; + +public class PasswordMismatchException extends RuntimeException { + public PasswordMismatchException(String message) { + super(message); + } +} diff --git a/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml index d98c7299..60dcf956 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/AuthMapper.xml @@ -48,4 +48,16 @@ + + + + UPDATE user + SET password = #{newPassword} + WHERE email = #{email} + + \ No newline at end of file diff --git a/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java b/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java index 4fe3b00d..edebfce4 100644 --- a/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java +++ b/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java @@ -5,7 +5,9 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import java.util.HashMap; import java.util.Map; @@ -16,6 +18,7 @@ import org.springframework.mock.web.MockHttpSession; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.transaction.annotation.Transactional; import com.epages.restdocs.apispec.ResourceSnippetParameters; @@ -27,115 +30,319 @@ executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Transactional class AuthApiIntegrationTest extends IntegrationTestSupport { - @Test - @DisplayName("사용자 로그인 성공") - void login_success() throws Exception { - // given - Map loginRequest = new HashMap<>(); - loginRequest.put("email", "admin@icebang.site"); - loginRequest.put("password", "qwer1234!A"); - - // MockMvc로 REST Docs + OpenAPI 생성 - mockMvc - .perform( - post(getApiUrlForDocs("/v0/auth/login")) - .contentType(MediaType.APPLICATION_JSON) - .header("Origin", "https://admin.icebang.site") - .header("Referer", "https://admin.icebang.site/") - .content(objectMapper.writeValueAsString(loginRequest))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.status").value("OK")) - .andExpect(jsonPath("$.message").value("OK")) - .andExpect(jsonPath("$.data").isEmpty()) - .andDo( - document( - "auth-login", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Authentication") - .summary("사용자 로그인") - .description("이메일과 비밀번호로 사용자 인증을 수행합니다") - .requestFields( - fieldWithPath("email") - .type(JsonFieldType.STRING) - .description("사용자 이메일 주소"), - fieldWithPath("password") - .type(JsonFieldType.STRING) - .description("사용자 비밀번호")) - .responseFields( - fieldWithPath("success") - .type(JsonFieldType.BOOLEAN) - .description("요청 성공 여부"), - fieldWithPath("data") - .type(JsonFieldType.NULL) - .description("응답 데이터 (로그인 성공 시 null)"), - fieldWithPath("message") - .type(JsonFieldType.STRING) - .description("응답 메시지"), - fieldWithPath("status") - .type(JsonFieldType.STRING) - .description("HTTP 상태")) - .build()))); - } - - @Test - @DisplayName("사용자 로그아웃 성공") - void logout_success() throws Exception { - // given - 먼저 로그인 - Map loginRequest = new HashMap<>(); - loginRequest.put("email", "admin@icebang.site"); - loginRequest.put("password", "qwer1234!A"); - - MockHttpSession session = new MockHttpSession(); - - // 로그인 먼저 수행 - mockMvc - .perform( - post(getApiUrlForDocs("/v0/auth/login")) - .contentType(MediaType.APPLICATION_JSON) - .session(session) - .content(objectMapper.writeValueAsString(loginRequest))) - .andExpect(status().isOk()); - - // when & then - 로그아웃 수행 - mockMvc - .perform( - post(getApiUrlForDocs("/v0/auth/logout")) - .contentType(MediaType.APPLICATION_JSON) - .session(session) - .header("Origin", "https://admin.icebang.site") - .header("Referer", "https://admin.icebang.site/")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.status").value("OK")) - .andExpect(jsonPath("$.message").value("OK")) - .andExpect(jsonPath("$.data").isEmpty()) - .andDo( - document( - "auth-logout", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Authentication") - .summary("사용자 로그아웃") - .description("현재 인증된 사용자의 세션을 무효화합니다") - .responseFields( - fieldWithPath("success") - .type(JsonFieldType.BOOLEAN) - .description("요청 성공 여부"), - fieldWithPath("data") - .type(JsonFieldType.NULL) - .description("응답 데이터 (로그아웃 성공 시 null)"), - fieldWithPath("message") - .type(JsonFieldType.STRING) - .description("응답 메시지"), - fieldWithPath("status") - .type(JsonFieldType.STRING) - .description("HTTP 상태")) - .build()))); - } -} + @Test + @DisplayName("사용자 로그인 성공") + void login_success() throws Exception { + // given + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + // MockMvc로 REST Docs + OpenAPI 생성 + mockMvc + .perform( + post(getApiUrlForDocs("/v0/auth/login")) + .contentType(MediaType.APPLICATION_JSON) + .header("Origin", "https://admin.icebang.site") + .header("Referer", "https://admin.icebang.site/") + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andExpect(jsonPath("$.data").isEmpty()) + .andDo( + document( + "auth-login", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Authentication") + .summary("사용자 로그인") + .description("이메일과 비밀번호로 사용자 인증을 수행합니다") + .requestFields( + fieldWithPath("email") + .type(JsonFieldType.STRING) + .description("사용자 이메일 주소"), + fieldWithPath("password") + .type(JsonFieldType.STRING) + .description("사용자 비밀번호")) + .responseFields( + fieldWithPath("success") + .type(JsonFieldType.BOOLEAN) + .description("요청 성공 여부"), + fieldWithPath("data") + .type(JsonFieldType.NULL) + .description("응답 데이터 (로그인 성공 시 null)"), + fieldWithPath("message") + .type(JsonFieldType.STRING) + .description("응답 메시지"), + fieldWithPath("status") + .type(JsonFieldType.STRING) + .description("HTTP 상태")) + .build()))); + } + + @Test + @DisplayName("사용자 로그아웃 성공") + void logout_success() throws Exception { + // given - 먼저 로그인 + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + MockHttpSession session = new MockHttpSession(); + + // 로그인 먼저 수행 + mockMvc + .perform( + post(getApiUrlForDocs("/v0/auth/login")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isOk()); + + // when & then - 로그아웃 수행 + mockMvc + .perform( + post(getApiUrlForDocs("/v0/auth/logout")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .header("Origin", "https://admin.icebang.site") + .header("Referer", "https://admin.icebang.site/")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andExpect(jsonPath("$.data").isEmpty()) + .andDo( + document( + "auth-logout", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Authentication") + .summary("사용자 로그아웃") + .description("현재 인증된 사용자의 세션을 무효화합니다") + .responseFields( + fieldWithPath("success") + .type(JsonFieldType.BOOLEAN) + .description("요청 성공 여부"), + fieldWithPath("data") + .type(JsonFieldType.NULL) + .description("응답 데이터 (로그아웃 성공 시 null)"), + fieldWithPath("message") + .type(JsonFieldType.STRING) + .description("응답 메시지"), + fieldWithPath("status") + .type(JsonFieldType.STRING) + .description("HTTP 상태")) + .build()))); + } + + @Test + @DisplayName("비밀번호 변경 성공") + void changePassword_success() throws Exception { + // given - 먼저 로그인해서 인증된 세션 생성 + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + MockHttpSession session = new MockHttpSession(); + + // 로그인 먼저 수행 + mockMvc + .perform( + post(getApiUrlForDocs("/v0/auth/login")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isOk()); + + // 비밀번호 변경 요청 데이터 + Map changePasswordRequest = new HashMap<>(); + changePasswordRequest.put("currentPassword", "qwer1234!A"); + changePasswordRequest.put("newPassword", "newPassword123!A"); + changePasswordRequest.put("confirmPassword", "newPassword123!A"); + + // when & then - 비밀번호 변경 수행 + mockMvc + .perform( + patch(getApiUrlForDocs("/v0/auth/change-password")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .header("Origin", "https://admin.icebang.site") + .header("Referer", "https://admin.icebang.site/") + .content(objectMapper.writeValueAsString(changePasswordRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andExpect(jsonPath("$.data").isEmpty()) + .andDo( + document( + "auth-change-password", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Authentication") + .summary("비밀번호 변경") + .description("현재 로그인된 사용자의 비밀번호를 변경합니다") + .requestFields( + fieldWithPath("currentPassword") + .type(JsonFieldType.STRING) + .description("현재 비밀번호"), + fieldWithPath("newPassword") + .type(JsonFieldType.STRING) + .description("새 비밀번호 (최소 8자 이상)"), + fieldWithPath("confirmPassword") + .type(JsonFieldType.STRING) + .description("새 비밀번호 확인")) + .responseFields( + fieldWithPath("success") + .type(JsonFieldType.BOOLEAN) + .description("요청 성공 여부"), + fieldWithPath("data") + .type(JsonFieldType.NULL) + .description("응답 데이터 (비밀번호 변경 성공 시 null)"), + fieldWithPath("message") + .type(JsonFieldType.STRING) + .description("응답 메시지"), + fieldWithPath("status") + .type(JsonFieldType.STRING) + .description("HTTP 상태")) + .build()))); + } + + @Test + @DisplayName("비밀번호 변경 실패 - 잘못된 현재 비밀번호") + void changePassword_fail_wrongCurrentPassword() throws Exception { + // given - 로그인된 세션 + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + MockHttpSession session = new MockHttpSession(); + + mockMvc.perform( + post(getApiUrlForDocs("/v0/auth/login")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isOk()); + + // 잘못된 현재 비밀번호 + Map changePasswordRequest = new HashMap<>(); + changePasswordRequest.put("currentPassword", "wrongPassword"); + changePasswordRequest.put("newPassword", "newPassword123!A"); + changePasswordRequest.put("confirmPassword", "newPassword123!A"); + + // when & then + mockMvc.perform( + patch(getApiUrlForDocs("/v0/auth/change-password")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .content(objectMapper.writeValueAsString(changePasswordRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("현재 비밀번호가 올바르지 않습니다")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + @DisplayName("비밀번호 변경 실패 - 새 비밀번호와 확인 비밀번호 불일치") + void changePassword_fail_passwordMismatch() throws Exception { + // given - 로그인된 세션 + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + MockHttpSession session = new MockHttpSession(); + + mockMvc.perform( + post(getApiUrlForDocs("/v0/auth/login")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isOk()); + + // 새 비밀번호와 확인 비밀번호 불일치 + Map changePasswordRequest = new HashMap<>(); + changePasswordRequest.put("currentPassword", "qwer1234!A"); + changePasswordRequest.put("newPassword", "newPassword123!A"); + changePasswordRequest.put("confirmPassword", "differentPassword123!A"); + + // when & then + mockMvc.perform( + patch(getApiUrlForDocs("/v0/auth/change-password")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .content(objectMapper.writeValueAsString(changePasswordRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("새 비밀번호가 일치하지 않습니다")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + @DisplayName("비밀번호 변경 실패 - 새 비밀번호 길이 부족") + void changePassword_fail_passwordTooShort() throws Exception { + // given - 로그인된 세션 + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + MockHttpSession session = new MockHttpSession(); + + mockMvc.perform( + post(getApiUrlForDocs("/v0/auth/login")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .content(objectMapper.writeValueAsString(loginRequest))) + .andExpect(status().isOk()); + + // 8자 미만의 새 비밀번호 + Map changePasswordRequest = new HashMap<>(); + changePasswordRequest.put("currentPassword", "qwer1234!A"); + changePasswordRequest.put("newPassword", "123!A"); // 5자 + changePasswordRequest.put("confirmPassword", "123!A"); + + // when & then + mockMvc.perform( + patch(getApiUrlForDocs("/v0/auth/change-password")) + .contentType(MediaType.APPLICATION_JSON) + .session(session) + .content(objectMapper.writeValueAsString(changePasswordRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + // Validation 메시지는 @Size(message="...") 지정한 값과 동일하게 맞추면 됨 + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("비밀번호"))) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + @DisplayName("비밀번호 변경 실패 - 미인증 사용자") + void changePassword_fail_unauthorized() throws Exception { + // given - 로그인하지 않은 상태 + Map changePasswordRequest = new HashMap<>(); + changePasswordRequest.put("currentPassword", "qwer1234!A"); + changePasswordRequest.put("newPassword", "newPassword123!A"); + changePasswordRequest.put("confirmPassword", "newPassword123!A"); + + // when & then + mockMvc.perform( + patch(getApiUrlForDocs("/v0/auth/change-password")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(changePasswordRequest))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value("UNAUTHORIZED")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + } \ No newline at end of file