Update bean validation

This commit is contained in:
xushanchuan 2021-03-03 15:26:48 +08:00
parent 1d6e4af94a
commit 3602ebdc81
No known key found for this signature in database
GPG Key ID: 44D23C44E00838D6
9 changed files with 964 additions and 706 deletions

View File

@ -1,19 +1,28 @@
package io.spring.api; package io.spring.api;
import com.fasterxml.jackson.annotation.JsonRootName; 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.ArticleQueryService;
import io.spring.application.Page;
import io.spring.core.article.Article; import io.spring.core.article.Article;
import io.spring.core.article.ArticleRepository; import io.spring.core.article.ArticleRepository;
import io.spring.core.user.User; 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.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; 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.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; 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.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.HashMap;
@RestController @RestController
@RequestMapping(path = "/articles") @RequestMapping(path = "/articles")
public class ArticlesApi { public class ArticlesApi {
private ArticleRepository articleRepository; private ArticleRepository articleRepository;
private ArticleQueryService articleQueryService; private ArticleQueryService articleQueryService;
@Autowired @Autowired
public ArticlesApi(ArticleRepository articleRepository, ArticleQueryService articleQueryService) { public ArticlesApi(ArticleRepository articleRepository, ArticleQueryService articleQueryService) {
this.articleRepository = articleRepository; this.articleRepository = articleRepository;
this.articleQueryService = articleQueryService; this.articleQueryService = articleQueryService;
} }
@PostMapping @PostMapping
public ResponseEntity createArticle(@Valid @RequestBody NewArticleParam newArticleParam, public ResponseEntity createArticle(
BindingResult bindingResult, @Valid @RequestBody NewArticleParam newArticleParam, @AuthenticationPrincipal User user) {
@AuthenticationPrincipal User user) { Article article =
if (bindingResult.hasErrors()) { new Article(
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(
newArticleParam.getTitle(), newArticleParam.getTitle(),
newArticleParam.getDescription(), newArticleParam.getDescription(),
newArticleParam.getBody(), newArticleParam.getBody(),
newArticleParam.getTagList(), newArticleParam.getTagList(),
user.getId()); user.getId());
articleRepository.save(article); articleRepository.save(article);
return ResponseEntity.ok(new HashMap<String, Object>() {{ return ResponseEntity.ok(
new HashMap<String, Object>() {
{
put("article", articleQueryService.findById(article.getId(), user).get()); put("article", articleQueryService.findById(article.getId(), user).get());
}}); }
} });
}
@GetMapping(path = "feed") @GetMapping(path = "feed")
public ResponseEntity getFeed(@RequestParam(value = "offset", defaultValue = "0") int offset, public ResponseEntity getFeed(
@RequestParam(value = "limit", defaultValue = "20") int limit, @RequestParam(value = "offset", defaultValue = "0") int offset,
@AuthenticationPrincipal User user) { @RequestParam(value = "limit", defaultValue = "20") int limit,
return ResponseEntity.ok(articleQueryService.findUserFeed(user, new Page(offset, limit))); @AuthenticationPrincipal User user) {
} return ResponseEntity.ok(articleQueryService.findUserFeed(user, new Page(offset, limit)));
}
@GetMapping @GetMapping
public ResponseEntity getArticles(@RequestParam(value = "offset", defaultValue = "0") int offset, public ResponseEntity getArticles(
@RequestParam(value = "limit", defaultValue = "20") int limit, @RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "tag", required = false) String tag, @RequestParam(value = "limit", defaultValue = "20") int limit,
@RequestParam(value = "favorited", required = false) String favoritedBy, @RequestParam(value = "tag", required = false) String tag,
@RequestParam(value = "author", required = false) String author, @RequestParam(value = "favorited", required = false) String favoritedBy,
@AuthenticationPrincipal User user) { @RequestParam(value = "author", required = false) String author,
return ResponseEntity.ok(articleQueryService.findRecentArticles(tag, author, favoritedBy, new Page(offset, limit), user)); @AuthenticationPrincipal User user) {
} return ResponseEntity.ok(
articleQueryService.findRecentArticles(
tag, author, favoritedBy, new Page(offset, limit), user));
}
} }
@Getter @Getter
@JsonRootName("article") @JsonRootName("article")
@NoArgsConstructor @NoArgsConstructor
class NewArticleParam { class NewArticleParam {
@NotBlank(message = "can't be empty") @NotBlank(message = "can't be empty")
private String title; @DuplicatedArticleConstraint
@NotBlank(message = "can't be empty") private String title;
private String description;
@NotBlank(message = "can't be empty") @NotBlank(message = "can't be empty")
private String body; private String description;
private String[] tagList;
} @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<? extends Payload>[] payload() default {};
}
class DuplicatedArticleValidator
implements ConstraintValidator<DuplicatedArticleConstraint, String> {
@Autowired private ArticleQueryService articleQueryService;
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return !articleQueryService.findBySlug(Article.toSlug(value), null).isPresent();
}
}

View File

