package org.synyx.urlaubsverwaltung.web.application; import org.joda.time.DateMidnight; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.validation.Errors; import org.synyx.urlaubsverwaltung.core.application.domain.Application; import org.synyx.urlaubsverwaltung.core.application.domain.VacationCategory; import org.synyx.urlaubsverwaltung.core.application.domain.VacationType; import org.synyx.urlaubsverwaltung.core.application.service.CalculationService; import org.synyx.urlaubsverwaltung.core.overtime.OvertimeService; import org.synyx.urlaubsverwaltung.core.period.DayLength; import org.synyx.urlaubsverwaltung.core.person.Person; import org.synyx.urlaubsverwaltung.core.settings.Settings; import org.synyx.urlaubsverwaltung.core.settings.SettingsService; import org.synyx.urlaubsverwaltung.core.workingtime.OverlapCase; import org.synyx.urlaubsverwaltung.core.workingtime.OverlapService; import org.synyx.urlaubsverwaltung.core.workingtime.WorkDaysService; import org.synyx.urlaubsverwaltung.core.workingtime.WorkingTimeService; import org.synyx.urlaubsverwaltung.test.TestDataCreator; import java.math.BigDecimal; import java.sql.Time; import java.util.Optional; import java.util.function.Consumer; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; /** * Unit test for {@link ApplicationValidator}. * * @author Aljona Murygina - murygina@synyx.de */ public class ApplicationValidatorTest { private ApplicationValidator validator; private WorkingTimeService workingTimeService; private WorkDaysService calendarService; private OverlapService overlapService; private CalculationService calculationService; private SettingsService settingsService; private Errors errors; private ApplicationForLeaveForm appForm; private Settings settings; private OvertimeService overtimeService; @Before public void setUp() { settingsService = Mockito.mock(SettingsService.class); settings = new Settings(); settings.getWorkingTimeSettings().setOvertimeActive(true); when(settingsService.getSettings()).thenReturn(settings); calendarService = Mockito.mock(WorkDaysService.class); overlapService = Mockito.mock(OverlapService.class); calculationService = Mockito.mock(CalculationService.class); workingTimeService = Mockito.mock(WorkingTimeService.class); overtimeService = Mockito.mock(OvertimeService.class); validator = new ApplicationValidator(workingTimeService, calendarService, overlapService, calculationService, settingsService, overtimeService); errors = Mockito.mock(Errors.class); appForm = new ApplicationForLeaveForm(); appForm.setVacationType(TestDataCreator.createVacationType(VacationCategory.HOLIDAY)); appForm.setDayLength(DayLength.FULL); appForm.setStartDate(DateMidnight.now()); appForm.setEndDate(DateMidnight.now().plusDays(2)); // Default: everything is alright, override for negative cases when(errors.hasErrors()).thenReturn(Boolean.FALSE); when(workingTimeService.getByPersonAndValidityDateEqualsOrMinorDate(any(Person.class), any(DateMidnight.class))) .thenReturn(Optional.of(TestDataCreator.createWorkingTime())); when(calendarService.getWorkDays(any(DayLength.class), any(DateMidnight.class), any(DateMidnight.class), any(Person.class))).thenReturn(BigDecimal.ONE); when(overlapService.checkOverlap(any(Application.class))).thenReturn(OverlapCase.NO_OVERLAPPING); when(calculationService.checkApplication(any(Application.class))).thenReturn(Boolean.TRUE); when(overtimeService.getLeftOvertimeForPerson(any(Person.class))).thenReturn(BigDecimal.TEN); } // Supports -------------------------------------------------------------------------------------------------------- @Test public void ensureSupportsAppFormClass() { assertTrue(validator.supports(ApplicationForLeaveForm.class)); } @Test public void ensureDoesNotSupportNull() { assertFalse(validator.supports(null)); } @Test public void ensureDoesNotSupportOtherClass() { assertFalse(validator.supports(Person.class)); } // Validate period (date) ------------------------------------------------------------------------------------------ @Test public void ensureStartDateIsMandatory() { appForm.setDayLength(DayLength.FULL); appForm.setStartDate(null); validator.validate(appForm, errors); Mockito.verify(errors).rejectValue("startDate", "error.entry.mandatory"); } @Test public void ensureEndDateIsMandatory() { appForm.setDayLength(DayLength.FULL); appForm.setEndDate(null); validator.validate(appForm, errors); Mockito.verify(errors).rejectValue("endDate", "error.entry.mandatory"); } @Test public void ensureStartDateMustBeBeforeEndDate() { appForm.setDayLength(DayLength.FULL); appForm.setStartDate(new DateMidnight(2012, 1, 17)); appForm.setEndDate(new DateMidnight(2012, 1, 12)); validator.validate(appForm, errors); Mockito.verify(errors).reject("error.entry.invalidPeriod"); } @Test public void ensureVeryPastDateIsNotValid() { DateMidnight pastDate = DateMidnight.now().minusYears(10); appForm.setDayLength(DayLength.FULL); appForm.setStartDate(pastDate); appForm.setEndDate(pastDate.plusDays(1)); validator.validate(appForm, errors); Mockito.verify(errors).reject("application.error.tooFarInThePast"); } @Test public void ensureVeryFutureDateIsNotValid() { DateMidnight futureDate = DateMidnight.now().plusYears(10); appForm.setDayLength(DayLength.FULL); appForm.setStartDate(futureDate); appForm.setEndDate(futureDate.plusDays(1)); validator.validate(appForm, errors); Mockito.verify(errors) .reject("application.error.tooFarInTheFuture", new Object[] { settings.getAbsenceSettings().getMaximumMonthsToApplyForLeaveInAdvance() }, null); } @Test public void ensureMorningApplicationForLeaveMustBeOnSameDate() { appForm.setDayLength(DayLength.MORNING); appForm.setStartDate(DateMidnight.now()); appForm.setEndDate(DateMidnight.now().plusDays(1)); validator.validate(appForm, errors); Mockito.verify(errors).reject("application.error.halfDayPeriod"); } @Test public void ensureNoonApplicationForLeaveMustBeOnSameDate() { appForm.setDayLength(DayLength.NOON); appForm.setStartDate(DateMidnight.now()); appForm.setEndDate(DateMidnight.now().plusDays(1)); validator.validate(appForm, errors); Mockito.verify(errors).reject("application.error.halfDayPeriod"); } @Test public void ensureSameDateAsStartAndEndDateIsValidForFullDayPeriod() { DateMidnight date = DateMidnight.now(); appForm.setDayLength(DayLength.FULL); appForm.setStartDate(date); appForm.setEndDate(date); validator.validate(appForm, errors); Mockito.verify(errors, Mockito.never()).reject(Mockito.anyString()); } @Test public void ensureSameDateAsStartAndEndDateIsValidForMorningPeriod() { DateMidnight date = DateMidnight.now(); appForm.setDayLength(DayLength.MORNING); appForm.setStartDate(date); appForm.setEndDate(date); validator.validate(appForm, errors); Mockito.verify(errors, Mockito.never()).reject(Mockito.anyString()); } @Test public void ensureSameDateAsStartAndEndDateIsValidForNoonPeriod() { DateMidnight date = DateMidnight.now(); appForm.setDayLength(DayLength.NOON); appForm.setStartDate(date); appForm.setEndDate(date); validator.validate(appForm, errors); Mockito.verify(errors, Mockito.never()).reject(Mockito.anyString()); } // Validate period (time) ------------------------------------------------------------------------------------------ @Test public void ensureTimeIsNotMandatory() { appForm.setStartTime(null); appForm.setEndTime(null); validator.validate(appForm, errors); Mockito.verify(errors, Mockito.never()).reject(Mockito.anyString()); } @Test public void ensureProvidingStartTimeWithoutEndTimeIsInvalid() { appForm.setStartTime(Time.valueOf("09:15:00")); appForm.setEndTime(null); validator.validate(appForm, errors); Mockito.verify(errors).reject("error.entry.invalidPeriod"); } @Test public void ensureProvidingEndTimeWithoutStartTimeIsInvalid() { appForm.setStartTime(null); appForm.setEndTime(Time.valueOf("09:15:00")); validator.validate(appForm, errors); Mockito.verify(errors).reject("error.entry.invalidPeriod"); } @Test public void ensureStartTimeMustBeBeforeEndTime() { DateMidnight date = DateMidnight.now(); appForm.setDayLength(DayLength.MORNING); appForm.setStartDate(date); appForm.setEndDate(date); appForm.setStartTime(Time.valueOf("13:30:00")); appForm.setEndTime(Time.valueOf("09:15:00")); validator.validate(appForm, errors); Mockito.verify(errors).reject("error.entry.invalidPeriod"); } @Test public void ensureStartTimeAndEndTimeMustNotBeEquals() { DateMidnight date = DateMidnight.now(); Time time = Time.valueOf("13:30:00"); appForm.setDayLength(DayLength.MORNING); appForm.setStartDate(date); appForm.setEndDate(date); appForm.setStartTime(time); appForm.setEndTime(time); validator.validate(appForm, errors); Mockito.verify(errors).reject("error.entry.invalidPeriod"); } // Validate reason ------------------------------------------------------------------------------------------------- @Test public void ensureReasonIsNotMandatoryForHoliday() { VacationType vacationType = TestDataCreator.createVacationType(VacationCategory.HOLIDAY); appForm.setVacationType(vacationType); appForm.setReason(""); validator.validate(appForm, errors); Mockito.verify(errors, Mockito.never()).reject(Mockito.anyString()); Mockito.verify(errors, Mockito.never()).rejectValue(Mockito.eq("reason"), Mockito.anyString()); } @Test public void ensureReasonIsNotMandatoryForUnpaidLeave() { VacationType vacationType = TestDataCreator.createVacationType(VacationCategory.UNPAIDLEAVE); appForm.setVacationType(vacationType); appForm.setReason(""); validator.validate(appForm, errors); Mockito.verify(errors, Mockito.never()).reject(Mockito.anyString()); Mockito.verify(errors, Mockito.never()).rejectValue(Mockito.eq("reason"), Mockito.anyString()); } @Test public void ensureReasonIsNotMandatoryForOvertime() { VacationType vacationType = TestDataCreator.createVacationType(VacationCategory.OVERTIME); appForm.setVacationType(vacationType); appForm.setReason(""); validator.validate(appForm, errors); Mockito.verify(errors, Mockito.never()).reject(Mockito.anyString()); Mockito.verify(errors, Mockito.never()).rejectValue(Mockito.eq("reason"), Mockito.anyString()); } @Test public void ensureReasonIsMandatoryForSpecialLeave() { VacationType vacationType = TestDataCreator.createVacationType(VacationCategory.SPECIALLEAVE); appForm.setVacationType(vacationType); appForm.setReason(""); validator.validate(appForm, errors); Mockito.verify(errors).rejectValue("reason", "application.error.missingReasonForSpecialLeave"); } // Validate address ------------------------------------------------------------------------------------------------ @Test public void ensureThereIsAMaximumCharLength() { appForm.setAddress( "Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt" + " ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud " + "exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. "); validator.validate(appForm, errors); Mockito.verify(errors).rejectValue("address", "error.entry.tooManyChars"); } // Validate vacation days ------------------------------------------------------------------------------------------ @Test public void ensureApplicationForLeaveWithZeroVacationDaysIsNotValid() { when(errors.hasErrors()).thenReturn(Boolean.FALSE); when(calendarService.getWorkDays(any(DayLength.class), any(DateMidnight.class), any(DateMidnight.class), any(Person.class))).thenReturn(BigDecimal.ZERO); validator.validate(appForm, errors); Mockito.verify(errors).reject("application.error.zeroDays"); Mockito.verifyZeroInteractions(overlapService); Mockito.verifyZeroInteractions(calculationService); } @Test public void ensureApplyingForLeaveWithNotEnoughVacationDaysIsNotValid() { appForm.setDayLength(DayLength.FULL); appForm.setStartDate(DateMidnight.now()); appForm.setEndDate(DateMidnight.now()); appForm.setVacationType(TestDataCreator.createVacationType(VacationCategory.HOLIDAY)); Mockito.when(errors.hasErrors()).thenReturn(Boolean.FALSE); when(calendarService.getWorkDays(any(DayLength.class), any(DateMidnight.class), any(DateMidnight.class), any(Person.class))).thenReturn(BigDecimal.ONE); when(overlapService.checkOverlap(any(Application.class))).thenReturn(OverlapCase.NO_OVERLAPPING); when(calculationService.checkApplication(any(Application.class))).thenReturn(Boolean.FALSE); validator.validate(appForm, errors); Mockito.verify(errors).reject("application.error.notEnoughVacationDays"); } @Test public void ensureApplyingHalfDayForLeaveWithNotEnoughVacationDaysIsNotValid() { appForm.setDayLength(DayLength.NOON); appForm.setStartDate(DateMidnight.now()); appForm.setEndDate(DateMidnight.now()); appForm.setVacationType(TestDataCreator.createVacationType(VacationCategory.HOLIDAY)); when(errors.hasErrors()).thenReturn(Boolean.FALSE); Mockito.when(calendarService.getWorkDays(Mockito.eq(appForm.getDayLength()), Mockito.eq(appForm.getStartDate()), Mockito.eq(appForm.getEndDate()), Mockito.eq(appForm.getPerson()))) .thenReturn(BigDecimal.ONE); when(overlapService.checkOverlap(any(Application.class))).thenReturn(OverlapCase.NO_OVERLAPPING); when(calculationService.checkApplication(any(Application.class))).thenReturn(Boolean.FALSE); validator.validate(appForm, errors); Mockito.verify(errors).reject("application.error.notEnoughVacationDays"); } // Validate overlapping -------------------------------------------------------------------------------------------- @Test public void ensureOverlappingApplicationForLeaveIsNotValid() { when(errors.hasErrors()).thenReturn(Boolean.FALSE); when(calendarService.getWorkDays(any(DayLength.class), any(DateMidnight.class), any(DateMidnight.class), any(Person.class))).thenReturn(BigDecimal.ONE); when(overlapService.checkOverlap(any(Application.class))).thenReturn(OverlapCase.FULLY_OVERLAPPING); validator.validate(appForm, errors); Mockito.verify(errors).reject("application.error.overlap"); Mockito.verifyZeroInteractions(calculationService); } // Validate hours -------------------------------------------------------------------------------------------------- @Test public void ensureHoursIsMandatoryForOvertime() { appForm.setVacationType(TestDataCreator.createVacationType(VacationCategory.OVERTIME)); appForm.setHours(null); validator.validate(appForm, errors); Mockito.verify(errors).rejectValue("hours", "application.error.missingHoursForOvertime"); } @Test public void ensureHoursIsNotMandatoryForOtherTypesOfVacation() { Consumer<VacationType> assertHoursNotMandatory = (type) -> { appForm.setVacationType(type); appForm.setHours(null); validator.validate(appForm, errors); Mockito.verify(errors, Mockito.never()).rejectValue(Mockito.eq("hours"), Mockito.anyString()); }; VacationType holiday = TestDataCreator.createVacationType(VacationCategory.HOLIDAY); VacationType specialLeave = TestDataCreator.createVacationType(VacationCategory.SPECIALLEAVE); VacationType unpaidLeave = TestDataCreator.createVacationType(VacationCategory.UNPAIDLEAVE); assertHoursNotMandatory.accept(holiday); assertHoursNotMandatory.accept(specialLeave); assertHoursNotMandatory.accept(unpaidLeave); } @Test public void ensureHoursIsNotMandatoryForOvertimeIfOvertimeFunctionIsDeactivated() { settings.getWorkingTimeSettings().setOvertimeActive(false); appForm.setVacationType(TestDataCreator.createVacationType(VacationCategory.OVERTIME)); appForm.setHours(null); validator.validate(appForm, errors); Mockito.verify(errors, Mockito.never()).rejectValue(Mockito.eq("hours"), Mockito.anyString()); } @Test public void ensureHoursCanNotBeZero() { appForm.setHours(BigDecimal.ZERO); validator.validate(appForm, errors); Mockito.verify(errors).rejectValue("hours", "application.error.invalidHoursForOvertime"); } @Test public void ensureHoursCanNotBeNegative() { appForm.setHours(BigDecimal.ONE.negate()); validator.validate(appForm, errors); Mockito.verify(errors).rejectValue("hours", "application.error.invalidHoursForOvertime"); } @Test public void ensureDecimalHoursAreValid() { appForm.setVacationType(TestDataCreator.createVacationType(VacationCategory.OVERTIME)); appForm.setHours(new BigDecimal("0.5")); validator.validate(appForm, errors); Mockito.verify(errors, Mockito.never()).rejectValue(Mockito.eq("hours"), Mockito.anyString()); } @Test public void ensureNoErrorMessageForMandatoryIfHoursIsNullBecauseOfTypeMismatch() { settings.getWorkingTimeSettings().setOvertimeActive(true); appForm.setVacationType(TestDataCreator.createVacationType(VacationCategory.OVERTIME)); appForm.setHours(null); when(errors.hasFieldErrors("hours")).thenReturn(true); validator.validate(appForm, errors); Mockito.verify(errors).hasFieldErrors("hours"); Mockito.verify(errors, Mockito.never()).rejectValue("hours", "application.error.missingHoursForOvertime"); } // Validate working time exists ------------------------------------------------------------------------------------ @Test public void ensureWorkingTimeConfigurationMustExistForPeriodOfApplicationForLeave() { when(errors.hasErrors()).thenReturn(Boolean.FALSE); when(workingTimeService.getByPersonAndValidityDateEqualsOrMinorDate(any(Person.class), Mockito.eq(appForm.getStartDate()))).thenReturn(Optional.empty()); validator.validate(appForm, errors); Mockito.verify(errors).reject("application.error.noValidWorkingTime"); Mockito.verify(workingTimeService) .getByPersonAndValidityDateEqualsOrMinorDate(appForm.getPerson(), appForm.getStartDate()); Mockito.verifyZeroInteractions(calendarService); Mockito.verifyZeroInteractions(overlapService); Mockito.verifyZeroInteractions(calculationService); } @Test public void ensureWorkingTimeConfigurationMustExistForHalfDayApplicationForLeave() { // Yes, this can really happen... appForm.setStartDate(null); appForm.setEndDate(null); appForm.setStartDate(DateMidnight.now()); appForm.setEndDate(DateMidnight.now()); appForm.setDayLength(DayLength.MORNING); when(errors.hasErrors()).thenReturn(Boolean.FALSE); Mockito.when(workingTimeService.getByPersonAndValidityDateEqualsOrMinorDate(Mockito.any(Person.class), Mockito.eq(appForm.getStartDate()))) .thenReturn(Optional.empty()); validator.validate(appForm, errors); Mockito.verify(errors).reject("application.error.noValidWorkingTime"); Mockito.verify(workingTimeService) .getByPersonAndValidityDateEqualsOrMinorDate(appForm.getPerson(), appForm.getStartDate()); Mockito.verifyZeroInteractions(calendarService); Mockito.verifyZeroInteractions(overlapService); Mockito.verifyZeroInteractions(calculationService); } // Validate maximal overtime reduction ----------------------------------------------------------------------------- /** * User tries to make an application (overtime reduction) but has not enough overtime left and minimum overtime is * reached. * * <p>0h overtime - 6h (application) < -5h overtime minimum</p> */ @Test public void ensureErrorDueToMinimumOvertimeReached() { BigDecimal overtimeReductionHours = new BigDecimal("6"); overtimeMinimumTest(overtimeReductionHours); Mockito.verify(overtimeService).getLeftOvertimeForPerson(appForm.getPerson()); Mockito.verify(errors).reject("application.error.notEnoughOvertime"); } /** * User tries to make an application (overtime reduction) but has not enough overtime left and minimum overtime is * reached exactly -> application is valid. * * <p>0h overtime - 5h (application) == -5h overtime minimum</p> */ @Test public void ensureNoErrorDueToExactMinimumOvertimeReached() { BigDecimal overtimeReductionHours = new BigDecimal("5"); overtimeMinimumTest(overtimeReductionHours); Mockito.verify(overtimeService).getLeftOvertimeForPerson(appForm.getPerson()); assertFalse(errors.hasErrors()); } private void overtimeMinimumTest(BigDecimal hours) { VacationType vacationType = TestDataCreator.createVacationType(VacationCategory.OVERTIME); appForm.setHours(hours); appForm.setVacationType(vacationType); settings.getWorkingTimeSettings().setOvertimeActive(true); settings.getWorkingTimeSettings().setMinimumOvertime(5); when(overtimeService.getLeftOvertimeForPerson(any(Person.class))).thenReturn(BigDecimal.ZERO); validator.validate(appForm, errors); } }