From 3602ebdc81fe58dfd93333135a778511bf1a3dd4 Mon Sep 17 00:00:00 2001 From: xushanchuan Date: Wed, 3 Mar 2021 15:26:48 +0800 Subject: [PATCH] Update bean validation --- src/main/java/io/spring/api/ArticlesApi.java | 141 ++++--- src/main/java/io/spring/api/CommentsApi.java | 137 ++++--- .../java/io/spring/api/CurrentUserApi.java | 217 ++++++---- src/main/java/io/spring/api/UsersApi.java | 195 +++++---- .../exception/CustomizeExceptionHandler.java | 108 ++++- .../InvalidAuthenticationException.java | 8 + .../java/io/spring/api/ArticlesApiTest.java | 203 +++++----- .../io/spring/api/CurrentUserApiTest.java | 285 ++++++------- src/test/java/io/spring/api/UsersApiTest.java | 376 +++++++++--------- 9 files changed, 964 insertions(+), 706 deletions(-) create mode 100644 src/main/java/io/spring/api/exception/InvalidAuthenticationException.java diff --git a/src/main/java/io/spring/api/ArticlesApi.java b/src/main/java/io/spring/api/ArticlesApi.java index 3f33cc1..c69092a 100644 --- a/src/main/java/io/spring/api/ArticlesApi.java +++ b/src/main/java/io/spring/api/ArticlesApi.java @@ -1,19 +1,28 @@ package io.spring.api; import com.fasterxml.jackson.annotation.JsonRootName; -import io.spring.api.exception.InvalidRequestException; -import io.spring.application.Page; import io.spring.application.ArticleQueryService; +import io.spring.application.Page; import io.spring.core.article.Article; import io.spring.core.article.ArticleRepository; import io.spring.core.user.User; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.HashMap; +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.Payload; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; import lombok.Getter; import lombok.NoArgsConstructor; -import javax.validation.constraints.NotBlank; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -21,73 +30,95 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; -import java.util.HashMap; - @RestController @RequestMapping(path = "/articles") public class ArticlesApi { - private ArticleRepository articleRepository; - private ArticleQueryService articleQueryService; + private ArticleRepository articleRepository; + private ArticleQueryService articleQueryService; - @Autowired - public ArticlesApi(ArticleRepository articleRepository, ArticleQueryService articleQueryService) { - this.articleRepository = articleRepository; - this.articleQueryService = articleQueryService; - } + @Autowired + public ArticlesApi(ArticleRepository articleRepository, ArticleQueryService articleQueryService) { + this.articleRepository = articleRepository; + this.articleQueryService = articleQueryService; + } - @PostMapping - public ResponseEntity createArticle(@Valid @RequestBody NewArticleParam newArticleParam, - BindingResult bindingResult, - @AuthenticationPrincipal User user) { - if (bindingResult.hasErrors()) { - throw new InvalidRequestException(bindingResult); - } - - if (articleQueryService.findBySlug(Article.toSlug(newArticleParam.getTitle()), null).isPresent()) { - bindingResult.rejectValue("title", "DUPLICATED", "article name exists"); - throw new InvalidRequestException(bindingResult); - } - - Article article = new Article( + @PostMapping + public ResponseEntity createArticle( + @Valid @RequestBody NewArticleParam newArticleParam, @AuthenticationPrincipal User user) { + Article article = + new Article( newArticleParam.getTitle(), newArticleParam.getDescription(), newArticleParam.getBody(), newArticleParam.getTagList(), user.getId()); - articleRepository.save(article); - return ResponseEntity.ok(new HashMap() {{ + articleRepository.save(article); + return ResponseEntity.ok( + new HashMap() { + { put("article", articleQueryService.findById(article.getId(), user).get()); - }}); - } + } + }); + } - @GetMapping(path = "feed") - public ResponseEntity getFeed(@RequestParam(value = "offset", defaultValue = "0") int offset, - @RequestParam(value = "limit", defaultValue = "20") int limit, - @AuthenticationPrincipal User user) { - return ResponseEntity.ok(articleQueryService.findUserFeed(user, new Page(offset, limit))); - } + @GetMapping(path = "feed") + public ResponseEntity getFeed( + @RequestParam(value = "offset", defaultValue = "0") int offset, + @RequestParam(value = "limit", defaultValue = "20") int limit, + @AuthenticationPrincipal User user) { + return ResponseEntity.ok(articleQueryService.findUserFeed(user, new Page(offset, limit))); + } - @GetMapping - public ResponseEntity getArticles(@RequestParam(value = "offset", defaultValue = "0") int offset, - @RequestParam(value = "limit", defaultValue = "20") int limit, - @RequestParam(value = "tag", required = false) String tag, - @RequestParam(value = "favorited", required = false) String favoritedBy, - @RequestParam(value = "author", required = false) String author, - @AuthenticationPrincipal User user) { - return ResponseEntity.ok(articleQueryService.findRecentArticles(tag, author, favoritedBy, new Page(offset, limit), user)); - } + @GetMapping + public ResponseEntity getArticles( + @RequestParam(value = "offset", defaultValue = "0") int offset, + @RequestParam(value = "limit", defaultValue = "20") int limit, + @RequestParam(value = "tag", required = false) String tag, + @RequestParam(value = "favorited", required = false) String favoritedBy, + @RequestParam(value = "author", required = false) String author, + @AuthenticationPrincipal User user) { + return ResponseEntity.ok( + articleQueryService.findRecentArticles( + tag, author, favoritedBy, new Page(offset, limit), user)); + } } @Getter @JsonRootName("article") @NoArgsConstructor class NewArticleParam { - @NotBlank(message = "can't be empty") - private String title; - @NotBlank(message = "can't be empty") - private String description; - @NotBlank(message = "can't be empty") - private String body; - private String[] tagList; -} \ No newline at end of file + @NotBlank(message = "can't be empty") + @DuplicatedArticleConstraint + private String title; + + @NotBlank(message = "can't be empty") + private String description; + + @NotBlank(message = "can't be empty") + private String body; + + private String[] tagList; +} + +@Documented +@Constraint(validatedBy = DuplicatedArticleValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +@interface DuplicatedArticleConstraint { + String message() default "article name exists"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} + +class DuplicatedArticleValidator + implements ConstraintValidator { + + @Autowired private ArticleQueryService articleQueryService; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return !articleQueryService.findBySlug(Article.toSlug(value), null).isPresent(); + } +} diff --git a/src/main/java/io/spring/api/CommentsApi.java b/src/main/java/io/spring/api/CommentsApi.java index ce38bd8..6af6372 100644 --- a/src/main/java/io/spring/api/CommentsApi.java +++ b/src/main/java/io/spring/api/CommentsApi.java @@ -1,24 +1,26 @@ package io.spring.api; import com.fasterxml.jackson.annotation.JsonRootName; -import io.spring.api.exception.InvalidRequestException; import io.spring.api.exception.NoAuthorizationException; import io.spring.api.exception.ResourceNotFoundException; -import io.spring.core.service.AuthorizationService; -import io.spring.application.data.CommentData; import io.spring.application.CommentQueryService; +import io.spring.application.data.CommentData; import io.spring.core.article.Article; import io.spring.core.article.ArticleRepository; import io.spring.core.comment.Comment; import io.spring.core.comment.CommentRepository; +import io.spring.core.service.AuthorizationService; import io.spring.core.user.User; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; import lombok.Getter; import lombok.NoArgsConstructor; -import javax.validation.constraints.NotBlank; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -27,80 +29,87 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - @RestController @RequestMapping(path = "/articles/{slug}/comments") public class CommentsApi { - private ArticleRepository articleRepository; - private CommentRepository commentRepository; - private CommentQueryService commentQueryService; + private ArticleRepository articleRepository; + private CommentRepository commentRepository; + private CommentQueryService commentQueryService; - @Autowired - public CommentsApi(ArticleRepository articleRepository, - CommentRepository commentRepository, - CommentQueryService commentQueryService) { - this.articleRepository = articleRepository; - this.commentRepository = commentRepository; - this.commentQueryService = commentQueryService; - } + @Autowired + public CommentsApi( + ArticleRepository articleRepository, + CommentRepository commentRepository, + CommentQueryService commentQueryService) { + this.articleRepository = articleRepository; + this.commentRepository = commentRepository; + this.commentQueryService = commentQueryService; + } - @PostMapping - public ResponseEntity createComment(@PathVariable("slug") String slug, - @AuthenticationPrincipal User user, - @Valid @RequestBody NewCommentParam newCommentParam, - BindingResult bindingResult) { - Article article = findArticle(slug); - if (bindingResult.hasErrors()) { - throw new InvalidRequestException(bindingResult); - } - Comment comment = new Comment(newCommentParam.getBody(), user.getId(), article.getId()); - commentRepository.save(comment); - return ResponseEntity.status(201).body(commentResponse(commentQueryService.findById(comment.getId(), user).get())); - } + @PostMapping + public ResponseEntity createComment( + @PathVariable("slug") String slug, + @AuthenticationPrincipal User user, + @Valid @RequestBody NewCommentParam newCommentParam) { + Article article = findArticle(slug); + Comment comment = new Comment(newCommentParam.getBody(), user.getId(), article.getId()); + commentRepository.save(comment); + return ResponseEntity.status(201) + .body(commentResponse(commentQueryService.findById(comment.getId(), user).get())); + } - @GetMapping - public ResponseEntity getComments(@PathVariable("slug") String slug, - @AuthenticationPrincipal User user) { - Article article = findArticle(slug); - List comments = commentQueryService.findByArticleId(article.getId(), user); - return ResponseEntity.ok(new HashMap() {{ + @GetMapping + public ResponseEntity getComments( + @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { + Article article = findArticle(slug); + List comments = commentQueryService.findByArticleId(article.getId(), user); + return ResponseEntity.ok( + new HashMap() { + { put("comments", comments); - }}); - } + } + }); + } - @RequestMapping(path = "{id}", method = RequestMethod.DELETE) - public ResponseEntity deleteComment(@PathVariable("slug") String slug, - @PathVariable("id") String commentId, - @AuthenticationPrincipal User user) { - Article article = findArticle(slug); - return commentRepository.findById(article.getId(), commentId).map(comment -> { - if (!AuthorizationService.canWriteComment(user, article, comment)) { + @RequestMapping(path = "{id}", method = RequestMethod.DELETE) + public ResponseEntity deleteComment( + @PathVariable("slug") String slug, + @PathVariable("id") String commentId, + @AuthenticationPrincipal User user) { + Article article = findArticle(slug); + return commentRepository + .findById(article.getId(), commentId) + .map( + comment -> { + if (!AuthorizationService.canWriteComment(user, article, comment)) { throw new NoAuthorizationException(); - } - commentRepository.remove(comment); - return ResponseEntity.noContent().build(); - }).orElseThrow(ResourceNotFoundException::new); - } + } + commentRepository.remove(comment); + return ResponseEntity.noContent().build(); + }) + .orElseThrow(ResourceNotFoundException::new); + } - private Article findArticle(String slug) { - return articleRepository.findBySlug(slug).map(article -> article).orElseThrow(ResourceNotFoundException::new); - } + private Article findArticle(String slug) { + return articleRepository + .findBySlug(slug) + .map(article -> article) + .orElseThrow(ResourceNotFoundException::new); + } - private Map commentResponse(CommentData commentData) { - return new HashMap() {{ - put("comment", commentData); - }}; - } + private Map commentResponse(CommentData commentData) { + return new HashMap() { + { + put("comment", commentData); + } + }; + } } @Getter @NoArgsConstructor @JsonRootName("comment") class NewCommentParam { - @NotBlank(message = "can't be empty") - private String body; + @NotBlank(message = "can't be empty") + private String body; } diff --git a/src/main/java/io/spring/api/CurrentUserApi.java b/src/main/java/io/spring/api/CurrentUserApi.java index 7de1775..e4e98cc 100644 --- a/src/main/java/io/spring/api/CurrentUserApi.java +++ b/src/main/java/io/spring/api/CurrentUserApi.java @@ -1,19 +1,28 @@ package io.spring.api; import com.fasterxml.jackson.annotation.JsonRootName; -import io.spring.api.exception.InvalidRequestException; import io.spring.application.UserQueryService; -import io.spring.application.data.UserWithToken; import io.spring.application.data.UserData; +import io.spring.application.data.UserWithToken; import io.spring.core.user.User; import io.spring.core.user.UserRepository; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.Map; +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.Valid; +import javax.validation.constraints.Email; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import javax.validation.constraints.Email; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.BindingResult; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -21,90 +30,140 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - @RestController @RequestMapping(path = "/user") public class CurrentUserApi { - private UserQueryService userQueryService; - private UserRepository userRepository; - @Autowired - public CurrentUserApi(UserQueryService userQueryService, UserRepository userRepository) { - this.userQueryService = userQueryService; - this.userRepository = userRepository; - } - - @GetMapping - public ResponseEntity currentUser(@AuthenticationPrincipal User currentUser, - @RequestHeader(value = "Authorization") String authorization) { - UserData userData = userQueryService.findById(currentUser.getId()).get(); - return ResponseEntity.ok(userResponse( - new UserWithToken(userData, authorization.split(" ")[1]) - )); - } - - @PutMapping - public ResponseEntity updateProfile(@AuthenticationPrincipal User currentUser, - @RequestHeader("Authorization") String token, - @Valid @RequestBody UpdateUserParam updateUserParam, - BindingResult bindingResult) { - if (bindingResult.hasErrors()) { - throw new InvalidRequestException(bindingResult); - } - checkUniquenessOfUsernameAndEmail(currentUser, updateUserParam, bindingResult); - - currentUser.update( - updateUserParam.getEmail(), - updateUserParam.getUsername(), - updateUserParam.getPassword(), - updateUserParam.getBio(), - updateUserParam.getImage()); - userRepository.save(currentUser); - UserData userData = userQueryService.findById(currentUser.getId()).get(); - return ResponseEntity.ok(userResponse( - new UserWithToken(userData, token.split(" ")[1]) - )); - } - - private void checkUniquenessOfUsernameAndEmail(User currentUser, UpdateUserParam updateUserParam, BindingResult bindingResult) { - if (!"".equals(updateUserParam.getUsername())) { - Optional byUsername = userRepository.findByUsername(updateUserParam.getUsername()); - if (byUsername.isPresent() && !byUsername.get().equals(currentUser)) { - bindingResult.rejectValue("username", "DUPLICATED", "username already exist"); - } - } - - if (!"".equals(updateUserParam.getEmail())) { - Optional byEmail = userRepository.findByEmail(updateUserParam.getEmail()); - if (byEmail.isPresent() && !byEmail.get().equals(currentUser)) { - bindingResult.rejectValue("email", "DUPLICATED", "email already exist"); - } - } - - if (bindingResult.hasErrors()) { - throw new InvalidRequestException(bindingResult); - } - } - - private Map userResponse(UserWithToken userWithToken) { - return new HashMap() {{ - put("user", userWithToken); - }}; + private UserQueryService userQueryService; + private UserService userService; + + @Autowired + public CurrentUserApi(UserQueryService userQueryService, UserService userService) { + this.userQueryService = userQueryService; + this.userService = userService; + } + + @GetMapping + public ResponseEntity currentUser( + @AuthenticationPrincipal User currentUser, + @RequestHeader(value = "Authorization") String authorization) { + UserData userData = userQueryService.findById(currentUser.getId()).get(); + return ResponseEntity.ok( + userResponse(new UserWithToken(userData, authorization.split(" ")[1]))); + } + + @PutMapping + public ResponseEntity updateProfile( + @AuthenticationPrincipal User currentUser, + @RequestHeader("Authorization") String token, + @Valid @RequestBody UpdateUserParam updateUserParam) { + + userService.updateUser(new UpdateUserCommand(currentUser, updateUserParam)); + UserData userData = userQueryService.findById(currentUser.getId()).get(); + return ResponseEntity.ok(userResponse(new UserWithToken(userData, token.split(" ")[1]))); + } + + private Map userResponse(UserWithToken userWithToken) { + return new HashMap() { + { + put("user", userWithToken); + } + }; + } +} + +@Validated +@Service +class UserService { + + private UserRepository userRepository; + + @Autowired + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public void updateUser(@Valid UpdateUserCommand command) { + User user = command.getTargetUser(); + UpdateUserParam updateUserParam = command.getParam(); + user.update( + updateUserParam.getEmail(), + updateUserParam.getUsername(), + updateUserParam.getPassword(), + updateUserParam.getBio(), + updateUserParam.getImage()); + userRepository.save(user); + } +} + +@Getter +@AllArgsConstructor +@UpdateUserConstraint +class UpdateUserCommand { + + private User targetUser; + private UpdateUserParam param; +} + +@Constraint(validatedBy = UpdateUserValidator.class) +@Retention(RetentionPolicy.RUNTIME) +@interface UpdateUserConstraint { + + String message() default "invalid update param"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} + +class UpdateUserValidator implements ConstraintValidator { + + @Autowired private UserRepository userRepository; + + @Override + public boolean isValid(UpdateUserCommand value, ConstraintValidatorContext context) { + String inputEmail = value.getParam().getEmail(); + String inputUsername = value.getParam().getUsername(); + final User targetUser = value.getTargetUser(); + + boolean isEmailValid = + userRepository.findByEmail(inputEmail).map(user -> user.equals(targetUser)).orElse(true); + boolean isUsernameValid = + userRepository + .findByUsername(inputUsername) + .map(user -> user.equals(targetUser)) + .orElse(true); + if (isEmailValid && isUsernameValid) { + return true; + } else { + context.disableDefaultConstraintViolation(); + if (!isEmailValid) { + context + .buildConstraintViolationWithTemplate("email already exist") + .addPropertyNode("email") + .addConstraintViolation(); + } + if (!isUsernameValid) { + context + .buildConstraintViolationWithTemplate("username already exist") + .addPropertyNode("username") + .addConstraintViolation(); + } + return false; } + } } @Getter @JsonRootName("user") @NoArgsConstructor class UpdateUserParam { - @Email(message = "should be an email") - private String email = ""; - private String password = ""; - private String username = ""; - private String bio = ""; - private String image = ""; + + @Email(message = "should be an email") + private String email = ""; + + private String password = ""; + private String username = ""; + private String bio = ""; + private String image = ""; } diff --git a/src/main/java/io/spring/api/UsersApi.java b/src/main/java/io/spring/api/UsersApi.java index a6e599f..48dbc0f 100644 --- a/src/main/java/io/spring/api/UsersApi.java +++ b/src/main/java/io/spring/api/UsersApi.java @@ -1,125 +1,162 @@ package io.spring.api; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + import com.fasterxml.jackson.annotation.JsonRootName; -import io.spring.api.exception.InvalidRequestException; +import io.spring.api.exception.InvalidAuthenticationException; import io.spring.application.UserQueryService; -import io.spring.application.data.UserWithToken; import io.spring.application.data.UserData; +import io.spring.application.data.UserWithToken; import io.spring.core.service.JwtService; import io.spring.core.user.EncryptService; import io.spring.core.user.User; import io.spring.core.user.UserRepository; -import lombok.Getter; -import lombok.NoArgsConstructor; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.Payload; +import javax.validation.Valid; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static org.springframework.web.bind.annotation.RequestMethod.POST; - @RestController public class UsersApi { - private UserRepository userRepository; - private UserQueryService userQueryService; - private String defaultImage; - private EncryptService encryptService; - private JwtService jwtService; + private UserRepository userRepository; + private UserQueryService userQueryService; + private String defaultImage; + private EncryptService encryptService; + private JwtService jwtService; - @Autowired - public UsersApi(UserRepository userRepository, - UserQueryService userQueryService, - EncryptService encryptService, - @Value("${image.default}") String defaultImage, - JwtService jwtService) { - this.userRepository = userRepository; - this.userQueryService = userQueryService; - this.encryptService = encryptService; - this.defaultImage = defaultImage; - this.jwtService = jwtService; - } + @Autowired + public UsersApi( + UserRepository userRepository, + UserQueryService userQueryService, + EncryptService encryptService, + @Value("${image.default}") String defaultImage, + JwtService jwtService) { + this.userRepository = userRepository; + this.userQueryService = userQueryService; + this.encryptService = encryptService; + this.defaultImage = defaultImage; + this.jwtService = jwtService; + } - @RequestMapping(path = "/users", method = POST) - public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam, BindingResult bindingResult) { - checkInput(registerParam, bindingResult); - - User user = new User( + @RequestMapping(path = "/users", method = POST) + public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam) { + User user = + new User( registerParam.getEmail(), registerParam.getUsername(), encryptService.encrypt(registerParam.getPassword()), "", defaultImage); - userRepository.save(user); - UserData userData = userQueryService.findById(user.getId()).get(); - return ResponseEntity.status(201).body(userResponse(new UserWithToken(userData, jwtService.toToken(user)))); + userRepository.save(user); + UserData userData = userQueryService.findById(user.getId()).get(); + return ResponseEntity.status(201) + .body(userResponse(new UserWithToken(userData, jwtService.toToken(user)))); + } + + @RequestMapping(path = "/users/login", method = POST) + public ResponseEntity userLogin(@Valid @RequestBody LoginParam loginParam) { + Optional optional = userRepository.findByEmail(loginParam.getEmail()); + if (optional.isPresent() + && encryptService.check(loginParam.getPassword(), optional.get().getPassword())) { + UserData userData = userQueryService.findById(optional.get().getId()).get(); + return ResponseEntity.ok( + userResponse(new UserWithToken(userData, jwtService.toToken(optional.get())))); + } else { + throw new InvalidAuthenticationException(); } + } - private void checkInput(@Valid @RequestBody RegisterParam registerParam, BindingResult bindingResult) { - if (bindingResult.hasErrors()) { - throw new InvalidRequestException(bindingResult); - } - if (userRepository.findByUsername(registerParam.getUsername()).isPresent()) { - bindingResult.rejectValue("username", "DUPLICATED", "duplicated username"); - } + private Map userResponse(UserWithToken userWithToken) { + return new HashMap() { + { + put("user", userWithToken); + } + }; + } +} - if (userRepository.findByEmail(registerParam.getEmail()).isPresent()) { - bindingResult.rejectValue("email", "DUPLICATED", "duplicated email"); - } +@Constraint(validatedBy = DuplicatedEmailValidator.class) +@Retention(RetentionPolicy.RUNTIME) +@interface DuplicatedEmailConstraint { + String message() default "duplicated email"; - if (bindingResult.hasErrors()) { - throw new InvalidRequestException(bindingResult); - } - } + Class[] groups() default {}; - @RequestMapping(path = "/users/login", method = POST) - public ResponseEntity userLogin(@Valid @RequestBody LoginParam loginParam, BindingResult bindingResult) { - Optional optional = userRepository.findByEmail(loginParam.getEmail()); - if (optional.isPresent() && encryptService.check(loginParam.getPassword(), optional.get().getPassword())) { - UserData userData = userQueryService.findById(optional.get().getId()).get(); - return ResponseEntity.ok(userResponse(new UserWithToken(userData, jwtService.toToken(optional.get())))); - } else { - bindingResult.rejectValue("password", "INVALID", "invalid email or password"); - throw new InvalidRequestException(bindingResult); - } - } + Class[] payload() default {}; +} - private Map userResponse(UserWithToken userWithToken) { - return new HashMap() {{ - put("user", userWithToken); - }}; - } +class DuplicatedEmailValidator implements ConstraintValidator { + + @Autowired private UserRepository userRepository; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return (value == null || value.isEmpty()) || !userRepository.findByEmail(value).isPresent(); + } +} + +@Constraint(validatedBy = DuplicatedUsernameValidator.class) +@Retention(RetentionPolicy.RUNTIME) +@interface DuplicatedUsernameConstraint { + String message() default "duplicated username"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} + +class DuplicatedUsernameValidator + implements ConstraintValidator { + + @Autowired private UserRepository userRepository; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return (value == null || value.isEmpty()) || !userRepository.findByUsername(value).isPresent(); + } } @Getter @JsonRootName("user") @NoArgsConstructor class LoginParam { - @NotBlank(message = "can't be empty") - @Email(message = "should be an email") - private String email; - @NotBlank(message = "can't be empty") - private String password; + @NotBlank(message = "can't be empty") + @Email(message = "should be an email") + private String email; + + @NotBlank(message = "can't be empty") + private String password; } @Getter @JsonRootName("user") @NoArgsConstructor class RegisterParam { - @NotBlank(message = "can't be empty") - @Email(message = "should be an email") - private String email; - @NotBlank(message = "can't be empty") - private String username; - @NotBlank(message = "can't be empty") - private String password; + @NotBlank(message = "can't be empty") + @Email(message = "should be an email") + @DuplicatedEmailConstraint + private String email; + + @NotBlank(message = "can't be empty") + @DuplicatedUsernameConstraint + private String username; + + @NotBlank(message = "can't be empty") + private String password; } diff --git a/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java b/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java index c577eee..ade3ff4 100644 --- a/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java +++ b/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java @@ -1,37 +1,109 @@ package io.spring.api.exception; +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import java.util.List; -import java.util.stream.Collectors; - -import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; - @RestControllerAdvice public class CustomizeExceptionHandler extends ResponseEntityExceptionHandler { - @ExceptionHandler({InvalidRequestException.class}) - public ResponseEntity handleInvalidRequest(RuntimeException e, WebRequest request) { - InvalidRequestException ire = (InvalidRequestException) e; + @ExceptionHandler({InvalidRequestException.class}) + public ResponseEntity handleInvalidRequest(RuntimeException e, WebRequest request) { + InvalidRequestException ire = (InvalidRequestException) e; - List errorResources = ire.getErrors().getFieldErrors().stream().map(fieldError -> - new FieldErrorResource( - fieldError.getObjectName(), - fieldError.getField(), - fieldError.getCode(), - fieldError.getDefaultMessage())).collect(Collectors.toList()); + List errorResources = + ire.getErrors().getFieldErrors().stream() + .map( + fieldError -> + new FieldErrorResource( + fieldError.getObjectName(), + fieldError.getField(), + fieldError.getCode(), + fieldError.getDefaultMessage())) + .collect(Collectors.toList()); - ErrorResource error = new ErrorResource(errorResources); + ErrorResource error = new ErrorResource(errorResources); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); - return handleExceptionInternal(e, error, headers, UNPROCESSABLE_ENTITY, request); + return handleExceptionInternal(e, error, headers, UNPROCESSABLE_ENTITY, request); + } + + @ExceptionHandler(InvalidAuthenticationException.class) + public ResponseEntity handleInvalidAuthentication( + InvalidAuthenticationException e, WebRequest request) { + return ResponseEntity.status(UNPROCESSABLE_ENTITY) + .body( + new HashMap() { + { + put("message", e.getMessage()); + } + }); + } + + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + List errorResources = + e.getBindingResult().getFieldErrors().stream() + .map( + fieldError -> + new FieldErrorResource( + fieldError.getObjectName(), + fieldError.getField(), + fieldError.getCode(), + fieldError.getDefaultMessage())) + .collect(Collectors.toList()); + + return ResponseEntity.status(UNPROCESSABLE_ENTITY).body(new ErrorResource(errorResources)); + } + + @ExceptionHandler({ConstraintViolationException.class}) + @ResponseStatus(UNPROCESSABLE_ENTITY) + @ResponseBody + public ErrorResource handleConstraintViolation( + ConstraintViolationException ex, WebRequest request) { + List errors = new ArrayList<>(); + for (ConstraintViolation violation : ex.getConstraintViolations()) { + FieldErrorResource fieldErrorResource = + new FieldErrorResource( + violation.getRootBeanClass().getName(), + getParam(violation.getPropertyPath().toString()), + violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(), + violation.getMessage()); + errors.add(fieldErrorResource); } + + return new ErrorResource(errors); + } + + private String getParam(String s) { + String[] splits = s.split("\\."); + if (splits.length == 1) { + return s; + } else { + return String.join(".", Arrays.copyOfRange(splits, 2, splits.length)); + } + } } diff --git a/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java b/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java new file mode 100644 index 0000000..96af7a8 --- /dev/null +++ b/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java @@ -0,0 +1,8 @@ +package io.spring.api.exception; + +public class InvalidAuthenticationException extends RuntimeException { + + public InvalidAuthenticationException() { + super("invalid email or password"); + } +} diff --git a/src/test/java/io/spring/api/ArticlesApiTest.java b/src/test/java/io/spring/api/ArticlesApiTest.java index dfe89b8..ce5cbca 100644 --- a/src/test/java/io/spring/api/ArticlesApiTest.java +++ b/src/test/java/io/spring/api/ArticlesApiTest.java @@ -1,5 +1,12 @@ package io.spring.api; +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import io.restassured.module.mockmvc.RestAssuredMockMvc; import io.spring.JacksonCustomizations; import io.spring.api.security.WebSecurityConfig; @@ -8,6 +15,10 @@ import io.spring.application.data.ArticleData; import io.spring.application.data.ProfileData; import io.spring.core.article.Article; import io.spring.core.article.ArticleRepository; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import org.joda.time.DateTime; import org.junit.Before; import org.junit.Test; @@ -17,48 +28,33 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @WebMvcTest({ArticlesApi.class}) @Import({WebSecurityConfig.class, JacksonCustomizations.class}) public class ArticlesApiTest extends TestWithCurrentUser { - @Autowired - private MockMvc mvc; + @Autowired private MockMvc mvc; - @MockBean - private ArticleRepository articleRepository; + @MockBean private ArticleRepository articleRepository; - @MockBean - private ArticleQueryService articleQueryService; + @MockBean private ArticleQueryService articleQueryService; - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - RestAssuredMockMvc.mockMvc(mvc); - } + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + RestAssuredMockMvc.mockMvc(mvc); + } - @Test - public void should_create_article_success() throws Exception { - String title = "How to train your dragon"; - String slug = "how-to-train-your-dragon"; - String description = "Ever wonder how?"; - String body = "You have to believe"; - String[] tagList = {"reactjs", "angularjs", "dragons"}; - Map param = prepareParam(title, description, body, tagList); + @Test + public void should_create_article_success() throws Exception { + String title = "How to train your dragon"; + String slug = "how-to-train-your-dragon"; + String description = "Ever wonder how?"; + String body = "You have to believe"; + String[] tagList = {"reactjs", "angularjs", "dragons"}; + Map param = prepareParam(title, description, body, tagList); - ArticleData articleData = new ArticleData( + ArticleData articleData = + new ArticleData( "123", slug, title, @@ -71,59 +67,60 @@ public class ArticlesApiTest extends TestWithCurrentUser { Arrays.asList(tagList), new ProfileData("userid", user.getUsername(), user.getBio(), user.getImage(), false)); - when(articleQueryService.findBySlug(eq(Article.toSlug(title)), any())).thenReturn(Optional.empty()); + when(articleQueryService.findBySlug(eq(Article.toSlug(title)), any())) + .thenReturn(Optional.empty()); - when(articleQueryService.findById(any(), any())).thenReturn(Optional.of(articleData)); + when(articleQueryService.findById(any(), any())).thenReturn(Optional.of(articleData)); - given() - .contentType("application/json") - .header("Authorization", "Token " + token) - .body(param) - .when() - .post("/articles") - .then() - .statusCode(200) - .body("article.title", equalTo(title)) - .body("article.favorited", equalTo(false)) - .body("article.body", equalTo(body)) - .body("article.favoritesCount", equalTo(0)) - .body("article.author.username", equalTo(user.getUsername())) - .body("article.author.id", equalTo(null)); + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/articles") + .then() + .statusCode(200) + .body("article.title", equalTo(title)) + .body("article.favorited", equalTo(false)) + .body("article.body", equalTo(body)) + .body("article.favoritesCount", equalTo(0)) + .body("article.author.username", equalTo(user.getUsername())) + .body("article.author.id", equalTo(null)); - verify(articleRepository).save(any()); - } + verify(articleRepository).save(any()); + } - @Test - public void should_get_error_message_with_wrong_parameter() throws Exception { - String title = "How to train your dragon"; - String description = "Ever wonder how?"; - String body = ""; - String[] tagList = {"reactjs", "angularjs", "dragons"}; - Map param = prepareParam(title, description, body, tagList); + @Test + public void should_get_error_message_with_wrong_parameter() throws Exception { + String title = "How to train your dragon"; + String description = "Ever wonder how?"; + String body = ""; + String[] tagList = {"reactjs", "angularjs", "dragons"}; + Map param = prepareParam(title, description, body, tagList); - given() - .contentType("application/json") - .header("Authorization", "Token " + token) - .body(param) - .when() - .post("/articles") - .prettyPeek() - .then() - .statusCode(422) - .body("errors.body[0]", equalTo("can't be empty")); + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/articles") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.body[0]", equalTo("can't be empty")); + } - } + @Test + public void should_get_error_message_with_duplicated_title() { + String title = "How to train your dragon"; + String slug = "how-to-train-your-dragon"; + String description = "Ever wonder how?"; + String body = "You have to believe"; + String[] tagList = {"reactjs", "angularjs", "dragons"}; + Map param = prepareParam(title, description, body, tagList); - @Test - public void should_get_error_message_with_duplicated_title() { - String title = "How to train your dragon"; - String slug = "how-to-train-your-dragon"; - String description = "Ever wonder how?"; - String body = "You have to believe"; - String[] tagList = {"reactjs", "angularjs", "dragons"}; - Map param = prepareParam(title, description, body, tagList); - - ArticleData articleData = new ArticleData( + ArticleData articleData = + new ArticleData( "123", slug, title, @@ -136,29 +133,37 @@ public class ArticlesApiTest extends TestWithCurrentUser { Arrays.asList(tagList), new ProfileData("userid", user.getUsername(), user.getBio(), user.getImage(), false)); - when(articleQueryService.findBySlug(eq(Article.toSlug(title)), any())).thenReturn(Optional.of(articleData)); + when(articleQueryService.findBySlug(eq(Article.toSlug(title)), any())) + .thenReturn(Optional.of(articleData)); - when(articleQueryService.findById(any(), any())).thenReturn(Optional.of(articleData)); + when(articleQueryService.findById(any(), any())).thenReturn(Optional.of(articleData)); - given() - .contentType("application/json") - .header("Authorization", "Token " + token) - .body(param) - .when() - .post("/articles") - .then() - .statusCode(422); + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/articles") + .prettyPeek() + .then() + .statusCode(422); + } - } - - private HashMap prepareParam(final String title, final String description, final String body, final String[] tagList) { - return new HashMap() {{ - put("article", new HashMap() {{ + private HashMap prepareParam( + final String title, final String description, final String body, final String[] tagList) { + return new HashMap() { + { + put( + "article", + new HashMap() { + { put("title", title); put("description", description); put("body", body); put("tagList", tagList); - }}); - }}; - } -} \ No newline at end of file + } + }); + } + }; + } +} diff --git a/src/test/java/io/spring/api/CurrentUserApiTest.java b/src/test/java/io/spring/api/CurrentUserApiTest.java index 5ae2475..ed44369 100644 --- a/src/test/java/io/spring/api/CurrentUserApiTest.java +++ b/src/test/java/io/spring/api/CurrentUserApiTest.java @@ -1,161 +1,176 @@ package io.spring.api; -import io.restassured.module.mockmvc.RestAssuredMockMvc; -import io.spring.JacksonCustomizations; -import io.spring.api.security.WebSecurityConfig; -import io.spring.application.UserQueryService; -import io.spring.core.user.User; -import org.junit.Before; -import org.junit.Test; -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.context.annotation.Import; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; import static org.hamcrest.core.IsEqual.equalTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import io.spring.JacksonCustomizations; +import io.spring.api.security.WebSecurityConfig; +import io.spring.application.UserQueryService; +import io.spring.core.user.User; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + @WebMvcTest(CurrentUserApi.class) -@Import({WebSecurityConfig.class, JacksonCustomizations.class}) +@Import({ + WebSecurityConfig.class, + JacksonCustomizations.class, + UserService.class, + ValidationAutoConfiguration.class +}) public class CurrentUserApiTest extends TestWithCurrentUser { - @Autowired - private MockMvc mvc; + @Autowired private MockMvc mvc; - @MockBean - private UserQueryService userQueryService; + @MockBean private UserQueryService userQueryService; - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - RestAssuredMockMvc.mockMvc(mvc); - } + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + RestAssuredMockMvc.mockMvc(mvc); + } - @Test - public void should_get_current_user_with_token() throws Exception { - when(userQueryService.findById(any())).thenReturn(Optional.of(userData)); + @Test + public void should_get_current_user_with_token() throws Exception { + when(userQueryService.findById(any())).thenReturn(Optional.of(userData)); - given() - .header("Authorization", "Token " + token) - .contentType("application/json") - .when() - .get("/user") - .then() - .statusCode(200) - .body("user.email", equalTo(email)) - .body("user.username", equalTo(username)) - .body("user.bio", equalTo("")) - .body("user.image", equalTo(defaultAvatar)) - .body("user.token", equalTo(token)); - } + given() + .header("Authorization", "Token " + token) + .contentType("application/json") + .when() + .get("/user") + .then() + .statusCode(200) + .body("user.email", equalTo(email)) + .body("user.username", equalTo(username)) + .body("user.bio", equalTo("")) + .body("user.image", equalTo(defaultAvatar)) + .body("user.token", equalTo(token)); + } - @Test - public void should_get_401_without_token() throws Exception { - given() - .contentType("application/json") - .when() - .get("/user") - .then() - .statusCode(401); + @Test + public void should_get_401_without_token() throws Exception { + given().contentType("application/json").when().get("/user").then().statusCode(401); + } - } + @Test + public void should_get_401_with_invalid_token() throws Exception { + String invalidToken = "asdfasd"; + when(jwtService.getSubFromToken(eq(invalidToken))).thenReturn(Optional.empty()); + given() + .contentType("application/json") + .header("Authorization", "Token " + invalidToken) + .when() + .get("/user") + .then() + .statusCode(401); + } - @Test - public void should_get_401_with_invalid_token() throws Exception { - String invalidToken = "asdfasd"; - when(jwtService.getSubFromToken(eq(invalidToken))).thenReturn(Optional.empty()); - given() - .contentType("application/json") - .header("Authorization", "Token " + invalidToken) - .when() - .get("/user") - .then() - .statusCode(401); - } + @Test + public void should_update_current_user_profile() throws Exception { + String newEmail = "newemail@example.com"; + String newBio = "updated"; + String newUsername = "newusernamee"; - @Test - public void should_update_current_user_profile() throws Exception { - String newEmail = "newemail@example.com"; - String newBio = "updated"; - String newUsername = "newusernamee"; + Map param = + new HashMap() { + { + put( + "user", + new HashMap() { + { + put("email", newEmail); + put("bio", newBio); + put("username", newUsername); + } + }); + } + }; - Map param = new HashMap() {{ - put("user", new HashMap() {{ + when(userRepository.findByUsername(eq(newUsername))).thenReturn(Optional.empty()); + when(userRepository.findByEmail(eq(newEmail))).thenReturn(Optional.empty()); + + when(userQueryService.findById(eq(user.getId()))).thenReturn(Optional.of(userData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .put("/user") + .then() + .statusCode(200); + } + + @Test + public void should_get_error_if_email_exists_when_update_user_profile() throws Exception { + String newEmail = "newemail@example.com"; + String newBio = "updated"; + String newUsername = "newusernamee"; + + Map param = prepareUpdateParam(newEmail, newBio, newUsername); + + when(userRepository.findByEmail(eq(newEmail))) + .thenReturn(Optional.of(new User(newEmail, "username", "123", "", ""))); + when(userRepository.findByUsername(eq(newUsername))).thenReturn(Optional.empty()); + + when(userQueryService.findById(eq(user.getId()))).thenReturn(Optional.of(userData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .put("/user") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.email[0]", equalTo("email already exist")); + } + + private HashMap prepareUpdateParam( + final String newEmail, final String newBio, final String newUsername) { + return new HashMap() { + { + put( + "user", + new HashMap() { + { put("email", newEmail); put("bio", newBio); put("username", newUsername); - }}); - }}; + } + }); + } + }; + } - when(userRepository.findByUsername(eq(newUsername))).thenReturn(Optional.empty()); - when(userRepository.findByEmail(eq(newEmail))).thenReturn(Optional.empty()); - - when(userQueryService.findById(eq(user.getId()))).thenReturn(Optional.of(userData)); - - given() - .contentType("application/json") - .header("Authorization", "Token " + token) - .body(param) - .when() - .put("/user") - .then() - .statusCode(200); - } - - @Test - public void should_get_error_if_email_exists_when_update_user_profile() throws Exception { - String newEmail = "newemail@example.com"; - String newBio = "updated"; - String newUsername = "newusernamee"; - - Map param = prepareUpdateParam(newEmail, newBio, newUsername); - - when(userRepository.findByEmail(eq(newEmail))).thenReturn(Optional.of(new User(newEmail, "username", "123", "", ""))); - when(userRepository.findByUsername(eq(newUsername))).thenReturn(Optional.empty()); - - when(userQueryService.findById(eq(user.getId()))).thenReturn(Optional.of(userData)); - - given() - .contentType("application/json") - .header("Authorization", "Token " + token) - .body(param) - .when() - .put("/user") - .prettyPeek() - .then() - .statusCode(422) - .body("errors.email[0]", equalTo("email already exist")); - - } - - private HashMap prepareUpdateParam(final String newEmail, final String newBio, final String newUsername) { - return new HashMap() {{ - put("user", new HashMap() {{ - put("email", newEmail); - put("bio", newBio); - put("username", newUsername); - }}); - }}; - } - - @Test - public void should_get_401_if_not_login() throws Exception { - given() - .contentType("application/json") - .body(new HashMap() {{ + @Test + public void should_get_401_if_not_login() throws Exception { + given() + .contentType("application/json") + .body( + new HashMap() { + { put("user", new HashMap()); - }}) - .when() - .put("/user") - .then().statusCode(401); - } + } + }) + .when() + .put("/user") + .then() + .statusCode(401); + } } diff --git a/src/test/java/io/spring/api/UsersApiTest.java b/src/test/java/io/spring/api/UsersApiTest.java index e13ef20..934f86e 100644 --- a/src/test/java/io/spring/api/UsersApiTest.java +++ b/src/test/java/io/spring/api/UsersApiTest.java @@ -1,5 +1,12 @@ package io.spring.api; +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import io.restassured.module.mockmvc.RestAssuredMockMvc; import io.spring.JacksonCustomizations; import io.spring.api.security.WebSecurityConfig; @@ -10,6 +17,9 @@ import io.spring.core.user.User; import io.spring.core.user.UserRepository; import io.spring.infrastructure.mybatis.readservice.UserReadService; import io.spring.infrastructure.service.NaiveEncryptService; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -20,224 +30,236 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @RunWith(SpringRunner.class) @WebMvcTest(UsersApi.class) -@Import({WebSecurityConfig.class, UserQueryService.class, NaiveEncryptService.class, JacksonCustomizations.class}) +@Import({ + WebSecurityConfig.class, + UserQueryService.class, + NaiveEncryptService.class, + JacksonCustomizations.class +}) public class UsersApiTest { - @Autowired - private MockMvc mvc; + @Autowired private MockMvc mvc; - @MockBean - private UserRepository userRepository; + @MockBean private UserRepository userRepository; - @MockBean - private JwtService jwtService; + @MockBean private JwtService jwtService; - @MockBean - private UserReadService userReadService; - private String defaultAvatar; + @MockBean private UserReadService userReadService; + private String defaultAvatar; + @Before + public void setUp() throws Exception { + RestAssuredMockMvc.mockMvc(mvc); + defaultAvatar = "https://static.productionready.io/images/smiley-cyrus.jpg"; + } - @Before - public void setUp() throws Exception { - RestAssuredMockMvc.mockMvc(mvc); - defaultAvatar = "https://static.productionready.io/images/smiley-cyrus.jpg"; - } + @Test + public void should_create_user_success() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob"; - @Test - public void should_create_user_success() throws Exception { - String email = "john@jacob.com"; - String username = "johnjacob"; + when(jwtService.toToken(any())).thenReturn("123"); + User user = new User(email, username, "123", "", defaultAvatar); + UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar); + when(userReadService.findById(any())).thenReturn(userData); - when(jwtService.toToken(any())).thenReturn("123"); - User user = new User(email, username, "123", "", defaultAvatar); - UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar); - when(userReadService.findById(any())).thenReturn(userData); + when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty()); + when(userRepository.findByEmail(eq(email))).thenReturn(Optional.empty()); - when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty()); - when(userRepository.findByEmail(eq(email))).thenReturn(Optional.empty()); + Map param = prepareRegisterParameter(email, username); - Map param = prepareRegisterParameter(email, username); + given() + .contentType("application/json") + .body(param) + .when() + .post("/users") + .then() + .statusCode(201) + .body("user.email", equalTo(email)) + .body("user.username", equalTo(username)) + .body("user.bio", equalTo("")) + .body("user.image", equalTo(defaultAvatar)) + .body("user.token", equalTo("123")); - given() - .contentType("application/json") - .body(param) - .when() - .post("/users") - .then() - .statusCode(201) - .body("user.email", equalTo(email)) - .body("user.username", equalTo(username)) - .body("user.bio", equalTo("")) - .body("user.image", equalTo(defaultAvatar)) - .body("user.token", equalTo("123")); + verify(userRepository).save(any()); + } - verify(userRepository).save(any()); - } + @Test + public void should_show_error_message_for_blank_username() throws Exception { - @Test - public void should_show_error_message_for_blank_username() throws Exception { + String email = "john@jacob.com"; + String username = ""; - String email = "john@jacob.com"; - String username = ""; + Map param = prepareRegisterParameter(email, username); - Map param = prepareRegisterParameter(email, username); + given() + .contentType("application/json") + .body(param) + .when() + .post("/users") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.username[0]", equalTo("can't be empty")); + } - given() - .contentType("application/json") - .body(param) - .when() - .post("/users") - .then() - .statusCode(422) - .body("errors.username[0]", equalTo("can't be empty")); - } + @Test + public void should_show_error_message_for_invalid_email() throws Exception { + String email = "johnxjacob.com"; + String username = "johnjacob"; - @Test - public void should_show_error_message_for_invalid_email() throws Exception { - String email = "johnxjacob.com"; - String username = "johnjacob"; + Map param = prepareRegisterParameter(email, username); - Map param = prepareRegisterParameter(email, username); + given() + .contentType("application/json") + .body(param) + .when() + .post("/users") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.email[0]", equalTo("should be an email")); + } - given() - .contentType("application/json") - .body(param) - .when() - .post("/users") - .then() - .statusCode(422) - .body("errors.email[0]", equalTo("should be an email")); + @Test + public void should_show_error_for_duplicated_username() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob"; - } + when(userRepository.findByUsername(eq(username))) + .thenReturn(Optional.of(new User(email, username, "123", "bio", ""))); + when(userRepository.findByEmail(any())).thenReturn(Optional.empty()); - @Test - public void should_show_error_for_duplicated_username() throws Exception { - String email = "john@jacob.com"; - String username = "johnjacob"; + Map param = prepareRegisterParameter(email, username); - when(userRepository.findByUsername(eq(username))).thenReturn(Optional.of(new User( - email, username, "123", "bio", "" - ))); - when(userRepository.findByEmail(any())).thenReturn(Optional.empty()); + given() + .contentType("application/json") + .body(param) + .when() + .post("/users") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.username[0]", equalTo("duplicated username")); + } - Map param = prepareRegisterParameter(email, username); + @Test + public void should_show_error_for_duplicated_email() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob2"; - given() - .contentType("application/json") - .body(param) - .when() - .post("/users") - .then() - .statusCode(422) - .body("errors.username[0]", equalTo("duplicated username")); - } + when(userRepository.findByEmail(eq(email))) + .thenReturn(Optional.of(new User(email, username, "123", "bio", ""))); - @Test - public void should_show_error_for_duplicated_email() throws Exception { - String email = "john@jacob.com"; - String username = "johnjacob2"; + when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty()); - when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(new User( - email, username, "123", "bio", "" - ))); + Map param = prepareRegisterParameter(email, username); - when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty()); + given() + .contentType("application/json") + .body(param) + .when() + .post("/users") + .then() + .statusCode(422) + .body("errors.email[0]", equalTo("duplicated email")); + } - Map param = prepareRegisterParameter(email, username); - - given() - .contentType("application/json") - .body(param) - .when() - .post("/users") - .then() - .statusCode(422) - .body("errors.email[0]", equalTo("duplicated email")); - } - - private HashMap prepareRegisterParameter(final String email, final String username) { - return new HashMap() {{ - put("user", new HashMap() {{ + private HashMap prepareRegisterParameter( + final String email, final String username) { + return new HashMap() { + { + put( + "user", + new HashMap() { + { put("email", email); put("password", "johnnyjacob"); put("username", username); - }}); - }}; - } + } + }); + } + }; + } - @Test - public void should_login_success() throws Exception { - String email = "john@jacob.com"; - String username = "johnjacob2"; - String password = "123"; + @Test + public void should_login_success() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob2"; + String password = "123"; - User user = new User(email, username, password, "", defaultAvatar); - UserData userData = new UserData("123", email, username, "", defaultAvatar); + User user = new User(email, username, password, "", defaultAvatar); + UserData userData = new UserData("123", email, username, "", defaultAvatar); - when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user)); - when(userReadService.findByUsername(eq(username))).thenReturn(userData); - when(userReadService.findById(eq(user.getId()))).thenReturn(userData); - when(jwtService.toToken(any())).thenReturn("123"); + when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user)); + when(userReadService.findByUsername(eq(username))).thenReturn(userData); + when(userReadService.findById(eq(user.getId()))).thenReturn(userData); + when(jwtService.toToken(any())).thenReturn("123"); - Map param = new HashMap() {{ - put("user", new HashMap() {{ - put("email", email); - put("password", password); - }}); - }}; + Map param = + new HashMap() { + { + put( + "user", + new HashMap() { + { + put("email", email); + put("password", password); + } + }); + } + }; - given() - .contentType("application/json") - .body(param) - .when() - .post("/users/login") - .then() - .statusCode(200) - .body("user.email", equalTo(email)) - .body("user.username", equalTo(username)) - .body("user.bio", equalTo("")) - .body("user.image", equalTo(defaultAvatar)) - .body("user.token", equalTo("123"));; - } + given() + .contentType("application/json") + .body(param) + .when() + .post("/users/login") + .then() + .statusCode(200) + .body("user.email", equalTo(email)) + .body("user.username", equalTo(username)) + .body("user.bio", equalTo("")) + .body("user.image", equalTo(defaultAvatar)) + .body("user.token", equalTo("123")); + ; + } - @Test - public void should_fail_login_with_wrong_password() throws Exception { - String email = "john@jacob.com"; - String username = "johnjacob2"; - String password = "123"; + @Test + public void should_fail_login_with_wrong_password() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob2"; + String password = "123"; - User user = new User(email, username, password, "", defaultAvatar); - UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar); + User user = new User(email, username, password, "", defaultAvatar); + UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar); - when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user)); - when(userReadService.findByUsername(eq(username))).thenReturn(userData); + when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user)); + when(userReadService.findByUsername(eq(username))).thenReturn(userData); - Map param = new HashMap() {{ - put("user", new HashMap() {{ - put("email", email); - put("password", "123123"); - }}); - }}; + Map param = + new HashMap() { + { + put( + "user", + new HashMap() { + { + put("email", email); + put("password", "123123"); + } + }); + } + }; - given() - .contentType("application/json") - .body(param) - .when() - .post("/users/login") - .then() - .statusCode(422) - .body("errors.password[0]", equalTo("invalid email or password")); - } + given() + .contentType("application/json") + .body(param) + .when() + .post("/users/login") + .prettyPeek() + .then() + .statusCode(422) + .body("message", equalTo("invalid email or password")); + } }