diff --git a/src/main/java/com/study/realworld/config/auth/JwtAuthenticationFilter.java b/src/main/java/com/study/realworld/config/auth/JwtAuthenticationFilter.java index 2ab46801..6240f481 100644 --- a/src/main/java/com/study/realworld/config/auth/JwtAuthenticationFilter.java +++ b/src/main/java/com/study/realworld/config/auth/JwtAuthenticationFilter.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -27,15 +28,15 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { log.info(DOFILTERMESSAGE, request.getMethod(), request.getRequestURI(), request.getHeader(HttpHeaders.AUTHORIZATION)); - String header = request.getHeader(HttpHeaders.AUTHORIZATION); + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); - if(header != null) { - String tokenPrefix = header.split(" ")[0]; - String accessToken = header.split(" ")[1]; + if(tokenHeader != null && tokenHeader.split(" ").length == 2) { + String tokenPrefix = tokenHeader.split(" ")[0]; + String accessToken = tokenHeader.split(" ")[1]; if("Token".equals(tokenPrefix)) { String email = jwtProvider.getSubjectFromToken(accessToken); - final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(email, accessToken, Collections.emptyList()); + final Authentication authentication = new UsernamePasswordAuthenticationToken(email, accessToken, Collections.emptyList()); SecurityContextHolder.getContext().setAuthentication(authentication); } diff --git a/src/main/java/com/study/realworld/user/domain/User.java b/src/main/java/com/study/realworld/user/domain/User.java index fb34625b..df924fe7 100644 --- a/src/main/java/com/study/realworld/user/domain/User.java +++ b/src/main/java/com/study/realworld/user/domain/User.java @@ -44,14 +44,13 @@ public User(String email, String username, String password, String bio, String i this.image = image; } - public static User of(User user) { - return User.builder() - .email(user.getEmail()) - .username(user.getUsername()) - .password(user.getPassword()) - .bio(user.getBio()) - .image(user.getImage()) - .build(); + public User updateUser(User user) { + if(Objects.nonNull(user.getEmail())) this.email = user.getEmail(); + if(Objects.nonNull(user.getUsername())) this.username = user.getUsername(); + if(Objects.nonNull(user.getPassword())) this.password = user.getPassword(); + if(Objects.nonNull(user.getBio())) this.bio = user.getBio(); + if(Objects.nonNull(user.getImage())) this.image = user.getImage(); + return this; } public void encodePassword(PasswordEncoder passwordEncoder) { diff --git a/src/main/java/com/study/realworld/user/domain/UserRepository.java b/src/main/java/com/study/realworld/user/domain/UserRepository.java index bfeb320c..020bc073 100644 --- a/src/main/java/com/study/realworld/user/domain/UserRepository.java +++ b/src/main/java/com/study/realworld/user/domain/UserRepository.java @@ -6,5 +6,6 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + boolean existsByEmail(String email); } diff --git a/src/main/java/com/study/realworld/user/dto/UserUpdateRequest.java b/src/main/java/com/study/realworld/user/dto/UserUpdateRequest.java new file mode 100644 index 00000000..7cecfef5 --- /dev/null +++ b/src/main/java/com/study/realworld/user/dto/UserUpdateRequest.java @@ -0,0 +1,44 @@ +package com.study.realworld.user.dto; + + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.study.realworld.user.domain.User; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static com.fasterxml.jackson.annotation.JsonTypeInfo.As.WRAPPER_OBJECT; +import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME; + +@JsonTypeName("user") +@JsonTypeInfo(include = WRAPPER_OBJECT, use = NAME) +@Getter +@NoArgsConstructor +public class UserUpdateRequest { + private String email; + private String username; + private String password; + private String bio; + private String image; + + @Builder + public UserUpdateRequest(String email, String username, String password, String bio, String image) { + this.email = email; + this.username = username; + this.password = password; + this.bio = bio; + this.image = image; + } + + public static User toUser(UserUpdateRequest request) { + return User.builder() + .email(request.getEmail()) + .username(request.getUsername()) + .password(request.getPassword()) + .bio(request.getBio()) + .image(request.getImage()) + .build(); + } + +} diff --git a/src/main/java/com/study/realworld/user/service/UserService.java b/src/main/java/com/study/realworld/user/service/UserService.java index f522dd4f..18923e15 100644 --- a/src/main/java/com/study/realworld/user/service/UserService.java +++ b/src/main/java/com/study/realworld/user/service/UserService.java @@ -4,16 +4,18 @@ import com.study.realworld.user.domain.UserRepository; import com.study.realworld.user.dto.UserJoinRequest; import com.study.realworld.user.dto.UserLoginRequest; +import com.study.realworld.user.dto.UserUpdateRequest; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.NoSuchElementException; +import static java.util.Objects.nonNull; + @RequiredArgsConstructor @Transactional(readOnly = true) @Service @@ -36,7 +38,7 @@ public User findByEmail(String email) { .orElseThrow(() -> new NoSuchElementException(email + " not found")); } - @Transactional(propagation = Propagation.NEVER) + @Transactional public User save(UserJoinRequest request) { User user = UserJoinRequest.toUser(request); user.encodePassword(passwordEncoder); @@ -45,7 +47,7 @@ public User save(UserJoinRequest request) { return userRepository.save(user); } - + public User login(UserLoginRequest request) { User user = this.findByEmail(request.getEmail()); @@ -54,6 +56,17 @@ public User login(UserLoginRequest request) { return user; } + @Transactional + public User update(UserUpdateRequest request, String principal) { + validateExistUser(request.getEmail()); + + User requestUser = UserUpdateRequest.toUser(request); + if(nonNull(requestUser.getPassword())) requestUser.encodePassword(passwordEncoder); + + User user = this.findByEmail(principal); + return user.updateUser(requestUser); + } + private void validateDuplicateUser(String email) { if(userRepository.findByEmail(email).isPresent()) { throw new DuplicateKeyException(email + " duplicated email"); @@ -66,4 +79,10 @@ private void validateMatchesPassword(User user, String rawPassword) { } } + private void validateExistUser(String email) { + if(userRepository.existsByEmail(email)) { + throw new DuplicateKeyException(email); + } + } + } diff --git a/src/main/java/com/study/realworld/user/web/UserController.java b/src/main/java/com/study/realworld/user/web/UserController.java index b0905fe3..3b6129cb 100644 --- a/src/main/java/com/study/realworld/user/web/UserController.java +++ b/src/main/java/com/study/realworld/user/web/UserController.java @@ -5,17 +5,14 @@ import com.study.realworld.user.dto.UserJoinRequest; import com.study.realworld.user.dto.UserLoginRequest; import com.study.realworld.user.dto.UserResponse; +import com.study.realworld.user.dto.UserUpdateRequest; import com.study.realworld.user.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import javax.servlet.http.HttpServletRequest; - @Slf4j @RequiredArgsConstructor @RequestMapping("/api") @@ -40,15 +37,23 @@ public ResponseEntity loginUser(@RequestBody UserLoginRequest requ String token = jwtProvider.generateJwtToken(user); return ResponseEntity.ok() - .header(HttpHeaders.AUTHORIZATION, "Token " + token) .body(UserResponse.of(user, token)); } @GetMapping("/user") - public ResponseEntity getUser(HttpServletRequest servletRequest) { - String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - User user = userService.findByEmail(email); - String token = servletRequest.getHeader(HttpHeaders.AUTHORIZATION).split(" ")[1]; + public ResponseEntity getUser(@AuthenticationPrincipal String principal) { + User user = userService.findByEmail(principal); + String token = jwtProvider.generateJwtToken(user); + + return ResponseEntity.ok() + .body(UserResponse.of(user, token)); + } + + @PutMapping("/user") + public ResponseEntity updateUser(@AuthenticationPrincipal String principal, + @RequestBody UserUpdateRequest request) { + User user = userService.update(request, principal); + String token = jwtProvider.generateJwtToken(user); return ResponseEntity.ok() .body(UserResponse.of(user, token)); diff --git a/src/test/java/com/study/realworld/config/auth/JwtProviderTest.java b/src/test/java/com/study/realworld/config/auth/JwtProviderTest.java index 587ebbcf..947da07d 100644 --- a/src/test/java/com/study/realworld/config/auth/JwtProviderTest.java +++ b/src/test/java/com/study/realworld/config/auth/JwtProviderTest.java @@ -1,6 +1,7 @@ package com.study.realworld.config.auth; import com.study.realworld.user.domain.User; +import io.jsonwebtoken.JwtException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -8,6 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; @ExtendWith(MockitoExtension.class) class JwtProviderTest { @@ -15,6 +17,7 @@ class JwtProviderTest { private static final Long ID = 1L; private static final String EMAIL = "email"; private static final String TOKEN = "token"; + private static final String WRONG_TOKEN = "wrong-token"; private static final String USERNAME = "username"; private static final String PASSWORD = "password"; private static final String BIO = "bio"; @@ -52,4 +55,14 @@ void JwtProviderTest() { ); } + @Test + void getClaimsFromToken_exception() { + // given + + // when + + // then + assertThrows(JwtException.class, () -> jwtProvider.getClaimsFromToken(WRONG_TOKEN)); + } + } diff --git a/src/test/java/com/study/realworld/user/service/UserServiceTest.java b/src/test/java/com/study/realworld/user/service/UserServiceTest.java index d3e1e173..f5711eef 100644 --- a/src/test/java/com/study/realworld/user/service/UserServiceTest.java +++ b/src/test/java/com/study/realworld/user/service/UserServiceTest.java @@ -4,7 +4,7 @@ import com.study.realworld.user.domain.UserRepository; import com.study.realworld.user.dto.UserJoinRequest; import com.study.realworld.user.dto.UserLoginRequest; -import org.junit.jupiter.api.Disabled; +import com.study.realworld.user.dto.UserUpdateRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -52,13 +52,13 @@ void findAll() { .image(IMAGE) .build(); + // when when(userRepository.findAll()) .thenReturn(List.of(user)); - // when + // then User result = userService.findAll().get(0); - // then verify(userRepository).findAll(); assertAll( () -> assertEquals(user.getEmail(), result.getEmail()), @@ -80,13 +80,13 @@ void findById() { .image(IMAGE) .build(); + // when when(userRepository.findById(anyLong())) .thenReturn(Optional.ofNullable(user)); - // when + // then User result = userService.findById(ID); - // then verify(userRepository).findById(ID); assertAll( () -> assertEquals(user.getEmail(), result.getEmail()), @@ -108,13 +108,13 @@ void findByEmail() { .image(IMAGE) .build(); + // when when(userRepository.findByEmail(anyString())) .thenReturn(Optional.ofNullable(user)); - // when + // then User result = userService.findByEmail(user.getEmail()); - // then verify(userRepository).findByEmail(user.getEmail()); assertAll( () -> assertEquals(user.getId(), result.getId()), @@ -143,13 +143,13 @@ void save() { .image(IMAGE) .build(); + // when when(userRepository.save(any())) .thenReturn(user); - // when + // then User result = userService.save(request); - // then verify(userRepository).save(user); assertAll( () -> assertEquals(request.getEmail(), result.getEmail()), @@ -157,6 +157,31 @@ void save() { ); } + @Test + void save_validateDuplicateUser_exception() { + // given + final UserJoinRequest request = UserJoinRequest.builder() + .email(EMAIL) + .username(USERNAME) + .password(PASSWORD) + .build(); + + final User user = User.builder() + .email(EMAIL) + .username(USERNAME) + .password(PASSWORD) + .bio(BIO) + .image(IMAGE) + .build(); + + // when + when(userRepository.findByEmail(anyString())) + .thenReturn(Optional.ofNullable(user)); + + // then + assertThrows(DuplicateKeyException.class, () -> userService.save(request)); + } + @Test void login() { // given @@ -173,27 +198,15 @@ void login() { .image(IMAGE) .build(); + // when when(userRepository.findByEmail(anyString())) .thenReturn(Optional.ofNullable(user)); - - /** - * This exception may occur if matchers are combined with raw values: - * //incorrect: - * someMethod(anyObject(), "raw String"); - * When using matchers, all arguments have to be provided by matchers. - * For example: - * //correct: - * someMethod(anyObject(), eq("String by matcher")); - * - * For more info see javadoc for Matchers class. - */ when(passwordEncoder.matches(any(), any())) .thenReturn(true); - // when + // then User result = userService.login(request); - // then assertAll( () -> assertEquals(request.getEmail(), result.getEmail()), () -> assertTrue(result.matchesPassword(request.getPassword(), passwordEncoder)) @@ -201,11 +214,10 @@ void login() { } @Test - void save_validateDuplicateUser_exception() { + void login_validateMatchesPassword_exception() { // given - final UserJoinRequest request = UserJoinRequest.builder() + final UserLoginRequest request = UserLoginRequest.builder() .email(EMAIL) - .username(USERNAME) .password(PASSWORD) .build(); @@ -220,19 +232,21 @@ void save_validateDuplicateUser_exception() { // when when(userRepository.findByEmail(anyString())) .thenReturn(Optional.ofNullable(user)); + // then - assertThrows(DuplicateKeyException.class, () -> userService.save(request)); + assertThrows(NoSuchElementException.class, () -> userService.login(request)); } @Test - void login_validateMatchesPassword_exception() { + void update() { // given - final UserLoginRequest request = UserLoginRequest.builder() - .email(EMAIL) - .password(PASSWORD) + UserUpdateRequest request = UserUpdateRequest.builder() + .email(EMAIL+EMAIL) + .password(PASSWORD+PASSWORD) + .bio(BIO+BIO) .build(); - final User user = User.builder() + User user = User.builder() .email(EMAIL) .username(USERNAME) .password(PASSWORD) @@ -240,13 +254,38 @@ void login_validateMatchesPassword_exception() { .image(IMAGE) .build(); + // when + when(userRepository.existsByEmail(anyString())) + .thenReturn(false); when(userRepository.findByEmail(anyString())) .thenReturn(Optional.ofNullable(user)); + // then + User result = userService.update(request, EMAIL); + + assertAll( + () -> assertEquals(request.getEmail(), result.getEmail()), + () -> assertEquals(user.getUsername(), result.getUsername()), + () -> assertEquals(request.getBio(), result.getBio()), + () -> assertEquals(user.getImage(), result.getImage()) + ); + } + + @Test + void update_validateExistUser_exception() { + // given + UserUpdateRequest request = UserUpdateRequest.builder() + .email(EMAIL+EMAIL) + .password(PASSWORD+PASSWORD) + .bio(BIO+BIO) + .build(); + // when + when(userRepository.existsByEmail(anyString())) + .thenReturn(true); // then - assertThrows(NoSuchElementException.class, () -> userService.login(request)); + assertThrows(DuplicateKeyException.class, () -> userService.update(request, EMAIL)); } } diff --git a/src/test/java/com/study/realworld/user/web/UserControllerTest.java b/src/test/java/com/study/realworld/user/web/UserControllerTest.java index 10d430cf..a9c3a617 100644 --- a/src/test/java/com/study/realworld/user/web/UserControllerTest.java +++ b/src/test/java/com/study/realworld/user/web/UserControllerTest.java @@ -6,6 +6,7 @@ import com.study.realworld.user.dto.UserJoinRequest; import com.study.realworld.user.dto.UserLoginRequest; import com.study.realworld.user.dto.UserResponse; +import com.study.realworld.user.dto.UserUpdateRequest; import com.study.realworld.user.service.UserService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,6 +16,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.MockMvc; @@ -29,13 +31,12 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(controllers = UserController.class -// includeFilters = @ComponentScan.Filter( +// excludeFilters = @ComponentScan.Filter( // type = FilterType.REGEX, pattern = "com.study.realworld.config.auth.*") ) class UserControllerTest { @@ -51,6 +52,9 @@ class UserControllerTest { @MockBean private SecurityContext securityContext; + @MockBean + private Authentication authentication; + @MockBean private JwtProvider jwtProvider; @@ -68,11 +72,11 @@ void setUp() { mvc = MockMvcBuilders .webAppContextSetup(context) .build(); + SecurityContextHolder.clearContext(); } @Test void POST_api_users_joinUser() throws Exception { - // given final String URL = "/api/users"; @@ -98,17 +102,17 @@ void POST_api_users_joinUser() throws Exception { .image(IMAGE) .build(); + // when when(userService.save(any())) .thenReturn(user); - // when + // https://www.infoworld.com/article/3543268/junit-5-tutorial-part-2-unit-testing-spring-mvc-with-junit-5.html?page=3 + // then ResultActions result = mvc.perform(post(URL) .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(request))) .andDo(print()); - // https://www.infoworld.com/article/3543268/junit-5-tutorial-part-2-unit-testing-spring-mvc-with-junit-5.html?page=3 - // then verify(userService).save(any()); result .andExpect(status().isOk()) @@ -121,7 +125,6 @@ void POST_api_users_joinUser() throws Exception { @Test void POST_api_users_login_loginUser() throws Exception { - // given final String URL = "/api/users/login"; @@ -138,18 +141,18 @@ void POST_api_users_login_loginUser() throws Exception { .image(IMAGE) .build(); + // when when(userService.login(any())) .thenReturn(user); when(jwtProvider.generateJwtToken(any())) .thenReturn(TOKEN); - // when + // then ResultActions result = mvc.perform(post(URL) .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(request))) .andDo(print()); - // then verify(userService).login(any()); verify(jwtProvider).generateJwtToken(any()); result @@ -164,7 +167,6 @@ void POST_api_users_login_loginUser() throws Exception { @Test void GET_api_user_getUser() throws Exception { - // given final String URL = "/api/user"; @@ -176,24 +178,23 @@ void GET_api_user_getUser() throws Exception { .image(IMAGE) .build(); - final UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(EMAIL, TOKEN, Collections.emptyList()); - - when(securityContext.getAuthentication()) - .thenReturn(authentication); - SecurityContextHolder.setContext(securityContext); + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(EMAIL, TOKEN, Collections.emptyList())); + // when when(userService.findByEmail(anyString())) .thenReturn(user); + when(jwtProvider.generateJwtToken(any())) + .thenReturn(TOKEN); - // when + // then ResultActions result = mvc.perform(get(URL) .contentType(MediaType.APPLICATION_JSON) .header(HttpHeaders.AUTHORIZATION, "Token " + TOKEN)) .andDo(print()); - // then verify(userService).findByEmail(anyString()); + verify(jwtProvider).generateJwtToken(any()); result .andExpect(status().isOk()) .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)) @@ -204,4 +205,51 @@ void GET_api_user_getUser() throws Exception { .andExpect(jsonPath("$.user.image", is(IMAGE))); } + @Test + void PUT_api_user_updateUser() throws Exception { + // given + final String URL = "/api/user"; + + final UserUpdateRequest request = UserUpdateRequest.builder() + .email(EMAIL+EMAIL) + .bio(BIO+BIO) + .build(); + + final User user = User.builder() + .email(EMAIL+EMAIL) + .username(USERNAME) + .password(PASSWORD) + .bio(BIO+BIO) + .image(IMAGE) + .build(); + + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(EMAIL, TOKEN, Collections.emptyList())); + + // when + when(userService.update(any(), any())) + .thenReturn(user); + when(jwtProvider.generateJwtToken(any())) + .thenReturn(TOKEN); + + // then + ResultActions result = mvc.perform(put(URL) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, "Token " + TOKEN)) + .andDo(print()); + + verify(userService).update(any(), any()); + verify(jwtProvider).generateJwtToken(any()); + result + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.user.email", is(request.getEmail()))) + .andExpect(jsonPath("$.user.token", is(TOKEN))) + .andExpect(jsonPath("$.user.username", is(USERNAME))) + .andExpect(jsonPath("$.user.bio", is(request.getBio()))) + .andExpect(jsonPath("$.user.image", is(IMAGE))); + + } + }