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;
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,9 +30,6 @@ 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 {
@ -37,45 +43,43 @@ public class ArticlesApi {
}
@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(
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<String, Object>() {{
return ResponseEntity.ok(
new HashMap<String, Object>() {
{
put("article", articleQueryService.findById(article.getId(), user).get());
}});
}
});
}
@GetMapping(path = "feed")
public ResponseEntity getFeed(@RequestParam(value = "offset", defaultValue = "0") int offset,
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,
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));
return ResponseEntity.ok(
articleQueryService.findRecentArticles(
tag, author, favoritedBy, new Page(offset, limit), user));
}
}
@ -84,10 +88,37 @@ public class ArticlesApi {
@NoArgsConstructor
class NewArticleParam {
@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<? 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;
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,11 +29,6 @@ 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 {
@ -40,7 +37,8 @@ public class CommentsApi {
private CommentQueryService commentQueryService;
@Autowired
public CommentsApi(ArticleRepository articleRepository,
public CommentsApi(
ArticleRepository articleRepository,
CommentRepository commentRepository,
CommentQueryService commentQueryService) {
this.articleRepository = articleRepository;
@ -49,51 +47,62 @@ public class CommentsApi {
}
@PostMapping
public ResponseEntity<?> createComment(@PathVariable("slug") String slug,
public ResponseEntity<?> createComment(
@PathVariable("slug") String slug,
@AuthenticationPrincipal User user,
@Valid @RequestBody NewCommentParam newCommentParam,
BindingResult bindingResult) {
@Valid @RequestBody NewCommentParam newCommentParam) {
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()));
return ResponseEntity.status(201)
.body(commentResponse(commentQueryService.findById(comment.getId(), user).get()));
}
@GetMapping
public ResponseEntity getComments(@PathVariable("slug") String slug,
@AuthenticationPrincipal User user) {
public ResponseEntity getComments(
@PathVariable("slug") String slug, @AuthenticationPrincipal User user) {
Article article = findArticle(slug);
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);
}});
}
});
}
@RequestMapping(path = "{id}", method = RequestMethod.DELETE)
public ResponseEntity deleteComment(@PathVariable("slug") String slug,
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 -> {
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);
})
.orElseThrow(ResourceNotFoundException::new);
}
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) {
return new HashMap<String, Object>() {{
return new HashMap<String, Object>() {
{
put("comment", commentData);
}};
}
};
}
}

View File

