Skip to content

Commit

Permalink
more meaningful error messages in EventDtoRequestValidator (#8058)
Browse files Browse the repository at this point in the history
* more meaningful error messages in EventDtoRequestValidator

* small refactoring

* remove opening of mock
  • Loading branch information
holotsvan authored Jan 23, 2025
1 parent 45f59cf commit 10dd328
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -479,21 +479,6 @@ public final ResponseEntity<Object> handleUnsupportedSortException(
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exceptionResponse);
}

/**
* Customize the response for EventDtoValidationException.
*
* @param ex the exception
* @param request the current request
* @return a {@code ResponseEntity} message
*/
@ExceptionHandler(EventDtoValidationException.class)
public final ResponseEntity<Object> handleEventDtoValidationException(
EventDtoValidationException ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(getErrorAttributes(request));
log.warn(ex.getMessage(), ex);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exceptionResponse);
}

/**
* Customize the response for WrongIdException.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,22 @@ public class EventDtoRequestValidator
*/
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value instanceof AddEventDtoRequest addEventDtoRequest) {
validateDateLocations(addEventDtoRequest.getDatesLocations());
convertToUTC(addEventDtoRequest.getDatesLocations());
validateEventDateLocations(addEventDtoRequest.getDatesLocations());
validateTags(addEventDtoRequest.getTags());
} else if (value instanceof UpdateEventRequestDto updateEventDto) {
validateDateLocations(updateEventDto.getDatesLocations());
convertToUTC(updateEventDto.getDatesLocations());
validateEventDateLocations(updateEventDto.getDatesLocations());
validateTags(updateEventDto.getTags());
} else {
try {
switch (value) {
case AddEventDtoRequest addEventDtoRequest ->
validateEventDto(addEventDtoRequest.getDatesLocations(), addEventDtoRequest.getTags());
case UpdateEventRequestDto updateEventDto ->
validateEventDto(updateEventDto.getDatesLocations(), updateEventDto.getTags());
default -> {
return false;
}
}
return true;
} catch (EventDtoValidationException ex) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(ex.getMessage()).addConstraintViolation();
return false;
}
return true;
}

private <T extends AbstractEventDateLocationDto> void validateDateLocations(List<T> dates) {
Expand All @@ -76,6 +78,12 @@ private <T extends AbstractEventDateLocationDto> void convertToUTC(List<T> dates
}

private <T extends AbstractEventDateLocationDto> void validateEventDateLocations(List<T> eventDateLocationDtos) {
validateUniqueDates(eventDateLocationDtos);
validateDateRanges(eventDateLocationDtos);
validateLocationDetails(eventDateLocationDtos);
}

private <T extends AbstractEventDateLocationDto> void validateUniqueDates(List<T> eventDateLocationDtos) {
Set<LocalDateTime> startDateSet = new HashSet<>();
Set<LocalDateTime> finishDateSet = new HashSet<>();

Expand All @@ -86,14 +94,22 @@ private <T extends AbstractEventDateLocationDto> void validateEventDateLocations
if (!startDateSet.add(startDate) || !finishDateSet.add(finishDate)) {
throw new EventDtoValidationException(ErrorMessage.SAME_EVENT_DATES);
}
}
}

private <T extends AbstractEventDateLocationDto> void validateDateRanges(List<T> eventDateLocationDtos) {
for (T eventDateLocationDto : eventDateLocationDtos) {
if (eventDateLocationDto.getStartDate().isBefore(ZonedDateTime.now(ZoneOffset.UTC))
|| eventDateLocationDto.getStartDate().isAfter(eventDateLocationDto.getFinishDate())
|| eventDateLocationDto.getStartDate().isAfter(ZonedDateTime.now(ZoneOffset.UTC)
.plusYears(MAX_YEARS_OF_PLANNING))) {
throw new EventDtoValidationException(ErrorMessage.EVENT_START_DATE_AFTER_FINISH_DATE_OR_IN_PAST);
}
}
}