@ -1,24 +1,26 @@
package io.spring.api; package io.spring.api;
import com.fasterxml.jackson.annotation.JsonRootName; import com.fasterxml.jackson.annotation.JsonRootName;
import io.spring.api.exception.InvalidRequestException;
import io.spring.api.exception.NoAuthorizationException; import io.spring.api.exception.NoAuthorizationException;
import io.spring.api.exception.ResourceNotFoundException; 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.CommentQueryService;
import io.spring.application.data.CommentData;
import io.spring.core.article.Article; import io.spring.core.article.Article;
import io.spring.core.article.ArticleRepository; import io.spring.core.article.ArticleRepository;
import io.spring.core.comment.Comment; import io.spring.core.comment.Comment;
import io.spring.core.comment.CommentRepository; import io.spring.core.comment.CommentRepository;
import io.spring.core.service.AuthorizationService;
import io.spring.core.user.User; 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.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; 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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; 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.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController @RestController
@RequestMapping(path = "/articles/{slug}/comments") @RequestMapping(path = "/articles/{slug}/comments")
public class CommentsApi { public class CommentsApi {
private ArticleRepository articleRepository; private ArticleRepository articleRepository;
private CommentRepository commentRepository; private CommentRepository commentRepository;
private CommentQueryService commentQueryService; private CommentQueryService commentQueryService;
@Autowired @Autowired
public CommentsApi(ArticleRepository articleRepository, public CommentsApi(
CommentRepository commentRepository, ArticleRepository articleRepository,
CommentQueryService commentQueryService) { CommentRepository commentRepository,
this.articleRepository = articleRepository; CommentQueryService commentQueryService) {
this.commentRepository = commentRepository; this.articleRepository = articleRepository;
this.commentQueryService = commentQueryService; this.commentRepository = commentRepository;
} this.commentQueryService = commentQueryService;
}
@PostMapping @PostMapping
public ResponseEntity<?> createComment(@PathVariable("slug") String slug, public ResponseEntity<?> createComment(
@AuthenticationPrincipal User user, @PathVariable("slug") String slug,
@Valid @RequestBody NewCommentParam newCommentParam, @AuthenticationPrincipal User user,
BindingResult bindingResult) { @Valid @RequestBody NewCommentParam newCommentParam) {
Article article = findArticle(slug); Article article = findArticle(slug);
if (bindingResult.hasErrors()) { Comment comment = new Comment(newCommentParam.getBody(), user.getId(), article.getId());
throw new InvalidRequestException(bindingResult); commentRepository.save(comment);
} return ResponseEntity.status(201)
Comment comment = new Comment(newCommentParam.getBody(), user.getId(), article.getId()); .body(commentResponse(commentQueryService.findById(comment.getId(), user).get()));
commentRepository.save(comment); }
return ResponseEntity.status(201).body(commentResponse(commentQueryService.findById(comment.getId(), user).get()));
}
@GetMapping @GetMapping
public ResponseEntity getComments(@PathVariable("slug") String slug, public ResponseEntity getComments(
@AuthenticationPrincipal User user) { @PathVariable("slug") String slug, @AuthenticationPrincipal User user) {
Article article = findArticle(slug); Article article = findArticle(slug);
List<CommentData> comments = commentQueryService.findByArticleId(article.getId(), user); List<CommentData> comments = commentQueryService.findByArticleId(article.getId(), user);
return ResponseEntity.ok(new HashMap<String, Object>() {{ return ResponseEntity.ok(
new HashMap<String, Object>() {
{
put("comments", comments); put("comments", comments);
}}); }
} });
}
@RequestMapping(path = "{id}", method = RequestMethod.DELETE) @RequestMapping(path = "{id}", method = RequestMethod.DELETE)
public ResponseEntity deleteComment(@PathVariable("slug") String slug, public ResponseEntity deleteComment(
@PathVariable("id") String commentId, @PathVariable("slug") String slug,
@AuthenticationPrincipal User user) { @PathVariable("id") String commentId,
Article article = findArticle(slug); @AuthenticationPrincipal User user) {
return commentRepository.findById(article.getId(), commentId).map(comment -> { Article article = findArticle(slug);
if (!AuthorizationService.canWriteComment(user, article, comment)) { return commentRepository
.findById(article.getId(), commentId)
.map(
comment -> {
if (!AuthorizationService.canWriteComment(user, article, comment)) {
throw new NoAuthorizationException(); throw new NoAuthorizationException();
} }
commentRepository.remove(comment); commentRepository.remove(comment);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
}).orElseThrow(ResourceNotFoundException::new); })
} .orElseThrow(ResourceNotFoundException::new);
}
private Article findArticle(String slug) { private Article findArticle(String slug) {
return articleRepository.findBySlug(slug).map(article -> article).orElseThrow(ResourceNotFoundException::new); return articleRepository
} .findBySlug(slug)
.map(article -> article)
.orElseThrow(ResourceNotFoundException::new);
}
private Map<String, Object> commentResponse(CommentData commentData) { private Map<String, Object> commentResponse(CommentData commentData) {
return new HashMap<String, Object>() {{ return new HashMap<String, Object>() {
put("comment", commentData); {
}}; put("comment", commentData);
} }
};
}
} }
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
@JsonRootName("comment") @JsonRootName("comment")
class NewCommentParam { class NewCommentParam {
@NotBlank(message = "can't be empty") @NotBlank(message = "can't be empty")
private String body; private String body;
} }

