From 88c2ce0ba503e9e15a89a4c629f43c3d3489dd53 Mon Sep 17 00:00:00 2001 From: "fernanda.mello" Date: Sun, 5 Oct 2025 18:28:44 -0300 Subject: [PATCH 1/6] Corrige filtro de interprete por data e horario disponivel para verificar agendamentos --- .../services/InterpreterService.java | 36 +-- .../spec/InterpreterSpecification.java | 152 +++++++++-- ...terpreterSpecificationIntegrationTest.java | 249 ++++++++++++++++++ .../spec/InterpreterSpecificationTest.java | 167 ------------ 4 files changed, 386 insertions(+), 218 deletions(-) create mode 100644 pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java delete mode 100644 pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationTest.java diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/InterpreterService.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/InterpreterService.java index ad7a67d7..5f08f721 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/InterpreterService.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/InterpreterService.java @@ -9,7 +9,6 @@ import com.pointtils.pointtils.src.application.mapper.InterpreterResponseMapper; import com.pointtils.pointtils.src.application.mapper.LocationMapper; import com.pointtils.pointtils.src.core.domain.entities.Interpreter; -import com.pointtils.pointtils.src.core.domain.entities.enums.DayOfWeek; import com.pointtils.pointtils.src.core.domain.entities.enums.Gender; import com.pointtils.pointtils.src.core.domain.entities.enums.InterpreterModality; import com.pointtils.pointtils.src.core.domain.entities.enums.UserStatus; @@ -24,7 +23,6 @@ import java.math.BigDecimal; import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.List; @@ -77,16 +75,14 @@ public void delete(UUID id) { repository.save(interpreter); } - public List findAll( - String modality, - String gender, - String city, - String uf, - String neighborhood, - String specialty, - String availableDate, - String name) { - + public List findAll(String modality, + String gender, + String city, + String uf, + String neighborhood, + String specialty, + String availableDate, + String name) { InterpreterModality modalityEnum = null; if (modality != null) { modalityEnum = InterpreterModality.valueOf(modality.toUpperCase()); @@ -97,16 +93,9 @@ public List findAll( genderEnum = Gender.valueOf(gender.toUpperCase()); } - DayOfWeek dayOfWeek = null; - LocalTime requestedStart = null; - LocalTime requestedEnd = null; - + LocalDateTime dateTime = null; if (availableDate != null) { - LocalDateTime dateTime = LocalDateTime.parse(availableDate, - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); - dayOfWeek = DayOfWeek.valueOf(dateTime.getDayOfWeek().name().substring(0, 3)); - requestedStart = dateTime.toLocalTime(); - requestedEnd = requestedStart.plusHours(1); + dateTime = LocalDateTime.parse(availableDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); } List specialtyList = null; @@ -116,9 +105,8 @@ public List findAll( .toList(); } - return repository.findAll( - InterpreterSpecification.filter(modalityEnum, uf, city, neighborhood, specialtyList, genderEnum, dayOfWeek, - requestedStart, requestedEnd, name)) + return repository.findAll(InterpreterSpecification.filter(modalityEnum, uf, city, neighborhood, specialtyList, + genderEnum, dateTime, name)) .stream() .map(responseMapper::toListResponseDTO) .toList(); diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecification.java b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecification.java index a64cbbb2..0e942a49 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecification.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecification.java @@ -1,9 +1,11 @@ package com.pointtils.pointtils.src.infrastructure.repositories.spec; +import com.pointtils.pointtils.src.core.domain.entities.Appointment; import com.pointtils.pointtils.src.core.domain.entities.Interpreter; import com.pointtils.pointtils.src.core.domain.entities.Location; import com.pointtils.pointtils.src.core.domain.entities.Schedule; import com.pointtils.pointtils.src.core.domain.entities.UserSpecialty; +import com.pointtils.pointtils.src.core.domain.entities.enums.AppointmentStatus; import com.pointtils.pointtils.src.core.domain.entities.enums.DayOfWeek; import com.pointtils.pointtils.src.core.domain.entities.enums.Gender; import com.pointtils.pointtils.src.core.domain.entities.enums.InterpreterModality; @@ -13,17 +15,24 @@ import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Subquery; import lombok.experimental.UtilityClass; import org.springframework.data.jpa.domain.Specification; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; +import java.util.Objects; import java.util.UUID; @UtilityClass public class InterpreterSpecification { + private static final String START_TIME_FIELD = "startTime"; + private static final String END_TIME_FIELD = "endTime"; + @SuppressWarnings("null") public static Specification filter( InterpreterModality modality, @@ -32,50 +41,139 @@ public static Specification filter( String neighborhood, List specialties, Gender gender, - DayOfWeek dayOfWeek, - LocalTime requestedStart, - LocalTime requestedEnd, + LocalDateTime availableDate, String name ) { return (root, query, cb) -> { query.distinct(true); - Join location = root.join("locations", JoinType.LEFT); - Join schedule = root.join("schedules", JoinType.LEFT); - Predicate predicate = cb.conjunction(); - if (modality == InterpreterModality.ALL) { - predicate = cb.and(predicate, root.get("modality").in(InterpreterModality.ALL, InterpreterModality.ONLINE, InterpreterModality.PERSONALLY)); - } else if (modality != null) { - predicate = cb.and(predicate, root.get("modality").in(InterpreterModality.ALL, modality)); - } - if (name != null) predicate = cb.and(predicate, cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%")); - if (gender != null) predicate = cb.and(predicate, cb.equal(root.get("gender"), gender)); - if (uf != null) predicate = cb.and(predicate, cb.equal(location.get("uf"), uf)); - if (city != null) predicate = cb.and(predicate, cb.like(cb.lower(location.get("city")), "%" + city.toLowerCase() + "%")); - if (neighborhood != null) predicate = cb.and(predicate, cb.like(cb.lower(location.get("neighborhood")), "%" + neighborhood.toLowerCase() + "%")); - if (!Collections.isEmpty(specialties)) { - Subquery subquery = buildSpecialtiesSubquery(query, specialties, cb); - predicate = cb.and(predicate, root.get("id").in(subquery)); + if (name != null) { + predicate = cb.and(predicate, cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%")); } - if (dayOfWeek != null && requestedStart != null && requestedEnd != null) { - predicate = cb.and(predicate, cb.equal(schedule.get("day"), dayOfWeek)); - predicate = cb.and(predicate, cb.lessThanOrEqualTo(schedule.get("startTime"), requestedStart)); - predicate = cb.and(predicate, cb.greaterThanOrEqualTo(schedule.get("endTime"), requestedEnd)); + if (gender != null) { + predicate = cb.and(predicate, cb.equal(root.get("gender"), gender)); } - + predicate = checkIfInterpreterHasModality(predicate, cb, root, modality); + predicate = checkIfInterpreterHasLocation(predicate, cb, root, uf, city, neighborhood); + predicate = checkIfInterpreterHasSpecialties(predicate, query, cb, root, specialties); + predicate = checkIfInterpreterIsAvailableOnRequestedDate(predicate, query, cb, root, availableDate); return predicate; }; } - private static Subquery buildSpecialtiesSubquery(CriteriaQuery query, List specialties, - CriteriaBuilder criteriaBuilder) { + private static Predicate checkIfInterpreterHasModality(Predicate predicate, + CriteriaBuilder criteriaBuilder, + Root root, + InterpreterModality modality) { + if (modality == InterpreterModality.ALL) { + predicate = criteriaBuilder.and(predicate, root.get("modality") + .in(InterpreterModality.ALL, InterpreterModality.ONLINE, InterpreterModality.PERSONALLY)); + } else if (Objects.nonNull(modality)) { + predicate = criteriaBuilder.and(predicate, root.get("modality").in(InterpreterModality.ALL, modality)); + } + return predicate; + } + + private static Predicate checkIfInterpreterHasLocation(Predicate predicate, + CriteriaBuilder criteriaBuilder, + Root root, + String uf, + String city, + String neighborhood) { + + Join location = root.join("locations", JoinType.LEFT); + if (Objects.nonNull(uf)) { + predicate = criteriaBuilder.and(predicate, criteriaBuilder.equal(location.get("uf"), uf)); + } + if (Objects.nonNull(city)) { + predicate = criteriaBuilder.and(predicate, criteriaBuilder + .like(criteriaBuilder.lower(location.get("city")), "%" + city.toLowerCase() + "%")); + } + if (Objects.nonNull(neighborhood)) { + predicate = criteriaBuilder.and(predicate, criteriaBuilder + .like(criteriaBuilder.lower(location.get("neighborhood")), "%" + neighborhood.toLowerCase() + "%")); + } + return predicate; + } + + private static Predicate checkIfInterpreterHasSpecialties(Predicate predicate, + CriteriaQuery query, + CriteriaBuilder criteriaBuilder, + Root root, + List specialties) { + if (Collections.isEmpty(specialties)) { + return predicate; + } Subquery subquery = query.subquery(UUID.class); var subRoot = subquery.from(UserSpecialty.class); subquery.select(subRoot.get("user").get("id")) .where(subRoot.get("specialty").get("id").in(specialties)) .groupBy(subRoot.get("user").get("id")) .having(criteriaBuilder.equal(criteriaBuilder.countDistinct(subRoot.get("specialty").get("id")), specialties.size())); - return subquery; + return criteriaBuilder.and(predicate, root.get("id").in(subquery)); + } + + private static Predicate checkIfInterpreterIsAvailableOnRequestedDate(Predicate predicate, + CriteriaQuery query, + CriteriaBuilder criteriaBuilder, + Root root, + LocalDateTime availableDate) { + if (Objects.isNull(availableDate)) { + return predicate; + } + + LocalDate requestedDate = availableDate.toLocalDate(); + DayOfWeek dayOfWeek = DayOfWeek.valueOf(availableDate.getDayOfWeek().name().substring(0, 3)); + LocalTime requestedStart = availableDate.toLocalTime(); + LocalTime requestedEnd = requestedStart.plusHours(1); + + predicate = checkIfInterpreterHasScheduleOnRequestedDate(predicate, criteriaBuilder, root, dayOfWeek, requestedStart, requestedEnd); + return checkIfInterpreterHasNoAppointmentOnRequestDate(predicate, query, criteriaBuilder, root, requestedDate, requestedStart, requestedEnd); + } + + private static Predicate checkIfInterpreterHasScheduleOnRequestedDate(Predicate predicate, + CriteriaBuilder criteriaBuilder, + Root root, + DayOfWeek dayOfWeek, + LocalTime requestedStart, + LocalTime requestedEnd) { + Join schedule = root.join("schedules", JoinType.LEFT); + predicate = criteriaBuilder.and(predicate, criteriaBuilder.equal(schedule.get("day"), dayOfWeek)); + predicate = criteriaBuilder.and(predicate, criteriaBuilder + .lessThanOrEqualTo(schedule.get(START_TIME_FIELD), requestedStart)); + return criteriaBuilder.and(predicate, criteriaBuilder + .greaterThanOrEqualTo(schedule.get(END_TIME_FIELD), requestedEnd)); + } + + private static Predicate checkIfInterpreterHasNoAppointmentOnRequestDate(Predicate predicate, + CriteriaQuery query, + CriteriaBuilder criteriaBuilder, + Root root, + LocalDate requestedDate, + LocalTime requestedStart, + LocalTime requestedEnd) { + Subquery subquery = query.subquery(UUID.class); + var subRoot = subquery.from(Appointment.class); + subquery.select(subRoot.get("interpreter").get("id")) + .where(criteriaBuilder.and( + subRoot.get("status").in(AppointmentStatus.ACCEPTED, AppointmentStatus.COMPLETED), + criteriaBuilder.equal(subRoot.get("date"), requestedDate), + criteriaBuilder.or( + criteriaBuilder.and( + criteriaBuilder.greaterThan(subRoot.get(START_TIME_FIELD), requestedStart), + criteriaBuilder.lessThan(subRoot.get(START_TIME_FIELD), requestedEnd) + ), + criteriaBuilder.and( + criteriaBuilder.greaterThan(subRoot.get(END_TIME_FIELD), requestedStart), + criteriaBuilder.lessThan(subRoot.get(END_TIME_FIELD), requestedEnd) + ), + criteriaBuilder.and( + criteriaBuilder.lessThanOrEqualTo(subRoot.get(START_TIME_FIELD), requestedStart), + criteriaBuilder.greaterThanOrEqualTo(subRoot.get(END_TIME_FIELD), requestedEnd) + ) + ) + )); + return criteriaBuilder.and(predicate, criteriaBuilder.not(root.get("id").in(subquery))); } } \ No newline at end of file diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java new file mode 100644 index 00000000..edac81ea --- /dev/null +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java @@ -0,0 +1,249 @@ +package com.pointtils.pointtils.src.infrastructure.repositories.spec; + +import com.pointtils.pointtils.src.core.domain.entities.Appointment; +import com.pointtils.pointtils.src.core.domain.entities.Interpreter; +import com.pointtils.pointtils.src.core.domain.entities.Location; +import com.pointtils.pointtils.src.core.domain.entities.Schedule; +import com.pointtils.pointtils.src.core.domain.entities.Specialty; +import com.pointtils.pointtils.src.core.domain.entities.UserSpecialty; +import com.pointtils.pointtils.src.core.domain.entities.enums.AppointmentModality; +import com.pointtils.pointtils.src.core.domain.entities.enums.AppointmentStatus; +import com.pointtils.pointtils.src.core.domain.entities.enums.DayOfWeek; +import com.pointtils.pointtils.src.core.domain.entities.enums.Gender; +import com.pointtils.pointtils.src.core.domain.entities.enums.InterpreterModality; +import com.pointtils.pointtils.src.core.domain.entities.enums.UserStatus; +import com.pointtils.pointtils.src.core.domain.entities.enums.UserTypeE; +import com.pointtils.pointtils.src.infrastructure.repositories.AppointmentRepository; +import com.pointtils.pointtils.src.infrastructure.repositories.InterpreterRepository; +import com.pointtils.pointtils.src.infrastructure.repositories.ScheduleRepository; +import com.pointtils.pointtils.src.infrastructure.repositories.SpecialtyRepository; +import com.pointtils.pointtils.src.infrastructure.repositories.UserSpecialtyRepository; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.jpa.domain.Specification; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Transactional +@SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) +class InterpreterSpecificationIntegrationTest { + + @Autowired + private InterpreterRepository interpreterRepository; + @Autowired + private SpecialtyRepository specialtyRepository; + @Autowired + private UserSpecialtyRepository userSpecialtyRepository; + @Autowired + private ScheduleRepository scheduleRepository; + @Autowired + private AppointmentRepository appointmentRepository; + + @AfterEach + void clearDatabase() { + interpreterRepository.deleteAll(); + } + + @Test + void shouldFilterInterpretersByName() { + Interpreter interpreter = buildInterpreter(); + interpreterRepository.save(interpreter); + + Specification spec = InterpreterSpecification.filter( + null, null, null, null, null, null, null, "Roberto" + ); + + List result = interpreterRepository.findAll(spec); + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); + } + + @ParameterizedTest + @CsvSource(value = { + "ONLINE,ONLINE", + "ALL,ONLINE", + "PERSONALLY,PERSONALLY", + "ALL,PERSONALLY", + "ONLINE,ALL", + "PERSONALLY,ALL", + "ALL,ALL" + }) + void shouldFilterInterpretersByModality(String interpreterModality, String modalityToFilterBy) { + Interpreter interpreter = buildInterpreter(); + InterpreterModality convertedInterpreterModality = InterpreterModality.fromString(interpreterModality); + interpreter.setModality(convertedInterpreterModality); + interpreterRepository.save(interpreter); + + Specification spec = InterpreterSpecification.filter( + InterpreterModality.fromString(modalityToFilterBy), null, null, null, null, null, null, null + ); + + List result = interpreterRepository.findAll(spec); + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); + assertThat(result.get(0).getModality()).isEqualTo(convertedInterpreterModality); + } + + @ParameterizedTest + @CsvSource(value = { + "ONLINE,PERSONALLY", + "PERSONALLY,ONLINE" + }) + void shouldFilterZeroInterpretersByModality(String interpreterModality, String modalityToFilterBy) { + Interpreter interpreter = buildInterpreter(); + InterpreterModality convertedInterpreterModality = InterpreterModality.fromString(interpreterModality); + interpreter.setModality(convertedInterpreterModality); + interpreterRepository.save(interpreter); + + Specification spec = InterpreterSpecification.filter( + InterpreterModality.fromString(modalityToFilterBy), null, null, null, null, null, null, null + ); + + List result = interpreterRepository.findAll(spec); + assertEquals(0, result.size()); + } + + @Test + void shouldFilterInterpretersByLocation() { + Interpreter interpreter = buildInterpreter(); + Location location = buildLocation(interpreter); + interpreter.setLocations(List.of(location)); + interpreterRepository.save(interpreter); + + Specification spec = InterpreterSpecification.filter( + null, "RS", "Porto Alegre", "Auxiliadora", null, null, null, null + ); + + List result = interpreterRepository.findAll(spec); + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); + assertThat(result.get(0).getLocations().get(0).getUf()).isEqualTo("RS"); + assertThat(result.get(0).getLocations().get(0).getCity()).isEqualTo("Porto Alegre"); + assertThat(result.get(0).getLocations().get(0).getNeighborhood()).isEqualTo("Auxiliadora"); + } + + @Test + void shouldFilterInterpretersByGender() { + Interpreter interpreter = buildInterpreter(); + interpreterRepository.save(interpreter); + + Specification spec = InterpreterSpecification.filter( + null, null, null, null, null, Gender.MALE, null, null + ); + + List result = interpreterRepository.findAll(spec); + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); + } + + @Test + void shouldFilterInterpretersByMultipleSpecialties() { + Interpreter interpreter = buildInterpreter(); + interpreter = interpreterRepository.save(interpreter); + + Specialty firstSpecialty = new Specialty("Intérprete Tátil"); + firstSpecialty = specialtyRepository.save(firstSpecialty); + Specialty secondSpecialty = new Specialty("Intérprete de Libras"); + secondSpecialty = specialtyRepository.save(secondSpecialty); + + userSpecialtyRepository.save(new UserSpecialty(firstSpecialty, interpreter)); + userSpecialtyRepository.save(new UserSpecialty(secondSpecialty, interpreter)); + + Specification spec = InterpreterSpecification.filter( + null, null, null, null, List.of(firstSpecialty.getId(), secondSpecialty.getId()), null, null, null + ); + + List result = interpreterRepository.findAll(spec); + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); + } + + @ParameterizedTest + @CsvSource(value = { + "10:00,11:00,10:00", //requested time starts when appointment starts + "10:00,11:00,10:30", //requested time starts in the middle of the appointment + "10:00,11:00,09:30", //requested time ends in the middle of the appointment + "10:40,10:50,10:00", //requested time envelops the appointment + }) + @Disabled + void shouldFilterZeroInterpretersIfDateTimeConflictsWithAppointment(String appointmentStartTime, + String appointmentEndTime, + String requestedTime) { + Interpreter interpreter = buildInterpreter(); + interpreter = interpreterRepository.save(interpreter); + + Schedule schedule = buildSchedule(interpreter); + scheduleRepository.save(schedule); + + LocalDate date = LocalDate.of(2025, 10, 6); + Appointment appointment = buildAppointment(interpreter, date, LocalTime.parse(appointmentStartTime), LocalTime.parse(appointmentEndTime)); + appointmentRepository.save(appointment); + + LocalDateTime requestedDateTime = LocalDateTime.of(date, LocalTime.parse(requestedTime)); + Specification spec = InterpreterSpecification.filter( + null, null, null, null, null, null, requestedDateTime, null + ); + + List result = interpreterRepository.findAll(spec); + assertEquals(0, result.size()); + } + + private Interpreter buildInterpreter() { + return Interpreter.builder() + .name("Carlos Roberto Júnior") + .cpf("11122233344") + .gender(Gender.MALE) + .birthday(LocalDate.of(1990, 1, 1)) + .email("carlos.roberto@email.com") + .password("password") + .phone("54987548754") + .status(UserStatus.ACTIVE) + .type(UserTypeE.INTERPRETER) + .build(); + } + + private Location buildLocation(Interpreter interpreter) { + Location location = new Location(); + location.setUf("RS"); + location.setCity("Porto Alegre"); + location.setNeighborhood("Auxiliadora"); + location.setInterpreter(interpreter); + return location; + } + + private Schedule buildSchedule(Interpreter interpreter) { + return Schedule.builder() + .day(DayOfWeek.MON) + .startTime(LocalTime.of(8, 0)) + .startTime(LocalTime.of(20, 0)) + .interpreter(interpreter) + .build(); + } + + private Appointment buildAppointment(Interpreter interpreter, LocalDate date, LocalTime startTime, LocalTime endTime) { + return Appointment.builder() + .modality(AppointmentModality.ONLINE) + .status(AppointmentStatus.ACCEPTED) + .description("Appointment") + .date(date) + .startTime(startTime) + .endTime(endTime) + .user(interpreter) + .interpreter(interpreter) + .build(); + } +} \ No newline at end of file diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationTest.java deleted file mode 100644 index 810f10c9..00000000 --- a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationTest.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.pointtils.pointtils.src.infrastructure.repositories.spec; - -import com.pointtils.pointtils.src.core.domain.entities.Interpreter; -import com.pointtils.pointtils.src.core.domain.entities.Schedule; -import com.pointtils.pointtils.src.core.domain.entities.enums.DayOfWeek; -import com.pointtils.pointtils.src.core.domain.entities.enums.Gender; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Expression; -import jakarta.persistence.criteria.Join; -import jakarta.persistence.criteria.JoinType; -import jakarta.persistence.criteria.Path; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; -import org.junit.jupiter.api.Test; -import org.springframework.data.jpa.domain.Specification; - -import java.time.LocalTime; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@SuppressWarnings({"rawtypes", "unchecked"}) -class InterpreterSpecificationTest { - - @Test - void shouldBuildPredicateWithGender() { - Root root = mock(Root.class); - CriteriaQuery query = mock(CriteriaQuery.class); - CriteriaBuilder cb = mock(CriteriaBuilder.class); - Predicate basePredicate = mock(Predicate.class); - Predicate genderPredicate = mock(Predicate.class); - - when(cb.conjunction()).thenReturn(basePredicate); - when(cb.equal(root.get("gender"), Gender.MALE)).thenReturn(genderPredicate); - when(cb.and(basePredicate, genderPredicate)).thenReturn(genderPredicate); - - Specification spec = InterpreterSpecification.filter(null, null, null, null, - null, Gender.MALE, null, null, null, null); - - Predicate result = spec.toPredicate(root, query, cb); - - assertThat(result).isEqualTo(genderPredicate); - verify(cb).equal(root.get("gender"), Gender.MALE); - } - - @Test - void shouldBuildPredicateWithCity() { - Root root = mock(Root.class); - CriteriaQuery query = mock(CriteriaQuery.class); - CriteriaBuilder cb = mock(CriteriaBuilder.class); - Predicate basePredicate = mock(Predicate.class); - Predicate cityPredicate = mock(Predicate.class); - - Join locationJoin = (Join) mock(Join.class); - - Path cityPath = mock(Path.class); - Expression lowerCityExpression = mock(Expression.class); - - when(root.join("locations", JoinType.LEFT)).thenReturn((Join) locationJoin); - when(locationJoin.get("city")).thenReturn(cityPath); - when(cb.lower(cityPath)).thenReturn(lowerCityExpression); - when(cb.like(lowerCityExpression, "%são paulo%")).thenReturn(cityPredicate); - when(cb.and(basePredicate, cityPredicate)).thenReturn(cityPredicate); - when(cb.conjunction()).thenReturn(basePredicate); - - Specification spec = InterpreterSpecification.filter(null, null, "São Paulo", - null, null, null, null, null, null, null); - - Predicate result = spec.toPredicate(root, query, cb); - - assertThat(result).isEqualTo(cityPredicate); - verify(cb).like(lowerCityExpression, "%são paulo%"); - } - - @Test - void shouldBuildPredicateWithUF() { - Root root = mock(Root.class); - CriteriaQuery query = mock(CriteriaQuery.class); - CriteriaBuilder cb = mock(CriteriaBuilder.class); - Predicate basePredicate = mock(Predicate.class); - Predicate ufPredicate = mock(Predicate.class); - - Join locationJoin = (Join) mock(Join.class); - - Path ufPath = mock(Path.class); - - when(root.join("locations", JoinType.LEFT)).thenReturn((Join) locationJoin); - when(locationJoin.get("uf")).thenReturn(ufPath); - when(cb.equal(ufPath, "RS")).thenReturn(ufPredicate); - when(cb.and(basePredicate, ufPredicate)).thenReturn(ufPredicate); - when(cb.conjunction()).thenReturn(basePredicate); - - Specification spec = InterpreterSpecification.filter(null, "RS", null, - null, null, null, null, null, null, null); - - Predicate result = spec.toPredicate(root, query, cb); - - assertThat(result).isEqualTo(ufPredicate); - verify(cb).equal(ufPath, "RS"); - } - - @Test - void shouldBuildPredicateWithScheduleAndTime() { - Root root = mock(Root.class); - CriteriaQuery query = mock(CriteriaQuery.class); - CriteriaBuilder cb = mock(CriteriaBuilder.class); - Predicate basePredicate = mock(Predicate.class); - Predicate dayPredicate = mock(Predicate.class); - Predicate startPredicate = mock(Predicate.class); - Predicate endPredicate = mock(Predicate.class); - - Join scheduleJoin = (Join) (Join) mock(Join.class); - - when(root.join("schedules", JoinType.LEFT)).thenReturn((Join) scheduleJoin); - when(cb.conjunction()).thenReturn(basePredicate); - - LocalTime requestedStart = LocalTime.of(10, 0); - LocalTime requestedEnd = requestedStart.plusHours(1); - - when(cb.equal(scheduleJoin.get("day"), DayOfWeek.MON)).thenReturn(dayPredicate); - when(cb.lessThanOrEqualTo(scheduleJoin.get("startTime"), requestedStart)).thenReturn(startPredicate); - when(cb.greaterThanOrEqualTo(scheduleJoin.get("endTime"), requestedEnd)).thenReturn(endPredicate); - - when(cb.and(basePredicate, dayPredicate)).thenReturn(dayPredicate); - when(cb.and(dayPredicate, startPredicate)).thenReturn(startPredicate); - when(cb.and(startPredicate, endPredicate)).thenReturn(endPredicate); - - Specification spec = InterpreterSpecification.filter(null, null, null, null, - null, null, DayOfWeek.MON, requestedStart, requestedEnd, null); - - Predicate result = spec.toPredicate(root, query, cb); - - assertThat(result).isEqualTo(endPredicate); - verify(cb).equal(scheduleJoin.get("day"), DayOfWeek.MON); - verify(cb).lessThanOrEqualTo(scheduleJoin.get("startTime"), requestedStart); - verify(cb).greaterThanOrEqualTo(scheduleJoin.get("endTime"), requestedEnd); - } - - @Test - void shouldBuildPredicateWithName() { - Root root = mock(Root.class); - CriteriaQuery query = mock(CriteriaQuery.class); - CriteriaBuilder cb = mock(CriteriaBuilder.class); - Predicate basePredicate = mock(Predicate.class); - Predicate namePredicate = mock(Predicate.class); - - Path namePath = mock(Path.class); - Expression lowerNameExpression = mock(Expression.class); - - when(cb.conjunction()).thenReturn(basePredicate); - when(root.get("name")).thenReturn(namePath); - when(cb.lower(namePath)).thenReturn(lowerNameExpression); - when(cb.like(lowerNameExpression, "%souza%")).thenReturn(namePredicate); - when(cb.and(basePredicate, namePredicate)).thenReturn(namePredicate); - - Specification spec = InterpreterSpecification.filter(null, null, null, null, - null, null, null, null, null, "SOUZA"); - - Predicate result = spec.toPredicate(root, query, cb); - - assertThat(result).isEqualTo(namePredicate); - verify(cb).like(lowerNameExpression, "%souza%"); - } -} From a3e404363203383c9542dba075f8badf58268d49 Mon Sep 17 00:00:00 2001 From: "fernanda.mello" Date: Sun, 5 Oct 2025 20:32:44 -0300 Subject: [PATCH 2/6] Corrige testes integrados para InterpreterSpecification --- pointtils/pom.xml | 14 ++++- ...terpreterSpecificationIntegrationTest.java | 52 +++++++++++++++++-- .../application-testcontainers.properties | 33 ++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 pointtils/src/test/resources/application-testcontainers.properties diff --git a/pointtils/pom.xml b/pointtils/pom.xml index 64559072..d886ad32 100644 --- a/pointtils/pom.xml +++ b/pointtils/pom.xml @@ -76,7 +76,19 @@ h2 test - + + org.testcontainers + junit-jupiter + 1.19.8 + test + + + org.testcontainers + postgresql + 1.19.6 + test + + org.springframework.boot spring-boot-starter-security diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java index edac81ea..044bb106 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java @@ -20,14 +20,15 @@ import com.pointtils.pointtils.src.infrastructure.repositories.UserSpecialtyRepository; import jakarta.transaction.Transactional; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.junit.jupiter.Testcontainers; import java.time.LocalDate; import java.time.LocalDateTime; @@ -38,8 +39,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @Transactional +@Testcontainers @SpringBootTest -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) +@ActiveProfiles(value = "testcontainers") class InterpreterSpecificationIntegrationTest { @Autowired @@ -179,7 +181,6 @@ void shouldFilterInterpretersByMultipleSpecialties() { "10:00,11:00,09:30", //requested time ends in the middle of the appointment "10:40,10:50,10:00", //requested time envelops the appointment }) - @Disabled void shouldFilterZeroInterpretersIfDateTimeConflictsWithAppointment(String appointmentStartTime, String appointmentEndTime, String requestedTime) { @@ -202,6 +203,47 @@ void shouldFilterZeroInterpretersIfDateTimeConflictsWithAppointment(String appoi assertEquals(0, result.size()); } + @Test + void shouldFilterZeroInterpretersIfDateTimeIsNotDuringSchedule() { + Interpreter interpreter = buildInterpreter(); + interpreter = interpreterRepository.save(interpreter); + + Schedule schedule = buildSchedule(interpreter); + scheduleRepository.save(schedule); + + LocalDateTime requestedDateTime = LocalDateTime.of(2025, 10, 6, 20, 0); + Specification spec = InterpreterSpecification.filter( + null, null, null, null, null, null, requestedDateTime, null + ); + + List result = interpreterRepository.findAll(spec); + assertEquals(0, result.size()); + } + + @ParameterizedTest + @EnumSource(value = AppointmentStatus.class, names = {"CANCELED", "PENDING"}) + void shouldFilterInterpretersIfDateTimeConflictsWithInactiveAppointment(AppointmentStatus status) { + Interpreter interpreter = buildInterpreter(); + interpreter = interpreterRepository.save(interpreter); + + Schedule schedule = buildSchedule(interpreter); + scheduleRepository.save(schedule); + + LocalDate date = LocalDate.of(2025, 10, 6); + Appointment appointment = buildAppointment(interpreter, date, LocalTime.of(13, 30), LocalTime.of(16, 0)); + appointment.setStatus(status); + appointmentRepository.save(appointment); + + LocalDateTime requestedDateTime = LocalDateTime.of(date, LocalTime.of(14, 0)); + Specification spec = InterpreterSpecification.filter( + null, null, null, null, null, null, requestedDateTime, null + ); + + List result = interpreterRepository.findAll(spec); + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); + } + private Interpreter buildInterpreter() { return Interpreter.builder() .name("Carlos Roberto Júnior") @@ -229,7 +271,7 @@ private Schedule buildSchedule(Interpreter interpreter) { return Schedule.builder() .day(DayOfWeek.MON) .startTime(LocalTime.of(8, 0)) - .startTime(LocalTime.of(20, 0)) + .endTime(LocalTime.of(20, 0)) .interpreter(interpreter) .build(); } diff --git a/pointtils/src/test/resources/application-testcontainers.properties b/pointtils/src/test/resources/application-testcontainers.properties new file mode 100644 index 00000000..0fc6bf21 --- /dev/null +++ b/pointtils/src/test/resources/application-testcontainers.properties @@ -0,0 +1,33 @@ +# Spring Application Configuration +spring.application.name=pointtils-api-test +server.port=0 + +# Test Database Configuration (PostgreSQL with Test Containers) +spring.datasource.url=jdbc:tc:postgresql:15:///testdb +spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=test +spring.datasource.password=test +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=create-drop + +spring.jpa.show-sql=false + +# JWT Configuration for tests +security.jwt.secret-key=dGVzdHNlY3JldGtleWZvcnRlc3RzMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0 +security.jwt.expiration-time=900000 +security.jwt.refresh-expiration-time=86400000 + +# Flyway Configuration (disabled for tests since we use H2 with create-drop) +spring.flyway.enabled=false + +# Swagger/OpenAPI Configuration (disabled for tests) +springdoc.api-docs.enabled=false +springdoc.swagger-ui.enabled=false +springdoc.swagger-ui.path=/swagger-ui.html + +# Clientes HTTP +client.ibge.state-url=http://exemplo.com.br/estados +client.ibge.city-url=http://exemplo.com.br/estados/{state}/municipios + +cloud.aws.region.static=us-east-2 +aws.region=us-east-2 \ No newline at end of file From 43dbb18de81d0dc90884567b9bd272d926103163 Mon Sep 17 00:00:00 2001 From: "fernanda.mello" Date: Sun, 5 Oct 2025 20:55:30 -0300 Subject: [PATCH 3/6] Remove anotacao AfterEach de InterpreterSpecificationIntegrationTest devido ao uso de Transactional --- .../spec/InterpreterSpecificationIntegrationTest.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java index 044bb106..d2a535d7 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java @@ -19,7 +19,6 @@ import com.pointtils.pointtils.src.infrastructure.repositories.SpecialtyRepository; import com.pointtils.pointtils.src.infrastructure.repositories.UserSpecialtyRepository; import jakarta.transaction.Transactional; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -41,7 +40,7 @@ @Transactional @Testcontainers @SpringBootTest -@ActiveProfiles(value = "testcontainers") +@ActiveProfiles("testcontainers") class InterpreterSpecificationIntegrationTest { @Autowired @@ -55,11 +54,6 @@ class InterpreterSpecificationIntegrationTest { @Autowired private AppointmentRepository appointmentRepository; - @AfterEach - void clearDatabase() { - interpreterRepository.deleteAll(); - } - @Test void shouldFilterInterpretersByName() { Interpreter interpreter = buildInterpreter(); From ac9e049b1c604b8204d35cb351c5b4bac8eeb31c Mon Sep 17 00:00:00 2001 From: "fernanda.mello" Date: Sun, 5 Oct 2025 21:06:02 -0300 Subject: [PATCH 4/6] Corrige InterpreterSpecificationIntegrationTest para definir propriedades do testcontainers via TestPropertySource --- ...terpreterSpecificationIntegrationTest.java | 9 +++-- .../application-testcontainers.properties | 33 ------------------- 2 files changed, 7 insertions(+), 35 deletions(-) delete mode 100644 pointtils/src/test/resources/application-testcontainers.properties diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java index d2a535d7..43070605 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java @@ -26,7 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.jpa.domain.Specification; -import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; import org.testcontainers.junit.jupiter.Testcontainers; import java.time.LocalDate; @@ -40,7 +40,12 @@ @Transactional @Testcontainers @SpringBootTest -@ActiveProfiles("testcontainers") +@TestPropertySource(properties = { + "spring.datasource.url=jdbc:tc:postgresql:15:///testdb", + "spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver", + "spring.datasource.username=test", + "spring.datasource.password=test" +}) class InterpreterSpecificationIntegrationTest { @Autowired diff --git a/pointtils/src/test/resources/application-testcontainers.properties b/pointtils/src/test/resources/application-testcontainers.properties deleted file mode 100644 index 0fc6bf21..00000000 --- a/pointtils/src/test/resources/application-testcontainers.properties +++ /dev/null @@ -1,33 +0,0 @@ -# Spring Application Configuration -spring.application.name=pointtils-api-test -server.port=0 - -# Test Database Configuration (PostgreSQL with Test Containers) -spring.datasource.url=jdbc:tc:postgresql:15:///testdb -spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver -spring.datasource.username=test -spring.datasource.password=test -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=create-drop - -spring.jpa.show-sql=false - -# JWT Configuration for tests -security.jwt.secret-key=dGVzdHNlY3JldGtleWZvcnRlc3RzMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0 -security.jwt.expiration-time=900000 -security.jwt.refresh-expiration-time=86400000 - -# Flyway Configuration (disabled for tests since we use H2 with create-drop) -spring.flyway.enabled=false - -# Swagger/OpenAPI Configuration (disabled for tests) -springdoc.api-docs.enabled=false -springdoc.swagger-ui.enabled=false -springdoc.swagger-ui.path=/swagger-ui.html - -# Clientes HTTP -client.ibge.state-url=http://exemplo.com.br/estados -client.ibge.city-url=http://exemplo.com.br/estados/{state}/municipios - -cloud.aws.region.static=us-east-2 -aws.region=us-east-2 \ No newline at end of file From 367016abf34aabafe1687111f9d004e96b741630 Mon Sep 17 00:00:00 2001 From: "fernanda.mello" Date: Sun, 5 Oct 2025 21:11:19 -0300 Subject: [PATCH 5/6] Corrige versoes utilizadas de testcontainers --- pointtils/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pointtils/pom.xml b/pointtils/pom.xml index d886ad32..4aa963b3 100644 --- a/pointtils/pom.xml +++ b/pointtils/pom.xml @@ -85,7 +85,7 @@ org.testcontainers postgresql - 1.19.6 + 1.19.8 test From e8058d5e09070656d799a96d60d44147a8ff9e71 Mon Sep 17 00:00:00 2001 From: "fernanda.mello" Date: Wed, 8 Oct 2025 21:25:24 -0300 Subject: [PATCH 6/6] Corrige testes unitarios --- pointtils/pom.xml | 12 - ...terpreterSpecificationIntegrationTest.java | 290 ------------------ .../spec/InterpreterSpecificationTest.java | 272 ++++++++++++++++ 3 files changed, 272 insertions(+), 302 deletions(-) delete mode 100644 pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java create mode 100644 pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationTest.java diff --git a/pointtils/pom.xml b/pointtils/pom.xml index e652292d..55616bbc 100644 --- a/pointtils/pom.xml +++ b/pointtils/pom.xml @@ -76,18 +76,6 @@ h2 test - - org.testcontainers - junit-jupiter - 1.19.8 - test - - - org.testcontainers - postgresql - 1.19.8 - test - org.springframework.boot spring-boot-starter-security diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java deleted file mode 100644 index 43070605..00000000 --- a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationIntegrationTest.java +++ /dev/null @@ -1,290 +0,0 @@ -package com.pointtils.pointtils.src.infrastructure.repositories.spec; - -import com.pointtils.pointtils.src.core.domain.entities.Appointment; -import com.pointtils.pointtils.src.core.domain.entities.Interpreter; -import com.pointtils.pointtils.src.core.domain.entities.Location; -import com.pointtils.pointtils.src.core.domain.entities.Schedule; -import com.pointtils.pointtils.src.core.domain.entities.Specialty; -import com.pointtils.pointtils.src.core.domain.entities.UserSpecialty; -import com.pointtils.pointtils.src.core.domain.entities.enums.AppointmentModality; -import com.pointtils.pointtils.src.core.domain.entities.enums.AppointmentStatus; -import com.pointtils.pointtils.src.core.domain.entities.enums.DayOfWeek; -import com.pointtils.pointtils.src.core.domain.entities.enums.Gender; -import com.pointtils.pointtils.src.core.domain.entities.enums.InterpreterModality; -import com.pointtils.pointtils.src.core.domain.entities.enums.UserStatus; -import com.pointtils.pointtils.src.core.domain.entities.enums.UserTypeE; -import com.pointtils.pointtils.src.infrastructure.repositories.AppointmentRepository; -import com.pointtils.pointtils.src.infrastructure.repositories.InterpreterRepository; -import com.pointtils.pointtils.src.infrastructure.repositories.ScheduleRepository; -import com.pointtils.pointtils.src.infrastructure.repositories.SpecialtyRepository; -import com.pointtils.pointtils.src.infrastructure.repositories.UserSpecialtyRepository; -import jakarta.transaction.Transactional; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.test.context.TestPropertySource; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - -@Transactional -@Testcontainers -@SpringBootTest -@TestPropertySource(properties = { - "spring.datasource.url=jdbc:tc:postgresql:15:///testdb", - "spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver", - "spring.datasource.username=test", - "spring.datasource.password=test" -}) -class InterpreterSpecificationIntegrationTest { - - @Autowired - private InterpreterRepository interpreterRepository; - @Autowired - private SpecialtyRepository specialtyRepository; - @Autowired - private UserSpecialtyRepository userSpecialtyRepository; - @Autowired - private ScheduleRepository scheduleRepository; - @Autowired - private AppointmentRepository appointmentRepository; - - @Test - void shouldFilterInterpretersByName() { - Interpreter interpreter = buildInterpreter(); - interpreterRepository.save(interpreter); - - Specification spec = InterpreterSpecification.filter( - null, null, null, null, null, null, null, "Roberto" - ); - - List result = interpreterRepository.findAll(spec); - assertThat(result).hasSize(1); - assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); - } - - @ParameterizedTest - @CsvSource(value = { - "ONLINE,ONLINE", - "ALL,ONLINE", - "PERSONALLY,PERSONALLY", - "ALL,PERSONALLY", - "ONLINE,ALL", - "PERSONALLY,ALL", - "ALL,ALL" - }) - void shouldFilterInterpretersByModality(String interpreterModality, String modalityToFilterBy) { - Interpreter interpreter = buildInterpreter(); - InterpreterModality convertedInterpreterModality = InterpreterModality.fromString(interpreterModality); - interpreter.setModality(convertedInterpreterModality); - interpreterRepository.save(interpreter); - - Specification spec = InterpreterSpecification.filter( - InterpreterModality.fromString(modalityToFilterBy), null, null, null, null, null, null, null - ); - - List result = interpreterRepository.findAll(spec); - assertThat(result).hasSize(1); - assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); - assertThat(result.get(0).getModality()).isEqualTo(convertedInterpreterModality); - } - - @ParameterizedTest - @CsvSource(value = { - "ONLINE,PERSONALLY", - "PERSONALLY,ONLINE" - }) - void shouldFilterZeroInterpretersByModality(String interpreterModality, String modalityToFilterBy) { - Interpreter interpreter = buildInterpreter(); - InterpreterModality convertedInterpreterModality = InterpreterModality.fromString(interpreterModality); - interpreter.setModality(convertedInterpreterModality); - interpreterRepository.save(interpreter); - - Specification spec = InterpreterSpecification.filter( - InterpreterModality.fromString(modalityToFilterBy), null, null, null, null, null, null, null - ); - - List result = interpreterRepository.findAll(spec); - assertEquals(0, result.size()); - } - - @Test - void shouldFilterInterpretersByLocation() { - Interpreter interpreter = buildInterpreter(); - Location location = buildLocation(interpreter); - interpreter.setLocations(List.of(location)); - interpreterRepository.save(interpreter); - - Specification spec = InterpreterSpecification.filter( - null, "RS", "Porto Alegre", "Auxiliadora", null, null, null, null - ); - - List result = interpreterRepository.findAll(spec); - assertThat(result).hasSize(1); - assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); - assertThat(result.get(0).getLocations().get(0).getUf()).isEqualTo("RS"); - assertThat(result.get(0).getLocations().get(0).getCity()).isEqualTo("Porto Alegre"); - assertThat(result.get(0).getLocations().get(0).getNeighborhood()).isEqualTo("Auxiliadora"); - } - - @Test - void shouldFilterInterpretersByGender() { - Interpreter interpreter = buildInterpreter(); - interpreterRepository.save(interpreter); - - Specification spec = InterpreterSpecification.filter( - null, null, null, null, null, Gender.MALE, null, null - ); - - List result = interpreterRepository.findAll(spec); - assertThat(result).hasSize(1); - assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); - } - - @Test - void shouldFilterInterpretersByMultipleSpecialties() { - Interpreter interpreter = buildInterpreter(); - interpreter = interpreterRepository.save(interpreter); - - Specialty firstSpecialty = new Specialty("Intérprete Tátil"); - firstSpecialty = specialtyRepository.save(firstSpecialty); - Specialty secondSpecialty = new Specialty("Intérprete de Libras"); - secondSpecialty = specialtyRepository.save(secondSpecialty); - - userSpecialtyRepository.save(new UserSpecialty(firstSpecialty, interpreter)); - userSpecialtyRepository.save(new UserSpecialty(secondSpecialty, interpreter)); - - Specification spec = InterpreterSpecification.filter( - null, null, null, null, List.of(firstSpecialty.getId(), secondSpecialty.getId()), null, null, null - ); - - List result = interpreterRepository.findAll(spec); - assertThat(result).hasSize(1); - assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); - } - - @ParameterizedTest - @CsvSource(value = { - "10:00,11:00,10:00", //requested time starts when appointment starts - "10:00,11:00,10:30", //requested time starts in the middle of the appointment - "10:00,11:00,09:30", //requested time ends in the middle of the appointment - "10:40,10:50,10:00", //requested time envelops the appointment - }) - void shouldFilterZeroInterpretersIfDateTimeConflictsWithAppointment(String appointmentStartTime, - String appointmentEndTime, - String requestedTime) { - Interpreter interpreter = buildInterpreter(); - interpreter = interpreterRepository.save(interpreter); - - Schedule schedule = buildSchedule(interpreter); - scheduleRepository.save(schedule); - - LocalDate date = LocalDate.of(2025, 10, 6); - Appointment appointment = buildAppointment(interpreter, date, LocalTime.parse(appointmentStartTime), LocalTime.parse(appointmentEndTime)); - appointmentRepository.save(appointment); - - LocalDateTime requestedDateTime = LocalDateTime.of(date, LocalTime.parse(requestedTime)); - Specification spec = InterpreterSpecification.filter( - null, null, null, null, null, null, requestedDateTime, null - ); - - List result = interpreterRepository.findAll(spec); - assertEquals(0, result.size()); - } - - @Test - void shouldFilterZeroInterpretersIfDateTimeIsNotDuringSchedule() { - Interpreter interpreter = buildInterpreter(); - interpreter = interpreterRepository.save(interpreter); - - Schedule schedule = buildSchedule(interpreter); - scheduleRepository.save(schedule); - - LocalDateTime requestedDateTime = LocalDateTime.of(2025, 10, 6, 20, 0); - Specification spec = InterpreterSpecification.filter( - null, null, null, null, null, null, requestedDateTime, null - ); - - List result = interpreterRepository.findAll(spec); - assertEquals(0, result.size()); - } - - @ParameterizedTest - @EnumSource(value = AppointmentStatus.class, names = {"CANCELED", "PENDING"}) - void shouldFilterInterpretersIfDateTimeConflictsWithInactiveAppointment(AppointmentStatus status) { - Interpreter interpreter = buildInterpreter(); - interpreter = interpreterRepository.save(interpreter); - - Schedule schedule = buildSchedule(interpreter); - scheduleRepository.save(schedule); - - LocalDate date = LocalDate.of(2025, 10, 6); - Appointment appointment = buildAppointment(interpreter, date, LocalTime.of(13, 30), LocalTime.of(16, 0)); - appointment.setStatus(status); - appointmentRepository.save(appointment); - - LocalDateTime requestedDateTime = LocalDateTime.of(date, LocalTime.of(14, 0)); - Specification spec = InterpreterSpecification.filter( - null, null, null, null, null, null, requestedDateTime, null - ); - - List result = interpreterRepository.findAll(spec); - assertThat(result).hasSize(1); - assertThat(result.get(0).getName()).isEqualTo("Carlos Roberto Júnior"); - } - - private Interpreter buildInterpreter() { - return Interpreter.builder() - .name("Carlos Roberto Júnior") - .cpf("11122233344") - .gender(Gender.MALE) - .birthday(LocalDate.of(1990, 1, 1)) - .email("carlos.roberto@email.com") - .password("password") - .phone("54987548754") - .status(UserStatus.ACTIVE) - .type(UserTypeE.INTERPRETER) - .build(); - } - - private Location buildLocation(Interpreter interpreter) { - Location location = new Location(); - location.setUf("RS"); - location.setCity("Porto Alegre"); - location.setNeighborhood("Auxiliadora"); - location.setInterpreter(interpreter); - return location; - } - - private Schedule buildSchedule(Interpreter interpreter) { - return Schedule.builder() - .day(DayOfWeek.MON) - .startTime(LocalTime.of(8, 0)) - .endTime(LocalTime.of(20, 0)) - .interpreter(interpreter) - .build(); - } - - private Appointment buildAppointment(Interpreter interpreter, LocalDate date, LocalTime startTime, LocalTime endTime) { - return Appointment.builder() - .modality(AppointmentModality.ONLINE) - .status(AppointmentStatus.ACCEPTED) - .description("Appointment") - .date(date) - .startTime(startTime) - .endTime(endTime) - .user(interpreter) - .interpreter(interpreter) - .build(); - } -} \ No newline at end of file diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationTest.java new file mode 100644 index 00000000..935dedde --- /dev/null +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/repositories/spec/InterpreterSpecificationTest.java @@ -0,0 +1,272 @@ +package com.pointtils.pointtils.src.infrastructure.repositories.spec; + +import com.pointtils.pointtils.src.core.domain.entities.Appointment; +import com.pointtils.pointtils.src.core.domain.entities.Interpreter; +import com.pointtils.pointtils.src.core.domain.entities.Schedule; +import com.pointtils.pointtils.src.core.domain.entities.enums.AppointmentStatus; +import com.pointtils.pointtils.src.core.domain.entities.enums.DayOfWeek; +import com.pointtils.pointtils.src.core.domain.entities.enums.Gender; +import com.pointtils.pointtils.src.core.domain.entities.enums.InterpreterModality; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; +import org.junit.jupiter.api.Test; +import org.springframework.data.jpa.domain.Specification; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +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; + +@SuppressWarnings({"rawtypes", "unchecked"}) +class InterpreterSpecificationTest { + + @Test + void shouldBuildPredicateWithGender() { + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + Predicate basePredicate = mock(Predicate.class); + Predicate genderPredicate = mock(Predicate.class); + + when(cb.conjunction()).thenReturn(basePredicate); + when(cb.equal(root.get("gender"), Gender.MALE)).thenReturn(genderPredicate); + when(cb.and(basePredicate, genderPredicate)).thenReturn(genderPredicate); + + Specification spec = InterpreterSpecification.filter(null, null, null, null, + null, Gender.MALE, null, null); + + Predicate result = spec.toPredicate(root, query, cb); + + assertThat(result).isEqualTo(genderPredicate); + verify(cb).equal(root.get("gender"), Gender.MALE); + } + + @Test + void shouldBuildPredicateWithAllModality() { + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + Predicate basePredicate = mock(Predicate.class); + when(cb.conjunction()).thenReturn(basePredicate); + + Predicate modalityPredicate = mock(Predicate.class); + Path modalityPath = mock(Path.class); + when(root.get("modality")).thenReturn(modalityPath); + when(modalityPath.in(InterpreterModality.ALL, InterpreterModality.ONLINE, InterpreterModality.PERSONALLY)) + .thenReturn(modalityPredicate); + + Predicate resultPredicate = mock(Predicate.class); + when(cb.and(basePredicate, modalityPredicate)).thenReturn(resultPredicate); + + Specification spec = InterpreterSpecification.filter(InterpreterModality.ALL, null, null, + null, null, null, null, null); + + Predicate result = spec.toPredicate(root, query, cb); + assertEquals(resultPredicate, result); + } + + @Test + void shouldBuildPredicateWithOnlineModality() { + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + Predicate basePredicate = mock(Predicate.class); + when(cb.conjunction()).thenReturn(basePredicate); + + Predicate modalityPredicate = mock(Predicate.class); + Path modalityPath = mock(Path.class); + when(root.get("modality")).thenReturn(modalityPath); + when(modalityPath.in(InterpreterModality.ALL, InterpreterModality.ONLINE)) + .thenReturn(modalityPredicate); + + Predicate resultPredicate = mock(Predicate.class); + when(cb.and(basePredicate, modalityPredicate)).thenReturn(resultPredicate); + + Specification spec = InterpreterSpecification.filter(InterpreterModality.ONLINE, null, null, + null, null, null, null, null); + + Predicate result = spec.toPredicate(root, query, cb); + assertEquals(resultPredicate, result); + } + + @Test + void shouldBuildPredicateWithCity() { + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + Predicate basePredicate = mock(Predicate.class); + Predicate cityPredicate = mock(Predicate.class); + + Join locationJoin = (Join) mock(Join.class); + + Path cityPath = mock(Path.class); + Expression lowerCityExpression = mock(Expression.class); + + when(root.join("locations", JoinType.LEFT)).thenReturn((Join) locationJoin); + when(locationJoin.get("city")).thenReturn(cityPath); + when(cb.lower(cityPath)).thenReturn(lowerCityExpression); + when(cb.like(lowerCityExpression, "%são paulo%")).thenReturn(cityPredicate); + when(cb.and(basePredicate, cityPredicate)).thenReturn(cityPredicate); + when(cb.conjunction()).thenReturn(basePredicate); + + Specification spec = InterpreterSpecification.filter(null, null, "São Paulo", + null, null, null, null, null); + + Predicate result = spec.toPredicate(root, query, cb); + + assertThat(result).isEqualTo(cityPredicate); + verify(cb).like(lowerCityExpression, "%são paulo%"); + } + + @Test + void shouldBuildPredicateWithUF() { + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + Predicate basePredicate = mock(Predicate.class); + Predicate ufPredicate = mock(Predicate.class); + + Join locationJoin = (Join) mock(Join.class); + + Path ufPath = mock(Path.class); + + when(root.join("locations", JoinType.LEFT)).thenReturn((Join) locationJoin); + when(locationJoin.get("uf")).thenReturn(ufPath); + when(cb.equal(ufPath, "RS")).thenReturn(ufPredicate); + when(cb.and(basePredicate, ufPredicate)).thenReturn(ufPredicate); + when(cb.conjunction()).thenReturn(basePredicate); + + Specification spec = InterpreterSpecification.filter(null, "RS", null, + null, null, null, null, null); + + Predicate result = spec.toPredicate(root, query, cb); + + assertThat(result).isEqualTo(ufPredicate); + verify(cb).equal(ufPath, "RS"); + } + + @Test + void shouldBuildPredicateWithAvailableDate() { + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + Predicate basePredicate = mock(Predicate.class); + + Join scheduleJoin = (Join) (Join) mock(Join.class); + when(cb.conjunction()).thenReturn(basePredicate); + + LocalDateTime availableDate = LocalDateTime.of(2025, 10, 6, 10, 0); + LocalTime requestedStart = availableDate.toLocalTime(); + LocalTime requestedEnd = requestedStart.plusHours(1); + + // Mock schedule predicates + Predicate schedulePredicate = mock(Predicate.class); + Path scheduleStartTimePath = mock(Path.class); + Path scheduleEndTimePath = mock(Path.class); + when(scheduleJoin.get("startTime")).thenReturn(scheduleStartTimePath); + when(scheduleJoin.get("endTime")).thenReturn(scheduleEndTimePath); + + when(root.join("schedules", JoinType.LEFT)).thenReturn((Join) scheduleJoin); + when(cb.equal(scheduleJoin.get("day"), DayOfWeek.MON)).thenReturn(schedulePredicate); + when(cb.lessThanOrEqualTo(scheduleStartTimePath, requestedStart)).thenReturn(schedulePredicate); + when(cb.greaterThanOrEqualTo(scheduleEndTimePath, requestedEnd)).thenReturn(schedulePredicate); + when(cb.and(basePredicate, schedulePredicate)).thenReturn(schedulePredicate); + + // Mock subquery for appointments + Subquery subquery = mock(Subquery.class); + Root appointmentSubRoot = mock(Root.class); + when(query.subquery(UUID.class)).thenReturn(subquery); + when(subquery.from(Appointment.class)).thenReturn(appointmentSubRoot); + + Path interpreterPath = mock(Path.class); + Path idPath = mock(Path.class); + when(appointmentSubRoot.get("interpreter")).thenReturn(interpreterPath); + when(interpreterPath.get("id")).thenReturn(idPath); + when(subquery.select(idPath)).thenReturn(subquery); + + Predicate subqueryAndPredicate = mock(Predicate.class); + Predicate firstConditionPredicate = mock(Predicate.class); + Predicate secondConditionPredicate = mock(Predicate.class); + Predicate thirdConditionPredicate = mock(Predicate.class); + when(cb.and(firstConditionPredicate, secondConditionPredicate, thirdConditionPredicate)).thenReturn(subqueryAndPredicate); + + Path statusPath = mock(Path.class); + when(appointmentSubRoot.get("status")).thenReturn(statusPath); + when(statusPath.in(AppointmentStatus.ACCEPTED, AppointmentStatus.COMPLETED)).thenReturn(firstConditionPredicate); + + Path datePath = mock(Path.class); + when(appointmentSubRoot.get("date")).thenReturn(datePath); + when(cb.equal(datePath, LocalDate.of(2025, 10, 6))).thenReturn(secondConditionPredicate); + + Predicate greaterThanPredicate = mock(Predicate.class); + Predicate lessThanPredicate = mock(Predicate.class); + Predicate joinGreaterThanLessThanPredicate = mock(Predicate.class); + when(cb.greaterThan(any(), eq(requestedStart))).thenReturn(greaterThanPredicate); + when(cb.lessThan(any(), eq(requestedEnd))).thenReturn(lessThanPredicate); + when(cb.and(greaterThanPredicate, lessThanPredicate)).thenReturn(joinGreaterThanLessThanPredicate); + when(cb.lessThanOrEqualTo(any(), eq(requestedStart))).thenReturn(lessThanPredicate); + when(cb.greaterThanOrEqualTo(any(), eq(requestedEnd))).thenReturn(greaterThanPredicate); + when(cb.and(lessThanPredicate, greaterThanPredicate)).thenReturn(joinGreaterThanLessThanPredicate); + when(cb.or(joinGreaterThanLessThanPredicate, joinGreaterThanLessThanPredicate, joinGreaterThanLessThanPredicate)) + .thenReturn(thirdConditionPredicate); + + when(subquery.where(subqueryAndPredicate)).thenReturn(subquery); + + Predicate inSubqueryPredicate = mock(Predicate.class); + when(root.get("id")).thenReturn(idPath); + when(idPath.in(subquery)).thenReturn(inSubqueryPredicate); + + when(cb.not(idPath.in(subquery))).thenReturn(inSubqueryPredicate); + when(cb.and(schedulePredicate, inSubqueryPredicate)).thenReturn(inSubqueryPredicate); + + Specification spec = InterpreterSpecification.filter(null, null, null, null, + null, null, availableDate, null); + + assertDoesNotThrow(() -> spec.toPredicate(root, query, cb)); + verify(cb).equal(scheduleJoin.get("day"), DayOfWeek.MON); + verify(cb).lessThanOrEqualTo(scheduleStartTimePath, requestedStart); + verify(cb).greaterThanOrEqualTo(scheduleEndTimePath, requestedEnd); + } + + @Test + void shouldBuildPredicateWithName() { + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + Predicate basePredicate = mock(Predicate.class); + Predicate namePredicate = mock(Predicate.class); + + Path namePath = mock(Path.class); + Expression lowerNameExpression = mock(Expression.class); + + when(cb.conjunction()).thenReturn(basePredicate); + when(root.get("name")).thenReturn(namePath); + when(cb.lower(namePath)).thenReturn(lowerNameExpression); + when(cb.like(lowerNameExpression, "%souza%")).thenReturn(namePredicate); + when(cb.and(basePredicate, namePredicate)).thenReturn(namePredicate); + + Specification spec = InterpreterSpecification.filter(null, null, null, null, + null, null, null, "SOUZA"); + + Predicate result = spec.toPredicate(root, query, cb); + + assertThat(result).isEqualTo(namePredicate); + verify(cb).like(lowerNameExpression, "%souza%"); + } +} \ No newline at end of file