From 8bbfef04d1684e127f9489b9d9feae2dd6c7c353 Mon Sep 17 00:00:00 2001 From: constantine0621 Date: Mon, 29 Jun 2026 13:15:15 +0300 Subject: [PATCH 1/4] added Validator interface, created StudentValidator and StudentWebFacade, made changes to StudentMapper, updated StudentServiceTest and created tests for new classes --- .../uniplan/common/model/Validator.java | 5 + .../uniplan/student/StudentController.java | 44 ++----- .../unilab/uniplan/student/StudentMapper.java | 11 +- .../uniplan/student/StudentService.java | 67 +++------- .../uniplan/student/StudentValidator.java | 25 ++++ .../uniplan/student/StudentWebFacade.java | 65 ++++++++++ .../uniplan/student/StudentServiceTest.java | 99 +++----------- .../uniplan/student/StudentValidatorTest.java | 56 ++++++++ .../uniplan/student/StudentWebFacadeTest.java | 122 ++++++++++++++++++ 9 files changed, 324 insertions(+), 170 deletions(-) create mode 100644 src/main/java/org/unilab/uniplan/common/model/Validator.java create mode 100644 src/main/java/org/unilab/uniplan/student/StudentValidator.java create mode 100644 src/main/java/org/unilab/uniplan/student/StudentWebFacade.java create mode 100644 src/test/java/org/unilab/uniplan/student/StudentValidatorTest.java create mode 100644 src/test/java/org/unilab/uniplan/student/StudentWebFacadeTest.java diff --git a/src/main/java/org/unilab/uniplan/common/model/Validator.java b/src/main/java/org/unilab/uniplan/common/model/Validator.java new file mode 100644 index 00000000..e4207496 --- /dev/null +++ b/src/main/java/org/unilab/uniplan/common/model/Validator.java @@ -0,0 +1,5 @@ +package org.unilab.uniplan.common.model; + +public interface Validator { + void validate(final T entity); +} diff --git a/src/main/java/org/unilab/uniplan/student/StudentController.java b/src/main/java/org/unilab/uniplan/student/StudentController.java index b21f45bb..869dda1a 100644 --- a/src/main/java/org/unilab/uniplan/student/StudentController.java +++ b/src/main/java/org/unilab/uniplan/student/StudentController.java @@ -5,7 +5,6 @@ import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.UUID; -import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -16,10 +15,7 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.unilab.uniplan.student.dto.StudentCourseMajorDto; -import org.unilab.uniplan.student.dto.StudentDto; import org.unilab.uniplan.student.dto.StudentRequestDto; import org.unilab.uniplan.student.dto.StudentResponseDto; @@ -29,54 +25,38 @@ @Tag(name = "Students", description = "Manage students, including faculty numbers and enrollment in course") public class StudentController { - private final StudentService studentService; - private final StudentMapper studentMapper; + private final StudentWebFacade studentWebFacade; @PostMapping - public ResponseEntity createStudent(@RequestBody @NotNull - @Valid final StudentRequestDto studentRequestDTO) { - final StudentDto studentDTO = studentMapper.toInternalDto(studentRequestDTO); - studentService.createStudent(studentDTO); - return ResponseEntity.status(HttpStatus.CREATED) - .body(studentMapper.toResponseDto(studentDTO)); + public ResponseEntity createStudent(@RequestBody @NotNull @Valid final StudentRequestDto studentRequestDTO) { + studentWebFacade.createStudent(studentRequestDTO); + return ResponseEntity.status(HttpStatus.CREATED).build(); } @GetMapping("/{id}") - public ResponseEntity getStudent(@PathVariable + public ResponseEntity getStudentById(@PathVariable @NotNull final UUID id) { - final StudentResponseDto studentResponseDTO = studentMapper.toResponseDto(studentService.findStudentById( - id)); - - return ResponseEntity.ok(studentResponseDTO); + return ResponseEntity.ok(studentWebFacade.getStudentById(id)); } @GetMapping - public List getAllStudents() { - return studentMapper.toResponseDtoList(studentService.findAll()); - } - - @GetMapping("/student-course-major/getStudentCourseMajorInfo") - public List getStudentCourseMajorInfo(@RequestParam(required = false) @Size(max = 100) final String firstName, - @RequestParam(required = false) @Size(max = 100) final String lastName, - @RequestParam(required = false) final String facultyNumber, - @RequestParam(required = false) @Size(max = 200) final String majorName){ - return studentService.findStudentCourseMajorInfo(firstName, lastName, facultyNumber, majorName); + public ResponseEntity> getAllStudents() { + return ResponseEntity.ok(studentWebFacade.getAllStudents()); } @PutMapping("/{id}") - public ResponseEntity updateStudent(@PathVariable + public ResponseEntity updateStudent(@PathVariable @NotNull final UUID id, @RequestBody @NotNull @Valid final StudentRequestDto studentRequestDTO) { - final StudentDto studentDTO = studentMapper.toInternalDto(studentRequestDTO); - studentService.updateStudent(id, studentDTO); - return ResponseEntity.ok(studentMapper.toResponseDto(studentDTO)); + studentWebFacade.updateStudent(id, studentRequestDTO); + return ResponseEntity.noContent().build(); } @DeleteMapping("/{id}") public ResponseEntity deleteStudent(@PathVariable @NotNull final UUID id) { - studentService.deleteStudent(id); + studentWebFacade.deleteStudent(id); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/org/unilab/uniplan/student/StudentMapper.java b/src/main/java/org/unilab/uniplan/student/StudentMapper.java index 440e5043..0994e7a3 100644 --- a/src/main/java/org/unilab/uniplan/student/StudentMapper.java +++ b/src/main/java/org/unilab/uniplan/student/StudentMapper.java @@ -12,21 +12,22 @@ public interface StudentMapper { @Mapping(source = "courseId", target = "course.id") - Student toEntity(StudentDto studentDto); + @Mapping(target = "id", ignore = true) + Student toEntity(StudentRequestDto requestDto); @Mapping(source = "course.id", target = "courseId") StudentDto toDto(Student student); @Mapping(target = "id", ignore = true) @Mapping(source = "courseId", target = "course.id") - void updateEntityFromDto(StudentDto studentDto, @MappingTarget Student student); + void updateEntity(StudentRequestDto requestDto, @MappingTarget Student student); @Mapping(target = "id", ignore = true) StudentDto toInternalDto(StudentRequestDto student); - @Mapping(source = "courseId", target = "courseId") - StudentResponseDto toResponseDto(StudentDto studentDto); + @Mapping(source = "course.id", target = "courseId") + StudentResponseDto toResponseDto(Student student); - List toResponseDtoList(List students); + List toResponseDtoList(List students); } \ No newline at end of file diff --git a/src/main/java/org/unilab/uniplan/student/StudentService.java b/src/main/java/org/unilab/uniplan/student/StudentService.java index c616dbd3..cd3dc4e6 100644 --- a/src/main/java/org/unilab/uniplan/student/StudentService.java +++ b/src/main/java/org/unilab/uniplan/student/StudentService.java @@ -1,72 +1,35 @@ package org.unilab.uniplan.student; -import static org.unilab.uniplan.utils.ErrorConstants.STUDENT_NOT_FOUND; - -import jakarta.transaction.Transactional; import java.util.List; +import java.util.Optional; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.unilab.uniplan.exception.ResourceNotFoundException; -import org.unilab.uniplan.student.dto.StudentCourseMajorDto; -import org.unilab.uniplan.student.dto.StudentDto; +import org.unilab.uniplan.common.model.BaseService; @Service @RequiredArgsConstructor -public class StudentService { +public class StudentService implements BaseService { private final StudentRepository studentRepository; - private final StudentMapper studentMapper; - - @Transactional - public StudentDto createStudent(final StudentDto studentDTO) { - final Student student = studentMapper.toEntity(studentDTO); - return saveEntityAndConvertToDto(student); - } - - public StudentDto findStudentById(final UUID id) { - return studentRepository.findById(id) - .map(studentMapper::toDto) - .orElseThrow(() -> new ResourceNotFoundException(STUDENT_NOT_FOUND.getMessage( - String.valueOf(id)))); - } - - public List findAll() { - return studentRepository.findAll() - .stream().map(studentMapper::toDto).toList(); - } - - @Transactional - public StudentDto updateStudent(final UUID id, final StudentDto studentDTO) { - return studentRepository.findById(id) - .map(existingStudent -> updateEntityAndConvertToDto( - studentDTO, - existingStudent)) - .orElseThrow(() -> new ResourceNotFoundException(STUDENT_NOT_FOUND.getMessage( - String.valueOf(id)))); - } - @Transactional - public void deleteStudent(final UUID id) { - final Student student = studentRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException( - STUDENT_NOT_FOUND.getMessage(String.valueOf(id)))); - studentRepository.delete(student); + @Override + public void save(final Student entity) { + studentRepository.save(entity); } - public List findStudentCourseMajorInfo(final String firstName, final String lastName, - final String facultyNumber, final String majorName){ - return studentRepository.searchStudents(firstName, lastName, facultyNumber, majorName); + @Override + public List getAll() { + return studentRepository.findAll(); } - private StudentDto updateEntityAndConvertToDto(final StudentDto dto, - final Student entity) { - studentMapper.updateEntityFromDto(dto, entity); - return saveEntityAndConvertToDto(entity); + @Override + public Optional getById(final UUID id) { + return studentRepository.findById(id); } - private StudentDto saveEntityAndConvertToDto(final Student entity) { - final Student savedEntity = studentRepository.save(entity); - return studentMapper.toDto(savedEntity); + @Override + public void delete(final Student entity) { + studentRepository.delete(entity); } } diff --git a/src/main/java/org/unilab/uniplan/student/StudentValidator.java b/src/main/java/org/unilab/uniplan/student/StudentValidator.java new file mode 100644 index 00000000..17ae03f8 --- /dev/null +++ b/src/main/java/org/unilab/uniplan/student/StudentValidator.java @@ -0,0 +1,25 @@ +package org.unilab.uniplan.student; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.unilab.uniplan.common.model.Validator; +import org.unilab.uniplan.course.CourseRepository; +import org.unilab.uniplan.exception.ResourceNotFoundException; + +import java.util.UUID; + +import static org.unilab.uniplan.utils.ErrorConstants.COURSE_NOT_FOUND; + +@Component +@RequiredArgsConstructor +public class StudentValidator implements Validator { + private final CourseRepository courseRepository; + + @Override + public void validate(final Student entity) { + UUID id = entity.getCourse().getId(); + if (!courseRepository.existsById(id)){ + throw new ResourceNotFoundException(COURSE_NOT_FOUND.getMessage(id.toString())); + } + } +} diff --git a/src/main/java/org/unilab/uniplan/student/StudentWebFacade.java b/src/main/java/org/unilab/uniplan/student/StudentWebFacade.java new file mode 100644 index 00000000..6b8e79b8 --- /dev/null +++ b/src/main/java/org/unilab/uniplan/student/StudentWebFacade.java @@ -0,0 +1,65 @@ +package org.unilab.uniplan.student; + +import org.springframework.transaction.annotation.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.unilab.uniplan.exception.ResourceNotFoundException; +import org.unilab.uniplan.student.dto.StudentRequestDto; +import org.unilab.uniplan.student.dto.StudentResponseDto; +import java.util.List; +import java.util.UUID; + +import static org.unilab.uniplan.utils.ErrorConstants.STUDENT_NOT_FOUND; + +@Component +@Slf4j +@RequiredArgsConstructor +public class StudentWebFacade { + + private final StudentMapper studentMapper; + private final StudentService studentService; + private final StudentValidator studentValidator; + + @Transactional + public void createStudent(final StudentRequestDto request){ + final Student student = studentMapper.toEntity(request); + studentValidator.validate(student); + studentService.save(student); + } + + @Transactional(readOnly = true) + public List getAllStudents(){ + return studentMapper.toResponseDtoList(studentService.getAll()); + } + + @Transactional(readOnly = true) + public StudentResponseDto getStudentById(final UUID id){ + final Student student = getStudentOrThrow(id); + return studentMapper.toResponseDto(student); + } + + private Student getStudentOrThrow(final UUID id){ + return studentService.getById(id) + .orElseThrow(() -> new ResourceNotFoundException( + STUDENT_NOT_FOUND.getMessage(String.valueOf(id)) + )); + } + + @Transactional + public void updateStudent(final UUID id, + final StudentRequestDto request){ + final Student student = getStudentOrThrow(id); + studentValidator.validate(student); + studentMapper.updateEntity(request, student); + studentService.save(student); + log.info("updated student with ID: {}", student.getId()); + } + + @Transactional + public void deleteStudent(final UUID id){ + final Student student = getStudentOrThrow(id); + studentService.delete(student); + log.info("deleted student with ID: {}", id); + } +} diff --git a/src/test/java/org/unilab/uniplan/student/StudentServiceTest.java b/src/test/java/org/unilab/uniplan/student/StudentServiceTest.java index f20e52c6..6018b209 100644 --- a/src/test/java/org/unilab/uniplan/student/StudentServiceTest.java +++ b/src/test/java/org/unilab/uniplan/student/StudentServiceTest.java @@ -1,11 +1,7 @@ package org.unilab.uniplan.student; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -18,8 +14,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.unilab.uniplan.exception.ResourceNotFoundException; -import org.unilab.uniplan.student.dto.StudentDto; @ExtendWith(MockitoExtension.class) class StudentServiceTest { @@ -27,108 +21,51 @@ class StudentServiceTest { @Mock private StudentRepository studentRepository; - @Mock - private StudentMapper studentMapper; - @InjectMocks private StudentService studentService; - private UUID studentId; - private StudentDto studentDTO; private Student student; @BeforeEach - void beforeAll() { - studentId = UUID.randomUUID(); - UUID courseId = UUID.randomUUID(); - studentDTO = new StudentDto(studentId, "Petar", "Petrov", "2301261005", courseId); + void setUp() { student = new Student(); } @Test - void createStudentShouldReturnSaveAndReturnStudentDTO() { - when(studentMapper.toEntity(studentDTO)).thenReturn(student); - when(studentRepository.save(student)).thenReturn(student); - when(studentMapper.toDto(student)).thenReturn(studentDTO); - - StudentDto result = studentService.createStudent(studentDTO); - - assertEquals(studentDTO, result); + void saveShouldDelegateToRepository() { + studentService.save(student); verify(studentRepository).save(student); } @Test - void findStudentByIdShouldReturnStudentDTOIfExists() { - when(studentRepository.findById(studentId)).thenReturn(Optional.of(student)); - when(studentMapper.toDto(student)).thenReturn(studentDTO); - - StudentDto result = studentService.findStudentById(studentId); - - assertEquals(studentDTO, result); - } - - @Test - void findStudentByIdShouldReturnEmptyIfNotExists() { - when(studentRepository.findById(studentId)).thenReturn(Optional.empty()); - - ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, () -> studentService.findStudentById(studentId)); - - assertTrue(exception.getMessage().contains(String.valueOf(studentId))); - } - - @Test - void findAllShouldReturnMappedStudentDTOList() { - List students = List.of(student); - when(studentRepository.findAll()).thenReturn(students); - when(studentMapper.toDto(any(Student.class))).thenReturn(studentDTO); + void getByIdShouldReturnStudentIfExists() { + UUID id = UUID.randomUUID(); + when(studentRepository.findById(id)).thenReturn(Optional.of(student)); - List result = studentService.findAll(); + Optional result = studentService.getById(id); - assertEquals(1, result.size()); - assertEquals(studentDTO, result.getFirst()); - verify(studentRepository).findAll(); + assertTrue(result.isPresent()); + assertEquals(student, result.get()); } @Test - void updateStudentShouldReturnUpdatedStudentDTOIfExists() { - when(studentRepository.findById(studentId)).thenReturn(Optional.of(student)); - doNothing().when(studentMapper).updateEntityFromDto(studentDTO, student); - when(studentRepository.save(student)).thenReturn(student); - when(studentMapper.toDto(student)).thenReturn(studentDTO); - - StudentDto result = studentService.updateStudent(studentId, studentDTO); + void getByIdShouldReturnEmptyIfNotExists() { + UUID id = UUID.randomUUID(); + when(studentRepository.findById(id)).thenReturn(Optional.empty()); - assertEquals(studentDTO, result); - verify(studentRepository).save(student); + assertTrue(studentService.getById(id).isEmpty()); } @Test - void updateStudentShouldReturnEmptyIfNotFound() { - when(studentRepository.findById(studentId)).thenReturn(Optional.empty()); - - ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, () -> studentService.updateStudent(studentId, studentDTO)); + void getAllShouldReturnAllStudents() { + when(studentRepository.findAll()).thenReturn(List.of(student)); - assertTrue(exception.getMessage().contains(String.valueOf(studentId))); - verify(studentRepository, never()).save(any()); + assertEquals(1, studentService.getAll().size()); } @Test - void deleteStudentShouldRemoveStudentDTOIfExists() { - when(studentRepository.findById(studentId)).thenReturn(Optional.of(student)); - - studentService.deleteStudent(studentId); - + void deleteShouldDelegateToRepository() { + studentService.delete(student); verify(studentRepository).delete(student); } - - @Test - void deleteStudentShouldThrowIfNotFound() { - when(studentRepository.findById(studentId)).thenReturn(Optional.empty()); - - ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, - () -> studentService.deleteStudent(studentId)); - - assertTrue(exception.getMessage().contains(String.valueOf(studentId))); - verify(studentRepository, never()).delete(any()); - } } diff --git a/src/test/java/org/unilab/uniplan/student/StudentValidatorTest.java b/src/test/java/org/unilab/uniplan/student/StudentValidatorTest.java new file mode 100644 index 00000000..e8bc21f3 --- /dev/null +++ b/src/test/java/org/unilab/uniplan/student/StudentValidatorTest.java @@ -0,0 +1,56 @@ +package org.unilab.uniplan.student; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.unilab.uniplan.course.Course; +import org.unilab.uniplan.course.CourseRepository; +import org.unilab.uniplan.exception.ResourceNotFoundException; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class StudentValidatorTest { + + @Mock + private CourseRepository courseRepository; + + @InjectMocks + private StudentValidator studentValidator; + + private Student student; + private UUID courseId; + + @BeforeEach + void setUp() { + courseId = UUID.randomUUID(); + Course course = new Course(); + course.setId(courseId); + student = new Student(); + student.setCourse(course); + } + + @Test + void validateShouldPassWhenCourseExists() { + when(courseRepository.existsById(courseId)).thenReturn(true); + + assertDoesNotThrow(() -> studentValidator.validate(student)); + } + + @Test + void validateShouldThrowWhenCourseNotFound() { + when(courseRepository.existsById(courseId)).thenReturn(false); + + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, + () -> studentValidator.validate(student)); + + assertTrue(exception.getMessage().contains(courseId.toString())); + } +} diff --git a/src/test/java/org/unilab/uniplan/student/StudentWebFacadeTest.java b/src/test/java/org/unilab/uniplan/student/StudentWebFacadeTest.java new file mode 100644 index 00000000..8fafcff8 --- /dev/null +++ b/src/test/java/org/unilab/uniplan/student/StudentWebFacadeTest.java @@ -0,0 +1,122 @@ +package org.unilab.uniplan.student; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.unilab.uniplan.exception.ResourceNotFoundException; +import org.unilab.uniplan.student.dto.StudentRequestDto; +import org.unilab.uniplan.student.dto.StudentResponseDto; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class StudentWebFacadeTest { + + @Mock + private StudentMapper studentMapper; + + @Mock + private StudentService studentService; + + @Mock + private StudentValidator studentValidator; + + @InjectMocks + private StudentWebFacade studentWebFacade; + + private UUID studentId; + private UUID courseId; + private Student student; + private StudentRequestDto requestDto; + private StudentResponseDto responseDto; + + @BeforeEach + void setUp() { + studentId = UUID.randomUUID(); + courseId = UUID.randomUUID(); + student = new Student(); + requestDto = new StudentRequestDto("Petar", "Petrov", "2301261005", courseId); + responseDto = new StudentResponseDto(studentId, "Petar", "Petrov", "2301261005", courseId); + } + + @Test + void createStudentShouldMapValidateAndSave() { + when(studentMapper.toEntity(requestDto)).thenReturn(student); + + studentWebFacade.createStudent(requestDto); + + InOrder order = inOrder(studentMapper, studentValidator, studentService); + order.verify(studentMapper).toEntity(requestDto); + order.verify(studentValidator).validate(student); + order.verify(studentService).save(student); + } + + @Test + void updateStudentShouldThrowIfNotFound() { + when(studentService.getById(studentId)).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, + () -> studentWebFacade.updateStudent(studentId, requestDto)); + + verify(studentService, never()).save(any()); + } + + @Test + void updateStudentShouldValidateAndSave() { + when(studentService.getById(studentId)).thenReturn(Optional.of(student)); + + studentWebFacade.updateStudent(studentId, requestDto); + + InOrder order = inOrder(studentValidator, studentMapper, studentService); + order.verify(studentValidator).validate(student); + order.verify(studentMapper).updateEntity(requestDto, student); + order.verify(studentService).save(student); + } + + @Test + void getStudentByIdShouldReturnMappedDto() { + when(studentService.getById(studentId)).thenReturn(Optional.of(student)); + when(studentMapper.toResponseDto(student)).thenReturn(responseDto); + + assertEquals(responseDto, studentWebFacade.getStudentById(studentId)); + } + + @Test + void getStudentByIdShouldThrowIfNotFound() { + when(studentService.getById(studentId)).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, + () -> studentWebFacade.getStudentById(studentId)); + } + + @Test + void deleteStudentShouldDelegateIfExists() { + when(studentService.getById(studentId)).thenReturn(Optional.of(student)); + + studentWebFacade.deleteStudent(studentId); + + verify(studentService).delete(student); + } + + @Test + void deleteStudentShouldThrowIfNotFound() { + when(studentService.getById(studentId)).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, + () -> studentWebFacade.deleteStudent(studentId)); + + verify(studentService, never()).delete(any()); + } +} From 7d85e694010fc9d2dc9f444bb68ab3f9ea9e9e42 Mon Sep 17 00:00:00 2001 From: constantine0621 Date: Mon, 29 Jun 2026 14:00:50 +0300 Subject: [PATCH 2/4] removed toInternalDto --- src/main/java/org/unilab/uniplan/student/StudentMapper.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/org/unilab/uniplan/student/StudentMapper.java b/src/main/java/org/unilab/uniplan/student/StudentMapper.java index 0994e7a3..46148a13 100644 --- a/src/main/java/org/unilab/uniplan/student/StudentMapper.java +++ b/src/main/java/org/unilab/uniplan/student/StudentMapper.java @@ -22,9 +22,6 @@ public interface StudentMapper { @Mapping(source = "courseId", target = "course.id") void updateEntity(StudentRequestDto requestDto, @MappingTarget Student student); - @Mapping(target = "id", ignore = true) - StudentDto toInternalDto(StudentRequestDto student); - @Mapping(source = "course.id", target = "courseId") StudentResponseDto toResponseDto(Student student); From c164387d0b610337beaf0a737e0597a0e6706a57 Mon Sep 17 00:00:00 2001 From: constantine0621 Date: Tue, 30 Jun 2026 15:11:31 +0300 Subject: [PATCH 3/4] got rid of internal Dtos and responseDto validations, removed @NotNull validation on controller @PathVariable parameters --- .../uniplan/student/StudentController.java | 9 ++--- .../unilab/uniplan/student/StudentMapper.java | 4 --- .../uniplan/student/StudentRepository.java | 24 -------------- .../student/dto/StudentCourseMajorDto.java | 33 ------------------- .../uniplan/student/dto/StudentDto.java | 21 ------------ .../student/dto/StudentResponseDto.java | 6 ---- 6 files changed, 3 insertions(+), 94 deletions(-) delete mode 100644 src/main/java/org/unilab/uniplan/student/dto/StudentCourseMajorDto.java delete mode 100644 src/main/java/org/unilab/uniplan/student/dto/StudentDto.java diff --git a/src/main/java/org/unilab/uniplan/student/StudentController.java b/src/main/java/org/unilab/uniplan/student/StudentController.java index 869dda1a..2f757c45 100644 --- a/src/main/java/org/unilab/uniplan/student/StudentController.java +++ b/src/main/java/org/unilab/uniplan/student/StudentController.java @@ -34,8 +34,7 @@ public ResponseEntity createStudent(@RequestBody @NotNull @Valid final Stu } @GetMapping("/{id}") - public ResponseEntity getStudentById(@PathVariable - @NotNull final UUID id) { + public ResponseEntity getStudentById(@PathVariable final UUID id) { return ResponseEntity.ok(studentWebFacade.getStudentById(id)); } @@ -45,8 +44,7 @@ public ResponseEntity> getAllStudents() { } @PutMapping("/{id}") - public ResponseEntity updateStudent(@PathVariable - @NotNull final UUID id, + public ResponseEntity updateStudent(@PathVariable final UUID id, @RequestBody @NotNull @Valid final StudentRequestDto studentRequestDTO) { studentWebFacade.updateStudent(id, studentRequestDTO); @@ -54,8 +52,7 @@ public ResponseEntity updateStudent(@PathVariable } @DeleteMapping("/{id}") - public ResponseEntity deleteStudent(@PathVariable - @NotNull final UUID id) { + public ResponseEntity deleteStudent(@PathVariable final UUID id) { studentWebFacade.deleteStudent(id); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/org/unilab/uniplan/student/StudentMapper.java b/src/main/java/org/unilab/uniplan/student/StudentMapper.java index 46148a13..a3890b48 100644 --- a/src/main/java/org/unilab/uniplan/student/StudentMapper.java +++ b/src/main/java/org/unilab/uniplan/student/StudentMapper.java @@ -4,7 +4,6 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; -import org.unilab.uniplan.student.dto.StudentDto; import org.unilab.uniplan.student.dto.StudentRequestDto; import org.unilab.uniplan.student.dto.StudentResponseDto; @@ -15,9 +14,6 @@ public interface StudentMapper { @Mapping(target = "id", ignore = true) Student toEntity(StudentRequestDto requestDto); - @Mapping(source = "course.id", target = "courseId") - StudentDto toDto(Student student); - @Mapping(target = "id", ignore = true) @Mapping(source = "courseId", target = "course.id") void updateEntity(StudentRequestDto requestDto, @MappingTarget Student student); diff --git a/src/main/java/org/unilab/uniplan/student/StudentRepository.java b/src/main/java/org/unilab/uniplan/student/StudentRepository.java index 85904938..d373973d 100644 --- a/src/main/java/org/unilab/uniplan/student/StudentRepository.java +++ b/src/main/java/org/unilab/uniplan/student/StudentRepository.java @@ -1,33 +1,9 @@ package org.unilab.uniplan.student; -import java.util.List; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import org.unilab.uniplan.student.dto.StudentCourseMajorDto; @Repository public interface StudentRepository extends JpaRepository { - @Query(""" - select new org.unilab.uniplan.student.dto.StudentCourseMajorDto( - s.id, s.firstName, s.lastName, s.facultyNumber, - c.id, c.courseType, c.courseSubtype, c.courseYear, - m.id, m.majorName - ) - from Student s - join s.course c - join c.major m - where (:firstName is null or lower(s.firstName) like lower(concat('%', :firstName, '%'))) - and (:lastName is null or lower(s.lastName) like lower(concat('%', :lastName, '%'))) - and (:facultyNumber is null or s.facultyNumber like concat('%', :facultyNumber, '%')) - and (:majorName is null or lower(m.majorName) like lower(concat('%', :majorName, '%'))) - """) - List searchStudents( - @Param("firstName") String firstName, - @Param("lastName") String lastName, - @Param("facultyNumber") String facultyNumber, - @Param("majorName") String majorName - ); } diff --git a/src/main/java/org/unilab/uniplan/student/dto/StudentCourseMajorDto.java b/src/main/java/org/unilab/uniplan/student/dto/StudentCourseMajorDto.java deleted file mode 100644 index 9dc5a51d..00000000 --- a/src/main/java/org/unilab/uniplan/student/dto/StudentCourseMajorDto.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.unilab.uniplan.student.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import java.util.UUID; - -public record StudentCourseMajorDto( - UUID studentId, - @NotBlank - @Size(max = 100) - String firstName, - @NotBlank - @Size(max = 100) - String lastName, - @Size(max = 40) - String facultyNumber, - @NotNull - UUID courseId, - @NotNull - String courseType, - @NotNull - String courseSubType, - @NotNull - Byte courseYear, - @NotNull - UUID majorId, - @NotBlank - @Size(max = 200) - String majorName -) { - -} diff --git a/src/main/java/org/unilab/uniplan/student/dto/StudentDto.java b/src/main/java/org/unilab/uniplan/student/dto/StudentDto.java deleted file mode 100644 index 0329a527..00000000 --- a/src/main/java/org/unilab/uniplan/student/dto/StudentDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.unilab.uniplan.student.dto; - -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import java.util.UUID; - -public record StudentDto( - UUID id, - @NotNull - @Size(max = 100) - String firstName, - @NotNull - @Size(max = 100) - String lastName, - @Size(max = 40) - String facultyNumber, - @NotNull - UUID courseId -) { - -} diff --git a/src/main/java/org/unilab/uniplan/student/dto/StudentResponseDto.java b/src/main/java/org/unilab/uniplan/student/dto/StudentResponseDto.java index 8ed48fec..9cba5090 100644 --- a/src/main/java/org/unilab/uniplan/student/dto/StudentResponseDto.java +++ b/src/main/java/org/unilab/uniplan/student/dto/StudentResponseDto.java @@ -1,16 +1,10 @@ package org.unilab.uniplan.student.dto; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import java.util.UUID; public record StudentResponseDto( UUID id, - @NotNull - @Size(max = 100) String firstName, - @NotNull - @Size(max = 100) String lastName, String facultyNumber, UUID courseId From 545c212039838a28cbdf5625fefdda28b8d746fd Mon Sep 17 00:00:00 2001 From: constantine0621 Date: Wed, 1 Jul 2026 11:17:42 +0300 Subject: [PATCH 4/4] Added StudentMapperTest, changed StudentValidator to accept StudentRequestDto instead of Student entity, adjusted tests and changed test names to adhere to convention --- .../uniplan/common/model/Validator.java | 5 - .../uniplan/student/StudentValidator.java | 9 +- .../uniplan/student/StudentWebFacade.java | 19 +-- .../uniplan/student/StudentMapperTest.java | 110 ++++++++++++++++++ .../uniplan/student/StudentServiceTest.java | 10 +- .../uniplan/student/StudentValidatorTest.java | 17 ++- .../uniplan/student/StudentWebFacadeTest.java | 20 ++-- 7 files changed, 146 insertions(+), 44 deletions(-) delete mode 100644 src/main/java/org/unilab/uniplan/common/model/Validator.java create mode 100644 src/test/java/org/unilab/uniplan/student/StudentMapperTest.java diff --git a/src/main/java/org/unilab/uniplan/common/model/Validator.java b/src/main/java/org/unilab/uniplan/common/model/Validator.java deleted file mode 100644 index e4207496..00000000 --- a/src/main/java/org/unilab/uniplan/common/model/Validator.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.unilab.uniplan.common.model; - -public interface Validator { - void validate(final T entity); -} diff --git a/src/main/java/org/unilab/uniplan/student/StudentValidator.java b/src/main/java/org/unilab/uniplan/student/StudentValidator.java index 17ae03f8..5a2fcbe2 100644 --- a/src/main/java/org/unilab/uniplan/student/StudentValidator.java +++ b/src/main/java/org/unilab/uniplan/student/StudentValidator.java @@ -2,9 +2,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.unilab.uniplan.common.model.Validator; import org.unilab.uniplan.course.CourseRepository; import org.unilab.uniplan.exception.ResourceNotFoundException; +import org.unilab.uniplan.student.dto.StudentRequestDto; import java.util.UUID; @@ -12,12 +12,11 @@ @Component @RequiredArgsConstructor -public class StudentValidator implements Validator { +public class StudentValidator{ private final CourseRepository courseRepository; - @Override - public void validate(final Student entity) { - UUID id = entity.getCourse().getId(); + public void validate(final StudentRequestDto request) { + UUID id = request.courseId(); if (!courseRepository.existsById(id)){ throw new ResourceNotFoundException(COURSE_NOT_FOUND.getMessage(id.toString())); } diff --git a/src/main/java/org/unilab/uniplan/student/StudentWebFacade.java b/src/main/java/org/unilab/uniplan/student/StudentWebFacade.java index 6b8e79b8..3f41e0f7 100644 --- a/src/main/java/org/unilab/uniplan/student/StudentWebFacade.java +++ b/src/main/java/org/unilab/uniplan/student/StudentWebFacade.java @@ -23,9 +23,10 @@ public class StudentWebFacade { @Transactional public void createStudent(final StudentRequestDto request){ + studentValidator.validate(request); final Student student = studentMapper.toEntity(request); - studentValidator.validate(student); studentService.save(student); + log.info("created student with ID: {}", student.getId()); } @Transactional(readOnly = true) @@ -39,18 +40,11 @@ public StudentResponseDto getStudentById(final UUID id){ return studentMapper.toResponseDto(student); } - private Student getStudentOrThrow(final UUID id){ - return studentService.getById(id) - .orElseThrow(() -> new ResourceNotFoundException( - STUDENT_NOT_FOUND.getMessage(String.valueOf(id)) - )); - } - @Transactional public void updateStudent(final UUID id, final StudentRequestDto request){ + studentValidator.validate(request); final Student student = getStudentOrThrow(id); - studentValidator.validate(student); studentMapper.updateEntity(request, student); studentService.save(student); log.info("updated student with ID: {}", student.getId()); @@ -62,4 +56,11 @@ public void deleteStudent(final UUID id){ studentService.delete(student); log.info("deleted student with ID: {}", id); } + + private Student getStudentOrThrow(final UUID id){ + return studentService.getById(id) + .orElseThrow(() -> new ResourceNotFoundException( + STUDENT_NOT_FOUND.getMessage(String.valueOf(id)) + )); + } } diff --git a/src/test/java/org/unilab/uniplan/student/StudentMapperTest.java b/src/test/java/org/unilab/uniplan/student/StudentMapperTest.java new file mode 100644 index 00000000..7a6a5e8e --- /dev/null +++ b/src/test/java/org/unilab/uniplan/student/StudentMapperTest.java @@ -0,0 +1,110 @@ +package org.unilab.uniplan.student; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mapstruct.factory.Mappers; +import org.mockito.junit.jupiter.MockitoExtension; +import org.unilab.uniplan.course.Course; +import org.unilab.uniplan.student.dto.StudentRequestDto; +import org.unilab.uniplan.student.dto.StudentResponseDto; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.AssertionsKt.assertNull; + +@ExtendWith(MockitoExtension.class) +class StudentMapperTest { + + private final StudentMapper studentMapper = Mappers.getMapper(StudentMapper.class); + + private UUID studentId; + private UUID courseId; + private Student student; + private StudentRequestDto requestDto; + + @BeforeEach + void setUp() { + studentId = UUID.randomUUID(); + courseId = UUID.randomUUID(); + + requestDto = new StudentRequestDto("Petar", "Petrov", "2301261005", courseId); + + Course course = new Course(); + course.setId(courseId); + + student = new Student(); + student.setId(studentId); + student.setFirstName("Petar"); + student.setLastName("Petrov"); + student.setFacultyNumber("2301261005"); + student.setCourse(course); + } + + @Test + void toEntity_ShouldMapFieldsCorrectly() { + Student result = studentMapper.toEntity(requestDto); + + assertNull(result.getId()); + assertEquals("Petar", result.getFirstName()); + assertEquals("Petrov", result.getLastName()); + assertEquals("2301261005", result.getFacultyNumber()); + assertEquals(courseId, result.getCourse().getId()); + } + + @Test + void toEntity_ShouldIgnoreId() { + Student result = studentMapper.toEntity(requestDto); + + assertNull(result.getId()); + } + + @Test + void updateEntity_ShouldUpdateFieldsCorrectly() { + UUID newCourseId = UUID.randomUUID(); + StudentRequestDto updateDto = new StudentRequestDto("Ivan", "Ivanov", "1234567890", newCourseId); + + studentMapper.updateEntity(updateDto, student); + + assertEquals("Ivan", student.getFirstName()); + assertEquals("Ivanov", student.getLastName()); + assertEquals("1234567890", student.getFacultyNumber()); + assertEquals(newCourseId, student.getCourse().getId()); + } + + @Test + void updateEntity_ShouldNotAlterStudentId() { + studentMapper.updateEntity(requestDto, student); + + assertEquals(studentId, student.getId()); + } + + @Test + void toResponseDto_ShouldMapFieldsCorrectly() { + StudentResponseDto result = studentMapper.toResponseDto(student); + + assertEquals(studentId, result.id()); + assertEquals("Petar", result.firstName()); + assertEquals("Petrov", result.lastName()); + assertEquals("2301261005", result.facultyNumber()); + assertEquals(courseId, result.courseId()); + } + + @Test + void toResponseDtoList_ShouldMapAllElements() { + List result = studentMapper.toResponseDtoList(List.of(student)); + + assertEquals(1, result.size()); + assertEquals(studentId, result.getFirst().id()); + } + + @Test + void toResponseDtoList_ShouldReturnEmptyForEmptyInput() { + List result = studentMapper.toResponseDtoList(List.of()); + + assertTrue(result.isEmpty()); + } +} diff --git a/src/test/java/org/unilab/uniplan/student/StudentServiceTest.java b/src/test/java/org/unilab/uniplan/student/StudentServiceTest.java index 6018b209..ca1bc5e4 100644 --- a/src/test/java/org/unilab/uniplan/student/StudentServiceTest.java +++ b/src/test/java/org/unilab/uniplan/student/StudentServiceTest.java @@ -32,13 +32,13 @@ void setUp() { } @Test - void saveShouldDelegateToRepository() { + void save_ShouldDelegateToRepository() { studentService.save(student); verify(studentRepository).save(student); } @Test - void getByIdShouldReturnStudentIfExists() { + void getById_ShouldReturnStudent_IfExists() { UUID id = UUID.randomUUID(); when(studentRepository.findById(id)).thenReturn(Optional.of(student)); @@ -49,7 +49,7 @@ void getByIdShouldReturnStudentIfExists() { } @Test - void getByIdShouldReturnEmptyIfNotExists() { + void getById_ShouldReturnEmpty_IfNotExists() { UUID id = UUID.randomUUID(); when(studentRepository.findById(id)).thenReturn(Optional.empty()); @@ -57,14 +57,14 @@ void getByIdShouldReturnEmptyIfNotExists() { } @Test - void getAllShouldReturnAllStudents() { + void getAll_ShouldReturnAllStudents() { when(studentRepository.findAll()).thenReturn(List.of(student)); assertEquals(1, studentService.getAll().size()); } @Test - void deleteShouldDelegateToRepository() { + void delete_ShouldDelegateToRepository() { studentService.delete(student); verify(studentRepository).delete(student); } diff --git a/src/test/java/org/unilab/uniplan/student/StudentValidatorTest.java b/src/test/java/org/unilab/uniplan/student/StudentValidatorTest.java index e8bc21f3..1dd07d23 100644 --- a/src/test/java/org/unilab/uniplan/student/StudentValidatorTest.java +++ b/src/test/java/org/unilab/uniplan/student/StudentValidatorTest.java @@ -6,9 +6,9 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.unilab.uniplan.course.Course; import org.unilab.uniplan.course.CourseRepository; import org.unilab.uniplan.exception.ResourceNotFoundException; +import org.unilab.uniplan.student.dto.StudentRequestDto; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -25,31 +25,28 @@ class StudentValidatorTest { @InjectMocks private StudentValidator studentValidator; - private Student student; private UUID courseId; + private StudentRequestDto requestDto; @BeforeEach void setUp() { courseId = UUID.randomUUID(); - Course course = new Course(); - course.setId(courseId); - student = new Student(); - student.setCourse(course); + requestDto = new StudentRequestDto("Petar", "Petrov", "2301261005", courseId); } @Test - void validateShouldPassWhenCourseExists() { + void validate_ShouldPass_WhenCourseExists() { when(courseRepository.existsById(courseId)).thenReturn(true); - assertDoesNotThrow(() -> studentValidator.validate(student)); + assertDoesNotThrow(() -> studentValidator.validate(requestDto)); } @Test - void validateShouldThrowWhenCourseNotFound() { + void validate_ShouldThrow_WhenCourseNotFound() { when(courseRepository.existsById(courseId)).thenReturn(false); ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, - () -> studentValidator.validate(student)); + () -> studentValidator.validate(requestDto)); assertTrue(exception.getMessage().contains(courseId.toString())); } diff --git a/src/test/java/org/unilab/uniplan/student/StudentWebFacadeTest.java b/src/test/java/org/unilab/uniplan/student/StudentWebFacadeTest.java index 8fafcff8..cc459844 100644 --- a/src/test/java/org/unilab/uniplan/student/StudentWebFacadeTest.java +++ b/src/test/java/org/unilab/uniplan/student/StudentWebFacadeTest.java @@ -52,19 +52,19 @@ void setUp() { } @Test - void createStudentShouldMapValidateAndSave() { + void createStudent_ShouldMapValidateAndSave() { when(studentMapper.toEntity(requestDto)).thenReturn(student); studentWebFacade.createStudent(requestDto); - InOrder order = inOrder(studentMapper, studentValidator, studentService); + InOrder order = inOrder(studentValidator, studentMapper, studentService); + order.verify(studentValidator).validate(requestDto); order.verify(studentMapper).toEntity(requestDto); - order.verify(studentValidator).validate(student); order.verify(studentService).save(student); } @Test - void updateStudentShouldThrowIfNotFound() { + void updateStudent_ShouldThrow_IfNotFound() { when(studentService.getById(studentId)).thenReturn(Optional.empty()); assertThrows(ResourceNotFoundException.class, @@ -74,19 +74,19 @@ void updateStudentShouldThrowIfNotFound() { } @Test - void updateStudentShouldValidateAndSave() { + void updateStudent_ShouldValidateAndSave() { when(studentService.getById(studentId)).thenReturn(Optional.of(student)); studentWebFacade.updateStudent(studentId, requestDto); InOrder order = inOrder(studentValidator, studentMapper, studentService); - order.verify(studentValidator).validate(student); + order.verify(studentValidator).validate(requestDto); order.verify(studentMapper).updateEntity(requestDto, student); order.verify(studentService).save(student); } @Test - void getStudentByIdShouldReturnMappedDto() { + void getStudentById_ShouldReturnMappedDto() { when(studentService.getById(studentId)).thenReturn(Optional.of(student)); when(studentMapper.toResponseDto(student)).thenReturn(responseDto); @@ -94,7 +94,7 @@ void getStudentByIdShouldReturnMappedDto() { } @Test - void getStudentByIdShouldThrowIfNotFound() { + void getStudentById_ShouldThrow_IfNotFound() { when(studentService.getById(studentId)).thenReturn(Optional.empty()); assertThrows(ResourceNotFoundException.class, @@ -102,7 +102,7 @@ void getStudentByIdShouldThrowIfNotFound() { } @Test - void deleteStudentShouldDelegateIfExists() { + void deleteStudent_ShouldDelegateIfExists() { when(studentService.getById(studentId)).thenReturn(Optional.of(student)); studentWebFacade.deleteStudent(studentId); @@ -111,7 +111,7 @@ void deleteStudentShouldDelegateIfExists() { } @Test - void deleteStudentShouldThrowIfNotFound() { + void deleteStudent_ShouldThrow_IfNotFound() { when(studentService.getById(studentId)).thenReturn(Optional.empty()); assertThrows(ResourceNotFoundException.class,