From 80e6b45aa11447cf679e4109fa22b00001e42f32 Mon Sep 17 00:00:00 2001 From: Adrian Pionka Date: Sat, 16 Dec 2023 12:30:20 +0100 Subject: [PATCH] refactor ExamService --- ...xamAnswerForAllQuestionValidationRule.java | 40 +++++++++++++ .../exam/EndExamQuizExistValidationRule.java | 24 ++++++++ .../domain/exam/EndExamValidationRule.java | 11 ++++ .../quiz/domain/exam/EndExamValidator.java | 19 +++++++ .../pionas/quiz/domain/exam/ExamFactory.java | 43 -------------- .../quiz/domain/exam/ExamServiceImpl.java | 56 +++++++++++++++---- .../domain/exam/api/ExamAnswerRepository.java | 8 +++ .../quiz/domain/exam/api/ExamDetails.java | 5 ++ .../quiz/domain/exam/api/ExamRepository.java | 4 +- .../quiz/domain/exam/api/NewExamAnswer.java | 48 ++++++++++++++++ .../quiz/domain/exam/api/NewExamDetails.java | 17 ++++++ .../domain/quiz/api/QuizAnswerRepository.java | 8 +++ .../quiz/domain/quiz/api/QuizRepository.java | 4 +- .../quiz/domain/shared/DateTimeProvider.java | 8 +++ .../database/exam/ExamAnswerEntity.java | 16 ++++++ .../database/exam/ExamAnswerId.java | 13 +++++ .../database/exam/ExamAnswerJpaMapper.java | 15 +++++ .../exam/ExamAnswerJpaRepository.java | 11 ++++ .../exam/ExamAnswerRepositoryImpl.java | 21 +++++++ .../database/exam/ExamEntity.java | 20 +++++++ .../database/exam/ExamJpaMapper.java | 10 ++++ .../database/exam/ExamJpaRepository.java | 10 ++++ .../database/exam/ExamRepositoryImpl.java | 28 ++++++++++ .../quiz/QuizAnswerJpaRepository.java | 15 +++++ .../quiz/QuizAnswerRepositoryImpl.java | 19 +++++++ .../database/quiz/QuizRepositoryImpl.java | 9 ++- .../shared/DateTimeProviderImpl.java | 15 +++++ .../quiz/domain/exam/ExamServiceTest.java | 23 +++++--- 28 files changed, 454 insertions(+), 66 deletions(-) create mode 100644 src/main/java/info/pionas/quiz/domain/exam/EndExamAnswerForAllQuestionValidationRule.java create mode 100644 src/main/java/info/pionas/quiz/domain/exam/EndExamQuizExistValidationRule.java create mode 100644 src/main/java/info/pionas/quiz/domain/exam/EndExamValidationRule.java create mode 100644 src/main/java/info/pionas/quiz/domain/exam/EndExamValidator.java delete mode 100644 src/main/java/info/pionas/quiz/domain/exam/ExamFactory.java create mode 100644 src/main/java/info/pionas/quiz/domain/exam/api/ExamAnswerRepository.java create mode 100644 src/main/java/info/pionas/quiz/domain/exam/api/NewExamAnswer.java create mode 100644 src/main/java/info/pionas/quiz/domain/exam/api/NewExamDetails.java create mode 100644 src/main/java/info/pionas/quiz/domain/quiz/api/QuizAnswerRepository.java create mode 100644 src/main/java/info/pionas/quiz/domain/shared/DateTimeProvider.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerEntity.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerId.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerJpaMapper.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerJpaRepository.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerRepositoryImpl.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamEntity.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamJpaMapper.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamJpaRepository.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamRepositoryImpl.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizAnswerJpaRepository.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizAnswerRepositoryImpl.java create mode 100644 src/main/java/info/pionas/quiz/infrastructure/shared/DateTimeProviderImpl.java diff --git a/src/main/java/info/pionas/quiz/domain/exam/EndExamAnswerForAllQuestionValidationRule.java b/src/main/java/info/pionas/quiz/domain/exam/EndExamAnswerForAllQuestionValidationRule.java new file mode 100644 index 0000000..a7adf38 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/EndExamAnswerForAllQuestionValidationRule.java @@ -0,0 +1,40 @@ +package info.pionas.quiz.domain.exam; + +import info.pionas.quiz.domain.exam.api.AnswerForQuestionNotFoundException; +import info.pionas.quiz.domain.exam.api.PassExamAnswer; +import info.pionas.quiz.domain.quiz.api.Question; +import info.pionas.quiz.domain.quiz.api.Quiz; +import info.pionas.quiz.domain.quiz.api.QuizRepository; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Component +@AllArgsConstructor +class EndExamAnswerForAllQuestionValidationRule implements EndExamValidationRule { + + private final QuizRepository quizRepository; + + @Override + public void validate(UUID quizId, List answers) { + quizRepository.findById(quizId) + .map(Quiz::getQuestions) + .orElseGet(Collections::emptyList) + .forEach(question -> checkQuestionHasAnswer(question, answers)); + } + + private void checkQuestionHasAnswer(Question question, List answers) { + final var questionId = question.getId(); + boolean existAnswerForQuestion = Optional.ofNullable(answers) + .orElseGet(Collections::emptyList) + .stream() + .anyMatch(passExamAnswer -> passExamAnswer.isAnswerForQuestion(questionId)); + if (!existAnswerForQuestion) { + throw new AnswerForQuestionNotFoundException(questionId); + } + } +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/EndExamQuizExistValidationRule.java b/src/main/java/info/pionas/quiz/domain/exam/EndExamQuizExistValidationRule.java new file mode 100644 index 0000000..2ed4c27 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/EndExamQuizExistValidationRule.java @@ -0,0 +1,24 @@ +package info.pionas.quiz.domain.exam; + +import info.pionas.quiz.domain.exam.api.PassExamAnswer; +import info.pionas.quiz.domain.quiz.QuizNotFoundException; +import info.pionas.quiz.domain.quiz.api.QuizRepository; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.UUID; + +@Component +@AllArgsConstructor +class EndExamQuizExistValidationRule implements EndExamValidationRule { + + private final QuizRepository quizRepository; + + @Override + public void validate(UUID quizId, List answers) { + if (!quizRepository.existById(quizId)) { + throw new QuizNotFoundException(quizId); + } + } +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/EndExamValidationRule.java b/src/main/java/info/pionas/quiz/domain/exam/EndExamValidationRule.java new file mode 100644 index 0000000..6b9e025 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/EndExamValidationRule.java @@ -0,0 +1,11 @@ +package info.pionas.quiz.domain.exam; + +import info.pionas.quiz.domain.exam.api.PassExamAnswer; + +import java.util.List; +import java.util.UUID; + +interface EndExamValidationRule { + + void validate(UUID quizId, List answers); +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/EndExamValidator.java b/src/main/java/info/pionas/quiz/domain/exam/EndExamValidator.java new file mode 100644 index 0000000..8e59241 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/EndExamValidator.java @@ -0,0 +1,19 @@ +package info.pionas.quiz.domain.exam; + +import info.pionas.quiz.domain.exam.api.PassExamAnswer; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.UUID; + +@Component +@AllArgsConstructor +class EndExamValidator { + + private final List examValidationRuleList; + + void validate(UUID quizId, List answers) { + examValidationRuleList.forEach(endExamValidationRule -> endExamValidationRule.validate(quizId, answers)); + } +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/ExamFactory.java b/src/main/java/info/pionas/quiz/domain/exam/ExamFactory.java deleted file mode 100644 index bf96a03..0000000 --- a/src/main/java/info/pionas/quiz/domain/exam/ExamFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -package info.pionas.quiz.domain.exam; - - -import info.pionas.quiz.domain.exam.api.AnswerForQuestionNotFoundException; -import info.pionas.quiz.domain.exam.api.ExamAnswer; -import info.pionas.quiz.domain.exam.api.ExamDetails; -import info.pionas.quiz.domain.exam.api.PassExamAnswer; -import info.pionas.quiz.domain.quiz.api.Question; -import info.pionas.quiz.domain.quiz.api.Quiz; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -@Component -@NoArgsConstructor(access = AccessLevel.PACKAGE) -class ExamFactory { - - public ExamDetails endExam(String username, Quiz quiz, List answers) { - return ExamDetails.of(username, quiz, getAnswerResult(quiz.getQuestions(), answers)); - } - - private List getAnswerResult(List questions, List answers) { - return Optional.ofNullable(questions) - .orElseGet(Collections::emptyList) - .stream() - .map(question -> ExamAnswer.of(question, getAnswerForQuestion(question, answers))) - .toList(); - } - - private PassExamAnswer getAnswerForQuestion(Question question, List answers) { - final var questionId = question.getId(); - return Optional.ofNullable(answers) - .orElseGet(Collections::emptyList) - .stream() - .filter(passExamAnswer -> passExamAnswer.isAnswerForQuestion(questionId)) - .findAny() - .orElseThrow(() -> new AnswerForQuestionNotFoundException(questionId)); - } -} diff --git a/src/main/java/info/pionas/quiz/domain/exam/ExamServiceImpl.java b/src/main/java/info/pionas/quiz/domain/exam/ExamServiceImpl.java index d7d9635..7476cb5 100644 --- a/src/main/java/info/pionas/quiz/domain/exam/ExamServiceImpl.java +++ b/src/main/java/info/pionas/quiz/domain/exam/ExamServiceImpl.java @@ -1,31 +1,65 @@ package info.pionas.quiz.domain.exam; -import info.pionas.quiz.domain.exam.api.PassExamAnswer; -import info.pionas.quiz.domain.exam.api.ExamRepository; -import info.pionas.quiz.domain.exam.api.ExamResult; -import info.pionas.quiz.domain.exam.api.ExamService; -import info.pionas.quiz.domain.quiz.QuizNotFoundException; -import info.pionas.quiz.domain.quiz.api.QuizRepository; +import info.pionas.quiz.domain.exam.api.*; +import info.pionas.quiz.domain.quiz.api.QuizAnswerRepository; +import info.pionas.quiz.domain.shared.DateTimeProvider; import info.pionas.quiz.domain.shared.UuidGenerator; +import jakarta.transaction.Transactional; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.UUID; @Service @AllArgsConstructor class ExamServiceImpl implements ExamService { - private final QuizRepository quizRepository; private final ExamRepository examRepository; - private final ExamFactory examFactory; + private final ExamAnswerRepository examAnswerRepository; + private final QuizAnswerRepository quizAnswerRepository; + private final EndExamValidator endExamValidator; private final UuidGenerator uuidGenerator; + private final DateTimeProvider dateTimeProvider; @Override + @Transactional public ExamResult endExam(String username, UUID quizId, List answers) { - final var quiz = quizRepository.findById(quizId) - .orElseThrow(() -> new QuizNotFoundException(quizId)); - return examRepository.save(uuidGenerator.generate(), examFactory.endExam(username, quiz, answers)); + endExamValidator.validate(quizId, answers); + final var resultId = uuidGenerator.generate(); + final var dateTime = dateTimeProvider.now(); + + examRepository.save(getNewExamDetails(resultId, quizId, username, dateTime)); + examAnswerRepository.saveAll(getExamAnswers(resultId, answers, dateTime)); + return examRepository.getById(resultId); + } + + private NewExamDetails getNewExamDetails(UUID resultId, UUID quizId, String username, LocalDateTime dateTime) { + return NewExamDetails.builder() + .id(resultId) + .username(username) + .quizId(quizId) + .created(dateTime) + .build(); + } + + private List getExamAnswers(UUID resultId, List answers, LocalDateTime dateTime) { + return Optional.ofNullable(answers) + .orElseGet(Collections::emptyList) + .stream() + .map(answer -> prepareNewExamAnswer(resultId, answer, dateTime)) + .toList(); + } + + private NewExamAnswer prepareNewExamAnswer(UUID resultId, PassExamAnswer answer, LocalDateTime dateTime) { + if (quizAnswerRepository.isCorrectAnswer(answer.getQuestionId(), answer.getAnswerId())) { + return NewExamAnswer.correct(resultId, dateTime, answer.getQuestionId(), answer.getAnswerId()); + } else { + return NewExamAnswer.wrong(resultId, dateTime, answer.getQuestionId(), answer.getAnswerId()); + } } + } diff --git a/src/main/java/info/pionas/quiz/domain/exam/api/ExamAnswerRepository.java b/src/main/java/info/pionas/quiz/domain/exam/api/ExamAnswerRepository.java new file mode 100644 index 0000000..2cd3f4d --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/api/ExamAnswerRepository.java @@ -0,0 +1,8 @@ +package info.pionas.quiz.domain.exam.api; + +import java.util.List; + +public interface ExamAnswerRepository { + + void saveAll(List answers); +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/api/ExamDetails.java b/src/main/java/info/pionas/quiz/domain/exam/api/ExamDetails.java index 68a60b8..91d2ad8 100644 --- a/src/main/java/info/pionas/quiz/domain/exam/api/ExamDetails.java +++ b/src/main/java/info/pionas/quiz/domain/exam/api/ExamDetails.java @@ -6,6 +6,7 @@ import lombok.Getter; import java.util.List; +import java.util.UUID; @Builder @Getter @@ -15,4 +16,8 @@ public class ExamDetails { private String username; private Quiz quiz; private List answers; + + public UUID getQuizId() { + return quiz.getId(); + } } diff --git a/src/main/java/info/pionas/quiz/domain/exam/api/ExamRepository.java b/src/main/java/info/pionas/quiz/domain/exam/api/ExamRepository.java index ad782de..8efa3ee 100644 --- a/src/main/java/info/pionas/quiz/domain/exam/api/ExamRepository.java +++ b/src/main/java/info/pionas/quiz/domain/exam/api/ExamRepository.java @@ -4,5 +4,7 @@ public interface ExamRepository { - ExamResult save(UUID id, ExamDetails examDetails); + void save(NewExamDetails newExamDetails); + + ExamResult getById(UUID id); } diff --git a/src/main/java/info/pionas/quiz/domain/exam/api/NewExamAnswer.java b/src/main/java/info/pionas/quiz/domain/exam/api/NewExamAnswer.java new file mode 100644 index 0000000..03ade86 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/api/NewExamAnswer.java @@ -0,0 +1,48 @@ +package info.pionas.quiz.domain.exam.api; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Builder +@Getter +public class NewExamAnswer { + + private UUID resultId; + private UUID questionId; + private UUID answerId; + private Boolean correct; + private LocalDateTime created; + + public static NewExamAnswer of(UUID resultId, ExamAnswer answer, LocalDateTime localDateTime) { + return NewExamAnswer.builder() + .resultId(resultId) + .questionId(answer.getQuestionId()) + .answerId(answer.getAnswerId()) + .correct(answer.getCorrect()) + .created(localDateTime) + .build(); + } + + public static NewExamAnswer correct(UUID resultId, LocalDateTime dateTime, UUID questionId, UUID answerId) { + return NewExamAnswer.builder() + .resultId(resultId) + .questionId(questionId) + .answerId(answerId) + .correct(true) + .created(dateTime) + .build(); + } + + public static NewExamAnswer wrong(UUID resultId, LocalDateTime dateTime, UUID questionId, UUID answerId) { + return NewExamAnswer.builder() + .resultId(resultId) + .questionId(questionId) + .answerId(answerId) + .correct(false) + .created(dateTime) + .build(); + } +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/api/NewExamDetails.java b/src/main/java/info/pionas/quiz/domain/exam/api/NewExamDetails.java new file mode 100644 index 0000000..d352b3e --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/api/NewExamDetails.java @@ -0,0 +1,17 @@ +package info.pionas.quiz.domain.exam.api; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Builder +@Getter +public class NewExamDetails { + + private UUID id; + private String username; + private UUID quizId; + private LocalDateTime created; +} diff --git a/src/main/java/info/pionas/quiz/domain/quiz/api/QuizAnswerRepository.java b/src/main/java/info/pionas/quiz/domain/quiz/api/QuizAnswerRepository.java new file mode 100644 index 0000000..d270531 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/quiz/api/QuizAnswerRepository.java @@ -0,0 +1,8 @@ +package info.pionas.quiz.domain.quiz.api; + +import java.util.UUID; + +public interface QuizAnswerRepository { + + boolean isCorrectAnswer(UUID questionId, UUID answerId); +} diff --git a/src/main/java/info/pionas/quiz/domain/quiz/api/QuizRepository.java b/src/main/java/info/pionas/quiz/domain/quiz/api/QuizRepository.java index b3f16b0..4d92a66 100644 --- a/src/main/java/info/pionas/quiz/domain/quiz/api/QuizRepository.java +++ b/src/main/java/info/pionas/quiz/domain/quiz/api/QuizRepository.java @@ -7,5 +7,7 @@ public interface QuizRepository { Quiz save(Quiz quiz); - Optional findById(UUID uuid); + Optional findById(UUID id); + + boolean existById(UUID id); } diff --git a/src/main/java/info/pionas/quiz/domain/shared/DateTimeProvider.java b/src/main/java/info/pionas/quiz/domain/shared/DateTimeProvider.java new file mode 100644 index 0000000..e9d7cd6 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/shared/DateTimeProvider.java @@ -0,0 +1,8 @@ +package info.pionas.quiz.domain.shared; + +import java.time.LocalDateTime; + +public interface DateTimeProvider { + + LocalDateTime now(); +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerEntity.java b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerEntity.java new file mode 100644 index 0000000..ef079a4 --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerEntity.java @@ -0,0 +1,16 @@ +package info.pionas.quiz.infrastructure.database.exam; + +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; + +import java.time.LocalDateTime; + +@Entity +public class ExamAnswerEntity { + + @EmbeddedId + private ExamAnswerId id; + + private boolean correct; + private LocalDateTime created; +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerId.java b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerId.java new file mode 100644 index 0000000..c2f3cd2 --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerId.java @@ -0,0 +1,13 @@ +package info.pionas.quiz.infrastructure.database.exam; + +import jakarta.persistence.Embeddable; + +import java.util.UUID; + +@Embeddable +class ExamAnswerId { + + private UUID resultId; + private UUID questionId; + private UUID answerId; +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerJpaMapper.java b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerJpaMapper.java new file mode 100644 index 0000000..72b7c03 --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerJpaMapper.java @@ -0,0 +1,15 @@ +package info.pionas.quiz.infrastructure.database.exam; + +import info.pionas.quiz.domain.exam.api.NewExamAnswer; +import org.mapstruct.Mapper; + +import java.util.List; + +@Mapper(componentModel = "spring") +interface ExamAnswerJpaMapper { + + ExamAnswerEntity map(NewExamAnswer newExamAnswer); + + List map(List newExamAnswers); + +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerJpaRepository.java b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerJpaRepository.java new file mode 100644 index 0000000..265c452 --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerJpaRepository.java @@ -0,0 +1,11 @@ +package info.pionas.quiz.infrastructure.database.exam; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +interface ExamAnswerJpaRepository extends JpaRepository { + +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerRepositoryImpl.java b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerRepositoryImpl.java new file mode 100644 index 0000000..6a820ff --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamAnswerRepositoryImpl.java @@ -0,0 +1,21 @@ +package info.pionas.quiz.infrastructure.database.exam; + +import info.pionas.quiz.domain.exam.api.ExamAnswerRepository; +import info.pionas.quiz.domain.exam.api.NewExamAnswer; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@AllArgsConstructor +class ExamAnswerRepositoryImpl implements ExamAnswerRepository { + + private final ExamAnswerJpaRepository examAnswerJpaRepository; + private final ExamAnswerJpaMapper examAnswerJpaMapper; + + @Override + public void saveAll(List newExamAnswers) { + examAnswerJpaRepository.saveAll(examAnswerJpaMapper.map(newExamAnswers)); + } +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamEntity.java b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamEntity.java new file mode 100644 index 0000000..2384cec --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamEntity.java @@ -0,0 +1,20 @@ +package info.pionas.quiz.infrastructure.database.exam; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Entity +public class ExamEntity { + + @Id + private UUID id; + private String username; + private UUID quizId; + private LocalDateTime created; +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamJpaMapper.java b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamJpaMapper.java new file mode 100644 index 0000000..22d1bf5 --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamJpaMapper.java @@ -0,0 +1,10 @@ +package info.pionas.quiz.infrastructure.database.exam; + +import info.pionas.quiz.domain.exam.api.NewExamDetails; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +interface ExamJpaMapper { + + ExamEntity map(NewExamDetails newExamDetails); +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamJpaRepository.java b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamJpaRepository.java new file mode 100644 index 0000000..ae1d46a --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamJpaRepository.java @@ -0,0 +1,10 @@ +package info.pionas.quiz.infrastructure.database.exam; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +interface ExamJpaRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamRepositoryImpl.java b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamRepositoryImpl.java new file mode 100644 index 0000000..c4d2e41 --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/exam/ExamRepositoryImpl.java @@ -0,0 +1,28 @@ +package info.pionas.quiz.infrastructure.database.exam; + +import info.pionas.quiz.domain.exam.api.ExamDetails; +import info.pionas.quiz.domain.exam.api.ExamRepository; +import info.pionas.quiz.domain.exam.api.ExamResult; +import info.pionas.quiz.domain.exam.api.NewExamDetails; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +@AllArgsConstructor +class ExamRepositoryImpl implements ExamRepository { + + private final ExamJpaRepository examJpaRepository; + private final ExamJpaMapper examJpaMapper; + + @Override + public void save(NewExamDetails newExamDetails) { + examJpaRepository.save(examJpaMapper.map(newExamDetails)); + } + + @Override + public ExamResult getById(UUID id) { + return null; + } +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizAnswerJpaRepository.java b/src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizAnswerJpaRepository.java new file mode 100644 index 0000000..25a8b7b --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizAnswerJpaRepository.java @@ -0,0 +1,15 @@ +package info.pionas.quiz.infrastructure.database.quiz; + +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 java.util.UUID; + +@Repository +interface QuizAnswerJpaRepository extends JpaRepository { + + @Query("SELECT count(ae)>0 from AnswerEntity ae WHERE ae.questionEntity.id = :questionId AND ae.id = :answerId AND ae.correct = true") + boolean isCorrectAnswer(@Param("questionId") UUID questionId, @Param("answerId") UUID answerId); +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizAnswerRepositoryImpl.java b/src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizAnswerRepositoryImpl.java new file mode 100644 index 0000000..a47b97f --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizAnswerRepositoryImpl.java @@ -0,0 +1,19 @@ +package info.pionas.quiz.infrastructure.database.quiz; + +import info.pionas.quiz.domain.quiz.api.QuizAnswerRepository; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +@AllArgsConstructor +class QuizAnswerRepositoryImpl implements QuizAnswerRepository { + + private final QuizAnswerJpaRepository quizAnswerJpaRepository; + + @Override + public boolean isCorrectAnswer(UUID questionId, UUID answerId) { + return quizAnswerJpaRepository.isCorrectAnswer(questionId, answerId); + } +} diff --git a/src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizRepositoryImpl.java b/src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizRepositoryImpl.java index 981a817..1a3ffbc 100644 --- a/src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizRepositoryImpl.java +++ b/src/main/java/info/pionas/quiz/infrastructure/database/quiz/QuizRepositoryImpl.java @@ -21,8 +21,13 @@ public Quiz save(Quiz quiz) { } @Override - public Optional findById(UUID uuid) { - return quizJpaRepository.findById(uuid) + public Optional findById(UUID id) { + return quizJpaRepository.findById(id) .map(quizJpaMapper::map); } + + @Override + public boolean existById(UUID id) { + return quizJpaRepository.existsById(id); + } } diff --git a/src/main/java/info/pionas/quiz/infrastructure/shared/DateTimeProviderImpl.java b/src/main/java/info/pionas/quiz/infrastructure/shared/DateTimeProviderImpl.java new file mode 100644 index 0000000..af7f44a --- /dev/null +++ b/src/main/java/info/pionas/quiz/infrastructure/shared/DateTimeProviderImpl.java @@ -0,0 +1,15 @@ +package info.pionas.quiz.infrastructure.shared; + +import info.pionas.quiz.domain.shared.DateTimeProvider; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +class DateTimeProviderImpl implements DateTimeProvider { + + @Override + public LocalDateTime now() { + return LocalDateTime.now(); + } +} diff --git a/src/test/java/info/pionas/quiz/domain/exam/ExamServiceTest.java b/src/test/java/info/pionas/quiz/domain/exam/ExamServiceTest.java index 19d0c1a..a3a934a 100644 --- a/src/test/java/info/pionas/quiz/domain/exam/ExamServiceTest.java +++ b/src/test/java/info/pionas/quiz/domain/exam/ExamServiceTest.java @@ -2,10 +2,8 @@ import info.pionas.quiz.domain.exam.api.*; import info.pionas.quiz.domain.quiz.QuizNotFoundException; -import info.pionas.quiz.domain.quiz.api.Answer; -import info.pionas.quiz.domain.quiz.api.Question; -import info.pionas.quiz.domain.quiz.api.Quiz; -import info.pionas.quiz.domain.quiz.api.QuizRepository; +import info.pionas.quiz.domain.quiz.api.*; +import info.pionas.quiz.domain.shared.DateTimeProvider; import info.pionas.quiz.domain.shared.UuidGenerator; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -14,28 +12,33 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; class ExamServiceTest { private final QuizRepository quizRepository = Mockito.mock(QuizRepository.class); private final ExamRepository examRepository = Mockito.mock(ExamRepository.class); + private final ExamAnswerRepository examAnswerRepository = Mockito.mock(ExamAnswerRepository.class); + private final QuizAnswerRepository quizAnswerRepository = Mockito.mock(QuizAnswerRepository.class); private final UuidGenerator uuidGenerator = Mockito.mock(UuidGenerator.class); - private final ExamFactory examFactory = new ExamFactory(); - private final ExamService service = new ExamServiceImpl(quizRepository, examRepository, examFactory, uuidGenerator); + private final DateTimeProvider dateTimeProvider = Mockito.mock(DateTimeProvider.class); + private final EndExamValidator endExamValidator = new EndExamValidator(List.of(new EndExamQuizExistValidationRule(quizRepository), new EndExamAnswerForAllQuestionValidationRule(quizRepository))); + private final ExamService service = new ExamServiceImpl(examRepository, examAnswerRepository, quizAnswerRepository, endExamValidator, uuidGenerator, dateTimeProvider); @Test void should_throw_not_found_exception_when_quiz_by_id_not_exist() { //given final var quizId = UUID.fromString("b83d5c22-7b78-4435-9daa-17bb532c0f63"); final var answers = Collections.emptyList(); - when(quizRepository.findById(quizId)).thenReturn(Optional.empty()); + when(quizRepository.existById(quizId)).thenReturn(false); //when QuizNotFoundException exception = assertThrows(QuizNotFoundException.class, () -> service.endExam("username", quizId, answers)); //then assertThat(exception).isNotNull(); assertThat(exception.getMessage()) .isEqualTo(String.format("Quiz by ID %s not exist", quizId)); + Mockito.verify(quizRepository, times(0)).findById(quizId); } @Test @@ -43,6 +46,7 @@ void should_throw_answer_for_question_not_found_exception_when_question_has_not_ //given final var quizId = UUID.fromString("b83d5c22-7b78-4435-9daa-17bb532c0f63"); final var answers = Collections.emptyList(); + when(quizRepository.existById(quizId)).thenReturn(true); when(quizRepository.findById(quizId)).thenReturn(Optional.of(getQuiz(quizId))); //when AnswerForQuestionNotFoundException exception = assertThrows(AnswerForQuestionNotFoundException.class, () -> service.endExam("username", quizId, answers)); @@ -58,15 +62,18 @@ void should_end_exam() { final var examResultId = UUID.fromString("7a398eb6-1d20-4a05-b13b-c752c3c7c5d3"); final var quizId = UUID.fromString("b83d5c22-7b78-4435-9daa-17bb532c0f63"); final var answers = getAnswers(); + when(quizRepository.existById(quizId)).thenReturn(true); when(quizRepository.findById(quizId)).thenReturn(Optional.of(getQuiz(quizId))); - when(examRepository.save(Mockito.any(UUID.class), Mockito.any(ExamDetails.class))).thenReturn(createExamResult(examResultId, getQuiz(quizId), answers)); when(uuidGenerator.generate()).thenReturn(examResultId); + when(examRepository.getById(examResultId)).thenReturn(createExamResult(examResultId, getQuiz(quizId), answers)); //when ExamResult examResult = service.endExam("username", quizId, answers); //then assertThat(examResult).isNotNull(); assertThat(examResult.getId()).isEqualTo(examResultId); assertThat(examResult.getCorrectAnswer()).isEqualTo(1L); + Mockito.verify(examRepository, times(1)).save(Mockito.any()); + Mockito.verify(examAnswerRepository, times(1)).saveAll(Mockito.any()); } private Quiz getQuiz(UUID quizId) {