diff --git a/support-portal-backend/pom.xml b/support-portal-backend/pom.xml index 678a978..335e426 100644 --- a/support-portal-backend/pom.xml +++ b/support-portal-backend/pom.xml @@ -78,6 +78,11 @@ spring-boot-starter-mail + + org.springframework.boot + spring-boot-starter-validation + + org.springframework.boot spring-boot-starter-test diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UserResource.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UserResource.java index de29af6..e0e8707 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UserResource.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UserResource.java @@ -15,6 +15,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; +import javax.validation.Valid; + import static org.springframework.http.HttpStatus.OK; @Slf4j @@ -56,7 +58,7 @@ public class UserResource { } @PostMapping("add") - public User addNewUser(UserDto userDto) { + public User addNewUser(@Valid UserDto userDto) { log.debug("User DTO: {}", userDto); return userService.addNewUser(userDto); } diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/UserDto.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/UserDto.java index a526ae8..6a1900e 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/UserDto.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/domain/dto/UserDto.java @@ -7,15 +7,26 @@ import lombok.NoArgsConstructor; import net.shyshkin.study.fullstack.supportportal.backend.domain.Role; import org.springframework.web.multipart.MultipartFile; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + @Data @NoArgsConstructor @AllArgsConstructor @Builder public class UserDto { + + @NotEmpty(message = "Should not be empty") private String firstName; + @NotEmpty(message = "Should not be empty") private String lastName; + @NotEmpty(message = "Should not be empty") private String username; + @NotEmpty(message = "Should not be empty") + @Email(message = "Must match email format") private String email; + @NotNull(message = "Role is mandatory") private Role role; private boolean isNonLocked; private boolean isActive; diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/exception/ExceptionHandling.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/exception/ExceptionHandling.java index 7379273..1885e50 100644 --- a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/exception/ExceptionHandling.java +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/exception/ExceptionHandling.java @@ -13,6 +13,7 @@ import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.LockedException; +import org.springframework.validation.BindException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.persistence.NoResultException; import java.io.IOException; import java.util.Objects; +import java.util.stream.Collectors; import static net.shyshkin.study.fullstack.supportportal.backend.utility.HttpResponseUtility.createHttpResponse; import static org.springframework.http.HttpStatus.*; @@ -81,10 +83,19 @@ public class ExceptionHandling { @ExceptionHandler(Exception.class) public ResponseEntity internalServerErrorException(Exception exception) { - log.error(exception.getMessage()); + log.error(exception.getMessage(), exception); return createHttpResponse(INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR_MSG); } + @ExceptionHandler({BindException.class}) + public ResponseEntity validationExceptionHandler(BindException exception) { + String fieldsWithErrors = exception.getFieldErrors().stream() + .map(fe -> fe.getField() + ":" + fe.getDefaultMessage()) + .collect(Collectors.joining(",")); + String message = "Error(s) in parameters: [" + fieldsWithErrors + "]"; + return createHttpResponse(BAD_REQUEST, message); + } + @ExceptionHandler(NoResultException.class) public ResponseEntity notFoundException(NoResultException exception) { log.error(exception.getMessage()); diff --git a/support-portal-backend/src/test/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UserResourceUnSecureTest.java b/support-portal-backend/src/test/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UserResourceUnSecureTest.java index 5831923..8fc2d5d 100644 --- a/support-portal-backend/src/test/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UserResourceUnSecureTest.java +++ b/support-portal-backend/src/test/java/net/shyshkin/study/fullstack/supportportal/backend/controller/UserResourceUnSecureTest.java @@ -2,8 +2,10 @@ package net.shyshkin.study.fullstack.supportportal.backend.controller; import lombok.extern.slf4j.Slf4j; import net.shyshkin.study.fullstack.supportportal.backend.common.BaseUserTest; +import net.shyshkin.study.fullstack.supportportal.backend.domain.HttpResponse; import net.shyshkin.study.fullstack.supportportal.backend.domain.User; import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.UserDto; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -14,6 +16,7 @@ import org.springframework.test.context.TestPropertySource; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.OK; @Slf4j @@ -26,44 +29,191 @@ class UserResourceUnSecureTest extends BaseUserTest { @Autowired TestRestTemplate restTemplate; - @Test - void addNewUser() { + @Nested + class AddNewUserTests { - //given - UserDto userDto = createRandomUserDto(); - Map paramMap = Map.of( - "firstName", userDto.getFirstName(), - "lastName", userDto.getLastName(), - "username", userDto.getUsername(), - "email", userDto.getEmail(), - "role", userDto.getRole().name(), - "isActive", String.valueOf(userDto.isActive()), - "isNonLocked", String.valueOf(userDto.isNonLocked()) - ); + @Test + void addNewUser_correct() { - //when - ResponseEntity responseEntity = restTemplate - .postForEntity( - "/user/add?username={username}&email={email}" + - "&firstName={firstName}&lastName={lastName}" + - "&role={role}&active={isActive}&nonLocked={isNonLocked}", - null, - User.class, - paramMap - ); + //given + UserDto userDto = createRandomUserDto(); + Map paramMap = Map.of( + "firstName", userDto.getFirstName(), + "lastName", userDto.getLastName(), + "username", userDto.getUsername(), + "email", userDto.getEmail(), + "role", userDto.getRole().name(), + "isActive", String.valueOf(userDto.isActive()), + "isNonLocked", String.valueOf(userDto.isNonLocked()) + ); - //then - log.debug("Response Entity: {}", responseEntity); - assertThat(responseEntity.getStatusCode()).isEqualTo(OK); - assertThat(responseEntity.getBody()) - .isNotNull() - .hasNoNullFieldsOrPropertiesExcept("lastLoginDate", "lastLoginDateDisplay") - .hasFieldOrPropertyWithValue("username", userDto.getUsername()) - .hasFieldOrPropertyWithValue("email", userDto.getEmail()) - .hasFieldOrPropertyWithValue("firstName", userDto.getFirstName()) - .hasFieldOrPropertyWithValue("lastName", userDto.getLastName()) - .hasFieldOrPropertyWithValue("isActive", true) - .hasFieldOrPropertyWithValue("isNotLocked", true) - .hasFieldOrPropertyWithValue("role", "ROLE_ADMIN"); + //when + ResponseEntity responseEntity = restTemplate + .postForEntity( + "/user/add?username={username}&email={email}" + + "&firstName={firstName}&lastName={lastName}" + + "&role={role}&active={isActive}&nonLocked={isNonLocked}", + null, + User.class, + paramMap + ); + + //then + log.debug("Response Entity: {}", responseEntity); + assertThat(responseEntity.getStatusCode()).isEqualTo(OK); + assertThat(responseEntity.getBody()) + .isNotNull() + .hasNoNullFieldsOrPropertiesExcept("lastLoginDate", "lastLoginDateDisplay") + .hasFieldOrPropertyWithValue("username", userDto.getUsername()) + .hasFieldOrPropertyWithValue("email", userDto.getEmail()) + .hasFieldOrPropertyWithValue("firstName", userDto.getFirstName()) + .hasFieldOrPropertyWithValue("lastName", userDto.getLastName()) + .hasFieldOrPropertyWithValue("isActive", true) + .hasFieldOrPropertyWithValue("isNotLocked", true) + .hasFieldOrPropertyWithValue("role", "ROLE_ADMIN"); + } + + @Test + void addNewUser_missedFirstName() { + + //given + UserDto userDto = createRandomUserDto(); + Map paramMap = Map.of( + "lastName", userDto.getLastName(), + "username", userDto.getUsername(), + "email", userDto.getEmail(), + "role", userDto.getRole().name(), + "isActive", String.valueOf(userDto.isActive()), + "isNonLocked", String.valueOf(userDto.isNonLocked()) + ); + + //when + var responseEntity = restTemplate + .postForEntity( + "/user/add?username={username}&email={email}" + + "&lastName={lastName}" + + "&role={role}&active={isActive}&nonLocked={isNonLocked}", + null, + HttpResponse.class, + paramMap + ); + + //then + log.debug("Response Entity: {}", responseEntity); + assertThat(responseEntity.getStatusCode()).isEqualTo(BAD_REQUEST); + assertThat(responseEntity.getBody()) + .isNotNull() + .hasNoNullFieldsOrProperties() + .hasFieldOrPropertyWithValue("httpStatus", BAD_REQUEST) + .hasFieldOrPropertyWithValue("message", "ERROR(S) IN PARAMETERS: [FIRSTNAME:SHOULD NOT BE EMPTY]"); + } + + @Test + void addNewUser_wrongRole() { + + //given + UserDto userDto = createRandomUserDto(); + Map paramMap = Map.of( + "firstName", userDto.getFirstName(), + "lastName", userDto.getLastName(), + "username", userDto.getUsername(), + "email", userDto.getEmail(), + "role", "ROLE_FAKE", + "isActive", String.valueOf(userDto.isActive()), + "isNonLocked", String.valueOf(userDto.isNonLocked()) + ); + + //when + var responseEntity = restTemplate + .postForEntity( + "/user/add?username={username}&email={email}" + + "&firstName={firstName}&lastName={lastName}" + + "&role={role}&active={isActive}&nonLocked={isNonLocked}", + null, + HttpResponse.class, + paramMap + ); + + //then + log.debug("Response Entity: {}", responseEntity); + assertThat(responseEntity.getStatusCode()).isEqualTo(BAD_REQUEST); + assertThat(responseEntity.getBody()) + .isNotNull() + .hasNoNullFieldsOrProperties() + .hasFieldOrPropertyWithValue("httpStatus", BAD_REQUEST) + .hasFieldOrPropertyWithValue("message", "ERROR(S) IN PARAMETERS: [ROLE:FAILED TO CONVERT PROPERTY VALUE OF TYPE 'JAVA.LANG.STRING' TO REQUIRED TYPE 'NET.SHYSHKIN.STUDY.FULLSTACK.SUPPORTPORTAL.BACKEND.DOMAIN.ROLE' FOR PROPERTY 'ROLE'; NESTED EXCEPTION IS ORG.SPRINGFRAMEWORK.CORE.CONVERT.CONVERSIONFAILEDEXCEPTION: FAILED TO CONVERT FROM TYPE [JAVA.LANG.STRING] TO TYPE [@JAVAX.VALIDATION.CONSTRAINTS.NOTNULL NET.SHYSHKIN.STUDY.FULLSTACK.SUPPORTPORTAL.BACKEND.DOMAIN.ROLE] FOR VALUE 'ROLE_FAKE'; NESTED EXCEPTION IS JAVA.LANG.ILLEGALARGUMENTEXCEPTION: NO ENUM CONSTANT NET.SHYSHKIN.STUDY.FULLSTACK.SUPPORTPORTAL.BACKEND.DOMAIN.ROLE.ROLE_FAKE]"); + } + + @Test + void addNewUser_incorrectEmail() { + + //given + UserDto userDto = createRandomUserDto(); + Map paramMap = Map.of( + "firstName", userDto.getFirstName(), + "lastName", userDto.getLastName(), + "username", userDto.getUsername(), + "email", "not_an_email", + "role", userDto.getRole().name(), + "isActive", String.valueOf(userDto.isActive()), + "isNonLocked", String.valueOf(userDto.isNonLocked()) + ); + + //when + var responseEntity = restTemplate + .postForEntity( + "/user/add?username={username}&email={email}" + + "&firstName={firstName}&lastName={lastName}" + + "&role={role}&active={isActive}&nonLocked={isNonLocked}", + null, + HttpResponse.class, + paramMap + ); + + //then + log.debug("Response Entity: {}", responseEntity); + assertThat(responseEntity.getStatusCode()).isEqualTo(BAD_REQUEST); + assertThat(responseEntity.getBody()) + .isNotNull() + .hasNoNullFieldsOrProperties() + .hasFieldOrPropertyWithValue("httpStatus", BAD_REQUEST) + .hasFieldOrPropertyWithValue("message", "ERROR(S) IN PARAMETERS: [EMAIL:MUST MATCH EMAIL FORMAT]"); + } + + @Test + void addNewUser_incorrectBoolean() { + + //given + UserDto userDto = createRandomUserDto(); + Map paramMap = Map.of( + "firstName", userDto.getFirstName(), + "lastName", userDto.getLastName(), + "username", userDto.getUsername(), + "email", userDto.getEmail(), + "role", userDto.getRole().name(), + "isActive", "yes", + "isNonLocked", "not_a_boolean" + ); + + //when + var responseEntity = restTemplate + .postForEntity( + "/user/add?username={username}&email={email}" + + "&firstName={firstName}&lastName={lastName}" + + "&role={role}&active={isActive}&nonLocked={isNonLocked}", + null, + HttpResponse.class, + paramMap + ); + + //then + log.debug("Response Entity: {}", responseEntity); + assertThat(responseEntity.getStatusCode()).isEqualTo(BAD_REQUEST); + assertThat(responseEntity.getBody()) + .isNotNull() + .hasNoNullFieldsOrProperties() + .hasFieldOrPropertyWithValue("httpStatus", BAD_REQUEST) + .hasFieldOrPropertyWithValue("message", "ERROR(S) IN PARAMETERS: [NONLOCKED:FAILED TO CONVERT PROPERTY VALUE OF TYPE 'JAVA.LANG.STRING' TO REQUIRED TYPE 'BOOLEAN' FOR PROPERTY 'NONLOCKED'; NESTED EXCEPTION IS JAVA.LANG.ILLEGALARGUMENTEXCEPTION: INVALID BOOLEAN VALUE [NOT_A_BOOLEAN]]"); + } } } \ No newline at end of file