diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java index 2418847a2d483585f0d0a57505f0cfd148aa2094..7b4539a9ab48612adffdc7dce62372939a4dec89 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerCreateDto.java @@ -4,17 +4,19 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; @AllArgsConstructor +@NoArgsConstructor @Getter public class AnswerCreateDto { - @NotBlank + @NotBlank(message = "answer text is required") private String text; - @NotNull + @NotNull(message = "answer correctness must be specified") private boolean correct; - @NotNull + @NotNull(message = "answer must belong to a question, questionId can not be null") private long questionId; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerInQuestionCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerInQuestionCreateDto.java index 229eea30d0c9f12e27237841064dbd92489f7758..8add213c5a60f1b1e332d3fc783dc367bf9f25ce 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerInQuestionCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswerInQuestionCreateDto.java @@ -9,9 +9,9 @@ import lombok.Getter; @Getter public class AnswerInQuestionCreateDto { - @NotBlank + @NotBlank(message = "answer text is required") private String text; - @NotNull + @NotNull(message = "answer correctness must be specified") private boolean correct; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java index f2c7da24058c59d4a600d9a7d7d0403652e46832..438d96cafdc3867c6a8e07300dd87e00da0f3eab 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/AnswersCreateDto.java @@ -11,7 +11,7 @@ import java.util.List; @Getter public class AnswersCreateDto { - @NotNull + @NotNull(message = "answers must belong to a question, questionId can not be null") private long questionId; @Valid diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java index 6d1cb1a7727b669e683f775e63aafc0e781816fe..9f22fd51ce723cab2a1214fa70769bda8e9ed28d 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/ExerciseCreateDto.java @@ -5,20 +5,22 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; @AllArgsConstructor +@NoArgsConstructor @Getter public class ExerciseCreateDto { - @NotBlank + @NotBlank(message = "exercise name is required") private String name; - @NotBlank + @NotBlank(message = "exercise description is required") private String description; - @NotNull + @NotNull(message = "exercise difficulty is required") @PositiveOrZero private int difficulty; - @NotNull + @NotNull(message = "exercise must belong to a course, courseId can not be null") private long courseId; } diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java index cd215815305d8bb06de5b81e47f6b6e8380954d2..fa7c786f55bcd743ab02730cdac9d251c925dd7b 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionCreateDto.java @@ -5,17 +5,19 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.List; @AllArgsConstructor +@NoArgsConstructor @Getter public class QuestionCreateDto { - @NotBlank + @NotBlank(message = "question text is required") private String text; - @NotNull + @NotNull(message = "question must belong to an exercise, exerciseId can not be null") private long exerciseId; @Valid diff --git a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java index 59e0fda131a540c0c08da981fd36d156a49cb5ae..e23b02ec6aa7d28556f5d65232a9cea4098f2a1d 100644 --- a/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java +++ b/application/model/src/main/java/org/fuseri/model/dto/exercise/QuestionUpdateDto.java @@ -9,9 +9,9 @@ import lombok.Getter; @Builder public class QuestionUpdateDto { - @NotBlank + @NotBlank(message = "question text is required") private String text; - @NotNull + @NotNull(message = "question must belong to an exercise, exerciseId can not be null") private long exerciseId; } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java index b3829f98eff78333be094ae152fc0f6a5425d9d6..87ac6a7d115550a4053de3a7b47c3eaf77863f3b 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerController.java @@ -3,7 +3,6 @@ package org.fuseri.moduleexercise.answer; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; -import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import org.fuseri.model.dto.exercise.AnswerCreateDto; @@ -49,11 +48,7 @@ public class AnswerController { }) @PostMapping public ResponseEntity<AnswerDto> create(@Valid @RequestBody AnswerCreateDto dto) { - try { - return ResponseEntity.status(HttpStatus.CREATED).body(facade.create(dto)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + return ResponseEntity.status(HttpStatus.CREATED).body(facade.create(dto)); } /** @@ -72,11 +67,7 @@ public class AnswerController { }) @PutMapping("/{id}") public ResponseEntity<AnswerDto> update(@NotNull @PathVariable long id, @Valid @RequestBody AnswerCreateDto dto) { - try { - return ResponseEntity.ok(facade.update(id, dto)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + return ResponseEntity.ok(facade.update(id, dto)); } /** @@ -92,11 +83,7 @@ public class AnswerController { }) @DeleteMapping("/{id}") public ResponseEntity<Void> delete(@NotNull @PathVariable long id) { - try { - facade.delete(id); - return ResponseEntity.noContent().build(); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + facade.delete(id); + return ResponseEntity.noContent().build(); } } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java index f5346fc5993874b4de7ca7d49fe39ba8c17ee6ba..c7903475ff42ba8341446bdcdbb4a12cab98ef45 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/answer/AnswerService.java @@ -7,7 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; /** * Represent a service for managing Answer entities @@ -31,21 +30,6 @@ public class AnswerService extends DomainService<Answer> { this.repository = repository; } - /** - * Retrieve a list of Answer entities with the specified question ID - * - * @param questionId the ID of the question to retrieve answers for - * @return a list of Answer entities with the specified question ID - * @throws EntityNotFoundException if question with questionId does not exist - */ - @Transactional(readOnly = true) - public List<Answer> findAllByQuestionId(long questionId) { - if (!getRepository().existsById(questionId)) { - throw new EntityNotFoundException("Question with id " + questionId + " not found."); - } - return repository.findByQuestionId(questionId); - } - /** * Retrieve the Answer entity with the specified id * diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/ApiError.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/ApiError.java new file mode 100644 index 0000000000000000000000000000000000000000..6d1e2622531492e1f65615a71a50be2ab933cf78 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/ApiError.java @@ -0,0 +1,41 @@ +package org.fuseri.moduleexercise.exceptions; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Getter; +import lombok.ToString; +import org.springframework.http.HttpStatus; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@ToString +class ApiError { + + private HttpStatus status; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") + private LocalDateTime timestamp; + private String message; + private List<ApiSubError> subErrors; + private String path; + + private ApiError() { + timestamp = LocalDateTime.now(Clock.systemUTC()); + } + + ApiError(HttpStatus status, Throwable ex, String path) { + this(); + this.status = status; + this.path = path; + this.message = ex.getLocalizedMessage(); + } + + ApiError(HttpStatus status, List<ApiSubError> subErrors, Throwable ex, String path) { + this(); + this.status = status; + this.subErrors = subErrors; + this.path = path; + this.message = ex.getLocalizedMessage(); + } +} \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/ApiSubError.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/ApiSubError.java new file mode 100644 index 0000000000000000000000000000000000000000..3c34fae9c17a204a05c129c188f71fdd42f191f8 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/ApiSubError.java @@ -0,0 +1,4 @@ +package org.fuseri.moduleexercise.exceptions; + +interface ApiSubError { +} diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/ApiValidationError.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/ApiValidationError.java new file mode 100644 index 0000000000000000000000000000000000000000..6fd4ed3fcab6c7def0207ca4cf9cbfa2d625f397 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/ApiValidationError.java @@ -0,0 +1,19 @@ +package org.fuseri.moduleexercise.exceptions; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper = false) +@AllArgsConstructor +@Getter +@ToString +class ApiValidationError implements ApiSubError { + private String object; + private String field; + private Object rejectedValue; + private String message; +} \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/RestResponseEntityExceptionHandler.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/RestResponseEntityExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..04b438126aec94aace781412f58ed51f4f8688c9 --- /dev/null +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exceptions/RestResponseEntityExceptionHandler.java @@ -0,0 +1,76 @@ +package org.fuseri.moduleexercise.exceptions; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.util.UrlPathHelper; + +import java.util.List; + +@ControllerAdvice +public class RestResponseEntityExceptionHandler { + private static final UrlPathHelper URL_PATH_HELPER = new UrlPathHelper(); + + /** + * Handle ResourceNotFoundException exceptions + * + * @param ex exception + * @param request request + * @return response entity + */ + @ExceptionHandler(value = {EntityNotFoundException.class}) + public ResponseEntity<ApiError> handleNotFoundError(EntityNotFoundException ex, HttpServletRequest request) { + ApiError error = new ApiError( + HttpStatus.NOT_FOUND, + ex, + URL_PATH_HELPER.getRequestUri(request)); + return buildResponseEntity(error); + } + + /** + * Handle Validation exceptions + * + * @param ex exception + * @param request request + * @return response entity + */ + @ExceptionHandler(value = {MethodArgumentNotValidException.class, HttpMessageNotReadableException.class}) + public ResponseEntity<ApiError> handleValidationErrors(MethodArgumentNotValidException ex, HttpServletRequest request) { + List<ApiSubError> subErrors = ex.getBindingResult().getFieldErrors() + .stream() + .map(e -> (ApiSubError) new ApiValidationError(e.getObjectName(), e.getField(), e.getRejectedValue(), e.getDefaultMessage())) + .toList(); + ApiError error = new ApiError( + HttpStatus.BAD_REQUEST, + subErrors, + ex, + URL_PATH_HELPER.getRequestUri(request)); + return buildResponseEntity(error); + } + + /** + * Handle exceptions not matched by above handler methods + * + * @param ex exception + * @param request request + * @return response entity + */ + @ExceptionHandler({Exception.class}) + public ResponseEntity<ApiError> handleAll(final Exception ex, HttpServletRequest request) { + final ApiError error = new ApiError( + HttpStatus.INTERNAL_SERVER_ERROR, + ExceptionUtils.getRootCause(ex), + URL_PATH_HELPER.getRequestUri(request)); + return buildResponseEntity(error); + } + + private ResponseEntity<ApiError> buildResponseEntity(ApiError apiError) { + return new ResponseEntity<>(apiError, apiError.getStatus()); + } +} \ No newline at end of file diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java index 6070b8a70b26a36f3e87b4f4661a43a64625cfe5..d923ad8fd5d06bdaecfa1504a3247e3f20704dea 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/Exercise.java @@ -29,7 +29,7 @@ public class Exercise extends DomainObject { private int difficulty; - @Column(name = "lecture_id") + @Column(name = "course_id") private long courseId; @OneToMany(mappedBy="exercise", cascade = CascadeType.ALL) diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java index 96f91fc77b92040b231cc51218ff393a26184530..dd437b24997be84a50b5653ca8cc68554ae7ae6a 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/exercise/ExerciseController.java @@ -3,7 +3,6 @@ package org.fuseri.moduleexercise.exercise; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; -import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; @@ -75,11 +74,7 @@ public class ExerciseController { }) @GetMapping("/{id}") public ResponseEntity<ExerciseDto> find(@NotNull @PathVariable long id) { - try { - return ResponseEntity.ok(facade.find(id)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + return ResponseEntity.ok(facade.find(id)); } /** @@ -135,12 +130,8 @@ public class ExerciseController { @GetMapping("/{exercise-id}/questions") public ResponseEntity<Page<QuestionDto>> findQuestions(@NotNull @PathVariable("exercise-id") long exerciseId, @PositiveOrZero @RequestParam int page) { - try { - Page<QuestionDto> questions = facade.getQuestions(exerciseId, page); - return ResponseEntity.ok(questions); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + Page<QuestionDto> questions = facade.getQuestions(exerciseId, page); + return ResponseEntity.ok(questions); } /** @@ -159,11 +150,7 @@ public class ExerciseController { }) @PutMapping("/{id}") public ResponseEntity<ExerciseDto> update(@NotNull @PathVariable long id, @Valid @RequestBody ExerciseCreateDto dto) { - try { - return ResponseEntity.ok(facade.update(id, dto)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + return ResponseEntity.ok(facade.update(id, dto)); } /** diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java index 03f62910b63062589484675c9f4d1ba436ec60c5..b3ff308eba12f885063011f0a0f702e71b138ee8 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/Question.java @@ -2,6 +2,7 @@ package org.fuseri.moduleexercise.question; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; @@ -35,7 +36,7 @@ public class Question extends DomainObject { @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) private Set<Answer> answers = new HashSet<>(); - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "exercise_id", nullable = false) private Exercise exercise; @@ -46,5 +47,8 @@ public class Question extends DomainObject { */ public void addAnswers(Set<Answer> answersToAdd) { answers.addAll(answersToAdd); + for (var answer : answersToAdd) { + answer.setQuestion(this); + } } } diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java index 9857e1c3d46120979b04339b2ddf0293c22c0ed8..e494fcb0e7e2f5063d3ae91dd485cbdff9747e0d 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionController.java @@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; -import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import org.fuseri.model.dto.exercise.AnswerDto; @@ -63,11 +62,7 @@ public class QuestionController { }) @GetMapping("/{id}") public ResponseEntity<QuestionDto> find(@NotNull @PathVariable long id) { - try { - return ResponseEntity.ok(questionFacade.find(id)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + return ResponseEntity.ok(questionFacade.find(id)); } /** @@ -83,11 +78,7 @@ public class QuestionController { @ApiResponse(responseCode = "404", description = "Question not found") @GetMapping("/{id}/answers") public ResponseEntity<List<AnswerDto>> getQuestionAnswers(@NotNull @PathVariable long id) { - try { - return ResponseEntity.ok(questionFacade.getQuestionAnswers(id)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + return ResponseEntity.ok(questionFacade.getQuestionAnswers(id)); } /** @@ -104,13 +95,9 @@ public class QuestionController { @ApiResponse(responseCode = "404", description = "Exercise with the specified ID was not found.") }) @PostMapping - public ResponseEntity<QuestionDto> addQuestion(@Valid @RequestBody QuestionCreateDto dto) { - try { - QuestionDto createdQuestionDto = questionFacade.create(dto); - return ResponseEntity.status(HttpStatus.CREATED).body(createdQuestionDto); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + public ResponseEntity<QuestionDto> createQuestion(@Valid @RequestBody QuestionCreateDto dto) { + QuestionDto createdQuestionDto = questionFacade.create(dto); + return ResponseEntity.status(HttpStatus.CREATED).body(createdQuestionDto); } /** @@ -127,11 +114,7 @@ public class QuestionController { }) @PutMapping("/{id}") public ResponseEntity<QuestionDto> updateQuestion(@NotNull @PathVariable long id, @Valid @RequestBody QuestionUpdateDto dto) { - try { - return ResponseEntity.ok(questionFacade.patchUpdate(id, dto)); - } catch (EntityNotFoundException e) { - return ResponseEntity.notFound().build(); - } + return ResponseEntity.ok(questionFacade.patchUpdate(id, dto)); } /** diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java index 8fe1e2a19f4fd2663aa3a22e0e5f9848829f8800..65070d1376f9a196ef23fdb3654f45e21422e6ca 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionFacade.java @@ -7,10 +7,10 @@ import org.fuseri.model.dto.exercise.QuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionDto; import org.fuseri.model.dto.exercise.QuestionUpdateDto; import org.fuseri.moduleexercise.answer.AnswerMapper; -import org.fuseri.moduleexercise.answer.AnswerService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; /** @@ -21,17 +21,14 @@ import java.util.List; @Service public class QuestionFacade { private final QuestionService questionService; - private final AnswerService answerService; private final QuestionMapper questionMapper; private final AnswerMapper answerMapper; @Autowired public QuestionFacade( - QuestionService questionService, - AnswerService answerService, QuestionMapper questionMapper, + QuestionService questionService, QuestionMapper questionMapper, AnswerMapper answerMapper) { this.questionService = questionService; - this.answerService = answerService; this.questionMapper = questionMapper; this.answerMapper = answerMapper; } @@ -53,7 +50,8 @@ public class QuestionFacade { * @return a List of AnswerDto objects */ public List<AnswerDto> getQuestionAnswers(long questionId) { - return answerMapper.toDtoList(answerService.findAllByQuestionId(questionId)); + var question = questionService.find(questionId); + return answerMapper.toDtoList(new ArrayList<>(question.getAnswers())); } /** diff --git a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java index f0676eee28256d926e768b0fee5df3cf52275fac..d6a44f0b988642b23b34105001559f59dc6a6338 100644 --- a/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java +++ b/application/module-exercise/src/main/java/org/fuseri/moduleexercise/question/QuestionService.java @@ -83,6 +83,19 @@ public class QuestionService extends DomainService<Question> { throw new EntityNotFoundException( "Question with id: " + id + " was not found."); } + } + /** + * Create a question with answers + * + * @param question the question to create + * @return the created question + */ + @Override + public Question create(Question question) { + for (var answer: question.getAnswers()) { + answer.setQuestion(question); + } + return getRepository().save(question); } } diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerServiceTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerServiceTest.java index ee53178ff5e7f13e73f3ed97da52421e3c6e3dcb..04b364bbff8b255c6339406ef4eb778f3b464552 100644 --- a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerServiceTest.java +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/answer/AnswerServiceTest.java @@ -9,9 +9,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Optional; import static org.mockito.ArgumentMatchers.anyLong; @@ -62,17 +60,4 @@ class AnswerServiceTest { Assertions.assertEquals(answer, result); verify(answerRepository).findById(anyLong()); } - - @Test - void findAllByQuestionId() { - long id = 1; - List<Answer> list = Collections.emptyList(); - - when(answerRepository.existsById(id)).thenReturn(true); - when(answerRepository.findByQuestionId(id)).thenReturn(list); - List<Answer> result = service.findAllByQuestionId(1L); - - Assertions.assertEquals(list, result); - verify(answerRepository).findByQuestionId(id); - } } diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseMapperTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..239bfcdbbe463fdd4f6ef02b649f756303faf2a6 --- /dev/null +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/exercise/ExerciseMapperTest.java @@ -0,0 +1,47 @@ +package org.fuseri.moduleexercise.exercise; + +import org.fuseri.model.dto.exercise.ExerciseCreateDto; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ExerciseMapperTest { + + private final ExerciseCreateDto exerciseCreateDto = new ExerciseCreateDto( + "History exercise", "Great", 2, 4); + + + @Autowired + private ExerciseMapper exerciseMapper; + + @Test + void mapNullEntityToDto() { + var entity = exerciseMapper.fromCreateDto(null); + Assertions.assertNull(entity); + } + + @Test + void mapFromNullCreateDto() { + var entity = exerciseMapper.fromCreateDto(null); + Assertions.assertNull(entity); + } + + @Test + void mapFromCreateDto() { + var entity = exerciseMapper.fromCreateDto(exerciseCreateDto); + Assertions.assertNotNull(entity); + } + + + + + @Test + void mapNullToDto() { + var createdDto = exerciseMapper.toDto(null); + Assertions.assertNull(createdDto); + } + + +} diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionControllerTest.java similarity index 68% rename from application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionTest.java rename to application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionControllerTest.java index 8383971d23f186ac9740263ebb47ed4ab303be1f..0f5cbfa707c3660a91f6ab7efaf342fc14a88d3b 100644 --- a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionTest.java +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionControllerTest.java @@ -4,43 +4,32 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityNotFoundException; import org.fuseri.model.dto.exercise.AnswerDto; import org.fuseri.model.dto.exercise.AnswerInQuestionCreateDto; -import org.fuseri.model.dto.exercise.ExerciseCreateDto; import org.fuseri.model.dto.exercise.QuestionCreateDto; import org.fuseri.model.dto.exercise.QuestionDto; import org.fuseri.model.dto.exercise.QuestionUpdateDto; -import org.fuseri.moduleexercise.exercise.Exercise; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatchers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; + +import java.util.ArrayList; +import java.util.List; import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import java.util.ArrayList; -import java.util.List; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; - @SpringBootTest @AutoConfigureMockMvc -public class QuestionTest { +public class QuestionControllerTest { @Autowired ObjectMapper objectMapper; @@ -60,28 +49,9 @@ public class QuestionTest { } } - private void createExercise() { - var postExercise = new ExerciseCreateDto("idioms", "exercise on basic idioms", 2, 0L); - var postExercise2 = new ExerciseCreateDto("riddles", "simple english riddles", 2, 0L); - try { - mockMvc.perform(post("/exercises").content(asJsonString(postExercise)).contentType(MediaType.APPLICATION_JSON)); - mockMvc.perform(post("/exercises").content(asJsonString(postExercise2)).contentType(MediaType.APPLICATION_JSON)); - - } catch (Exception e) { - assert (false); - } - } - @BeforeEach void init() { -// createExercise(); - - qston = new QuestionDto("\"what is the meaning of: costs an arm and leg\"",1,new ArrayList<>()); - long id = 2; - - var question = new QuestionCreateDto("this statement is false", id, List.of(new AnswerInQuestionCreateDto("dis a logical paradox", true))); - var question2 = new QuestionCreateDto("What month of the year has 28 days?", id, List.of(new AnswerInQuestionCreateDto("February", false), new AnswerInQuestionCreateDto("All of them", true))); - ResultActions posted = null; + qston = new QuestionDto("\"what is the meaning of: costs an arm and leg\"", 1, new ArrayList<>()); } @@ -94,7 +64,7 @@ public class QuestionTest { posted.andExpect(status().isCreated()) .andExpect(jsonPath("$.text", is(qston.getText()))) - .andExpect(jsonPath("$.exerciseId", is((int)qston.getExerciseId()))); + .andExpect(jsonPath("$.exerciseId", is((int) qston.getExerciseId()))); } @@ -137,18 +107,10 @@ public class QuestionTest { posted.andExpect(status().is4xxClientError()); } - private QuestionDto createQuestion(long id) throws Exception { - var question = new QuestionCreateDto("this statement is false", id, List.of(new AnswerInQuestionCreateDto("dis a logical paradox", true))); - var posted = mockMvc.perform(post("/questions").content(asJsonString(question)).contentType(MediaType.APPLICATION_JSON)); - var cont = posted.andReturn().getResponse().getContentAsString(); - var res = objectMapper.readValue(cont, QuestionDto.class); - return res; - } - @Test void getQuestion() throws Exception { - var question = new QuestionDto("this statement is false",1L,new ArrayList<>()); + var question = new QuestionDto("this statement is false", 1L, new ArrayList<>()); when(facade.find(1)).thenReturn(question); var gets = mockMvc.perform(get("/questions/1")); gets.andExpect(status().isOk()).andExpect(jsonPath("$.text", is("this statement is false"))); @@ -164,8 +126,6 @@ public class QuestionTest { @Test void getAnswer() throws Exception { - - var sss = List.of(new AnswerDto("February", false), new AnswerDto("All of them", true)); when(facade.getQuestionAnswers(2)).thenReturn(sss); var gets = mockMvc.perform(get("/questions/2/answers")); @@ -177,7 +137,7 @@ public class QuestionTest { } @Test - void TestUpdate() throws Exception { + void testUpdate() throws Exception { var updated = """ { "text": "wat a paradox?", @@ -185,7 +145,7 @@ public class QuestionTest { } """; - var question = new QuestionDto("wat a paradox?",1,new ArrayList<>()); + var question = new QuestionDto("wat a paradox?", 1, new ArrayList<>()); when(facade.patchUpdate(ArgumentMatchers.eq(1), ArgumentMatchers.isA(QuestionUpdateDto.class))).thenReturn(question); var gets = mockMvc.perform(put("/questions/1").content(updated).contentType(MediaType.APPLICATION_JSON)); @@ -193,13 +153,13 @@ public class QuestionTest { gets.andExpect(status().isOk()); } -@Test + @Test void deleteExisting() { - try { - mockMvc.perform(delete("/questions/1")).andExpect(status().isNoContent()); - } catch (Exception e) { - throw new RuntimeException(e); + try { + mockMvc.perform(delete("/questions/1")).andExpect(status().isNoContent()); + } catch (Exception e) { + throw new RuntimeException(e); + } } -} } diff --git a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionFacadeTest.java b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionFacadeTest.java index 33e1ca0a8f8b9ff86d3ec16f12a5df86738c5d29..b9123098f93b35c413250cd96ec6b76d9d7e64f6 100644 --- a/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionFacadeTest.java +++ b/application/module-exercise/src/test/java/org/fuseri/moduleexercise/question/QuestionFacadeTest.java @@ -102,9 +102,10 @@ class QuestionFacadeTest { void getQuestionAnswers() { long id = 1L; List<Answer> answers = List.of(new Answer("BA", true, question)); + question.setAnswers(new HashSet<>(answers)); List<AnswerDto> answerDtos = List.of(new AnswerDto("BA", true)); + when(questionService.find(id)).thenReturn(question); when(answerMapper.toDtoList(answers)).thenReturn(answerDtos); - when(answerService.findAllByQuestionId(id)).thenReturn(answers); var actualAnswers = questionFacade.getQuestionAnswers(id); assertEquals(answerDtos, actualAnswers); }