@ -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,79 +30,127 @@ 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;
private UserService userService;
@Autowired
public CurrentUserApi(UserQueryService userQueryService, UserRepository userRepository) {
public CurrentUserApi(UserQueryService userQueryService, UserService userService) {
this.userQueryService = userQueryService;
this.userRepository = userRepository;
this.userService = userService;
}
@GetMapping
public ResponseEntity currentUser(@AuthenticationPrincipal User currentUser,
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])
));
return ResponseEntity.ok(
userResponse(new UserWithToken(userData, authorization.split(" ")[1])));
}
@PutMapping
public ResponseEntity updateProfile(@AuthenticationPrincipal User currentUser,
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);
@Valid @RequestBody UpdateUserParam updateUserParam) {
currentUser.update(
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<String, Object> userResponse(UserWithToken userWithToken) {
return new HashMap<String, Object>() {
{
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(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<User> byUsername = userRepository.findByUsername(updateUserParam.getUsername());
if (byUsername.isPresent() && !byUsername.get().equals(currentUser)) {
bindingResult.rejectValue("username", "DUPLICATED", "username already exist");
userRepository.save(user);
}
}
if (!"".equals(updateUserParam.getEmail())) {
Optional<User> byEmail = userRepository.findByEmail(updateUserParam.getEmail());
if (byEmail.isPresent() && !byEmail.get().equals(currentUser)) {
bindingResult.rejectValue("email", "DUPLICATED", "email already exist");
}
@Getter
@AllArgsConstructor
@UpdateUserConstraint
class UpdateUserCommand {
private User targetUser;
private UpdateUserParam param;
}
if (bindingResult.hasErrors()) {
throw new InvalidRequestException(bindingResult);
}
@Constraint(validatedBy = UpdateUserValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@interface UpdateUserConstraint {
String message() default "invalid update param";
Class[] groups() default {};
Class[] payload() default {};
}
private Map<String, Object> userResponse(UserWithToken userWithToken) {
return new HashMap<String, Object>() {{
put("user", userWithToken);
}};
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;
}
}
}
@ -101,8 +158,10 @@ public class CurrentUserApi {
@JsonRootName("user")
@NoArgsConstructor
class UpdateUserParam {
@Email(message = "should be an email")
private String email = "";
private String password = "";
private String username = "";
private String bio = "";

View File

@ -1,32 +1,36 @@
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 javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
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.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
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.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UsersApi {
@ -37,7 +41,8 @@ public class UsersApi {
private JwtService jwtService;
@Autowired
public UsersApi(UserRepository userRepository,
public UsersApi(
UserRepository userRepository,
UserQueryService userQueryService,
EncryptService encryptService,
@Value("${image.default}") String defaultImage,
@ -50,10 +55,9 @@ public class UsersApi {
}
@RequestMapping(path = "/users", method = POST)
public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam, BindingResult bindingResult) {
checkInput(registerParam, bindingResult);
User user = new User(
public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam) {
User user =
new User(
registerParam.getEmail(),
registerParam.getUsername(),
encryptService.encrypt(registerParam.getPassword()),
@ -61,42 +65,70 @@ public class UsersApi {
defaultImage);
userRepository.save(user);
UserData userData = userQueryService.findById(user.getId()).get();
return ResponseEntity.status(201).body(userResponse(new UserWithToken(userData, jwtService.toToken(user))));
}
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");
}
if (userRepository.findByEmail(registerParam.getEmail()).isPresent()) {
bindingResult.rejectValue("email", "DUPLICATED", "duplicated email");
}
if (bindingResult.hasErrors()) {
throw new InvalidRequestException(bindingResult);
}
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, BindingResult bindingResult) {
public ResponseEntity userLogin(@Valid @RequestBody LoginParam loginParam) {
Optional<User> optional = userRepository.findByEmail(loginParam.getEmail());
if (optional.isPresent() && encryptService.check(loginParam.getPassword(), optional.get().getPassword())) {
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()))));
return ResponseEntity.ok(
userResponse(new UserWithToken(userData, jwtService.toToken(optional.get()))));
} else {
bindingResult.rejectValue("password", "INVALID", "invalid email or password");
throw new InvalidRequestException(bindingResult);
throw new InvalidAuthenticationException();
}
}
private Map<String, Object> userResponse(UserWithToken userWithToken) {
return new HashMap<String, Object>() {{
return new HashMap<String, Object>() {
{
put("user", userWithToken);
}};
}
};
}
}
@Constraint(validatedBy = DuplicatedEmailValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@interface DuplicatedEmailConstraint {
String message() default "duplicated email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class DuplicatedEmailValidator implements ConstraintValidator<DuplicatedEmailConstraint, String> {
@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();
}
}
@ -107,6 +139,7 @@ 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;
}
@ -117,9 +150,13 @@ class LoginParam {
class RegisterParam {
@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;
}

View File

@ -1,18 +1,26 @@
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 {
@ -20,12 +28,16 @@ public class CustomizeExceptionHandler extends ResponseEntityExceptionHandler {
public ResponseEntity<Object> handleInvalidRequest(RuntimeException e, WebRequest request) {
InvalidRequestException ire = (InvalidRequestException) e;
List<FieldErrorResource> errorResources = ire.getErrors().getFieldErrors().stream().map(fieldError ->
List<FieldErrorResource> errorResources =
ire.getErrors().getFieldErrors().stream()
.map(
fieldError ->
new FieldErrorResource(
fieldError.getObjectName(),
fieldError.getField(),
fieldError.getCode(),
fieldError.getDefaultMessage())).collect(Collectors.toList());
fieldError.getDefaultMessage()))
.collect(Collectors.toList());
ErrorResource error = new ErrorResource(errorResources);
@ -34,4 +46,64 @@ public class CustomizeExceptionHandler extends ResponseEntityExceptionHandler {
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;
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,30 +28,14 @@ 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
@ -58,7 +53,8 @@ public class ArticlesApiTest extends TestWithCurrentUser {
String[] tagList = {"reactjs", "angularjs", "dragons"};
Map<String, Object> param = prepareParam(title, description, body, tagList);
ArticleData articleData = new ArticleData(
ArticleData articleData =
new ArticleData(
"123",
slug,
title,
@ -71,7 +67,8 @@ 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));
@ -111,7 +108,6 @@ public class ArticlesApiTest extends TestWithCurrentUser {
.then()
.statusCode(422)
.body("errors.body[0]", equalTo("can't be empty"));
}
@Test
@ -123,7 +119,8 @@ public class ArticlesApiTest extends TestWithCurrentUser {
String[] tagList = {"reactjs", "angularjs", "dragons"};
Map<String, Object> param = prepareParam(title, description, body, tagList);
ArticleData articleData = new ArticleData(
ArticleData articleData =
new ArticleData(
"123",
slug,
title,
@ -136,7 +133,8 @@ 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));
@ -146,19 +144,26 @@ public class ArticlesApiTest extends TestWithCurrentUser {
.body(param)
.when()
.post("/articles")
.prettyPeek()
.then()
.statusCode(422);
}
private HashMap<String, Object> prepareParam(final String title, final String description, final String body, final String[] tagList) {
return new HashMap<String, Object>() {{
put("article", new HashMap<String, Object>() {{
private HashMap<String, Object> prepareParam(
final String title, final String description, final String body, final String[] tagList) {
return new HashMap<String, Object>() {
{
put(
"article",
new HashMap<String, Object>() {
{
put("title", title);
put("description", description);
put("body", body);
put("tagList", tagList);
}});
}};
}
});
}
};
}
}

View File

@ -1,37 +1,40 @@
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
@ -60,13 +63,7 @@ public class CurrentUserApiTest extends TestWithCurrentUser {
@Test
public void should_get_401_without_token() throws Exception {
given()
.contentType("application/json")
.when()
.get("/user")
.then()
.statusCode(401);
given().contentType("application/json").when().get("/user").then().statusCode(401);
}
@Test
@ -88,13 +85,20 @@ public class CurrentUserApiTest extends TestWithCurrentUser {
String newBio = "updated";
String newUsername = "newusernamee";
Map<String, Object> param = new HashMap<String, Object>() {{
put("user", new HashMap<String, Object>() {{
Map<String, Object> param =
new HashMap<String, Object>() {
{
put(
"user",
new HashMap<String, Object>() {
{
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());
@ -119,7 +123,8 @@ public class CurrentUserApiTest extends TestWithCurrentUser {
Map<String, Object> param = prepareUpdateParam(newEmail, newBio, newUsername);
when(userRepository.findByEmail(eq(newEmail))).thenReturn(Optional.of(new User(newEmail, "username", "123", "", "")));
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));
@ -134,28 +139,38 @@ public class CurrentUserApiTest extends TestWithCurrentUser {
.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>() {{
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>() {{
.body(
new HashMap<String, Object>() {
{
put("user", new HashMap<String, Object>());
}})
}
})
.when()
.put("/user")
.then().statusCode(401);
.then()
.statusCode(401);
}
}

View File

@ -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,35 +30,24 @@ 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;
@MockBean private UserReadService userReadService;
private String defaultAvatar;
@Before
public void setUp() throws Exception {
RestAssuredMockMvc.mockMvc(mvc);
@ -99,6 +98,7 @@ public class UsersApiTest {
.body(param)
.when()
.post("/users")
.prettyPeek()
.then()
.statusCode(422)
.body("errors.username[0]", equalTo("can't be empty"));
@ -116,10 +116,10 @@ public class UsersApiTest {
.body(param)
.when()
.post("/users")
.prettyPeek()
.then()
.statusCode(422)
.body("errors.email[0]", equalTo("should be an email"));
}
@Test
@ -127,9 +127,8 @@ public class UsersApiTest {
String email = "john@jacob.com";
String username = "johnjacob";
when(userRepository.findByUsername(eq(username))).thenReturn(Optional.of(new User(
email, username, "123", "bio", ""
)));
when(userRepository.findByUsername(eq(username)))
.thenReturn(Optional.of(new User(email, username, "123", "bio", "")));
when(userRepository.findByEmail(any())).thenReturn(Optional.empty());
Map<String, Object> param = prepareRegisterParameter(email, username);
@ -139,6 +138,7 @@ public class UsersApiTest {
.body(param)
.when()
.post("/users")
.prettyPeek()
.then()
.statusCode(422)
.body("errors.username[0]", equalTo("duplicated username"));
@ -149,9 +149,8 @@ public class UsersApiTest {
String email = "john@jacob.com";
String username = "johnjacob2";
when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(new User(
email, username, "123", "bio", ""
)));
when(userRepository.findByEmail(eq(email)))
.thenReturn(Optional.of(new User(email, username, "123", "bio", "")));
when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty());
@ -167,14 +166,21 @@ public class UsersApiTest {
.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>() {{
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("password", "johnnyjacob");
put("username", username);
}});
}};
}
});
}
};
}
@Test
@ -191,12 +197,19 @@ public class UsersApiTest {
when(userReadService.findById(eq(user.getId()))).thenReturn(userData);
when(jwtService.toToken(any())).thenReturn("123");
Map<String, Object> param = new HashMap<String, Object>() {{
put("user", new HashMap<String, Object>() {{
Map<String, Object> param =
new HashMap<String, Object>() {
{
put(
"user",
new HashMap<String, Object>() {
{
put("email", email);
put("password", password);
}});
}};
}
});
}
};
given()
.contentType("application/json")
@ -209,7 +222,8 @@ public class UsersApiTest {
.body("user.username", equalTo(username))
.body("user.bio", equalTo(""))
.body("user.image", equalTo(defaultAvatar))
.body("user.token", equalTo("123"));;
.body("user.token", equalTo("123"));
;
}
@Test
@ -224,20 +238,28 @@ public class UsersApiTest {
when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user));
when(userReadService.findByUsername(eq(username))).thenReturn(userData);
Map<String, Object> param = new HashMap<String, Object>() {{
put("user", new HashMap<String, Object>() {{
Map<String, Object> param =
new HashMap<String, Object>() {
{
put(
"user",
new HashMap<String, Object>() {
{
put("email", email);
put("password", "123123");
}});
}};
}
});
}
};
given()
.contentType("application/json")
.body(param)
.when()
.post("/users/login")
.prettyPeek()
.then()
.statusCode(422)
.body("errors.password[0]", equalTo("invalid email or password"));
.body("message", equalTo("invalid email or password"));
}
}