View File

@ -1,19 +1,28 @@
package io.spring.api; package io.spring.api;
import com.fasterxml.jackson.annotation.JsonRootName; import com.fasterxml.jackson.annotation.JsonRootName;
import io.spring.api.exception.InvalidRequestException;
import io.spring.application.UserQueryService; import io.spring.application.UserQueryService;
import io.spring.application.data.UserWithToken;
import io.spring.application.data.UserData; import io.spring.application.data.UserData;
import io.spring.application.data.UserWithToken;
import io.spring.core.user.User; import io.spring.core.user.User;
import io.spring.core.user.UserRepository; 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.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import javax.validation.constraints.Email;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; 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.GetMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@RestController @RestController
@RequestMapping(path = "/user") @RequestMapping(path = "/user")
public class CurrentUserApi { public class CurrentUserApi {
private UserQueryService userQueryService;
private UserRepository userRepository;
@Autowired private UserQueryService userQueryService;
public CurrentUserApi(UserQueryService userQueryService, UserRepository userRepository) { private UserService userService;
this.userQueryService = userQueryService;
this.userRepository = userRepository; @Autowired
} public CurrentUserApi(UserQueryService userQueryService, UserService userService) {
this.userQueryService = userQueryService;
@GetMapping this.userService = userService;
public ResponseEntity currentUser(@AuthenticationPrincipal User currentUser, }
@RequestHeader(value = "Authorization") String authorization) {
UserData userData = userQueryService.findById(currentUser.getId()).get(); @GetMapping
return ResponseEntity.ok(userResponse( public ResponseEntity currentUser(
new UserWithToken(userData, authorization.split(" ")[1]) @AuthenticationPrincipal User currentUser,
)); @RequestHeader(value = "Authorization") String authorization) {
} UserData userData = userQueryService.findById(currentUser.getId()).get();
return ResponseEntity.ok(
@PutMapping userResponse(new UserWithToken(userData, authorization.split(" ")[1])));
public ResponseEntity updateProfile(@AuthenticationPrincipal User currentUser, }
@RequestHeader("Authorization") String token,
@Valid @RequestBody UpdateUserParam updateUserParam, @PutMapping
BindingResult bindingResult) { public ResponseEntity updateProfile(
if (bindingResult.hasErrors()) { @AuthenticationPrincipal User currentUser,
throw new InvalidRequestException(bindingResult); @RequestHeader("Authorization") String token,
} @Valid @RequestBody UpdateUserParam updateUserParam) {
checkUniquenessOfUsernameAndEmail(currentUser, updateUserParam, bindingResult);
userService.updateUser(new UpdateUserCommand(currentUser, updateUserParam));
currentUser.update( UserData userData = userQueryService.findById(currentUser.getId()).get();
updateUserParam.getEmail(), return ResponseEntity.ok(userResponse(new UserWithToken(userData, token.split(" ")[1])));
updateUserParam.getUsername(), }
updateUserParam.getPassword(),
updateUserParam.getBio(), private Map<String, Object> userResponse(UserWithToken userWithToken) {
updateUserParam.getImage()); return new HashMap<String, Object>() {
userRepository.save(currentUser); {
UserData userData = userQueryService.findById(currentUser.getId()).get(); put("user", userWithToken);
return ResponseEntity.ok(userResponse( }
new UserWithToken(userData, token.split(" ")[1]) };
)); }
} }
private void checkUniquenessOfUsernameAndEmail(User currentUser, UpdateUserParam updateUserParam, BindingResult bindingResult) { @Validated
if (!"".equals(updateUserParam.getUsername())) { @Service
Optional<User> byUsername = userRepository.findByUsername(updateUserParam.getUsername()); class UserService {
if (byUsername.isPresent() && !byUsername.get().equals(currentUser)) {
bindingResult.rejectValue("username", "DUPLICATED", "username already exist"); private UserRepository userRepository;
}
} @Autowired
public UserService(UserRepository userRepository) {
if (!"".equals(updateUserParam.getEmail())) { this.userRepository = userRepository;
Optional<User> byEmail = userRepository.findByEmail(updateUserParam.getEmail()); }
if (byEmail.isPresent() && !byEmail.get().equals(currentUser)) {
bindingResult.rejectValue("email", "DUPLICATED", "email already exist"); public void updateUser(@Valid UpdateUserCommand command) {
} User user = command.getTargetUser();
} UpdateUserParam updateUserParam = command.getParam();
user.update(
if (bindingResult.hasErrors()) { updateUserParam.getEmail(),
throw new InvalidRequestException(bindingResult); updateUserParam.getUsername(),
} updateUserParam.getPassword(),
} updateUserParam.getBio(),
updateUserParam.getImage());
private Map<String, Object> userResponse(UserWithToken userWithToken) { userRepository.save(user);
return new HashMap<String, Object>() {{ }
put("user", userWithToken); }
}};
@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<UpdateUserConstraint, UpdateUserCommand> {
@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 @Getter
@JsonRootName("user") @JsonRootName("user")
@NoArgsConstructor @NoArgsConstructor
class UpdateUserParam { class UpdateUserParam {
@Email(message = "should be an email")
private String email = ""; @Email(message = "should be an email")
private String password = ""; private String email = "";
private String username = "";
private String bio = ""; private String password = "";
private String image = ""; private String username = "";
private String bio = "";
private String image = "";
} }

View File

@ -1,125 +1,162 @@
package io.spring.api; package io.spring.api;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
import com.fasterxml.jackson.annotation.JsonRootName; 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.UserQueryService;
import io.spring.application.data.UserWithToken;
import io.spring.application.data.UserData; import io.spring.application.data.UserData;
import io.spring.application.data.UserWithToken;
import io.spring.core.service.JwtService; import io.spring.core.service.JwtService;
import io.spring.core.user.EncryptService; import io.spring.core.user.EncryptService;
import io.spring.core.user.User; import io.spring.core.user.User;
import io.spring.core.user.UserRepository; import io.spring.core.user.UserRepository;
import lombok.Getter; import java.lang.annotation.Retention;
import lombok.NoArgsConstructor; 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.Email;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; 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 @RestController
public class UsersApi { public class UsersApi {
private UserRepository userRepository; private UserRepository userRepository;
private UserQueryService userQueryService; private UserQueryService userQueryService;
private String defaultImage; private String defaultImage;
private EncryptService encryptService; private EncryptService encryptService;
private JwtService jwtService; private JwtService jwtService;
@Autowired @Autowired
public UsersApi(UserRepository userRepository, public UsersApi(
UserQueryService userQueryService, UserRepository userRepository,
EncryptService encryptService, UserQueryService userQueryService,
@Value("${image.default}") String defaultImage, EncryptService encryptService,
JwtService jwtService) { @Value("${image.default}") String defaultImage,
this.userRepository = userRepository; JwtService jwtService) {
this.userQueryService = userQueryService; this.userRepository = userRepository;
this.encryptService = encryptService; this.userQueryService = userQueryService;
this.defaultImage = defaultImage; this.encryptService = encryptService;
this.jwtService = jwtService; this.defaultImage = defaultImage;
} this.jwtService = jwtService;
}
@RequestMapping(path = "/users", method = POST) @RequestMapping(path = "/users", method = POST)
public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam, BindingResult bindingResult) { public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam) {
checkInput(registerParam, bindingResult); User user =
new User(
User user = new User(
registerParam.getEmail(), registerParam.getEmail(),
registerParam.getUsername(), registerParam.getUsername(),
encryptService.encrypt(registerParam.getPassword()), encryptService.encrypt(registerParam.getPassword()),
"", "",
defaultImage); defaultImage);
userRepository.save(user); userRepository.save(user);
UserData userData = userQueryService.findById(user.getId()).get(); UserData userData = userQueryService.findById(user.getId()).get();
return ResponseEntity.status(201).body(userResponse(new UserWithToken(userData, jwtService.toToken(user)))); 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<User> 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) { private Map<String, Object> userResponse(UserWithToken userWithToken) {
if (bindingResult.hasErrors()) { return new HashMap<String, Object>() {
throw new InvalidRequestException(bindingResult); {
} put("user", userWithToken);
if (userRepository.findByUsername(registerParam.getUsername()).isPresent()) { }
bindingResult.rejectValue("username", "DUPLICATED", "duplicated username"); };
} }
}
if (userRepository.findByEmail(registerParam.getEmail()).isPresent()) { @Constraint(validatedBy = DuplicatedEmailValidator.class)
bindingResult.rejectValue("email", "DUPLICATED", "duplicated email"); @Retention(RetentionPolicy.RUNTIME)
} @interface DuplicatedEmailConstraint {
String message() default "duplicated email";
if (bindingResult.hasErrors()) { Class<?>[] groups() default {};
throw new InvalidRequestException(bindingResult);
}
}
@RequestMapping(path = "/users/login", method = POST) Class<? extends Payload>[] payload() default {};
public ResponseEntity userLogin(@Valid @RequestBody LoginParam loginParam, BindingResult bindingResult) { }
Optional<User> 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);
}
}
private Map<String, Object> userResponse(UserWithToken userWithToken) { class DuplicatedEmailValidator implements ConstraintValidator<DuplicatedEmailConstraint, String> {
return new HashMap<String, Object>() {{
put("user", userWithToken); @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<? extends Payload>[] payload() default {};
}
class DuplicatedUsernameValidator
implements ConstraintValidator<DuplicatedUsernameConstraint, String> {
@Autowired private UserRepository userRepository;
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return (value == null || value.isEmpty()) || !userRepository.findByUsername(value).isPresent();
}
} }
@Getter @Getter
@JsonRootName("user") @JsonRootName("user")
@NoArgsConstructor @NoArgsConstructor
class LoginParam { class LoginParam {
@NotBlank(message = "can't be empty") @NotBlank(message = "can't be empty")
@Email(message = "should be an email") @Email(message = "should be an email")
private String email; private String email;
@NotBlank(message = "can't be empty")
private String password; @NotBlank(message = "can't be empty")
private String password;
} }
@Getter @Getter
@JsonRootName("user") @JsonRootName("user")
@NoArgsConstructor @NoArgsConstructor
class RegisterParam { class RegisterParam {
@NotBlank(message = "can't be empty") @NotBlank(message = "can't be empty")
@Email(message = "should be an email") @Email(message = "should be an email")
private String email; @DuplicatedEmailConstraint
@NotBlank(message = "can't be empty") private String email;
private String username;
@NotBlank(message = "can't be empty") @NotBlank(message = "can't be empty")
private String password; @DuplicatedUsernameConstraint
private String username;
@NotBlank(message = "can't be empty")
private String password;
} }

View File

@ -1,37 +1,109 @@
package io.spring.api.exception; 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.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; 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.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 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 @RestControllerAdvice
public class CustomizeExceptionHandler extends ResponseEntityExceptionHandler { public class CustomizeExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({InvalidRequestException.class}) @ExceptionHandler({InvalidRequestException.class})
public ResponseEntity<Object> handleInvalidRequest(RuntimeException e, WebRequest request) { public ResponseEntity<Object> handleInvalidRequest(RuntimeException e, WebRequest request) {
InvalidRequestException ire = (InvalidRequestException) e; InvalidRequestException ire = (InvalidRequestException) e;
List<FieldErrorResource> errorResources = ire.getErrors().getFieldErrors().stream().map(fieldError -> List<FieldErrorResource> errorResources =
new FieldErrorResource( ire.getErrors().getFieldErrors().stream()
fieldError.getObjectName(), .map(
fieldError.getField(), fieldError ->
fieldError.getCode(), new FieldErrorResource(
fieldError.getDefaultMessage())).collect(Collectors.toList()); 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(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON); 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<Object> handleInvalidAuthentication(
InvalidAuthenticationException e, WebRequest request) {
return ResponseEntity.status(UNPROCESSABLE_ENTITY)
.body(
new HashMap<String, Object>() {
{
put("message", e.getMessage());
}
});
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException e,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
List<FieldErrorResource> 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<FieldErrorResource> 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));
}
}
} }

View File

@ -0,0 +1,8 @@
package io.spring.api.exception;
public class InvalidAuthenticationException extends RuntimeException {
public InvalidAuthenticationException() {
super("invalid email or password");
}
}

View File

@ -1,5 +1,12 @@
package io.spring.api; 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.restassured.module.mockmvc.RestAssuredMockMvc;
import io.spring.JacksonCustomizations; import io.spring.JacksonCustomizations;
import io.spring.api.security.WebSecurityConfig; 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.application.data.ProfileData;
import io.spring.core.article.Article; import io.spring.core.article.Article;
import io.spring.core.article.ArticleRepository; 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.joda.time.DateTime;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; 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.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc; 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}) @WebMvcTest({ArticlesApi.class})
@Import({WebSecurityConfig.class, JacksonCustomizations.class}) @Import({WebSecurityConfig.class, JacksonCustomizations.class})
public class ArticlesApiTest extends TestWithCurrentUser { public class ArticlesApiTest extends TestWithCurrentUser {
@Autowired @Autowired private MockMvc mvc;
private MockMvc mvc;
@MockBean @MockBean private ArticleRepository articleRepository;
private ArticleRepository articleRepository;
@MockBean @MockBean private ArticleQueryService articleQueryService;
private ArticleQueryService articleQueryService;
@Override @Override
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
RestAssuredMockMvc.mockMvc(mvc); RestAssuredMockMvc.mockMvc(mvc);
} }
@Test @Test
public void should_create_article_success() throws Exception { public void should_create_article_success() throws Exception {
String title = "How to train your dragon"; String title = "How to train your dragon";
String slug = "how-to-train-your-dragon"; String slug = "how-to-train-your-dragon";
String description = "Ever wonder how?"; String description = "Ever wonder how?";
String body = "You have to believe"; String body = "You have to believe";
String[] tagList = {"reactjs", "angularjs", "dragons"}; String[] tagList = {"reactjs", "angularjs", "dragons"};
Map<String, Object> param = prepareParam(title, description, body, tagList); Map<String, Object> param = prepareParam(title, description, body, tagList);
ArticleData articleData = new ArticleData( ArticleData articleData =
new ArticleData(
"123", "123",
slug, slug,
title, title,
@ -71,59 +67,60 @@ public class ArticlesApiTest extends TestWithCurrentUser {
Arrays.asList(tagList), Arrays.asList(tagList),
new ProfileData("userid", user.getUsername(), user.getBio(), user.getImage(), false)); 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() given()
.contentType("application/json") .contentType("application/json")
.header("Authorization", "Token " + token) .header("Authorization", "Token " + token)
.body(param) .body(param)
.when() .when()
.post("/articles") .post("/articles")
.then() .then()
.statusCode(200) .statusCode(200)
.body("article.title", equalTo(title)) .body("article.title", equalTo(title))
.body("article.favorited", equalTo(false)) .body("article.favorited", equalTo(false))
.body("article.body", equalTo(body)) .body("article.body", equalTo(body))
.body("article.favoritesCount", equalTo(0)) .body("article.favoritesCount", equalTo(0))
.body("article.author.username", equalTo(user.getUsername())) .body("article.author.username", equalTo(user.getUsername()))
.body("article.author.id", equalTo(null)); .body("article.author.id", equalTo(null));
verify(articleRepository).save(any()); verify(articleRepository).save(any());
} }
@Test @Test
public void should_get_error_message_with_wrong_parameter() throws Exception { public void should_get_error_message_with_wrong_parameter() throws Exception {
String title = "How to train your dragon"; String title = "How to train your dragon";
String description = "Ever wonder how?"; String description = "Ever wonder how?";
String body = ""; String body = "";
String[] tagList = {"reactjs", "angularjs", "dragons"}; String[] tagList = {"reactjs", "angularjs", "dragons"};
Map<String, Object> param = prepareParam(title, description, body, tagList); Map<String, Object> param = prepareParam(title, description, body, tagList);
given() given()
.contentType("application/json") .contentType("application/json")
.header("Authorization", "Token " + token) .header("Authorization", "Token " + token)
.body(param) .body(param)
.when() .when()
.post("/articles") .post("/articles")
.prettyPeek() .prettyPeek()
.then() .then()
.statusCode(422) .statusCode(422)
.body("errors.body[0]", equalTo("can't be empty")); .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<String, Object> param = prepareParam(title, description, body, tagList);
@Test ArticleData articleData =
public void should_get_error_message_with_duplicated_title() { new ArticleData(
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<String, Object> param = prepareParam(title, description, body, tagList);
ArticleData articleData = new ArticleData(
"123", "123",
slug, slug,
title, title,
@ -136,29 +133,37 @@ public class ArticlesApiTest extends TestWithCurrentUser {
Arrays.asList(tagList), Arrays.asList(tagList),
new ProfileData("userid", user.getUsername(), user.getBio(), user.getImage(), false)); 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() given()
.contentType("application/json") .contentType("application/json")
.header("Authorization", "Token " + token) .header("Authorization", "Token " + token)
.body(param) .body(param)
.when() .when()
.post("/articles") .post("/articles")
.then() .prettyPeek()
.statusCode(422); .then()
.statusCode(422);
}
} private HashMap<String, Object> prepareParam(
final String title, final String description, final String body, final String[] tagList) {
private HashMap<String, Object> prepareParam(final String title, final String description, final String body, final String[] tagList) { return new HashMap<String, Object>() {
return new HashMap<String, Object>() {{ {
put("article", new HashMap<String, Object>() {{ put(
"article",
new HashMap<String, Object>() {
{
put("title", title); put("title", title);
put("description", description); put("description", description);
put("body", body); put("body", body);
put("tagList", tagList); put("tagList", tagList);
}}); }
}}; });
} }
} };
}
}

View File

@ -1,161 +1,176 @@
package io.spring.api; 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 io.restassured.module.mockmvc.RestAssuredMockMvc.given;
import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsEqual.equalTo;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when; 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) @WebMvcTest(CurrentUserApi.class)
@Import({WebSecurityConfig.class, JacksonCustomizations.class}) @Import({
WebSecurityConfig.class,
JacksonCustomizations.class,
UserService.class,
ValidationAutoConfiguration.class
})
public class CurrentUserApiTest extends TestWithCurrentUser { public class CurrentUserApiTest extends TestWithCurrentUser {
@Autowired @Autowired private MockMvc mvc;
private MockMvc mvc;
@MockBean @MockBean private UserQueryService userQueryService;
private UserQueryService userQueryService;
@Override @Override
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
RestAssuredMockMvc.mockMvc(mvc); RestAssuredMockMvc.mockMvc(mvc);
} }
@Test @Test
public void should_get_current_user_with_token() throws Exception { public void should_get_current_user_with_token() throws Exception {
when(userQueryService.findById(any())).thenReturn(Optional.of(userData)); when(userQueryService.findById(any())).thenReturn(Optional.of(userData));
given() given()
.header("Authorization", "Token " + token) .header("Authorization", "Token " + token)
.contentType("application/json") .contentType("application/json")
.when() .when()
.get("/user") .get("/user")
.then() .then()
.statusCode(200) .statusCode(200)
.body("user.email", equalTo(email)) .body("user.email", equalTo(email))
.body("user.username", equalTo(username)) .body("user.username", equalTo(username))
.body("user.bio", equalTo("")) .body("user.bio", equalTo(""))
.body("user.image", equalTo(defaultAvatar)) .body("user.image", equalTo(defaultAvatar))
.body("user.token", equalTo(token)); .body("user.token", equalTo(token));
} }
@Test @Test
public void should_get_401_without_token() throws Exception { public void should_get_401_without_token() throws Exception {
given() given().contentType("application/json").when().get("/user").then().statusCode(401);
.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 @Test
public void should_get_401_with_invalid_token() throws Exception { public void should_update_current_user_profile() throws Exception {
String invalidToken = "asdfasd"; String newEmail = "newemail@example.com";
when(jwtService.getSubFromToken(eq(invalidToken))).thenReturn(Optional.empty()); String newBio = "updated";
given() String newUsername = "newusernamee";
.contentType("application/json")
.header("Authorization", "Token " + invalidToken)
.when()
.get("/user")
.then()
.statusCode(401);
}
@Test Map<String, Object> param =
public void should_update_current_user_profile() throws Exception { new HashMap<String, Object>() {
String newEmail = "newemail@example.com"; {
String newBio = "updated"; put(
String newUsername = "newusernamee"; "user",
new HashMap<String, Object>() {
{
put("email", newEmail);
put("bio", newBio);
put("username", newUsername);
}
});
}
};
Map<String, Object> param = new HashMap<String, Object>() {{ when(userRepository.findByUsername(eq(newUsername))).thenReturn(Optional.empty());
put("user", new HashMap<String, Object>() {{ 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<String, Object> 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<String, Object> prepareUpdateParam(
final String newEmail, final String newBio, final String newUsername) {
return new HashMap<String, Object>() {
{
put(
"user",
new HashMap<String, Object>() {
{
put("email", newEmail); put("email", newEmail);
put("bio", newBio); put("bio", newBio);
put("username", newUsername); put("username", newUsername);
}}); }
}}; });
}
};
}
when(userRepository.findByUsername(eq(newUsername))).thenReturn(Optional.empty()); @Test
when(userRepository.findByEmail(eq(newEmail))).thenReturn(Optional.empty()); public void should_get_401_if_not_login() throws Exception {
given()
when(userQueryService.findById(eq(user.getId()))).thenReturn(Optional.of(userData)); .contentType("application/json")
.body(
given() new HashMap<String, Object>() {
.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<String, Object> 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<String, Object> prepareUpdateParam(final String newEmail, final String newBio, final String newUsername) {
return new HashMap<String, Object>() {{
put("user", new HashMap<String, Object>() {{
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<String, Object>() {{
put("user", new HashMap<String, Object>()); put("user", new HashMap<String, Object>());
}}) }
.when() })
.put("/user") .when()
.then().statusCode(401); .put("/user")
} .then()
.statusCode(401);
}
} }

View File

@ -1,5 +1,12 @@
package io.spring.api; 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.restassured.module.mockmvc.RestAssuredMockMvc;
import io.spring.JacksonCustomizations; import io.spring.JacksonCustomizations;
import io.spring.api.security.WebSecurityConfig; 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.core.user.UserRepository;
import io.spring.infrastructure.mybatis.readservice.UserReadService; import io.spring.infrastructure.mybatis.readservice.UserReadService;
import io.spring.infrastructure.service.NaiveEncryptService; 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.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; 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.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc; 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) @RunWith(SpringRunner.class)
@WebMvcTest(UsersApi.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 { public class UsersApiTest {
@Autowired @Autowired private MockMvc mvc;
private MockMvc mvc;
@MockBean @MockBean private UserRepository userRepository;
private UserRepository userRepository;
@MockBean @MockBean private JwtService jwtService;
private JwtService jwtService;
@MockBean @MockBean private UserReadService userReadService;
private UserReadService userReadService; private String defaultAvatar;
private String defaultAvatar;
@Before
public void setUp() throws Exception {
RestAssuredMockMvc.mockMvc(mvc);
defaultAvatar = "https://static.productionready.io/images/smiley-cyrus.jpg";
}
@Before @Test
public void setUp() throws Exception { public void should_create_user_success() throws Exception {
RestAssuredMockMvc.mockMvc(mvc); String email = "john@jacob.com";
defaultAvatar = "https://static.productionready.io/images/smiley-cyrus.jpg"; String username = "johnjacob";
}
@Test when(jwtService.toToken(any())).thenReturn("123");
public void should_create_user_success() throws Exception { User user = new User(email, username, "123", "", defaultAvatar);
String email = "john@jacob.com"; UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar);
String username = "johnjacob"; when(userReadService.findById(any())).thenReturn(userData);
when(jwtService.toToken(any())).thenReturn("123"); when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty());
User user = new User(email, username, "123", "", defaultAvatar); when(userRepository.findByEmail(eq(email))).thenReturn(Optional.empty());
UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar);
when(userReadService.findById(any())).thenReturn(userData);
when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty()); Map<String, Object> param = prepareRegisterParameter(email, username);
when(userRepository.findByEmail(eq(email))).thenReturn(Optional.empty());
Map<String, Object> 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() verify(userRepository).save(any());
.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()); @Test
} public void should_show_error_message_for_blank_username() throws Exception {
@Test String email = "john@jacob.com";
public void should_show_error_message_for_blank_username() throws Exception { String username = "";
String email = "john@jacob.com"; Map<String, Object> param = prepareRegisterParameter(email, username);
String username = "";
Map<String, Object> 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() @Test
.contentType("application/json") public void should_show_error_message_for_invalid_email() throws Exception {
.body(param) String email = "johnxjacob.com";
.when() String username = "johnjacob";
.post("/users")
.then()
.statusCode(422)
.body("errors.username[0]", equalTo("can't be empty"));
}
@Test Map<String, Object> param = prepareRegisterParameter(email, username);
public void should_show_error_message_for_invalid_email() throws Exception {
String email = "johnxjacob.com";
String username = "johnjacob";
Map<String, Object> 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() @Test
.contentType("application/json") public void should_show_error_for_duplicated_username() throws Exception {
.body(param) String email = "john@jacob.com";
.when() String username = "johnjacob";
.post("/users")
.then()
.statusCode(422)
.body("errors.email[0]", equalTo("should be an email"));
} when(userRepository.findByUsername(eq(username)))
.thenReturn(Optional.of(new User(email, username, "123", "bio", "")));
when(userRepository.findByEmail(any())).thenReturn(Optional.empty());
@Test Map<String, Object> param = prepareRegisterParameter(email, username);
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( given()
email, username, "123", "bio", "" .contentType("application/json")
))); .body(param)
when(userRepository.findByEmail(any())).thenReturn(Optional.empty()); .when()
.post("/users")
.prettyPeek()
.then()
.statusCode(422)
.body("errors.username[0]", equalTo("duplicated username"));
}
Map<String, Object> param = prepareRegisterParameter(email, username); @Test
public void should_show_error_for_duplicated_email() throws Exception {
String email = "john@jacob.com";
String username = "johnjacob2";
given() when(userRepository.findByEmail(eq(email)))
.contentType("application/json") .thenReturn(Optional.of(new User(email, username, "123", "bio", "")));
.body(param)
.when()
.post("/users")
.then()
.statusCode(422)
.body("errors.username[0]", equalTo("duplicated username"));
}
@Test when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty());
public void should_show_error_for_duplicated_email() throws Exception {
String email = "john@jacob.com";
String username = "johnjacob2";
when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(new User( Map<String, Object> param = prepareRegisterParameter(email, username);
email, username, "123", "bio", ""
)));
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<String, Object> param = prepareRegisterParameter(email, username); private HashMap<String, Object> prepareRegisterParameter(
final String email, final String username) {
given() return new HashMap<String, Object>() {
.contentType("application/json") {
.body(param) put(
.when() "user",
.post("/users") new HashMap<String, Object>() {
.then() {
.statusCode(422)
.body("errors.email[0]", equalTo("duplicated email"));
}
private HashMap<String, Object> prepareRegisterParameter(final String email, final String username) {
return new HashMap<String, Object>() {{
put("user", new HashMap<String, Object>() {{
put("email", email); put("email", email);
put("password", "johnnyjacob"); put("password", "johnnyjacob");
put("username", username); put("username", username);
}}); }
}}; });
} }
};
}
@Test @Test
public void should_login_success() throws Exception { public void should_login_success() throws Exception {
String email = "john@jacob.com"; String email = "john@jacob.com";
String username = "johnjacob2"; String username = "johnjacob2";
String password = "123"; String password = "123";
User user = new User(email, username, password, "", defaultAvatar); User user = new User(email, username, password, "", defaultAvatar);
UserData userData = new UserData("123", email, username, "", defaultAvatar); UserData userData = new UserData("123", email, username, "", defaultAvatar);
when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user)); when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user));
when(userReadService.findByUsername(eq(username))).thenReturn(userData); when(userReadService.findByUsername(eq(username))).thenReturn(userData);
when(userReadService.findById(eq(user.getId()))).thenReturn(userData); when(userReadService.findById(eq(user.getId()))).thenReturn(userData);
when(jwtService.toToken(any())).thenReturn("123"); when(jwtService.toToken(any())).thenReturn("123");
Map<String, Object> param = new HashMap<String, Object>() {{ Map<String, Object> param =
put("user", new HashMap<String, Object>() {{ new HashMap<String, Object>() {
put("email", email); {
put("password", password); put(
}}); "user",
}}; new HashMap<String, Object>() {
{
put("email", email);
put("password", password);
}
});
}
};
given() given()
.contentType("application/json") .contentType("application/json")
.body(param) .body(param)
.when() .when()
.post("/users/login") .post("/users/login")
.then() .then()
.statusCode(200) .statusCode(200)
.body("user.email", equalTo(email)) .body("user.email", equalTo(email))
.body("user.username", equalTo(username)) .body("user.username", equalTo(username))
.body("user.bio", equalTo("")) .body("user.bio", equalTo(""))
.body("user.image", equalTo(defaultAvatar)) .body("user.image", equalTo(defaultAvatar))
.body("user.token", equalTo("123"));; .body("user.token", equalTo("123"));
} ;
}
@Test @Test
public void should_fail_login_with_wrong_password() throws Exception { public void should_fail_login_with_wrong_password() throws Exception {
String email = "john@jacob.com"; String email = "john@jacob.com";
String username = "johnjacob2"; String username = "johnjacob2";
String password = "123"; String password = "123";
User user = new User(email, username, password, "", defaultAvatar); User user = new User(email, username, password, "", defaultAvatar);
UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar); UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar);
when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user)); when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user));
when(userReadService.findByUsername(eq(username))).thenReturn(userData); when(userReadService.findByUsername(eq(username))).thenReturn(userData);
Map<String, Object> param = new HashMap<String, Object>() {{ Map<String, Object> param =
put("user", new HashMap<String, Object>() {{ new HashMap<String, Object>() {
put("email", email); {
put("password", "123123"); put(
}}); "user",
}}; new HashMap<String, Object>() {
{
put("email", email);
put("password", "123123");
}
});
}
};
given() given()
.contentType("application/json") .contentType("application/json")
.body(param) .body(param)
.when() .when()
.post("/users/login") .post("/users/login")
.then() .prettyPeek()
.statusCode(422) .then()
.body("errors.password[0]", equalTo("invalid email or password")); .statusCode(422)
} .body("message", equalTo("invalid email or password"));
}
} }