From fd5a1b53bcc6be8c0ee456f743bb24cca15f71c2 Mon Sep 17 00:00:00 2001 From: Rebwon Date: Thu, 30 Sep 2021 12:35:43 +0900 Subject: [PATCH] Add Update Account HTTP API - Add Command API test - Refactor Interceptor, SessionManager, ArgumentResolver --- .../account/application/AccountFacade.java | 7 ++ ...thenticationPrincipalArgumentResolver.java | 4 +- .../SessionAuthInterceptor.java | 6 -- .../infrastructure/SessionManager.java | 9 +-- .../presentation/AccountCommandApi.java | 11 +++ .../presentation/UpdateAccountCommand.java | 21 +++++ .../account/presentation/AccountApiUri.java | 3 +- .../presentation/AccountCommandApiTests.java | 78 +++++++++++++++++++ 8 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/yam/app/account/presentation/UpdateAccountCommand.java diff --git a/src/main/java/com/yam/app/account/application/AccountFacade.java b/src/main/java/com/yam/app/account/application/AccountFacade.java index 8f0ebe3..1f3b380 100644 --- a/src/main/java/com/yam/app/account/application/AccountFacade.java +++ b/src/main/java/com/yam/app/account/application/AccountFacade.java @@ -11,6 +11,8 @@ import com.yam.app.account.presentation.AccountResponse; import com.yam.app.account.presentation.ConfirmRegisterAccountCommand; import com.yam.app.account.presentation.LoginAccountCommand; import com.yam.app.account.presentation.RegisterAccountCommand; +import com.yam.app.account.presentation.UpdateAccountCommand; +import com.yam.app.common.Authentication; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -61,4 +63,9 @@ public class AccountFacade { return translator.toResponse(accountReader.findByEmail(email) .orElseThrow(() -> new AccountNotFoundException(email))); } + + @Transactional + public void update(UpdateAccountCommand command, Authentication authentication) { + + } } diff --git a/src/main/java/com/yam/app/account/infrastructure/AuthenticationPrincipalArgumentResolver.java b/src/main/java/com/yam/app/account/infrastructure/AuthenticationPrincipalArgumentResolver.java index c0b8c93..79a0398 100644 --- a/src/main/java/com/yam/app/account/infrastructure/AuthenticationPrincipalArgumentResolver.java +++ b/src/main/java/com/yam/app/account/infrastructure/AuthenticationPrincipalArgumentResolver.java @@ -1,6 +1,7 @@ package com.yam.app.account.infrastructure; import com.yam.app.common.AuthenticationPrincipal; +import com.yam.app.common.UnauthorizedRequestException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.core.MethodParameter; @@ -24,6 +25,7 @@ public final class AuthenticationPrincipalArgumentResolver .getSession(false); var sessionManager = new SessionManager(session); - return sessionManager.fetchPrincipal(); + return sessionManager.fetchPrincipal() + .orElseThrow(() -> new UnauthorizedRequestException("Failed fetch principal")); } } diff --git a/src/main/java/com/yam/app/account/infrastructure/SessionAuthInterceptor.java b/src/main/java/com/yam/app/account/infrastructure/SessionAuthInterceptor.java index 584be68..58a3515 100644 --- a/src/main/java/com/yam/app/account/infrastructure/SessionAuthInterceptor.java +++ b/src/main/java/com/yam/app/account/infrastructure/SessionAuthInterceptor.java @@ -16,12 +16,6 @@ public final class SessionAuthInterceptor implements HandlerInterceptor { throw new UnauthorizedRequestException("Unauthorized request"); } - var sessionManager = new SessionManager(session); - - if (!sessionManager.isExistPrincipal()) { - throw new UnauthorizedRequestException("Failed fetch principal"); - } - return true; } } diff --git a/src/main/java/com/yam/app/account/infrastructure/SessionManager.java b/src/main/java/com/yam/app/account/infrastructure/SessionManager.java index c3a87c1..1e2d108 100644 --- a/src/main/java/com/yam/app/account/infrastructure/SessionManager.java +++ b/src/main/java/com/yam/app/account/infrastructure/SessionManager.java @@ -1,5 +1,6 @@ package com.yam.app.account.infrastructure; +import java.util.Optional; import javax.servlet.http.HttpSession; public final class SessionManager { @@ -16,12 +17,8 @@ public final class SessionManager { this.httpSession.setAttribute(LOGIN_ACCOUNT, principal); } - public AccountPrincipal fetchPrincipal() { - return (AccountPrincipal) httpSession.getAttribute(LOGIN_ACCOUNT); - } - - public boolean isExistPrincipal() { - return httpSession.getAttribute(LOGIN_ACCOUNT) != null; + public Optional fetchPrincipal() { + return Optional.ofNullable((AccountPrincipal) httpSession.getAttribute(LOGIN_ACCOUNT)); } public void removePrincipal() { diff --git a/src/main/java/com/yam/app/account/presentation/AccountCommandApi.java b/src/main/java/com/yam/app/account/presentation/AccountCommandApi.java index 2bd631a..7582761 100644 --- a/src/main/java/com/yam/app/account/presentation/AccountCommandApi.java +++ b/src/main/java/com/yam/app/account/presentation/AccountCommandApi.java @@ -2,6 +2,8 @@ package com.yam.app.account.presentation; import com.yam.app.account.application.AccountFacade; import com.yam.app.account.infrastructure.SessionManager; +import com.yam.app.common.Authentication; +import com.yam.app.common.AuthenticationPrincipal; import java.net.URI; import javax.servlet.http.HttpSession; import javax.validation.Valid; @@ -11,6 +13,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -65,4 +68,12 @@ public final class AccountCommandApi { session.removePrincipal(); return ResponseEntity.ok().build(); } + + @PatchMapping("/api/accounts/update") + public ResponseEntity update( + @RequestBody @Valid UpdateAccountCommand command, + @AuthenticationPrincipal Authentication authentication) { + accountFacade.update(command, authentication); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/yam/app/account/presentation/UpdateAccountCommand.java b/src/main/java/com/yam/app/account/presentation/UpdateAccountCommand.java new file mode 100644 index 0000000..74a0bd0 --- /dev/null +++ b/src/main/java/com/yam/app/account/presentation/UpdateAccountCommand.java @@ -0,0 +1,21 @@ +package com.yam.app.account.presentation; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import lombok.Data; + +@Data +public final class UpdateAccountCommand { + + @NotBlank + @Pattern(regexp = "^[A-Za-z1-9~!@#$%^&*()+|=]{8,12}$", + message = "Please enter the password in English, numbers, " + + "and special characters within 8-12 digits.") + private String password; + + @NotBlank + private String nickname; + + @NotBlank + private String image; +} diff --git a/src/test/java/com/yam/app/account/presentation/AccountApiUri.java b/src/test/java/com/yam/app/account/presentation/AccountApiUri.java index 3ff2f69..2ca4996 100644 --- a/src/test/java/com/yam/app/account/presentation/AccountApiUri.java +++ b/src/test/java/com/yam/app/account/presentation/AccountApiUri.java @@ -7,8 +7,7 @@ public final class AccountApiUri { public static final String LOGIN = "/api/accounts/login"; public static final String FIND_INFO = "/api/accounts/me"; public static final String LOGOUT = "/api/accounts/logout"; - - public static final String UNAUTHORIZED_REQUEST = "/api/accounts/error/UnauthorizedRequest"; + public static final String UPDATE = "/api/accounts/update"; private AccountApiUri() { } diff --git a/src/test/java/com/yam/app/account/presentation/AccountCommandApiTests.java b/src/test/java/com/yam/app/account/presentation/AccountCommandApiTests.java index c96bd34..5c88e40 100644 --- a/src/test/java/com/yam/app/account/presentation/AccountCommandApiTests.java +++ b/src/test/java/com/yam/app/account/presentation/AccountCommandApiTests.java @@ -4,8 +4,10 @@ import static com.yam.app.account.presentation.AccountApiUri.EMAIL_CONFIRM; import static com.yam.app.account.presentation.AccountApiUri.LOGIN; import static com.yam.app.account.presentation.AccountApiUri.LOGOUT; import static com.yam.app.account.presentation.AccountApiUri.REGISTER; +import static com.yam.app.account.presentation.AccountApiUri.UPDATE; import static org.mockito.Mockito.doThrow; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -24,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -46,6 +49,81 @@ final class AccountCommandApiTests { .andExpect(jsonPath("$.message").value("Invalid argument")); } + @Nested + @DisplayName("회원 정보 수정 HTTP API") + class UpdateAccountApi { + + @Test + @DisplayName("인증되지 않은 사용자가 정보 변경을 요청하면 401 에러를 반환한다.") + void not_authentication_update_command() throws Exception { + var command = new UpdateAccountCommand(); + command.setImage("temp.png"); + command.setNickname("jiwon"); + command.setPassword("password!2"); + + //Act + final var actions = mockMvc.perform(patch(UPDATE) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(command)) + ); + + //Assert + actions + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.data").doesNotExist()) + .andExpect(jsonPath("$.message").value("Unauthorized request")); + } + + @ParameterizedTest + @ValueSource(strings = {"1", "a", "1a234567890123456"}) + @DisplayName("인증된 사용자의 요청 Body 비밀번호 형식이 맞지 않은 경우 400 에러를 반환한다.") + void http_json_password_is_invalid(String args) throws Exception { + // Arrange + var session = new MockHttpSession(); + var command = new UpdateAccountCommand(); + command.setImage("temp.png"); + command.setNickname("jiwon"); + command.setPassword(args); + + // Act + final var actions = mockMvc.perform(patch(UPDATE) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(command)) + .session(session) + ); + + // Assert + assertThatInvalidArgumentError(actions); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("인증된 사용자의 요청 Body 정보가 null 혹은 empty인 경우 400 에러를 반환한다.") + void http_json_value_is_empty_or_null(String args) throws Exception { + //Arrange + var session = new MockHttpSession(); + var command = new UpdateAccountCommand(); + command.setNickname(args); + command.setPassword(args); + command.setImage(args); + + //Act + final var actions = mockMvc.perform(patch(UPDATE) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(command)) + .session(session) + ); + + //Assert + assertThatInvalidArgumentError(actions); + } + + } + @Nested @DisplayName("로그인 HTTP API") class LoginApi {