diff --git a/src/main/java/info/pionas/quiz/domain/exam/ExamFactory.java b/src/main/java/info/pionas/quiz/domain/exam/ExamFactory.java new file mode 100644 index 0000000..bf96a03 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/ExamFactory.java @@ -0,0 +1,43 @@ +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 new file mode 100644 index 0000000..d7d9635 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/ExamServiceImpl.java @@ -0,0 +1,31 @@ +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.shared.UuidGenerator; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +@AllArgsConstructor +class ExamServiceImpl implements ExamService { + + private final QuizRepository quizRepository; + private final ExamRepository examRepository; + private final ExamFactory examFactory; + private final UuidGenerator uuidGenerator; + + @Override + 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)); + } +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/api/AnswerForQuestionNotFoundException.java b/src/main/java/info/pionas/quiz/domain/exam/api/AnswerForQuestionNotFoundException.java new file mode 100644 index 0000000..a901490 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/api/AnswerForQuestionNotFoundException.java @@ -0,0 +1,11 @@ +package info.pionas.quiz.domain.exam.api; + +import info.pionas.quiz.domain.shared.exception.NotFoundException; + +import java.util.UUID; + +public class AnswerForQuestionNotFoundException extends NotFoundException { + public AnswerForQuestionNotFoundException(UUID questionId) { + super(String.format("There is no answer to the question %s", questionId)); + } +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/api/ExamAnswer.java b/src/main/java/info/pionas/quiz/domain/exam/api/ExamAnswer.java new file mode 100644 index 0000000..581b75d --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/api/ExamAnswer.java @@ -0,0 +1,24 @@ +package info.pionas.quiz.domain.exam.api; + +import info.pionas.quiz.domain.quiz.api.Question; +import lombok.Builder; +import lombok.Getter; + +import java.util.UUID; + +@Getter +@Builder +public class ExamAnswer { + + private UUID questionId; + private UUID answerId; + private Boolean correct; + + public static ExamAnswer of(Question question, PassExamAnswer answerForQuestion) { + return ExamAnswer.builder() + .questionId(question.getId()) + .answerId(answerForQuestion.getAnswerId()) + .correct(question.isCorrectAnswer(answerForQuestion)) + .build(); + } +} 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 new file mode 100644 index 0000000..68a60b8 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/api/ExamDetails.java @@ -0,0 +1,18 @@ +package info.pionas.quiz.domain.exam.api; + +import info.pionas.quiz.domain.quiz.api.Quiz; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +@AllArgsConstructor(staticName = "of") +public class ExamDetails { + + private String username; + private Quiz quiz; + private List answers; +} 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 new file mode 100644 index 0000000..ad782de --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/api/ExamRepository.java @@ -0,0 +1,8 @@ +package info.pionas.quiz.domain.exam.api; + +import java.util.UUID; + +public interface ExamRepository { + + ExamResult save(UUID id, ExamDetails examDetails); +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/api/ExamResult.java b/src/main/java/info/pionas/quiz/domain/exam/api/ExamResult.java new file mode 100644 index 0000000..56f3979 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/api/ExamResult.java @@ -0,0 +1,23 @@ +package info.pionas.quiz.domain.exam.api; + +import info.pionas.quiz.domain.quiz.api.Quiz; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +import java.util.UUID; + +@Builder +@Getter +public class ExamResult { + + private UUID id; + private Quiz quiz; + private List answers; + + public long getCorrectAnswer() { + return answers.stream() + .filter(ExamAnswer::getCorrect) + .count(); + } +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/api/ExamService.java b/src/main/java/info/pionas/quiz/domain/exam/api/ExamService.java new file mode 100644 index 0000000..48994a8 --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/api/ExamService.java @@ -0,0 +1,9 @@ +package info.pionas.quiz.domain.exam.api; + +import java.util.List; +import java.util.UUID; + +public interface ExamService { + + ExamResult endExam(String username, UUID uuid, List answers); +} diff --git a/src/main/java/info/pionas/quiz/domain/exam/api/PassExamAnswer.java b/src/main/java/info/pionas/quiz/domain/exam/api/PassExamAnswer.java new file mode 100644 index 0000000..4b485db --- /dev/null +++ b/src/main/java/info/pionas/quiz/domain/exam/api/PassExamAnswer.java @@ -0,0 +1,19 @@ +package info.pionas.quiz.domain.exam.api; + +import lombok.Builder; +import lombok.Getter; + +import java.util.Objects; +import java.util.UUID; + +@Getter +@Builder +public class PassExamAnswer { + + private UUID questionId; + private UUID answerId; + + public boolean isAnswerForQuestion(UUID questionId) { + return Objects.equals(this.questionId, questionId); + } +} diff --git a/src/main/java/info/pionas/quiz/domain/quiz/api/Answer.java b/src/main/java/info/pionas/quiz/domain/quiz/api/Answer.java index 8d4ccc2..979828a 100644 --- a/src/main/java/info/pionas/quiz/domain/quiz/api/Answer.java +++ b/src/main/java/info/pionas/quiz/domain/quiz/api/Answer.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; +import java.util.Objects; import java.util.UUID; @Getter @@ -15,4 +16,7 @@ public class Answer { private String content; private boolean correct; + public boolean isAnswerFor(UUID answerId) { + return Objects.equals(id, answerId); + } } diff --git a/src/main/java/info/pionas/quiz/domain/quiz/api/Question.java b/src/main/java/info/pionas/quiz/domain/quiz/api/Question.java index 30a38c6..b469107 100644 --- a/src/main/java/info/pionas/quiz/domain/quiz/api/Question.java +++ b/src/main/java/info/pionas/quiz/domain/quiz/api/Question.java @@ -1,5 +1,6 @@ package info.pionas.quiz.domain.quiz.api; +import info.pionas.quiz.domain.exam.api.PassExamAnswer; import lombok.Builder; import lombok.Getter; @@ -21,4 +22,10 @@ public void update(String content, List updateAnswers) { this.answers.clear(); this.answers.addAll(updateAnswers); } + + public boolean isCorrectAnswer(PassExamAnswer passExamAnswer) { + return answers.stream() + .filter(answer -> answer.isAnswerFor(passExamAnswer.getAnswerId())) + .allMatch(Answer::isCorrect); + } } diff --git a/src/test/java/info/pionas/quiz/domain/exam/ExamServiceTest.java b/src/test/java/info/pionas/quiz/domain/exam/ExamServiceTest.java new file mode 100644 index 0000000..19d0c1a --- /dev/null +++ b/src/test/java/info/pionas/quiz/domain/exam/ExamServiceTest.java @@ -0,0 +1,143 @@ +package info.pionas.quiz.domain.exam; + +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.shared.UuidGenerator; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +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 UuidGenerator uuidGenerator = Mockito.mock(UuidGenerator.class); + private final ExamFactory examFactory = new ExamFactory(); + private final ExamService service = new ExamServiceImpl(quizRepository, examRepository, examFactory, uuidGenerator); + + @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 + 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)); + } + + @Test + void should_throw_answer_for_question_not_found_exception_when_question_has_not_answer() { + //given + final var quizId = UUID.fromString("b83d5c22-7b78-4435-9daa-17bb532c0f63"); + final var answers = Collections.emptyList(); + when(quizRepository.findById(quizId)).thenReturn(Optional.of(getQuiz(quizId))); + //when + AnswerForQuestionNotFoundException exception = assertThrows(AnswerForQuestionNotFoundException.class, () -> service.endExam("username", quizId, answers)); + //then + assertThat(exception).isNotNull(); + assertThat(exception.getMessage()) + .isEqualTo("There is no answer to the question 03df4c0e-a760-4b23-aebe-ac0fd8761804"); + } + + @Test + void should_end_exam() { + //given + 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.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 + ExamResult examResult = service.endExam("username", quizId, answers); + //then + assertThat(examResult).isNotNull(); + assertThat(examResult.getId()).isEqualTo(examResultId); + assertThat(examResult.getCorrectAnswer()).isEqualTo(1L); + } + + private Quiz getQuiz(UUID quizId) { + final var questionId1 = UUID.fromString("03df4c0e-a760-4b23-aebe-ac0fd8761804"); + final var questionId2 = UUID.fromString("0feb6e91-a25c-4c9a-8f6c-acf489a5af6b"); + final var answerId1 = UUID.fromString("1f12ef49-9a35-45f2-9824-edb673602a7f"); + final var answerId2 = UUID.fromString("126b0faa-1de1-4b9e-b79c-8b03361e8839"); + final var answerId3 = UUID.fromString("9f5e41ad-9614-4a14-ac8a-dad55d4ce89f"); + final var answerId4 = UUID.fromString("5e02b9e3-c1d4-43da-b7df-45899da5b9a2"); + final var content1 = "Content 1"; + final var content2 = "Content 2"; + final var content3 = "Content 3"; + final var content4 = "Content 4"; + + final var question1 = Question.builder() + .id(questionId1) + .content("Spring is the best JAVA framework") + .answers(new ArrayList<>(Arrays.asList(createAnswer(answerId1, content1, true), createAnswer(answerId2, content2, false)))) + .build(); + final var question2 = Question.builder() + .id(questionId2) + .content("What is the best Front End framework") + .answers(new ArrayList<>(Arrays.asList(createAnswer(answerId3, content3, false), createAnswer(answerId4, content4, true)))) + .build(); + return Quiz.builder() + .id(quizId) + .title("Title") + .description("Description") + .questions(new ArrayList<>(Arrays.asList(question1, question2))) + .build(); + } + + private Answer createAnswer(UUID id, String content, Boolean correct) { + return Answer.builder() + .id(id) + .content(content) + .correct(correct) + .build(); + } + + private List getAnswers() { + final var questionId1 = UUID.fromString("03df4c0e-a760-4b23-aebe-ac0fd8761804"); + final var answerId1 = UUID.fromString("1f12ef49-9a35-45f2-9824-edb673602a7f"); + final var questionId2 = UUID.fromString("0feb6e91-a25c-4c9a-8f6c-acf489a5af6b"); + final var answerId2 = UUID.fromString("9f5e41ad-9614-4a14-ac8a-dad55d4ce89f"); + return List.of( + createPassExamAnswer(questionId1, answerId1), + createPassExamAnswer(questionId2, answerId2) + ); + } + + private PassExamAnswer createPassExamAnswer(UUID questionId, UUID answerId) { + return PassExamAnswer.builder() + .questionId(questionId) + .answerId(answerId) + .build(); + } + + private ExamResult createExamResult(UUID examResultId, Quiz quiz, List answers) { + final var questions = quiz.getQuestions(); + return ExamResult.builder() + .id(examResultId) + .quiz(quiz) + .answers(List.of( + crateExamAnswer(questions.getFirst(), answers.getFirst()), + crateExamAnswer(questions.getLast(), answers.getLast()) + )) + .build(); + } + + private ExamAnswer crateExamAnswer(Question question, PassExamAnswer passExamAnswer) { + return ExamAnswer.of(question, passExamAnswer); + } +} \ No newline at end of file