private <T extends AbstractEventDateLocationDto> void validateLocationDetails(List<T> eventDateLocationDtos) {
for (T eventDateLocationDto : eventDateLocationDtos) {
if (eventDateLocationDto.getOnlineLink() == null && eventDateLocationDto.getCoordinates() == null) {
throw new EventDtoValidationException(ErrorMessage.NO_EVENT_LINK_OR_ADDRESS);
}
Expand All @@ -110,4 +126,11 @@ private void validateTags(List<String> tags) {
throw new EventDtoValidationException(ErrorMessage.WRONG_COUNT_OF_TAGS_EXCEPTION);
}
}

private <T extends AbstractEventDateLocationDto> void validateEventDto(List<T> datesLocations, List<String> tags) {
validateDateLocations(datesLocations);
convertToUTC(datesLocations);
validateEventDateLocations(datesLocations);
validateTags(tags);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,76 +4,101 @@
import greencity.dto.event.AddEventDtoRequest;
import greencity.dto.event.UpdateEventDateLocationDto;
import greencity.dto.event.UpdateEventRequestDto;
import greencity.exception.exceptions.EventDtoValidationException;
import greencity.exception.exceptions.InvalidURLException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.List;
import jakarta.validation.ConstraintValidatorContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

@ExtendWith(SpringExtension.class)
class EventDtoRequestValidatorTest {
@InjectMocks
private EventDtoRequestValidator validator;

@Mock
private ConstraintValidatorContext constraintValidatorContext;

@Mock
private ConstraintValidatorContext.ConstraintViolationBuilder violationBuilder;

@BeforeEach
void setUp() {
when(constraintValidatorContext.buildConstraintViolationWithTemplate(anyString()))
.thenReturn(violationBuilder);
when(violationBuilder.addConstraintViolation()).thenReturn(constraintValidatorContext);
}

@Test
void withoutDatesException() {
AddEventDtoRequest addEventDtoRequest = ModelUtils.getEventDtoWithoutDates();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(addEventDtoRequest, null));
boolean isValid = validator.isValid(addEventDtoRequest, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for empty date locations");
}

@Test
void withZeroDatesException() {
AddEventDtoRequest addEventDtoRequest = ModelUtils.getEventDtoWithZeroDates();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(addEventDtoRequest, null));
boolean isValid = validator.isValid(addEventDtoRequest, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for zero date locations");
}

@Test
void withTooManyDatesException() {
AddEventDtoRequest addEventDtoRequest = ModelUtils.getEventDtoWithTooManyDates();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(addEventDtoRequest, null));
boolean isValid = validator.isValid(addEventDtoRequest, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for too many date locations");
}

@Test
void withStartDateInPastException() {
AddEventDtoRequest addEventDtoRequest = ModelUtils.getEventWithPastStartDate();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(addEventDtoRequest, null));
boolean isValid = validator.isValid(addEventDtoRequest, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for past start date");
}

@Test
void withStartDateAfterFinishDateException() {
AddEventDtoRequest addEventDtoRequest = ModelUtils.getEventWithStartDateAfterFinishDate();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(addEventDtoRequest, null));
boolean isValid = validator.isValid(addEventDtoRequest, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for start date after finish date");
}

@Test
void withoutAddressAndLinkException() {
AddEventDtoRequest addEventDtoRequest = ModelUtils.getEventWithoutAddressAndLink();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(addEventDtoRequest, null));
boolean isValid = validator.isValid(addEventDtoRequest, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for event without address and link");
}

@Test
void withInvalidLinkException() {
AddEventDtoRequest addEventDtoRequest = ModelUtils.getEventWithInvalidLink();
assertThrows(InvalidURLException.class, () -> validator.isValid(addEventDtoRequest, null));
assertThrows(InvalidURLException.class,
() -> validator.isValid(addEventDtoRequest, constraintValidatorContext));
}

@Test
void withTooManyTagsException() {
AddEventDtoRequest addEventDtoRequest = ModelUtils.getEventWithTooManyTags();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(addEventDtoRequest, null));
boolean isValid = validator.isValid(addEventDtoRequest, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for too many tags");
}

@Test
void validEvent() {
AddEventDtoRequest addEventDtoRequest = ModelUtils.getAddEventDtoRequest();
assertTrue(validator.isValid(addEventDtoRequest, null));
assertTrue(validator.isValid(addEventDtoRequest, constraintValidatorContext));
}

@Test
Expand All @@ -82,46 +107,51 @@ void saveEventWithSameDates() {
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneOffset.UTC).plusHours(2L);
addEventDto.getDatesLocations().forEach(e -> e.setStartDate(zonedDateTime));
addEventDto.getDatesLocations().forEach(e -> e.setFinishDate(zonedDateTime));
assertThrows(EventDtoValidationException.class, () -> validator.isValid(addEventDto, null));
boolean isValid = validator.isValid(addEventDto, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false same event with same dates");
}

@Test
void updateWithTooManyTagsException() {
UpdateEventRequestDto updateEventDto = ModelUtils.getUpdateEventDtoWithTooManyDates();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(updateEventDto, null));
boolean isValid = validator.isValid(updateEventDto, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for too many tags");
}

@Test
void updateWithEmptyDateLocations() {
UpdateEventRequestDto updateEventDto = ModelUtils.getUpdateEventDtoWithEmptyDateLocations();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(updateEventDto, null));
boolean isValid = validator.isValid(updateEventDto, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for empty date locations");
}

@Test
void updateEventDtoWithoutDates() {
UpdateEventRequestDto updateEventDto = ModelUtils.getUpdateEventDtoWithoutDates();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(updateEventDto, null));

boolean isValid = validator.isValid(updateEventDto, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false event without dates");
}

@Test
void updateWithInvalidLinkException() {
UpdateEventRequestDto updateEventDto = ModelUtils.getUpdateEventWithoutAddressAndLink();
assertThrows(EventDtoValidationException.class, () -> validator.isValid(updateEventDto, null));
boolean isValid = validator.isValid(updateEventDto, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for invalid link");
}

@Test
void updateWithoutLinkAndCoordinates() {
UpdateEventRequestDto updateEventDto = ModelUtils.getUpdateEventDto();
updateEventDto.getDatesLocations().forEach(e -> e.setOnlineLink(null));
updateEventDto.getDatesLocations().forEach(e -> e.setCoordinates(null));
assertThrows(EventDtoValidationException.class, () -> validator.isValid(updateEventDto, null));
boolean isValid = validator.isValid(updateEventDto, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for dto without link and coordinates");
}

@Test
void validEventUpdate() {
UpdateEventRequestDto updateEventDto = ModelUtils.getUpdateEventDto();
assertTrue(validator.isValid(updateEventDto, null));
assertTrue(validator.isValid(updateEventDto, constraintValidatorContext));
}

@Test
Expand All @@ -130,13 +160,14 @@ void updateEventWithSameDates() {
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneOffset.UTC).plusHours(2L);
updateEventDto.getDatesLocations().forEach(e -> e.setStartDate(zonedDateTime));
updateEventDto.getDatesLocations().forEach(e -> e.setFinishDate(zonedDateTime));
assertThrows(EventDtoValidationException.class, () -> validator.isValid(updateEventDto, null));
boolean isValid = validator.isValid(updateEventDto, constraintValidatorContext);
assertFalse(isValid, "Validation should fail and return false for event with same dates");
}

@Test
void invalidObjectType() {
Object value = new Object();
assertFalse(validator.isValid(value, null));
assertFalse(validator.isValid(value, constraintValidatorContext));
}

@Test
Expand All @@ -148,7 +179,7 @@ void invalidDates() {
.onlineLink("http://localhost:8060/swagger-ui.html#/")
.build()))
.tags(List.of("first", "second", "third")).build();
assertThrows(EventDtoValidationException.class,
() -> validator.isValid(updateEventRequestDto, null));
boolean isValid = validator.isValid(updateEventRequestDto, constraintValidatorContext);
assertFalse(isValid, "Validation should fail for null dates in UpdateEventRequestDto");
}
}

0 comments on commit 10dd328

Please sign in to comment.