diff --git a/src/main/java/org/pkwmtt/examCalendar/ExamControllerAdvice.java b/src/main/java/org/pkwmtt/examCalendar/ExamControllerAdvice.java index 20a9e8b..ea77710 100644 --- a/src/main/java/org/pkwmtt/examCalendar/ExamControllerAdvice.java +++ b/src/main/java/org/pkwmtt/examCalendar/ExamControllerAdvice.java @@ -45,4 +45,9 @@ public ResponseEntity handleConstraintViolationException(Const .collect(Collectors.joining(", ")); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponseDTO(message)); } + + @ExceptionHandler(ResourceAlreadyExistsException.class) + public ResponseEntity handleConflicts(RuntimeException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponseDTO(e.getMessage())); + } } diff --git a/src/main/java/org/pkwmtt/examCalendar/ExamService.java b/src/main/java/org/pkwmtt/examCalendar/ExamService.java index 58621b0..3bd6bb7 100644 --- a/src/main/java/org/pkwmtt/examCalendar/ExamService.java +++ b/src/main/java/org/pkwmtt/examCalendar/ExamService.java @@ -36,12 +36,15 @@ public int addExam(ExamDto examDto) { Set groups = verifyAndUpdateExamGroups(examDto); -// check if exam type exists ExamType examType = examTypeRepository.findByName(examDto.getExamType()) .orElseThrow(() -> new ExamTypeNotExistsException(examDto.getExamType())); -// save exam in repository and return id of created exam - return examRepository.save(ExamDtoMapper.mapToNewExam(examDto, groups, examType)).getExamId(); + Exam exam = ExamDtoMapper.mapToNewExam(examDto, groups, examType); + Set existingExam = examRepository.findAllByTitle(exam.getTitle()); + + if (existingExam.contains(exam)) + throw new ResourceAlreadyExistsException("Exam already exists"); + return examRepository.save(exam).getExamId(); } /** @@ -49,12 +52,11 @@ public int addExam(ExamDto examDto) { * @param id of exam that need to be modified */ public void modifyExam(ExamDto examDto, int id) { -// check if exam which would be modified exists + examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id)); Set groups = verifyAndUpdateExamGroups(examDto); -// check if exam type exists ExamType examType = examTypeRepository.findByName(examDto.getExamType()) .orElseThrow(() -> new ExamTypeNotExistsException(examDto.getExamType())); @@ -77,32 +79,23 @@ public Exam getExamById(int id) { return examRepository.findById(id).orElseThrow(() -> new NoSuchElementWithProvidedIdException(id)); } + /** + * @param generalGroups set of general groups from the same year of study + * e.g. 12K1, 12K2 are from 12K year of study, + * but 12A1 and 12B1 or 11A1 and 12A1 aren't from the same year + * @param subgroups subgroups that belong to provided general groups + * @return set of exams containing provided groups + */ public Set getExamByGroups(Set generalGroups, Set subgroups) { -// verify generalGroups identifiers + + String superiorGroup = trimLastDigit(generalGroups); verifyGeneralGroupsFormat(generalGroups); -// get exams for general groups - Set exams = new HashSet<>(examRepository.findAllByGroups_NameIn(generalGroups)); - exams = exams.stream() - .filter(exam -> exam.getGroups().stream() - .allMatch(group -> group.getName().matches("^\\d.*"))) - .collect(Collectors.toSet()); -// convert general group identifiers. e.g. 12K2 to 12K - Set superiorGroups = generalGroups.stream().map(g -> { - if (Character.isDigit(g.charAt(g.length() - 1))) - return g.substring(0, g.length() - 1); - return g; - }).collect(Collectors.toSet()); -// check if subgroups are provided - if (subgroups != null && !subgroups.isEmpty()) { -// verify subgroups identifiers - verifySubgroupsFormat(subgroups); -// check if superior group identifies the groups unambiguously - if (superiorGroups.size() != 1) - throw new InvalidGroupIdentifierException("ambiguous superior group identifier for subgroups"); - exams.addAll(examRepository.findAllBySubgroupsOfGeneralGroup(superiorGroups.iterator().next(), subgroups)); - } - return exams; + if(subgroups == null || subgroups.isEmpty()) + return examRepository.findAllByGroups_NameIn(generalGroups); + + verifySubgroupsFormat(subgroups); + return examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup(superiorGroup, generalGroups, subgroups); } /** @@ -112,87 +105,123 @@ public List getExamTypes() { return examTypeRepository.findAll(); } + /** - * verify if groups exists in timetable if exist updates database. - * when timetable service is unavailable verifies groups using groupsRepository - * + * verify if groups exists and updates database when it exists, but repository doesn't contain it. + * When timetable service is unavailable verifies groups using groupsRepository * @param examDto containing groups for verification + * @return single set of all kinds of provided groups as StudentGroup entities + * that are in database and could be safely attach to Exam entity */ private Set verifyAndUpdateExamGroups(ExamDto examDto) { - Set generalGroupsFromRepository; Set generalGroups = examDto.getGeneralGroups(); Set subgroups = examDto.getSubgroups(); -// if timetable service is unavailable verify general groups using GroupRepository + + if (generalGroups == null || generalGroups.isEmpty()) + throw new InvalidGroupIdentifierException("general group is missing"); + + verifyGeneralGroups(generalGroups); + + if (subgroups == null || subgroups.isEmpty()) + return saveNewStudentGroups(generalGroups); + + if (generalGroups.size() > 1) + throw new InvalidGroupIdentifierException("ambiguous general groups for subgroups"); + + String superiorGroup = generalGroups.iterator().next(); + verifySubgroups(superiorGroup, subgroups); + + subgroups.add(trimLastDigit(superiorGroup)); + return saveNewStudentGroups(subgroups); + } + + /** + * verifies provided generalGroups using timetable service or repository when service is unavailable + * @param generalGroups that would be verified + */ + private void verifyGeneralGroups(Set generalGroups) { try { - generalGroupsFromRepository = new HashSet<>(timetableService.getGeneralGroupList()); + Set existingGeneralGroups = new HashSet<>(timetableService.getGeneralGroupList()); + if (!existingGeneralGroups.containsAll(generalGroups)) + throw new InvalidGroupIdentifierException(existingGeneralGroups, generalGroups); } catch (WebPageContentNotAvailableException e) { - generalGroupsFromRepository = verifyGroupsUsingRepository(generalGroups); - } -// verify generalGroups using timetable service - if (!generalGroupsFromRepository.containsAll(generalGroups)) { - generalGroups.removeAll(generalGroupsFromRepository); - throw new InvalidGroupIdentifierException(generalGroups); + verifyGeneralGroupsUsingRepository(generalGroups); } -// if there are no subgroups save exam for exercise groups or whole year e.g. -// 12K2 - exercise group exam -// 12K1, 12K2, 12K3 - whole year exam - if (subgroups == null || subgroups.isEmpty()) { - return saveNewStudentGroups(generalGroups); -// exams for subgroups e.g. L04 must have only superior group to avoid ambiguity - } else if (generalGroups.size() == 1) { -// if there are only one group change it from Set to String - String superiorGroup = generalGroups.iterator().next(); - Set subGroupsFromTimetable; - try { - subGroupsFromTimetable = new HashSet<>(timetableService.getAvailableSubGroups(superiorGroup)); - } catch (JsonProcessingException | - SpecifiedGeneralGroupDoesntExistsException | - WebPageContentNotAvailableException e) { - throw new ServiceNotAvailableException("Couldn't verify groups using timetable service"); -// TODO: add verification with repository when timetable service is unavailable - } -// verify if subgroups for specific general group exists - if (!subGroupsFromTimetable.containsAll(subgroups)) { - subgroups.removeAll(subGroupsFromTimetable); - throw new InvalidGroupIdentifierException(subgroups); - } -// change superior group format e.g. 12K2 to 12K - if (Character.isDigit(superiorGroup.charAt(superiorGroup.length() - 1))) - superiorGroup = superiorGroup.substring(0, superiorGroup.length() - 1); -// save subgroups with superior group identifier - subgroups.add(superiorGroup); - return saveNewStudentGroups(subgroups); - } -// only one general group could be assigned to subgroups (when there are more than 1 general group and -// more than 0 subgroups) - else if (generalGroups.isEmpty()) - throw new InvalidGroupIdentifierException("general group is missing"); - else - throw new InvalidGroupIdentifierException("ambiguous general groups for subgroups"); } /** - * @param groups groups that would be verified using repository - * @return set of groups (String) when verification succeeded - * @throws WebPageContentNotAvailableException when verification not succeeded + * @param groups that would be verified using repository + * @throws ServiceNotAvailableException when verification not succeeded */ - private Set verifyGroupsUsingRepository(Set groups) throws WebPageContentNotAvailableException { + private void verifyGeneralGroupsUsingRepository(Set groups) throws ServiceNotAvailableException { verifyGeneralGroupsFormat(groups); Set groupsFromRepository = groupRepository.findAllByNameIn(groups).stream() .map(StudentGroup::getName) .collect(Collectors.toSet() ); - if (groupsFromRepository.containsAll(groups)) - return groups; - else - throw new ServiceNotAvailableException("Couldn't verify groups using repository"); + if (!groupsFromRepository.containsAll(groups)) + throw new ServiceNotAvailableException("Timetable service unavailable, couldn't verify groups using repository"); + } + + /** + * verifies provided subgroups using timetable service or repository when service is unavailable + * @param superiorGroup of provided subgroups + * @param subgroups that would be verified + */ + private void verifySubgroups(String superiorGroup, Set subgroups){ + try { + Set subGroupsFromTimetable = new HashSet<>(timetableService.getAvailableSubGroups(superiorGroup)); + if (!subGroupsFromTimetable.containsAll(subgroups)) + throw new InvalidGroupIdentifierException(subGroupsFromTimetable, subgroups); + } catch (JsonProcessingException | + SpecifiedGeneralGroupDoesntExistsException | + WebPageContentNotAvailableException e) { + verifySubgroupsUsingRepository(superiorGroup, subgroups); + } + } + + /** + * @param superiorGroup of provided subgroups + * @param groups subgroups for verification + * @throws ServiceNotAvailableException when verification not succeeded + */ + private void verifySubgroupsUsingRepository(String superiorGroup, Set groups) throws ServiceNotAvailableException { + groups.add(trimLastDigit(superiorGroup)); + if(examRepository.findCommonExamIdsForGroups(groups, groups.size()).isEmpty()) + throw new ServiceNotAvailableException("Timetable service unavailable, couldn't verify groups using repository"); + } + + /** + * extract superior group form general group e.g. 12K2 -> 12K + * @param generalGroup group for transformation + * @return superior group + */ + private static String trimLastDigit(String generalGroup) { + char lastChar = generalGroup.charAt(generalGroup.length() - 1); + if (Character.isDigit(lastChar)) + generalGroup = generalGroup.substring(0, generalGroup.length() - 1); + return generalGroup; + } + + /** + * extract common superior group form provided general groups e.g. 12K2 -> 12K + * @param superiorGroups set of general groups from the same year of study + * @return single superior group of provided general groups + * @throws InvalidGroupIdentifierException when not all provided groups belong to the same year of study + */ + private static String trimLastDigit(Set superiorGroups) throws InvalidGroupIdentifierException { + Set trimmedGroups = superiorGroups.stream() + .map(ExamService::trimLastDigit) + .collect(Collectors.toSet()); + if(trimmedGroups.size() > 1) + throw new InvalidGroupIdentifierException("ambiguous general groups for subgroups"); + return trimmedGroups.iterator().next(); } /** * saves groups to groupRepository, existing group names are filtered out before saving - * * @param groups groups that would be saved to repository - * @return set of StudentsGroup Entity with names from groups. + * @return set of StudentsGroup Entities with provided names */ private Set saveNewStudentGroups(Set groups) { // remove duplicates before saving records @@ -211,6 +240,10 @@ private Set saveNewStudentGroups(Set groups) { return existingGroups; } + /** + * @param generalGroups general groups for verification + * @throws SpecifiedGeneralGroupDoesntExistsException when format is invalid + */ private static void verifyGeneralGroupsFormat(Set generalGroups) throws SpecifiedGeneralGroupDoesntExistsException { generalGroups.forEach(group -> { if (!group.matches("^\\d.*")) @@ -218,10 +251,14 @@ private static void verifyGeneralGroupsFormat(Set generalGroups) throws }); } - private static void verifySubgroupsFormat(Set subgroups) { + /** + * @param subgroups subgroups for verification + * @throws SpecifiedSubGroupDoesntExistsException when format is invalid + */ + private static void verifySubgroupsFormat(Set subgroups) throws SpecifiedSubGroupDoesntExistsException { subgroups.forEach(group -> { if (!group.matches("^[A-Z].*")) throw new SpecifiedSubGroupDoesntExistsException(group); }); } -} +} \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/examCalendar/mapper/ExamDtoMapper.java b/src/main/java/org/pkwmtt/examCalendar/mapper/ExamDtoMapper.java index cf3c3aa..aae4659 100644 --- a/src/main/java/org/pkwmtt/examCalendar/mapper/ExamDtoMapper.java +++ b/src/main/java/org/pkwmtt/examCalendar/mapper/ExamDtoMapper.java @@ -11,7 +11,7 @@ import java.util.stream.Collectors; /** - * maps ExamDto to Exam entity. Couldn't be utility class, because needs ExamTypeRepository to validate exam types + * Utility class for mapping Exam entity to ExamDto and ExamDto to Exam entity */ @Slf4j public class ExamDtoMapper { @@ -51,10 +51,18 @@ public static Exam mapToExistingExam(ExamDto examDto, Set groups, .build(); } + /** + * @param exams Set of Exams that would be mapped + * @return List of ExamDtos + */ public static List mapToExamDto(Set exams) { return exams.stream().map(ExamDtoMapper::mapToExamDto).collect(Collectors.toList()); } + /** + * @param exam single exam that would be mapped + * @return ExamDto + */ public static ExamDto mapToExamDto(Exam exam) { Set groups = exam.getGroups().stream().map(StudentGroup::getName).collect(Collectors.toSet()); Set generalGroups = groups.stream().filter(group -> Character.isDigit(group.charAt(0))).collect(Collectors.toSet()); diff --git a/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java b/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java index c87b283..eec6202 100644 --- a/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java +++ b/src/main/java/org/pkwmtt/examCalendar/repository/ExamRepository.java @@ -5,27 +5,52 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.List; import java.util.Set; public interface ExamRepository extends JpaRepository { + Set findAllByTitle(String title); + + /** * @param groups set of generalGroups - * @return list of exams for generalGroups + * @return set of exams for generalGroups */ - List findAllByGroups_NameIn(Set groups); + Set findAllByGroups_NameIn(Set groups); + /** - * @param generalGroup superior group of subgroups e.g. 12K - * @param subgroup exam groups - * @return list of exams for subgroups + * @param superiorGroup group that identifies whole year of study e.g. 12K + * @param generalGroup set of exercise groups e.g. 12K2 + * @param subgroup set of subgroups of provided superior group e.g. L04 + * @return set of exams containing generalGroups or superiorGroup with at least one provided subgroup */ @Query(""" SELECT DISTINCT e FROM Exam e JOIN e.groups g1 JOIN FETCH e.groups g2 - WHERE g1.name = :general AND g2.name IN :sub + WHERE (g1.name = :superior AND g2.name IN :sub) + OR g2.name IN :general """) - Set findAllBySubgroupsOfGeneralGroup(@Param("general") String generalGroup, @Param("sub") Set subgroup); + Set findAllBySubgroupsOfSuperiorGroupAndGeneralGroup( + @Param("superior") String superiorGroup, + @Param("general") Set generalGroup, + @Param("sub") Set subgroup); + + + /** + * Method could be used to check if provided subgroups belong to superior group by finding existing exam + * related to provided groups in repository + * @param groups set of subgroups with one superior group of provided subgroups e.g. 12K2, K04, K05 + * @param expectedSize size of provided groups set + * @return set of ids of exams that contains all provided groups + */ + @Query(value = """ + SELECT exam_id FROM exams_groups + INNER JOIN student_groups ON exams_groups.group_id = student_groups.group_id + WHERE student_groups.name IN (:groups) + GROUP BY exam_id + HAVING COUNT(DISTINCT exams_groups.group_id) = :expectedSize + """, nativeQuery = true) + Set findCommonExamIdsForGroups(@Param("groups") Set groups, @Param("expectedSize") int expectedSize); } \ No newline at end of file diff --git a/src/main/java/org/pkwmtt/exceptions/InvalidGroupIdentifierException.java b/src/main/java/org/pkwmtt/exceptions/InvalidGroupIdentifierException.java index a47bf28..a5746fc 100644 --- a/src/main/java/org/pkwmtt/exceptions/InvalidGroupIdentifierException.java +++ b/src/main/java/org/pkwmtt/exceptions/InvalidGroupIdentifierException.java @@ -10,4 +10,13 @@ public InvalidGroupIdentifierException(String groupIdentifier) { public InvalidGroupIdentifierException(Set groupIdentifiers) { super("Invalid group identifiers: " + groupIdentifiers.toString()); } + + public InvalidGroupIdentifierException(Set all, Set provided) { + super("Invalid group identifiers: " + extractInvalidGroups(all, provided)); + } + + private static String extractInvalidGroups(Set all, Set provided) { + provided.removeAll(all); + return provided.toString(); + } } diff --git a/src/main/java/org/pkwmtt/exceptions/ResourceAlreadyExistsException.java b/src/main/java/org/pkwmtt/exceptions/ResourceAlreadyExistsException.java new file mode 100644 index 0000000..d1de752 --- /dev/null +++ b/src/main/java/org/pkwmtt/exceptions/ResourceAlreadyExistsException.java @@ -0,0 +1,7 @@ +package org.pkwmtt.exceptions; + +public class ResourceAlreadyExistsException extends RuntimeException { + public ResourceAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java index 44c1549..63bd489 100644 --- a/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java +++ b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java @@ -19,7 +19,6 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -128,6 +127,24 @@ void addExamWithCorrectData() throws Exception { assertEquals(examDtoRequest.getExamType(), examResponse.getExamType().getName()); } + @Test + @Transactional + void addExamTwice() throws Exception { +// given + createExampleExamType("Project"); + ExamDto examDtoRequest = createExampleExamDto("Project"); + + when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1","12K2","12K3")); + when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04","L04","P04")); + +// when + assertPostRequest(status().isCreated(), examDtoRequest); + MvcResult result = assertPostRequest(status().isConflict(), examDtoRequest); +// then + assertResponseMessage("Exam already exists", result); + assertEquals(1, examRepository.findAllByTitle(examDtoRequest.getTitle()).size()); + } + @Test void addExamWithBlankExamTitle() throws Exception { // given @@ -340,7 +357,7 @@ void addExamWithTooLongExamTitle() throws Exception { // given createExampleExamType("Project"); ExamDto requestData = ExamDto.builder() - .title("a".repeat(256)) // 256 znaków + .title("a".repeat(256)) .description("first exam") .date(LocalDateTime.now().plusDays(1)) .examType("Project") @@ -361,7 +378,7 @@ void addExamWithTooLongDescription() throws Exception { createExampleExamType("Project"); ExamDto requestData = ExamDto.builder() .title("Math exam") - .description("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") // 256 znaków + .description("a".repeat(256)) .date(LocalDateTime.now().plusDays(1)) .examType("Project") .generalGroups(Set.of("12K2")) @@ -604,7 +621,7 @@ void getExamsMultipleGeneralGroupsAndSubgroups() throws Exception { // when MvcResult result = assertGetByGroupsRequest(status().isBadRequest(), Set.of("11K2", "12A1"), Set.of("L04")); // then - assertResponseMessage("Invalid group identifier: ambiguous superior group identifier for subgroups",result); + assertResponseMessage("Invalid group identifier: ambiguous general groups for subgroups",result); } @Test @@ -618,7 +635,7 @@ void getExamsWithSwappedGroupNames() throws Exception { @Test void getExamsWithInvalidSubgroup() throws Exception { // when - MvcResult result = assertGetByGroupsRequest(status().isBadRequest(), Set.of("12K1,", "12K2"), Set.of("11K2")); + MvcResult result = assertGetByGroupsRequest(status().isBadRequest(), Set.of("12K1", "12K2"), Set.of("11K2")); // then assertResponseMessage("Specified sub group [11K2] doesn't exists",result); } diff --git a/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java b/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java index fd4edf7..f2c2fff 100644 --- a/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java +++ b/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -13,6 +12,7 @@ import org.pkwmtt.examCalendar.entity.Exam; import org.pkwmtt.examCalendar.entity.ExamType; import org.pkwmtt.examCalendar.entity.StudentGroup; +import org.pkwmtt.examCalendar.mapper.ExamDtoMapper; import org.pkwmtt.examCalendar.repository.ExamRepository; import org.pkwmtt.examCalendar.repository.ExamTypeRepository; import org.pkwmtt.examCalendar.repository.GroupRepository; @@ -217,6 +217,33 @@ void addExamForEmptyGeneralGroup() { assertEquals("Invalid group identifier: general group is missing", exception.getMessage()); } + @Test + void addExamThatAlreadyExists() throws JsonProcessingException { + // given + LocalDateTime date = LocalDateTime.now().plusDays(1); + ExamType examType = buildExampleExamType(); + ExamDto examDto = buildExampleExamDto(Set.of("12K2"), Set.of("L04"), date.plusSeconds(34)); + Set studentGroups = new HashSet<>(buildExampleStudentGroupList(Set.of("12K2", "L04"))); + Exam exam = ExamDtoMapper.mapToNewExam(examDto, studentGroups, examType); + + when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(List.of("12K1", "12K2", "12K3"))); + when(timetableService.getAvailableSubGroups("12K2")).thenReturn(new ArrayList<>(List.of("L04"))); + when(groupRepository.findAllByNameIn(any(Set.class))).thenReturn(studentGroups); +// + + when(examTypeRepository.findByName(examDto.getExamType())).thenReturn(Optional.of(examType)); + when(examRepository.findAllByTitle(examDto.getTitle())).thenReturn(Set.of(exam)); +// when + RuntimeException exception = assertThrows( + ResourceAlreadyExistsException.class, + () -> examService.addExam(examDto) + ); +// then + verify(examRepository, times(0)).save(exam); + assertEquals("Exam already exists", exception.getMessage()); + + } + // // @@ -355,6 +382,46 @@ void addExamForSingleGeneralGroupWithRepositoryContainingGroup() { assertExam(savedExam, date, savedId, generalGroups); } + @Test + void addExamWithNonUniqueTitle() { + // given + Set generalGroups = Set.of("12K2"); + Set subgroups = Set.of(); + + LocalDateTime date = LocalDateTime.now().plusDays(1); + ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date); + ExamType examType = buildExampleExamType(); + List studentGroups = buildExampleStudentGroupList(generalGroups); + Exam newExam = buildExamWithIdAndGroups(1, studentGroups); + Exam existingExam = Exam.builder() + .title("title") + .description("description") + .examDate(date.plusHours(4)) + .examType(examType) + .groups(new HashSet<>(studentGroups)) + .build(); + + when(examTypeRepository.findByName(examDto.getExamType())).thenReturn(Optional.of(examType)); + when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(generalGroups)); + + when(groupRepository.findAllByNameIn(generalGroups)).thenReturn(new HashSet<>(studentGroups)); + when(groupRepository.saveAll(any())).thenReturn(List.of()); + when(examRepository.findAllByTitle(any())).thenReturn(Set.of(existingExam)); + when(examRepository.save(any(Exam.class))).thenReturn(newExam); +// when + int savedId = examService.addExam(examDto); +// then + verify(examTypeRepository, times(1)).findByName(examDto.getExamType()); + verify(timetableService, times(1)).getGeneralGroupList(); + verify(groupRepository, times(1)).findAllByNameIn(any()); //??? + verify(groupRepository, times(1)).saveAll(List.of()); + + ArgumentCaptor examCaptor = ArgumentCaptor.forClass(Exam.class); + verify(examRepository, times(1)).save(examCaptor.capture()); + Exam savedExam = examCaptor.getValue(); + assertExam(savedExam, date, savedId, generalGroups); + } + /** * test specification * generalGroup - 1 item @@ -425,7 +492,7 @@ void unavailableServiceAndRepositoryDontMatch() { // when RuntimeException exception = assertThrows(ServiceNotAvailableException.class, () -> examService.addExam(examDto)); // then - assertEquals("Couldn't verify groups using repository", exception.getMessage()); + assertEquals("Timetable service unavailable, couldn't verify groups using repository", exception.getMessage()); verify(timetableService, times(1)).getGeneralGroupList(); verify(groupRepository, times(1)).findAllByNameIn(generalGroups); } @@ -456,7 +523,7 @@ void unavailableServiceAndRepositoryDontMatchForSubgroups() throws JsonProcessin // when RuntimeException exception = assertThrows(ServiceNotAvailableException.class, () -> examService.addExam(examDto)); // then - assertEquals("Couldn't verify groups using timetable service", exception.getMessage()); + assertEquals("Timetable service unavailable, couldn't verify groups using repository", exception.getMessage()); verify(timetableService, times(1)).getGeneralGroupList(); verify(groupRepository, times(1)).findAllByNameIn(generalGroups); } @@ -514,12 +581,11 @@ void addExamWhenServiceIsUnavailableAndRepositoryContainsGeneralGroups() { * groupRepository - contain provided groups */ @Test - @Disabled("Not supported yet") void addExamWhenServiceIsUnavailableAndRepositoryContainsGroups() throws JsonProcessingException { // given Set generalGroups = Set.of("12K2"); Set subgroups = Set.of("L04", "K04", "P04", "K05"); - Set combinedGroups = Set.of("12K2", "L04", "K04", "P04", "K05"); + Set combinedGroups = Set.of("12K", "12K2", "L04", "K04", "P04", "K05"); LocalDateTime date = LocalDateTime.now().plusDays(1); ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date); @@ -533,8 +599,11 @@ void addExamWhenServiceIsUnavailableAndRepositoryContainsGroups() throws JsonPro //noinspection unchecked when(groupRepository.findAllByNameIn(any(Set.class))).thenReturn(new HashSet<>(studentGroups)); + when(groupRepository.saveAll(anyList())).thenReturn(List.of()); when(examRepository.save(any(Exam.class))).thenReturn(exam); + //noinspection unchecked + when(examRepository.findCommonExamIdsForGroups(any(Set.class), any(Integer.class))).thenReturn(Set.of(1)); // when int savedId = examService.addExam(examDto); // then @@ -546,23 +615,12 @@ void addExamWhenServiceIsUnavailableAndRepositoryContainsGroups() throws JsonPro ArgumentCaptor examCaptor = ArgumentCaptor.forClass(Exam.class); verify(examRepository, times(1)).save(examCaptor.capture()); Exam savedExam = examCaptor.getValue(); - assertExam(savedExam, date, savedId, generalGroups); + assertExam(savedExam, date, savedId, combinedGroups); } // - /************************************************************************************/ //modify exam - @Test - void shouldModifyExamWhenIdExists() { - - } - - @Test - void shouldThrowWhenExamIdNotExists() { - // given - - } /************************************************************************************/ //delete exam @@ -630,20 +688,20 @@ void getExamsForNormalGroups() { // when examService.getExamByGroups(generalGroups, subgroups); // then - verify(examRepository, times(1)).findAllByGroups_NameIn(generalGroups); - verify(examRepository, times(1)).findAllBySubgroupsOfGeneralGroup("12K", subgroups); + verify(examRepository, never()).findAllByGroups_NameIn(any()); + verify(examRepository, times(1)).findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12K", Set.of("12K2"), subgroups); } @Test - void getExamsForGroupWithoutDigitAsFirstCharacter() { + void getExamsForGroupWithoutDigitAsLastCharacter() { // given Set generalGroups = Set.of("1Er"); Set subgroups = Set.of("L01", "K01", "P01"); // when examService.getExamByGroups(generalGroups, subgroups); // then - verify(examRepository, times(1)).findAllByGroups_NameIn(generalGroups); - verify(examRepository, times(1)).findAllBySubgroupsOfGeneralGroup("1Er", subgroups); + verify(examRepository, never()).findAllByGroups_NameIn(any()); + verify(examRepository, times(1)).findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("1Er", generalGroups, subgroups); } @Test @@ -655,7 +713,7 @@ void getExamsWithEmptySubgroups() { examService.getExamByGroups(generalGroups, subgroups); // then verify(examRepository, times(1)).findAllByGroups_NameIn(generalGroups); - verify(examRepository, never()).findAllBySubgroupsOfGeneralGroup(any(), any()); + verify(examRepository, never()).findAllBySubgroupsOfSuperiorGroupAndGeneralGroup(any(), any(), any()); } @Test @@ -667,7 +725,7 @@ void getExamsWithBlankSubgroups() { examService.getExamByGroups(generalGroups, subgroups); // then verify(examRepository, times(1)).findAllByGroups_NameIn(generalGroups); - verify(examRepository, never()).findAllBySubgroupsOfGeneralGroup(any(), any()); + verify(examRepository, never()).findAllBySubgroupsOfSuperiorGroupAndGeneralGroup(any(), any(), any()); } @Test @@ -678,8 +736,8 @@ void shouldNotThrowWhenGroupsAreFromTheSameYearOfStudy() { // when examService.getExamByGroups(generalGroups, subgroups); // then - verify(examRepository, times(1)).findAllByGroups_NameIn(generalGroups); - verify(examRepository, times(1)).findAllBySubgroupsOfGeneralGroup("12K", subgroups); + verify(examRepository, never()).findAllByGroups_NameIn(any()); + verify(examRepository, times(1)).findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12K", generalGroups, subgroups); } @Test @@ -689,7 +747,7 @@ void shouldThrowWhenSubgroupsAreSwappedWithGeneralGroups() { Set subgroups = new HashSet<>( Set.of("12K1")); // when then assertThrows( - SpecifiedGeneralGroupDoesntExistsException.class, + InvalidGroupIdentifierException.class, () -> examService.getExamByGroups(generalGroups, subgroups) ); } @@ -714,7 +772,7 @@ void shouldThrowWhenGeneralGroupsAreFromDifferentYearOfStudy() { // when RuntimeException exception = assertThrows(InvalidGroupIdentifierException.class, () -> examService.getExamByGroups(generalGroups, subgroups)); // then - assertEquals("Invalid group identifier: ambiguous superior group identifier for subgroups", exception.getMessage()); + assertEquals("Invalid group identifier: ambiguous general groups for subgroups", exception.getMessage()); } diff --git a/src/test/java/org/pkwmtt/examCalendar/dto/ExamDtoTest.java b/src/test/java/org/pkwmtt/examCalendar/dto/ExamDtoTest.java index a98fd0e..f278e3b 100644 --- a/src/test/java/org/pkwmtt/examCalendar/dto/ExamDtoTest.java +++ b/src/test/java/org/pkwmtt/examCalendar/dto/ExamDtoTest.java @@ -3,13 +3,9 @@ import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.pkwmtt.examCalendar.entity.StudentGroup; import java.time.LocalDateTime; -import java.util.HashSet; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/src/test/java/org/pkwmtt/examCalendar/repository/ExamRepositoryTest.java b/src/test/java/org/pkwmtt/examCalendar/repository/ExamRepositoryTest.java index 52490f4..e2dd528 100644 --- a/src/test/java/org/pkwmtt/examCalendar/repository/ExamRepositoryTest.java +++ b/src/test/java/org/pkwmtt/examCalendar/repository/ExamRepositoryTest.java @@ -11,7 +11,6 @@ import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import java.time.LocalDateTime; @@ -38,6 +37,13 @@ class ExamRepositoryTest { @Autowired private GroupRepository groupRepository; + private Integer ex1Id; + private Integer ex2Id; + private Integer ex3Id; + private Integer ex4Id; + private Integer ex5Id; + private Integer ex6Id; + @BeforeAll void setUp() { ExamType examType = ExamType.builder() @@ -104,7 +110,7 @@ void setUp() { .description("Linear Algebra") .examDate(LocalDateTime.now().plusDays(1)) .examType(examType) - .groups(Set.of(g12K1, g12K2, g12K3)) + .groups(Set.of(g12K1, g12K2)) .build(); Exam generalGroupExam2 = Exam.builder() @@ -123,17 +129,17 @@ void setUp() { .groups(Set.of(g12A1, g12A2)) .build(); - examRepository.save(smallGroupExam1); - examRepository.save(smallGroupExam2); - examRepository.save(smallGroupExam3); - examRepository.save(generalGroupExam1); - examRepository.save(generalGroupExam2); - examRepository.save(generalGroupExam3); + ex1Id = examRepository.save(smallGroupExam1).getExamId(); + ex2Id = examRepository.save(smallGroupExam2).getExamId(); + ex3Id = examRepository.save(smallGroupExam3).getExamId(); + ex4Id = examRepository.save(generalGroupExam1).getExamId(); + ex5Id = examRepository.save(generalGroupExam2).getExamId(); + ex6Id = examRepository.save(generalGroupExam3).getExamId(); } @Test void shouldReturnExamsWhenNotAllSubgroupsFromRepositoryMatched() { - Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12K", Set.of("L04")); + Set exams = examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12K", Set.of("12K3"), Set.of("L04")); assertEquals(2, exams.size()); List examTitles = exams.stream().map(Exam::getTitle).sorted().toList(); assertEquals("small Group Exam 1", examTitles.get(0)); @@ -142,7 +148,7 @@ void shouldReturnExamsWhenNotAllSubgroupsFromRepositoryMatched() { @Test void shouldReturnExamWhenNotAllSubgroupsFromArgumentsMatchedAndNotReturnExamsForWrongGeneralGroup() { - Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12K", Set.of("L05")); + Set exams = examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12K", Set.of("12K3"), Set.of("L05")); assertEquals(1, exams.size()); List examTitles = exams.stream().map(Exam::getTitle).sorted().toList(); assertEquals("small Group Exam 2", examTitles.getFirst()); @@ -150,28 +156,102 @@ void shouldReturnExamWhenNotAllSubgroupsFromArgumentsMatchedAndNotReturnExamsFor @Test void shouldReturnExamsWhenMultipleArgumentsMatch() { - Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12K", Set.of("L04", "L05")); + Set exams = examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12K", Set.of("12K3"), Set.of("L04", "L05")); assertEquals(2, exams.size()); Set examTitles = exams.stream().map(Exam::getTitle).collect(Collectors.toSet()); assertTrue(examTitles.contains("small Group Exam 1")); assertTrue(examTitles.contains("small Group Exam 2")); } + @Test + void shouldReturnOnlyExamsForGeneralGroups() { + Set exams = examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12K", Set.of("12K1"), Set.of("L01", "L08")); + assertEquals(2, exams.size()); + Set examTitles = exams.stream().map(Exam::getTitle).collect(Collectors.toSet()); + assertTrue(examTitles.contains("general Group Exam 1")); + assertTrue(examTitles.contains("general Group Exam 2")); + } + + @Test + void shouldReturnGeneralGroupExamsWhenSubgroupsIsEmpty() { + Set exams = examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12K", Set.of("12K1"), Set.of()); + assertEquals(2, exams.size()); + Set examTitles = exams.stream().map(Exam::getTitle).collect(Collectors.toSet()); + assertTrue(examTitles.contains("general Group Exam 1")); + assertTrue(examTitles.contains("general Group Exam 2")); + } + + @Test + void shouldReturnExamsForGeneralAndSubgroups() { + Set exams = examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12K", Set.of("12K2"), Set.of("L04", "L05")); + assertEquals(3, exams.size()); + Set examTitles = exams.stream().map(Exam::getTitle).collect(Collectors.toSet()); + assertTrue(examTitles.contains("small Group Exam 1")); + assertTrue(examTitles.contains("small Group Exam 2")); + assertTrue(examTitles.contains("general Group Exam 1")); + } + @Test void ShouldReturnEmptyListWhenSubgroupsSetIsEmpty() { - Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12K", Set.of()); + Set exams = examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12K", Set.of(), Set.of()); assertTrue(exams.isEmpty()); } @Test void shouldReturnEmptyListWhenGeneralGroupIdentifierHasInvalidFormat() { - Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12K2", Set.of("L04")); + Set exams = examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12K2", Set.of(), Set.of("L04")); assertTrue(exams.isEmpty()); } @Test void shouldReturnEmptyListWhenGeneralGroupIdentifierDontMatch() { - Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12B", Set.of("L04", "L05")); + Set exams = examRepository.findAllBySubgroupsOfSuperiorGroupAndGeneralGroup("12B", Set.of("12B1"), Set.of("L04", "L05")); assertTrue(exams.isEmpty()); } + +// findCommonExamIdsForGroups + + @Test + void shouldReturnWhenThereAreMoreGroupsInRepositoryThanArguments() { +// when + Set ids = examRepository.findCommonExamIdsForGroups(Set.of("12K", "L04"), 2); +// then + assertEquals(2, ids.size()); + assertTrue(ids.contains(ex1Id)); + assertTrue(ids.contains(ex2Id)); + } + + @Test + void shouldReturnOnlyWhenAllGroupsMatch() { +// when + Set ids = examRepository.findCommonExamIdsForGroups(Set.of("12K", "L04", "L05"), 3); +// then + assertEquals(1, ids.size()); + assertTrue(ids.contains(ex2Id)); + } + + @Test + void shouldReturnEmptySetWhenArgumentListIsEmpty() { +// when + Set ids = examRepository.findCommonExamIdsForGroups(Set.of(), 0); +// then + assertTrue(ids.isEmpty()); + } + + @Test + void shouldReturnEmptySetWhenThereAreMoreArgumentsThanGroupsInRepository() { +// when + Set ids = examRepository.findCommonExamIdsForGroups(Set.of("12K", "L04", "L05","L06"), 4); +// then + assertTrue(ids.isEmpty()); + } + + @Test + void shouldReturnEmptySetWhenSubgroupNotMatchSuperiorGroup() { +// when + Set ids = examRepository.findCommonExamIdsForGroups(Set.of("L04","G12A"), 2); +// then + assertTrue(ids.isEmpty()); + } + } \ No newline at end of file diff --git a/src/test/resources/application-database.properties b/src/test/resources/application-database.properties index 7f6d0ce..faff684 100644 --- a/src/test/resources/application-database.properties +++ b/src/test/resources/application-database.properties @@ -1,7 +1,3 @@ -#spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false -#spring.datasource.username=sa -#spring.datasource.password= - spring.sql.init.mode=always spring.sql.init.schema-locations=classpath:schema.sql @@ -9,7 +5,7 @@ spring.jpa.show-sql=true spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.datasource.driver-class-name=org.h2.Driver spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false -#spring.jpa.hibernate.ddl-auto=create-drop + spring.datasource.hikari.initialization-fail-timeout=0 spring.mail.host=localhost