From 3ddfac9e9dbc9b09ac1c06fdc06f2e4100b87b47 Mon Sep 17 00:00:00 2001 From: Narek_Meroyan Date: Tue, 14 Feb 2023 11:28:38 +0400 Subject: [PATCH 1/8] The bug connecting upload file is fixed --- .../course/module/ModuleRepository.java | 1 - .../course/module/quiz/EnrolledQuiz.java | 85 ++++++++ .../module/quiz/EnrolledQuizRepository.java | 5 + .../openschool/course/module/quiz/Quiz.java | 104 +++++++++ .../course/module/quiz/QuizController.java | 183 ++++++++++++++++ .../course/module/quiz/QuizRepository.java | 10 + .../course/module/quiz/QuizService.java | 29 +++ .../course/module/quiz/QuizServiceImpl.java | 202 ++++++++++++++++++ .../module/quiz/api/dto/CreateQuizDto.java | 70 ++++++ .../dto/EnrolledQuizAssessmentRequestDto.java | 22 ++ .../EnrolledQuizAssessmentResponseDto.java | 80 +++++++ .../module/quiz/api/dto/EnrolledQuizDto.java | 92 ++++++++ .../quiz/api/dto/ModifyQuizDataRequest.java | 51 +++++ .../api/dto/QuestionWithChosenAnswerDto.java | 34 +++ .../course/module/quiz/api/dto/QuizDto.java | 90 ++++++++ .../EnrolledQuizAssessmentResponseMapper.java | 19 ++ .../module/quiz/api/mapper/QuizMapper.java | 111 ++++++++++ .../course/module/quiz/question/Question.java | 114 ++++++++++ .../quiz/question/QuestionController.java | 113 ++++++++++ .../quiz/question/QuestionRepository.java | 9 + .../module/quiz/question/QuestionService.java | 16 ++ .../quiz/question/QuestionServiceImpl.java | 61 ++++++ .../quiz/question/answer/AnswerOption.java | 85 ++++++++ .../answer/api/dto/AnswerOptionDto.java | 32 +++ .../answer/api/dto/CreateAnswerOptionDto.java | 34 +++ .../answer/api/mapper/AnswerMapper.java | 40 ++++ .../question/api/dto/CreateQuestionDto.java | 61 ++++++ .../quiz/question/api/dto/QuestionDto.java | 72 +++++++ .../question/api/mapper/QuestionMapper.java | 75 +++++++ .../quiz/question/type/QuestionType.java | 55 +++++ .../course/module/quiz/status/QuizStatus.java | 56 +++++ .../db/migration/h2/V1_0_17_updates.sql | 110 ++++++++++ .../db/migration/mysql/V1_0_17__update.sql | 110 ++++++++++ 33 files changed, 2230 insertions(+), 1 deletion(-) create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuiz.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuizRepository.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/Quiz.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/QuizRepository.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/QuizService.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/api/dto/CreateQuizDto.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentRequestDto.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentResponseDto.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizDto.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/api/dto/ModifyQuizDataRequest.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuestionWithChosenAnswerDto.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuizDto.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapper.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/QuizMapper.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/Question.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionService.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/answer/AnswerOption.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/AnswerOptionDto.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/CreateAnswerOptionDto.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/mapper/AnswerMapper.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/QuestionDto.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/question/type/QuestionType.java create mode 100644 os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java create mode 100644 os-server/src/main/resources/db/migration/h2/V1_0_17_updates.sql create mode 100644 os-server/src/main/resources/db/migration/mysql/V1_0_17__update.sql diff --git a/os-server/src/main/java/app/openschool/course/module/ModuleRepository.java b/os-server/src/main/java/app/openschool/course/module/ModuleRepository.java index 1cf16073..a56eff88 100644 --- a/os-server/src/main/java/app/openschool/course/module/ModuleRepository.java +++ b/os-server/src/main/java/app/openschool/course/module/ModuleRepository.java @@ -1,6 +1,5 @@ package app.openschool.course.module; -import app.openschool.course.module.Module; import org.springframework.data.jpa.repository.JpaRepository; public interface ModuleRepository extends JpaRepository {} \ No newline at end of file diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuiz.java b/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuiz.java new file mode 100644 index 00000000..c7a913f2 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuiz.java @@ -0,0 +1,85 @@ +package app.openschool.course.module.quiz; + +import app.openschool.course.module.EnrolledModule; +import app.openschool.course.module.quiz.status.QuizStatus; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "enrolled_quiz") +public class EnrolledQuiz { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "student_grade") + private int studentGrade = 0; + + @ManyToOne + @JoinColumn(name = "quiz_status_id") + private QuizStatus quizStatus; + + @ManyToOne + @JoinColumn(name = "quiz_id") + private Quiz quiz; + + @ManyToOne + @JoinColumn(name = "enrolled_module_id") + private EnrolledModule enrolledModule; + + public EnrolledQuiz() {} + + public EnrolledQuiz(QuizStatus quizStatus, Quiz quiz, EnrolledModule enrolledModule) { + this.quizStatus = quizStatus; + this.quiz = quiz; + this.enrolledModule = enrolledModule; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public QuizStatus getQuizStatus() { + return quizStatus; + } + + public void setQuizStatus(QuizStatus quizStatus) { + this.quizStatus = quizStatus; + } + + public Quiz getQuiz() { + return quiz; + } + + public void setQuiz(Quiz quiz) { + this.quiz = quiz; + } + + public EnrolledModule getEnrolledModule() { + return enrolledModule; + } + + public void setEnrolledModule(EnrolledModule enrolledModule) { + this.enrolledModule = enrolledModule; + } + + public int getStudentGrade() { + return studentGrade; + } + + public void setStudentGrade(int studentGrade) { + this.studentGrade = studentGrade; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuizRepository.java b/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuizRepository.java new file mode 100644 index 00000000..5ad4b100 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/EnrolledQuizRepository.java @@ -0,0 +1,5 @@ +package app.openschool.course.module.quiz; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EnrolledQuizRepository extends JpaRepository {} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/Quiz.java b/os-server/src/main/java/app/openschool/course/module/quiz/Quiz.java new file mode 100644 index 00000000..368a20da --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/Quiz.java @@ -0,0 +1,104 @@ +package app.openschool.course.module.quiz; + +import app.openschool.course.module.Module; +import app.openschool.course.module.quiz.question.Question; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name = "quiz") +public class Quiz { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "title", nullable = false) + private String title; + + @Column(name = "description", nullable = false) + private String description; + + @Column(name = "max_grade", nullable = false) + private int maxGrade; + + @Column(name = "passing_score", nullable = false) + private int passingScore; + + @OneToMany(mappedBy = "quiz", cascade = CascadeType.ALL) + private Set questions; + + @ManyToOne + @JoinColumn(name = "module_id") + private Module module; + + public static Quiz getInstance() { + return new Quiz(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } + + public Set getQuestions() { + return questions; + } + + public void setQuestions(Set questions) { + this.questions = questions; + } + + public Module getModule() { + return module; + } + + public void setModule(Module module) { + this.module = module; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java new file mode 100644 index 00000000..577d74bb --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java @@ -0,0 +1,183 @@ +package app.openschool.course.module.quiz; + +import app.openschool.common.response.ResponseMessage; +import app.openschool.course.module.quiz.api.dto.CreateQuizDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentRequestDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; +import app.openschool.course.module.quiz.api.dto.ModifyQuizDataRequest; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.api.mapper.QuizMapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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 io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.util.Locale; +import javax.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + + + +@RestController +@RequestMapping("/api/v1/quizzes") +public class QuizController { + + private final QuizService quizService; + + public QuizController(QuizService quizService) { + this.quizService = quizService; + } + + @Operation( + summary = "create quiz for the module", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "201", + description = "Creates a quiz for the module and returns it"), + @ApiResponse( + responseCode = "404", + description = "Invalid module id supplied", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "403", + description = + "If the user have not mentor role, request body is not built correctly, etc.", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "400", + description = "if the module doesn't belong to the current user", + content = @Content(schema = @Schema())) + }) + @PreAuthorize("hasAuthority('MENTOR')") + @PostMapping("/{moduleId}") + public ResponseEntity createQuiz( + @Parameter(description = "Id of the module for which the quiz will be created") @PathVariable + Long moduleId, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Request object to create a quiz for a specific module") + @RequestBody + CreateQuizDto createQuizDto) { + return quizService + .createQuiz(moduleId, createQuizDto) + .map(quiz -> ResponseEntity.status(HttpStatus.CREATED).body(QuizMapper.quizToQuizDto(quiz))) + .orElse(ResponseEntity.notFound().build()); + } + + @Operation( + summary = "modify data of quizzes", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Modifies provided field or fields of quiz and returns updated quiz"), + @ApiResponse( + responseCode = "400", + description = + "Invalid quiz id, invalid new parent quiz id or" + + " invalid or existing quiz title supplied", + content = @Content(schema = @Schema(implementation = ResponseMessage.class))), + @ApiResponse( + responseCode = "403", + description = "Only user with admin role has access to this method", + content = @Content(schema = @Schema())) + }) + @PatchMapping(value = "/{id}") + public ResponseEntity updateQuiz( + @Parameter(description = "Id of quiz which fields will be modified") + @PathVariable(value = "id") + Long quizId, + @Parameter( + description = + "Request object for modifying quiz, " + + "which includes new title,new description that must be unique ") + @Valid + @RequestBody + ModifyQuizDataRequest request) { + return ResponseEntity.ok() + .body(QuizMapper.quizToQuizDto(quizService.updateQuiz(quizId, request))); + } + + @Operation(summary = "delete quiz", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "quiz successfully deleted"), + @ApiResponse( + responseCode = "404", + description = "Invalid quiz id supplied", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "403", + description = "If the user have not mentor role", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "400", + description = "if the quiz doesn't belong to the current user", + content = @Content(schema = @Schema())) + }) + @PreAuthorize("hasAuthority('MENTOR')") + @DeleteMapping("/{quizId}") + public ResponseEntity deleteQuiz( + @Parameter(description = "Id of the quiz which will be deleted") @PathVariable Long quizId) { + if (quizService.deleteQuiz(quizId)) { + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @GetMapping("/{id}") + public ResponseEntity findById( + @Parameter(description = "Id of the quiz which will be find") @PathVariable Long id) { + return ResponseEntity.ok(quizService.findById(id)); + } + + @GetMapping("/byModule/{id}") + public ResponseEntity> findAllByModuleId( + @Parameter(description = "Module ID that belongs to the quizzes being searched") @PathVariable + Long id, + @Parameter( + description = + "Includes parameters page, size, and sort which is not required. " + + "Page results page you want to retrieve (0..N). " + + "Size is count of records per page(1..N). " + + "Sorting criteria in the format: property(,asc|desc). " + + "Default sort order is ascending. " + + "Multiple sort criteria are supported.") + Pageable pageable) { + + return ResponseEntity.ok(quizService.findAllByModuleId(id, pageable)); + } + + @PostMapping("/enrolledQuizzes/{enrolledQuizId}") + public ResponseEntity completeEnrolledQuiz( + @Parameter(description = "Id of the enrolled quiz which will be completed") @PathVariable + Long enrolledQuizId, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Request object which contains questions and chosen answer options") + @RequestBody + EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto, + Locale locale) { + return quizService + .completeEnrolledQuiz(enrolledQuizId, enrolledQuizAssessmentRequestDto, locale) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizRepository.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizRepository.java new file mode 100644 index 00000000..dfaf13d2 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizRepository.java @@ -0,0 +1,10 @@ +package app.openschool.course.module.quiz; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface QuizRepository extends JpaRepository { + Page findAllByModuleId(Long id, Pageable pageable); +} + diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizService.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizService.java new file mode 100644 index 00000000..26b73398 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizService.java @@ -0,0 +1,29 @@ +package app.openschool.course.module.quiz; + +import app.openschool.course.module.quiz.api.dto.CreateQuizDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentRequestDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; +import app.openschool.course.module.quiz.api.dto.ModifyQuizDataRequest; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import java.util.Locale; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface QuizService { + + Optional createQuiz(Long moduleId, CreateQuizDto createQuizDto); + + Boolean deleteQuiz(Long quizId); + + QuizDto findById(Long id); + + Page findAllByModuleId(Long id, Pageable pageable); + + Optional completeEnrolledQuiz( + Long enrolledQuizId, + EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto, + Locale locale); + + Quiz updateQuiz(Long id, ModifyQuizDataRequest request); +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java new file mode 100644 index 00000000..f1f9c30f --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java @@ -0,0 +1,202 @@ +package app.openschool.course.module.quiz; + +import app.openschool.common.exceptionhandler.exception.PermissionDeniedException; +import app.openschool.course.module.Module; +import app.openschool.course.module.ModuleRepository; +import app.openschool.course.module.quiz.api.dto.CreateQuizDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentRequestDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; +import app.openschool.course.module.quiz.api.dto.ModifyQuizDataRequest; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.api.mapper.EnrolledQuizAssessmentResponseMapper; +import app.openschool.course.module.quiz.api.mapper.QuizMapper; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.status.QuizStatus; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import javax.transaction.Transactional; +import org.springframework.context.MessageSource; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + + +@Service +public class QuizServiceImpl implements QuizService { + + private final ModuleRepository moduleRepository; + private final QuizRepository quizRepository; + private final EnrolledQuizRepository enrolledQuizRepository; + private final MessageSource messageSource; + + public QuizServiceImpl( + ModuleRepository moduleRepository, + QuizRepository quizRepository, + EnrolledQuizRepository enrolledQuizRepository, + MessageSource messageSource) { + this.moduleRepository = moduleRepository; + this.quizRepository = quizRepository; + this.enrolledQuizRepository = enrolledQuizRepository; + this.messageSource = messageSource; + } + + @Override + public Optional createQuiz(Long moduleId, CreateQuizDto createQuizDto) { + return moduleRepository + .findById(moduleId) + .map( + module -> { + checkIfTheModuleBelongsToCurrentMentor(module); + Quiz quiz = QuizMapper.createQuizDtoToQuiz(createQuizDto, module); + return quizRepository.save(quiz); + }); + } + + @Override + public Boolean deleteQuiz(Long quizId) { + return quizRepository + .findById(quizId) + .map( + quiz -> { + checkIfTheQuizBelongsToCurrentMentor(quiz); + quizRepository.delete(quiz); + return true; + }) + .orElse(false); + } + + @Override + public QuizDto findById(Long id) { + return QuizMapper.quizToQuizDto( + quizRepository.findById(id).orElseThrow(IllegalArgumentException::new)); + } + + @Override + public Page findAllByModuleId(Long id, Pageable pageable) { + return QuizMapper.toQuizDtoPage(quizRepository.findAllByModuleId(id, pageable)); + } + + @Override + @Transactional + public Optional completeEnrolledQuiz( + Long enrolledQuizId, + EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto, + Locale locale) { + return enrolledQuizRepository + .findById(enrolledQuizId) + .map(enrolledQuiz -> completeQuiz(enrolledQuiz, enrolledQuizAssessmentRequestDto, locale)); + } + + @Override + public Quiz updateQuiz(Long id, ModifyQuizDataRequest request) { + Quiz quiz = quizRepository.findById(id).orElseThrow(IllegalArgumentException::new); + if (request.getTitle() != null) { + quiz.setTitle(request.getTitle()); + } + if (request.getTitle() != null) { + quiz.setDescription(request.getDescription()); + } + if (request.getPassingScore() != -1) { + quiz.setPassingScore(request.getPassingScore()); + } + if (request.getMaxGrade() != -1) { + quiz.setMaxGrade(request.getMaxGrade()); + } + + return quizRepository.save(quiz); + } + + private EnrolledQuizAssessmentResponseDto completeQuiz( + EnrolledQuiz enrolledQuiz, + EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto, + Locale locale) { + AtomicInteger rightAnswers = + checkQuestionsAndReturnRightAnswersCount( + enrolledQuiz.getQuiz(), enrolledQuizAssessmentRequestDto); + String assessmentResult = + getAssessmentResult(rightAnswers.get(), enrolledQuiz.getQuiz(), locale); + updateEnrolledQuiz(rightAnswers.get(), enrolledQuiz); + return EnrolledQuizAssessmentResponseMapper.toEnrolledQuizAssessmentResponseDto( + rightAnswers.get(), + assessmentResult, + enrolledQuiz.getQuiz().getPassingScore(), + enrolledQuiz.getQuiz().getMaxGrade()); + } + + private void updateEnrolledQuiz(int studentGrade, EnrolledQuiz enrolledQuiz) { + enrolledQuiz.setStudentGrade(studentGrade); + if (studentGrade >= enrolledQuiz.getQuiz().getPassingScore()) { + enrolledQuiz.setQuizStatus(QuizStatus.isCompleted()); + } else { + enrolledQuiz.setQuizStatus(QuizStatus.isFailed()); + } + enrolledQuizRepository.save(enrolledQuiz); + } + + private String getAssessmentResult(int rightAnswers, Quiz quiz, Locale locale) { + return quiz.getPassingScore() <= rightAnswers + ? messageSource.getMessage("quiz.completed", null, locale) + : messageSource.getMessage("quiz.failed", null, locale); + } + + private AtomicInteger checkQuestionsAndReturnRightAnswersCount( + Quiz quiz, EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto) { + AtomicInteger rightAnswers = new AtomicInteger(0); + quiz.getQuestions() + .forEach( + question -> { + if (checkQuestion(question, enrolledQuizAssessmentRequestDto)) { + rightAnswers.getAndIncrement(); + } + }); + return rightAnswers; + } + + private boolean checkQuestion( + Question question, EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto) { + AtomicBoolean isRightAnswer = new AtomicBoolean(false); + enrolledQuizAssessmentRequestDto + .getQuestionWithChosenAnswerDtoSet() + .forEach( + questionWithChosenAnswerDto -> { + if (Objects.equals(questionWithChosenAnswerDto.getQuestionId(), question.getId())) { + isRightAnswer.set( + checkQuestionAnswers( + question, questionWithChosenAnswerDto.getChosenAnswersIds())); + } + }); + return isRightAnswer.get(); + } + + private boolean checkQuestionAnswers(Question question, List chosenAnswerIds) { + AtomicInteger rightAnswers = new AtomicInteger(); + question + .getAnswerOptions() + .forEach( + answerOption -> { + if (chosenAnswerIds.contains(answerOption.getId()) && answerOption.isRightAnswer()) { + rightAnswers.getAndIncrement(); + } + }); + return rightAnswers.get() == question.getRightAnswersCount(); + } + + private void checkIfTheQuizBelongsToCurrentMentor(Quiz quiz) { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + if (!quiz.getModule().getCourse().getMentor().getEmail().equals(username)) { + throw new IllegalArgumentException(); + } + } + + private void checkIfTheModuleBelongsToCurrentMentor(Module module) { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + if (!module.getCourse().getMentor().getEmail().equals(username)) { + throw new PermissionDeniedException("permission.denied"); + } + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/CreateQuizDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/CreateQuizDto.java new file mode 100644 index 00000000..d9391e2b --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/CreateQuizDto.java @@ -0,0 +1,70 @@ +package app.openschool.course.module.quiz.api.dto; + +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class CreateQuizDto { + + @Schema(description = "Maximum possible grade", example = "10") + private int maxGrade; + + @Schema(description = "The minimum score to pass quiz", example = "7") + private int passingScore; + + @Schema(description = "Quiz title", example = "Quiz about ArrayList ") + private String title; + + @Schema( + description = "Quiz description", + example = "With the ArrayList quiz you must answer at least 70% of the questions") + private String description; + + @ArraySchema(schema = @Schema(implementation = CreateQuestionDto.class)) + private Set questions; + + public static CreateQuizDto getInstance() { + return new CreateQuizDto(); + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set getQuestions() { + return questions; + } + + public void setQuestions(Set questions) { + this.questions = questions; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentRequestDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentRequestDto.java new file mode 100644 index 00000000..56327785 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentRequestDto.java @@ -0,0 +1,22 @@ +package app.openschool.course.module.quiz.api.dto; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class EnrolledQuizAssessmentRequestDto { + + @ArraySchema(schema = @Schema(implementation = QuestionWithChosenAnswerDto.class)) + private Set questionWithChosenAnswerDtoSet; + + + + public Set getQuestionWithChosenAnswerDtoSet() { + return questionWithChosenAnswerDtoSet; + } + + public void setQuestionWithChosenAnswerDtoSet( + Set questionWithChosenAnswerDtoSet) { + this.questionWithChosenAnswerDtoSet = questionWithChosenAnswerDtoSet; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentResponseDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentResponseDto.java new file mode 100644 index 00000000..c014565e --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizAssessmentResponseDto.java @@ -0,0 +1,80 @@ +package app.openschool.course.module.quiz.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Objects; + +public class EnrolledQuizAssessmentResponseDto { + + @Schema(description = "Right answers count", example = "6") + private int rightAnswersCount; + + @Schema(description = "Assessment result", example = "FAILED") + private String assessmentResult; + + @Schema(description = "The minimum score to pass quiz", example = "7") + private int passingScore; + + @Schema(description = "Maximum possible grade", example = "10") + private int maxGrade; + + public EnrolledQuizAssessmentResponseDto() {} + + public EnrolledQuizAssessmentResponseDto( + int rightAnswersCount, String assessmentResult, int passingScore, int maxGrade) { + this.rightAnswersCount = rightAnswersCount; + this.assessmentResult = assessmentResult; + this.passingScore = passingScore; + this.maxGrade = maxGrade; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EnrolledQuizAssessmentResponseDto that = (EnrolledQuizAssessmentResponseDto) o; + return rightAnswersCount == that.rightAnswersCount + && passingScore == that.passingScore + && maxGrade == that.maxGrade + && Objects.equals(assessmentResult, that.assessmentResult); + } + + public static EnrolledQuizAssessmentResponseDto getInstant() { + return new EnrolledQuizAssessmentResponseDto(); + } + + public int getRightAnswersCount() { + return rightAnswersCount; + } + + public void setRightAnswersCount(int rightAnswersCount) { + this.rightAnswersCount = rightAnswersCount; + } + + public String getAssessmentResult() { + return assessmentResult; + } + + public void setAssessmentResult(String assessmentResult) { + this.assessmentResult = assessmentResult; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizDto.java new file mode 100644 index 00000000..f72375c2 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/EnrolledQuizDto.java @@ -0,0 +1,92 @@ +package app.openschool.course.module.quiz.api.dto; + +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class EnrolledQuizDto { + + @Schema(description = "Enrolled quiz id", example = "1") + private Long id; + + @Schema( + description = "Id of the enrolled module to which the enrolled quiz belongs", + example = "1") + private Long enrolledModuleId; + + @Schema(description = "Maximum possible grade", example = "10") + private int maxGrade; + + @Schema(description = "The student score", example = "9") + private int studentGrade; + + @Schema(description = "The quiz status", example = "FAILED") + private String quizStatus; + + @Schema(description = "The minimum score to pass quiz", example = "7") + private int passingScore; + + @ArraySchema(schema = @Schema(implementation = QuestionDto.class)) + private Set questions; + + public static EnrolledQuizDto getInstance() { + return new EnrolledQuizDto(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getEnrolledModuleId() { + return enrolledModuleId; + } + + public void setEnrolledModuleId(Long enrolledModuleId) { + this.enrolledModuleId = enrolledModuleId; + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } + + public int getStudentGrade() { + return studentGrade; + } + + public void setStudentGrade(int studentGrade) { + this.studentGrade = studentGrade; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } + + public String getQuizStatus() { + return quizStatus; + } + + public void setQuizStatus(String quizStatus) { + this.quizStatus = quizStatus; + } + + public Set getQuestions() { + return questions; + } + + public void setQuestions(Set questions) { + this.questions = questions; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/ModifyQuizDataRequest.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/ModifyQuizDataRequest.java new file mode 100644 index 00000000..16177efb --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/ModifyQuizDataRequest.java @@ -0,0 +1,51 @@ +package app.openschool.course.module.quiz.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class ModifyQuizDataRequest { + @Schema(description = "Quiz title", example = "Quiz about ArrayList ") + private String title; + + @Schema( + description = "Quiz description", + example = "With the ArrayList quiz you must answer at least 70% of the questions") + private String description; + + @Schema(description = "Quiz maximum grade", example = "7") + private int maxGrade = -1; + + @Schema(description = "Quiz passing score", example = "7") + private int passingScore = -1; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuestionWithChosenAnswerDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuestionWithChosenAnswerDto.java new file mode 100644 index 00000000..706dc86f --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuestionWithChosenAnswerDto.java @@ -0,0 +1,34 @@ +package app.openschool.course.module.quiz.api.dto; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public class QuestionWithChosenAnswerDto { + + @Schema(description = "Regular quiz question or multiple choice question id", example = "1") + private Long questionId; + + @ArraySchema(schema = @Schema(description = "One or multiple chosen answer's ids", example = "2")) + private List chosenAnswersIds; + + public static QuestionWithChosenAnswerDto getInstance() { + return new QuestionWithChosenAnswerDto(); + } + + public Long getQuestionId() { + return questionId; + } + + public void setQuestionId(Long questionId) { + this.questionId = questionId; + } + + public List getChosenAnswersIds() { + return chosenAnswersIds; + } + + public void setChosenAnswersIds(List chosenAnswersIds) { + this.chosenAnswersIds = chosenAnswersIds; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuizDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuizDto.java new file mode 100644 index 00000000..aa07e53e --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/dto/QuizDto.java @@ -0,0 +1,90 @@ +package app.openschool.course.module.quiz.api.dto; + +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class QuizDto { + + @Schema(description = "Quiz id", example = "1") + private Long id; + + @Schema(description = "Id of the module to which the quiz belongs", example = "1") + private Long moduleId; + + @Schema(description = "The title of the quiz", example = "title") + private String title; + + @Schema(description = "The description of the quiz", example = "description") + private String description; + + @Schema(description = "Maximum possible grade", example = "10") + private int maxGrade; + + @Schema(description = "The minimum score to pass quiz", example = "7") + private int passingScore; + + @ArraySchema(schema = @Schema(implementation = QuestionDto.class)) + private Set questions; + + public static QuizDto getInstance() { + return new QuizDto(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getModuleId() { + return moduleId; + } + + public void setModuleId(Long moduleId) { + this.moduleId = moduleId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getMaxGrade() { + return maxGrade; + } + + public void setMaxGrade(int maxGrade) { + this.maxGrade = maxGrade; + } + + public int getPassingScore() { + return passingScore; + } + + public void setPassingScore(int passingScore) { + this.passingScore = passingScore; + } + + public Set getQuestions() { + return questions; + } + + public void setQuestions(Set questions) { + this.questions = questions; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapper.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapper.java new file mode 100644 index 00000000..f0a2ed09 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapper.java @@ -0,0 +1,19 @@ +package app.openschool.course.module.quiz.api.mapper; + +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; + +public final class EnrolledQuizAssessmentResponseMapper { + + private EnrolledQuizAssessmentResponseMapper() {} + + public static EnrolledQuizAssessmentResponseDto toEnrolledQuizAssessmentResponseDto( + int rightAnswers, String assessmentResult, int passingScore, int maxGrade) { + EnrolledQuizAssessmentResponseDto enrolledQuizAssessmentResponseDto = + EnrolledQuizAssessmentResponseDto.getInstant(); + enrolledQuizAssessmentResponseDto.setRightAnswersCount(rightAnswers); + enrolledQuizAssessmentResponseDto.setAssessmentResult(assessmentResult); + enrolledQuizAssessmentResponseDto.setMaxGrade(maxGrade); + enrolledQuizAssessmentResponseDto.setPassingScore(passingScore); + return enrolledQuizAssessmentResponseDto; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/QuizMapper.java b/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/QuizMapper.java new file mode 100644 index 00000000..7d843560 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/api/mapper/QuizMapper.java @@ -0,0 +1,111 @@ +package app.openschool.course.module.quiz.api.mapper; + +import app.openschool.course.module.Module; +import app.openschool.course.module.quiz.EnrolledQuiz; +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.api.dto.CreateQuizDto; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizDto; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.answer.api.dto.AnswerOptionDto; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.api.mapper.QuestionMapper; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +public final class QuizMapper { + + private static final String MULTIPLE_CHOICE = "MULTIPLE_CHOICE"; + + private QuizMapper() {} + + public static Quiz createQuizDtoToQuiz(CreateQuizDto createQuizDto, Module module) { + Quiz quiz = Quiz.getInstance(); + quiz.setTitle(createQuizDto.getTitle()); + quiz.setDescription(createQuizDto.getDescription()); + quiz.setMaxGrade(createQuizDto.getMaxGrade()); + quiz.setPassingScore(createQuizDto.getPassingScore()); + quiz.setQuestions(createQuestionDtoToQuestions(createQuizDto.getQuestions(), quiz)); + quiz.setModule(module); + return quiz; + } + + public static QuizDto quizToQuizDto(Quiz quiz) { + QuizDto quizDto = QuizDto.getInstance(); + quizDto.setId(quiz.getId()); + quizDto.setTitle(quiz.getTitle()); + quizDto.setDescription(quiz.getDescription()); + quizDto.setModuleId(quiz.getModule().getId()); + quizDto.setMaxGrade(quiz.getMaxGrade()); + quizDto.setPassingScore(quiz.getPassingScore()); + quizDto.setQuestions(questionsToQuestionDto(quiz.getQuestions())); + return quizDto; + } + + public static Set enrolledQuizzesToEnrolledQuizDtoSet( + Set enrolledQuiz) { + return enrolledQuiz.stream().map(QuizMapper::toEnrolledQuizDtoSet).collect(Collectors.toSet()); + } + + public static Page toQuizDtoPage(Page quizzesPage) { + + List collect = + quizzesPage.toList().stream().map(QuizMapper::quizToQuizDto).collect(Collectors.toList()); + return new PageImpl<>(collect, quizzesPage.getPageable(), quizzesPage.getTotalElements()); + } + + private static Set createQuestionDtoToQuestions( + Set questions, Quiz quiz) { + return questions.stream() + .map( + createQuestionDto -> + QuestionMapper.createQuestionDtoToQuestion(createQuestionDto, quiz)) + .collect(Collectors.toSet()); + } + + private static Set questionsToQuestionDto(Set questions) { + return questions.stream() + .map( + question -> { + QuestionDto questionDto = QuestionDto.getInstance(); + questionDto.setId(question.getId()); + questionDto.setQuestion(question.getQuestion()); + questionDto.setRightAnswersCount(question.getRightAnswersCount()); + questionDto.setQuestionType(question.getQuestionType().getType()); + questionDto.setAnswerOptions( + answerOptionToAnswerOptionDto(question.getAnswerOptions())); + return questionDto; + }) + .collect(Collectors.toSet()); + } + + private static Set answerOptionToAnswerOptionDto( + Set answerOptions) { + return answerOptions.stream() + .map( + answerOption -> { + AnswerOptionDto answerOptionDto = AnswerOptionDto.getInstance(); + answerOptionDto.setId(answerOption.getId()); + answerOptionDto.setAnswerOption(answerOption.getAnswerOption()); + return answerOptionDto; + }) + .collect(Collectors.toSet()); + } + + private static EnrolledQuizDto toEnrolledQuizDtoSet(EnrolledQuiz enrolledQuiz) { + EnrolledQuizDto enrolledQuizDto = EnrolledQuizDto.getInstance(); + enrolledQuizDto.setId(enrolledQuiz.getId()); + enrolledQuizDto.setEnrolledModuleId(enrolledQuiz.getEnrolledModule().getId()); + enrolledQuizDto.setMaxGrade(enrolledQuiz.getQuiz().getMaxGrade()); + enrolledQuizDto.setStudentGrade(enrolledQuiz.getStudentGrade()); + enrolledQuizDto.setPassingScore(enrolledQuiz.getQuiz().getPassingScore()); + enrolledQuizDto.setQuizStatus(enrolledQuiz.getQuizStatus().getType()); + enrolledQuizDto.setQuestions(questionsToQuestionDto(enrolledQuiz.getQuiz().getQuestions())); + return enrolledQuizDto; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/Question.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/Question.java new file mode 100644 index 00000000..d11092be --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/Question.java @@ -0,0 +1,114 @@ +package app.openschool.course.module.quiz.question; + +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.type.QuestionType; +import java.util.Objects; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name = "questions") +public class Question { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "question", nullable = false) + private String question; + + @Column(name = "right_answer_count", nullable = false) + private int rightAnswersCount = 1; + + @ManyToOne + @JoinColumn(name = "question_type_id", nullable = false) + private QuestionType questionType; + + @OneToMany(mappedBy = "question", orphanRemoval = true, cascade = CascadeType.ALL) + private Set answerOptions; + + @ManyToOne + @JoinColumn(name = "quiz_id") + private Quiz quiz; + + public static Question getInstance() { + return new Question(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Question question1 = (Question) o; + return question.equals(question1.question) + && questionType.getType().equals(question1.questionType.getType()); + } + + @Override + public int hashCode() { + return Objects.hash(question); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getQuestion() { + return question; + } + + public void setQuestion(String question) { + this.question = question; + } + + public Quiz getQuiz() { + return quiz; + } + + public void setQuiz(Quiz quiz) { + this.quiz = quiz; + } + + public QuestionType getQuestionType() { + return questionType; + } + + public void setQuestionType(QuestionType questionType) { + this.questionType = questionType; + } + + public Set getAnswerOptions() { + return answerOptions; + } + + public void setAnswerOptions(Set answerOptions) { + this.answerOptions = answerOptions; + } + + public int getRightAnswersCount() { + return rightAnswersCount; + } + + public void setRightAnswersCount(int rightAnswersCount) { + this.rightAnswersCount = rightAnswersCount; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java new file mode 100644 index 00000000..a9bc9997 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java @@ -0,0 +1,113 @@ +package app.openschool.course.module.quiz.question; + +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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 io.swagger.v3.oas.annotations.security.SecurityRequirement; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RestController; + + +@RestController +@RequestMapping("/api/v1/questions") +public class QuestionController { + + private final QuestionService questionService; + + public QuestionController(QuestionService questionService) { + this.questionService = questionService; + } + + @Operation(summary = "delete question", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "question successfully deleted"), + @ApiResponse( + responseCode = "404", + description = "Invalid question id supplied", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "403", + description = "If the user have not mentor role", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "400", + description = "if the question doesn't belong to the current user", + content = @Content(schema = @Schema())) + }) + @PreAuthorize("hasAuthority('MENTOR')") + @DeleteMapping("/{questionId}") + public ResponseEntity deleteQuestion( + @Parameter(description = "Id of the question which will be deleted") @PathVariable + Long questionId) { + if (questionService.deleteQuestion(questionId)) { + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @Operation(summary = "update question", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "question successfully updated"), + @ApiResponse( + responseCode = "404", + description = "Invalid question id supplied", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "403", + description = "If the user have not mentor role", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "400", + description = "if the question doesn't belong to the current user", + content = @Content(schema = @Schema())) + }) + @PreAuthorize("hasAuthority('MENTOR')") + @PutMapping("/{questionId}") + public ResponseEntity updateQuestion( + @Parameter(description = "Id of the question which will be updated") @PathVariable + Long questionId, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Request object to update question") + @RequestBody + CreateQuestionDto createQuestionDto) { + if (questionService.updateQuestion(questionId, createQuestionDto)) { + return ResponseEntity.noContent().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @GetMapping("/{quizId}") + public ResponseEntity> findAllByQuizId( + @Parameter(description = "Quiz ID that belongs to the questions being searched") @PathVariable + Long quizId, + @Parameter( + description = + "Includes parameters page, size, and sort which is not required. " + + "Page results page you want to retrieve (0..N). " + + "Size is count of records per page(1..N). " + + "Sorting criteria in the format: property(,asc|desc). " + + "Default sort order is ascending. " + + "Multiple sort criteria are supported.") + Pageable pageable) { + + return ResponseEntity.ok(questionService.findAllByQuizId(quizId, pageable)); + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java new file mode 100644 index 00000000..3bd6641b --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java @@ -0,0 +1,9 @@ +package app.openschool.course.module.quiz.question; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface QuestionRepository extends JpaRepository { + Page findAllQuestionsByQuizId(Long id, Pageable pageable); +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionService.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionService.java new file mode 100644 index 00000000..d149b4dc --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionService.java @@ -0,0 +1,16 @@ +package app.openschool.course.module.quiz.question; + +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface QuestionService { + + boolean deleteQuestion(Long questionId); + + boolean updateQuestion(Long questionId, CreateQuestionDto createQuestionDto); + + Page findAllByQuizId(Long id, Pageable pageable); +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java new file mode 100644 index 00000000..f85909ec --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java @@ -0,0 +1,61 @@ +package app.openschool.course.module.quiz.question; + +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.api.mapper.QuestionMapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class QuestionServiceImpl implements QuestionService { + + private final QuestionRepository questionRepository; + + public QuestionServiceImpl(QuestionRepository questionRepository) { + this.questionRepository = questionRepository; + } + + @Override + public boolean deleteQuestion(Long questionId) { + return questionRepository + .findById(questionId) + .map( + question -> { + checkIfQuestionBelongsToCurrentMentor(question); + questionRepository.delete(question); + return true; + }) + .orElse(false); + } + + @Override + public boolean updateQuestion(Long questionId, CreateQuestionDto createQuestionDto) { + return questionRepository + .findById(questionId) + .map( + question -> { + checkIfQuestionBelongsToCurrentMentor(question); + Question updatedQuestion = + QuestionMapper.createQuestionDtoToQuestion(createQuestionDto, question.getQuiz()); + updatedQuestion.setId(question.getId()); + questionRepository.save(updatedQuestion); + return true; + }) + .orElse(false); + } + + @Override + public Page findAllByQuizId(Long id, Pageable pageable) { + return QuestionMapper.toQuestionDtoPage( + questionRepository.findAllQuestionsByQuizId(id, pageable)); + } + + private void checkIfQuestionBelongsToCurrentMentor(Question question) { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + if (!question.getQuiz().getModule().getCourse().getMentor().getEmail().equals(username)) { + throw new IllegalArgumentException(); + } + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/AnswerOption.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/AnswerOption.java new file mode 100644 index 00000000..84fdbdeb --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/AnswerOption.java @@ -0,0 +1,85 @@ +package app.openschool.course.module.quiz.question.answer; + +import app.openschool.course.module.quiz.question.Question; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "answer_options") +public class AnswerOption { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "answer_option", nullable = false) + private String answerOption; + + @Column(name = "is_right_answer", nullable = false) + private Boolean isRightAnswer; + + @ManyToOne + @JoinColumn(name = "question_id") + private Question question; + + public static AnswerOption getInstance() { + return new AnswerOption(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AnswerOption that = (AnswerOption) o; + return answerOption.equals(that.answerOption); + } + + @Override + public int hashCode() { + return Objects.hash(answerOption); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getAnswerOption() { + return answerOption; + } + + public void setAnswerOption(String answerOption) { + this.answerOption = answerOption; + } + + public Boolean isRightAnswer() { + return isRightAnswer; + } + + public void setRightAnswer(Boolean rightAnswer) { + isRightAnswer = rightAnswer; + } + + public Question getQuestion() { + return question; + } + + public void setQuestion(Question question) { + this.question = question; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/AnswerOptionDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/AnswerOptionDto.java new file mode 100644 index 00000000..00cde080 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/AnswerOptionDto.java @@ -0,0 +1,32 @@ +package app.openschool.course.module.quiz.question.answer.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class AnswerOptionDto { + + @Schema(description = "Answer option id", example = "1") + private Long id; + + @Schema(description = "One of the answers to the quiz question", example = "static") + private String answerOption; + + public static AnswerOptionDto getInstance() { + return new AnswerOptionDto(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getAnswerOption() { + return answerOption; + } + + public void setAnswerOption(String answerOption) { + this.answerOption = answerOption; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/CreateAnswerOptionDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/CreateAnswerOptionDto.java new file mode 100644 index 00000000..852f55de --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/dto/CreateAnswerOptionDto.java @@ -0,0 +1,34 @@ +package app.openschool.course.module.quiz.question.answer.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class CreateAnswerOptionDto { + + @Schema( + description = "One of the answers to the quiz question", + example = "Java bytecode is the instruction set for the Java Virtual Machine") + private String answerOption; + + @Schema(description = "Indicates if the answer is correct or not", example = "true") + private Boolean isRightAnswer; + + public static CreateAnswerOptionDto getInstance() { + return new CreateAnswerOptionDto(); + } + + public String getAnswerOption() { + return answerOption; + } + + public void setAnswerOption(String answerOption) { + this.answerOption = answerOption; + } + + public Boolean getRightAnswer() { + return isRightAnswer; + } + + public void setRightAnswer(Boolean rightAnswer) { + isRightAnswer = rightAnswer; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/mapper/AnswerMapper.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/mapper/AnswerMapper.java new file mode 100644 index 00000000..a8c20b70 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/answer/api/mapper/AnswerMapper.java @@ -0,0 +1,40 @@ +package app.openschool.course.module.quiz.question.answer.api.mapper; + +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.answer.api.dto.AnswerOptionDto; +import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; +import java.util.Set; +import java.util.stream.Collectors; + +public class AnswerMapper { + + public static AnswerOptionDto toAnswerDto(AnswerOption answerOption) { + AnswerOptionDto answerOptionDto = new AnswerOptionDto(); + answerOptionDto.setId(answerOptionDto.getId()); + answerOptionDto.setAnswerOption(answerOptionDto.getAnswerOption()); + + return answerOptionDto; + } + + public static AnswerOption toAnswerOption( + CreateAnswerOptionDto answerOptionDto, Question question) { + AnswerOption answerOption = new AnswerOption(); + answerOption.setQuestion(question); + answerOption.setRightAnswer(answerOptionDto.getRightAnswer()); + answerOption.setAnswerOption(answerOptionDto.getAnswerOption()); + + return answerOption; + } + + public static Set toAnswerDtoSet(Set answerOptions) { + return answerOptions.stream().map(AnswerMapper::toAnswerDto).collect(Collectors.toSet()); + } + + public static Set toAnswerOptionSet( + Set createAnswerOptionDtoSet, Question question) { + return createAnswerOptionDtoSet.stream() + .map(createAnswerOptionDto -> AnswerMapper.toAnswerOption(createAnswerOptionDto, question)) + .collect(Collectors.toSet()); + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java new file mode 100644 index 00000000..58967243 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java @@ -0,0 +1,61 @@ +package app.openschool.course.module.quiz.question.api.dto; + +import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class CreateQuestionDto { + + @Schema( + description = "Regular quiz question or multiple choice question", + example = "What is Java bytecode") + private String question; + + @Schema(description = "The right answers count", example = "3") + private int rightAnswersCount; + + @ArraySchema(schema = @Schema(implementation = CreateAnswerOptionDto.class)) + private Set answerOptions; + + @Schema( + description = "One of quiz question type (MATCHING or MULTIPLE_CHOICE)", + example = "MULTIPLE_CHOICE") + private String questionType; + + public static CreateQuestionDto getInstance() { + return new CreateQuestionDto(); + } + + public String getQuestion() { + return question; + } + + public void setQuestion(String question) { + this.question = question; + } + + public Set getAnswerOptions() { + return answerOptions; + } + + public void setAnswerOptions(Set answerOptions) { + this.answerOptions = answerOptions; + } + + public String getQuestionType() { + return questionType; + } + + public void setQuestionType(String questionType) { + this.questionType = questionType; + } + + public int getRightAnswersCount() { + return rightAnswersCount; + } + + public void setRightAnswersCount(int rightAnswersCount) { + this.rightAnswersCount = rightAnswersCount; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/QuestionDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/QuestionDto.java new file mode 100644 index 00000000..33563944 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/QuestionDto.java @@ -0,0 +1,72 @@ +package app.openschool.course.module.quiz.question.api.dto; + +import app.openschool.course.module.quiz.question.answer.api.dto.AnswerOptionDto; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +public class QuestionDto { + + @Schema(description = "Question id", example = "1") + private Long id; + + @Schema( + description = "Regular quiz question or multiple choice question", + example = "Which of the following is a Java keyword") + private String question; + + @Schema(description = "The right answers count", example = "3") + private int rightAnswersCount; + + @Schema( + description = "One of quiz question type (MATCHING or MULTIPLE_CHOICE)", + example = "MULTIPLE_CHOICE") + private String questionType; + + @ArraySchema(schema = @Schema(implementation = AnswerOptionDto.class)) + private Set answerOptions; + + public static QuestionDto getInstance() { + return new QuestionDto(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getQuestion() { + return question; + } + + public void setQuestion(String question) { + this.question = question; + } + + public String getQuestionType() { + return questionType; + } + + public void setQuestionType(String questionType) { + this.questionType = questionType; + } + + public Set getAnswerOptions() { + return answerOptions; + } + + public void setAnswerOptions(Set answerOptions) { + this.answerOptions = answerOptions; + } + + public int getRightAnswersCount() { + return rightAnswersCount; + } + + public void setRightAnswersCount(int rightAnswersCount) { + this.rightAnswersCount = rightAnswersCount; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java new file mode 100644 index 00000000..3ebfe547 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java @@ -0,0 +1,75 @@ +package app.openschool.course.module.quiz.question.api.mapper; + +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; +import app.openschool.course.module.quiz.question.answer.api.mapper.AnswerMapper; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.type.QuestionType; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + + + +public final class QuestionMapper { + + private static final String MULTIPLE_CHOICE = "MULTIPLE_CHOICE"; + + private QuestionMapper() {} + + public static Question createQuestionDtoToQuestion( + CreateQuestionDto createQuestionDto, Quiz quiz) { + Question question = Question.getInstance(); + question.setQuestion(createQuestionDto.getQuestion()); + question.setRightAnswersCount(createQuestionDto.getRightAnswersCount()); + question.setAnswerOptions( + AnswerMapper.toAnswerOptionSet(createQuestionDto.getAnswerOptions(), question)); + question.setQuestionType(createQuestionType(createQuestionDto.getQuestionType())); + question.setQuiz(quiz); + return question; + } + + public static Page toQuestionDtoPage(Page questionPage) { + + List collect = + questionPage.toList().stream() + .map(QuestionMapper::toQuestionDto) + .collect(Collectors.toList()); + return new PageImpl<>(collect, questionPage.getPageable(), questionPage.getTotalElements()); + } + + public static QuestionDto toQuestionDto(Question question) { + QuestionDto questionDto = new QuestionDto(); + questionDto.setId(question.getId()); + questionDto.setQuestion(question.getQuestion()); + questionDto.setQuestionType(question.getQuestionType().getType()); + questionDto.setAnswerOptions(AnswerMapper.toAnswerDtoSet(question.getAnswerOptions())); + questionDto.setRightAnswersCount(question.getRightAnswersCount()); + return questionDto; + } + + private static QuestionType createQuestionType(String questionType) { + return questionType.equals(MULTIPLE_CHOICE) + ? QuestionType.isMultipleChoice() + : QuestionType.isMatching(); + } + + private static Set createAnswerOptionDtoToAnswerOptions( + Set answerOptions, Question question) { + return answerOptions.stream() + .map( + createAnswerOptionDto -> { + AnswerOption answerOption = AnswerOption.getInstance(); + answerOption.setAnswerOption(createAnswerOptionDto.getAnswerOption()); + answerOption.setRightAnswer(createAnswerOptionDto.getRightAnswer()); + answerOption.setQuestion(question); + return answerOption; + }) + .collect(Collectors.toSet()); + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/type/QuestionType.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/type/QuestionType.java new file mode 100644 index 00000000..39bc9afe --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/type/QuestionType.java @@ -0,0 +1,55 @@ +package app.openschool.course.module.quiz.question.type; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "question_type") +public class QuestionType { + + private static final String MATCHING = "MATCHING"; + private static final String MULTIPLE_CHOICE = "MULTIPLE_CHOICE"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "type", nullable = false) + private String type; + + public QuestionType() {} + + public QuestionType(Long id, String type) { + this.id = id; + this.type = type; + } + + public static QuestionType isMatching() { + return new QuestionType(1L, MATCHING); + } + + public static QuestionType isMultipleChoice() { + return new QuestionType(2L, MULTIPLE_CHOICE); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java b/os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java new file mode 100644 index 00000000..c787b225 --- /dev/null +++ b/os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java @@ -0,0 +1,56 @@ +package app.openschool.course.module.quiz.status; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "quiz_status") +public class QuizStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "status_type") + private String type; + + public QuizStatus() {} + + public QuizStatus(Long id, String type) { + this.id = id; + this.type = type; + } + + public static QuizStatus isInProgress() { + return new QuizStatus(1L, "IN_PROGRESS"); + } + + public static QuizStatus isFailed() { + return new QuizStatus(3L, "FAILED"); + } + + public static QuizStatus isCompleted() { + return new QuizStatus(2L, "COMPLETED"); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/os-server/src/main/resources/db/migration/h2/V1_0_17_updates.sql b/os-server/src/main/resources/db/migration/h2/V1_0_17_updates.sql new file mode 100644 index 00000000..2e5c797c --- /dev/null +++ b/os-server/src/main/resources/db/migration/h2/V1_0_17_updates.sql @@ -0,0 +1,110 @@ +-- ----------------------------------------------------- +-- Table quiz_status +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS quiz_status ( + id BIGINT NOT NULL, + status_type VARCHAR(45) NOT NULL, +PRIMARY KEY (id) +); + +-- ----------------------------------------------------- +-- Table question_type +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS question_type ( + id BIGINT NOT NULL, + type VARCHAR(45) NOT NULL, +PRIMARY KEY (id) +); + +-- ----------------------------------------------------- +-- Table quiz +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS quiz ( + id BIGINT NOT NULL AUTO_INCREMENT, + title varchar(255) NOT NULL , + description TEXT NOT NULL , + max_grade INT NOT NULL, + passing_score INT NOT NULL, + module_id BIGINT NOT NULL, + PRIMARY KEY (id), +CONSTRAINT fk_quiz_module +FOREIGN KEY (module_id) +REFERENCES module (id) +ON DELETE CASCADE +ON UPDATE CASCADE +); + +CREATE INDEX fk_quiz_module_idx ON quiz (module_id ASC); + +-- ----------------------------------------------------- +-- Table enrolled_quiz +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS enrolled_quiz ( + id BIGINT NOT NULL AUTO_INCREMENT, + student_grade INT NOT NULL, + quiz_status_id BIGINT NOT NULL, + quiz_id BIGINT NOT NULL, + enrolled_module_id BIGINT NOT NULL, + PRIMARY KEY (id), +CONSTRAINT fk_enrolled_quiz_quiz_status +FOREIGN KEY (quiz_status_id) +REFERENCES quiz_status (id), +CONSTRAINT fk_enrolled_quiz_quiz +FOREIGN KEY (quiz_id) +REFERENCES quiz (id), +CONSTRAINT fk_enrolled_quiz_enrolled_module +FOREIGN KEY (enrolled_module_id) +REFERENCES enrolled_module (id) +); + +CREATE INDEX fk_enrolled_quiz_quiz_status_idx ON enrolled_quiz (quiz_status_id ASC); +CREATE INDEX fk_enrolled_quiz_quiz_idx ON enrolled_quiz (quiz_id ASC); +CREATE INDEX fk_enrolled_quiz_enrolled_module_idx ON enrolled_quiz (enrolled_module_id ASC); + +-- ----------------------------------------------------- +-- Table questions +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS questions ( + id BIGINT NOT NULL AUTO_INCREMENT, + question TEXT NOT NULL, + right_answer_count INT NOT NULL, + quiz_id BIGINT NOT NULL, + question_type_id BIGINT NOT NULL, + PRIMARY KEY (id), +CONSTRAINT fk_questions_quiz +FOREIGN KEY (quiz_id) +REFERENCES quiz (id) +ON DELETE CASCADE +ON UPDATE CASCADE, +CONSTRAINT fk_questions_question_type +FOREIGN KEY (question_type_id) +REFERENCES question_type (id) +); + +CREATE INDEX fk_questions_quiz_idx ON questions (quiz_id ASC); +CREATE INDEX fk_questions_question_type_idx ON questions (question_type_id ASC); + +-- ----------------------------------------------------- +-- Table answer_options +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS answer_options ( + id BIGINT NOT NULL AUTO_INCREMENT, + answer_option TEXT NOT NULL, + is_right_answer BOOLEAN NOT NULL, + question_id BIGINT NOT NULL, + PRIMARY KEY (id), +CONSTRAINT fk_answer_options_question +FOREIGN KEY (question_id) +REFERENCES questions (id) +ON DELETE CASCADE +ON UPDATE CASCADE +); + +CREATE INDEX fk_answer_options_question_idx ON answer_options(question_id ASC); + +INSERT INTO quiz_status(id, status_type) VALUES (1,'IN_PROGRESS'); +INSERT INTO quiz_status(id, status_type) VALUES (2,'COMPLETED'); +INSERT INTO quiz_status(id, status_type) VALUES (3,'FAILED'); + +INSERT INTO question_type(id, type) VALUES (1,'MATCHING'); +INSERT INTO question_type(id, type) VALUES (2,'MULTIPLE_CHOICE'); \ No newline at end of file diff --git a/os-server/src/main/resources/db/migration/mysql/V1_0_17__update.sql b/os-server/src/main/resources/db/migration/mysql/V1_0_17__update.sql new file mode 100644 index 00000000..08feb694 --- /dev/null +++ b/os-server/src/main/resources/db/migration/mysql/V1_0_17__update.sql @@ -0,0 +1,110 @@ +-- ----------------------------------------------------- +-- Table `open_school_db`.`quiz_status` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`quiz_status` ( + `id` BIGINT NOT NULL, + `status_type` VARCHAR(45) NOT NULL, +PRIMARY KEY (`id`)) +ENGINE = InnoDB; + +-- ----------------------------------------------------- +-- Table `open_school_db`.`question_type` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`question_type` ( + `id` BIGINT NOT NULL, + `type` VARCHAR(45) NOT NULL, +PRIMARY KEY (`id`)) +ENGINE = InnoDB; + +-- ----------------------------------------------------- +-- Table `open_school_db`.`quiz` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`quiz` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `title` VARCHAR(255) NOT NULL, + `description` TEXT NOT NULL, + `max_grade` INT NOT NULL, + + `passing_score` INT NOT NULL, + `module_id` BIGINT NOT NULL, + PRIMARY KEY (`id`), +INDEX `fk_quiz_module_idx` (`module_id` ASC) VISIBLE, +CONSTRAINT `fk_quiz_module` +FOREIGN KEY (`module_id`) +REFERENCES `open_school_db`.`module` (`id`) +ON DELETE CASCADE +ON UPDATE CASCADE +) +ENGINE = InnoDB; + +-- ----------------------------------------------------- +-- Table `open_school_db`.`enrolled_quiz` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`enrolled_quiz` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `student_grade` INT NOT NULL, + `quiz_status_id` BIGINT NOT NULL, + `quiz_id` BIGINT NOT NULL, + `enrolled_module_id` BIGINT NOT NULL, + PRIMARY KEY (`id`), +INDEX `fk_enrolled_quiz_quiz_status_idx` (`quiz_status_id` ASC) VISIBLE, +INDEX `fk_enrolled_quiz_quiz_idx` (`quiz_id` ASC) VISIBLE, +INDEX `fk_enrolled_quiz_enrolled_module_idx` (`enrolled_module_id` ASC) VISIBLE, +CONSTRAINT `fk_enrolled_quiz_quiz_status` +FOREIGN KEY (`quiz_status_id`) +REFERENCES `open_school_db`.`quiz_status` (`id`), +CONSTRAINT `fk_enrolled_quiz_quiz` +FOREIGN KEY (`quiz_id`) +REFERENCES `open_school_db`.`quiz` (`id`), +CONSTRAINT `fk_enrolled_quiz_enrolled_module` +FOREIGN KEY (`enrolled_module_id`) +REFERENCES `open_school_db`.`enrolled_module` (`id`) +) +ENGINE = InnoDB; + +-- ----------------------------------------------------- +-- Table `open_school_db`.`questions` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`questions` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `question` TEXT NOT NULL, + `right_answer_count` INT NOT NULL, + `quiz_id` BIGINT NOT NULL, + `question_type_id` BIGINT NOT NULL, + PRIMARY KEY (`id`), +INDEX `fk_questions_quiz_idx` (`quiz_id` ASC) VISIBLE, +INDEX `fk_questions_question_type_idx` (`question_type_id` ASC) VISIBLE, +CONSTRAINT `fk_questions_quiz` +FOREIGN KEY (`quiz_id`) +REFERENCES `open_school_db`.`quiz` (`id`) +ON DELETE CASCADE +ON UPDATE CASCADE, +CONSTRAINT `fk_questions_question_type` +FOREIGN KEY (`question_type_id`) +REFERENCES `open_school_db`.`question_type` (`id`) +) +ENGINE = InnoDB; + +-- ----------------------------------------------------- +-- Table `open_school_db`.`answer_options` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `open_school_db`.`answer_options` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `answer_option` TEXT NOT NULL, + `is_right_answer` BOOLEAN NOT NULL, + `question_id` BIGINT NOT NULL, + PRIMARY KEY (`id`), +INDEX `fk_answer_options_question_idx` (`question_id` ASC) VISIBLE, +CONSTRAINT `fk_answer_options_question` +FOREIGN KEY (`question_id`) +REFERENCES `open_school_db`.`questions` (`id`) +ON DELETE CASCADE +ON UPDATE CASCADE) +ENGINE = InnoDB; + +INSERT INTO quiz_status(id, status_type) VALUES (1,'IN_PROGRESS'); +INSERT INTO quiz_status(id, status_type) VALUES (2,'COMPLETED'); +INSERT INTO quiz_status(id, status_type) VALUES (3,'FAILED'); + +INSERT INTO question_type(id, type) VALUES (1,'MATCHING'); +INSERT INTO question_type(id, type) VALUES (2,'MULTIPLE_CHOICE'); \ No newline at end of file From 8662d985006d1b8dd63d12e68699098b731b1e6c Mon Sep 17 00:00:00 2001 From: DELL Date: Wed, 1 Mar 2023 12:49:25 +0400 Subject: [PATCH 2/8] updates --- .../security/SecurityConfiguration.java | 3 +- .../course/api/mapper/CourseMapper.java | 1 + .../api/mapper/EnrolledModuleMapper.java | 12 +- .../course/module/EnrolledModule.java | 16 +- .../app/openschool/course/module/Module.java | 15 +- .../course/module/quiz/QuizController.java | 98 ++++++- .../course/module/quiz/QuizServiceImpl.java | 21 +- .../quiz/question/QuestionController.java | 32 +- .../quiz/question/QuestionServiceImpl.java | 9 - .../question/api/dto/CreateQuestionDto.java | 2 +- .../course/module/quiz/status/QuizStatus.java | 8 + ..._0_17_updates.sql => V1_0_17__updates.sql} | 10 +- .../db/migration/mysql/V1_0_17__update.sql | 34 ++- .../src/main/resources/messages.properties | 2 + .../course/api/CourseGenerator.java | 18 +- .../course/module/api/ModuleGenerator.java | 23 ++ .../module/quiz/QuizControllerTest.java | 277 ++++++++++++++++++ .../module/quiz/QuizRepositoryTest.java | 116 ++++++++ .../module/quiz/QuizServiceImplTest.java | 195 ++++++++++++ .../quiz/question/util/QuestionGenerator.java | 21 ++ ...lledQuizAssessmentRequestDtoGenerator.java | 32 ++ ...ledQuizAssessmentResponseDtoGenerator.java | 17 ++ .../quiz/util/EnrolledQuizGenerator.java | 14 + .../module/quiz/util/QuizDtoGenerator.java | 87 ++++++ .../module/quiz/util/QuizGenerator.java | 74 +++++ .../openschool/user/api/UserGenerator.java | 23 ++ 26 files changed, 1087 insertions(+), 73 deletions(-) rename os-server/src/main/resources/db/migration/h2/{V1_0_17_updates.sql => V1_0_17__updates.sql} (95%) create mode 100644 os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentResponseDtoGenerator.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizGenerator.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java diff --git a/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java b/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java index ae1a2a03..6ab9c1ad 100644 --- a/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java +++ b/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java @@ -30,7 +30,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { "/api/v1/features/**", "/api/v1/categories/**", "/api/v1/mentors/**", - "/api/v1/courses/**" + "/api/v1/courses/**", + "/api/v1/*/quizzes/**" }; private final JwtAuthenticationFilter jwtAuthenticationFilter; diff --git a/os-server/src/main/java/app/openschool/course/api/mapper/CourseMapper.java b/os-server/src/main/java/app/openschool/course/api/mapper/CourseMapper.java index 7c7c7744..be664000 100644 --- a/os-server/src/main/java/app/openschool/course/api/mapper/CourseMapper.java +++ b/os-server/src/main/java/app/openschool/course/api/mapper/CourseMapper.java @@ -11,6 +11,7 @@ import app.openschool.course.module.EnrolledModule; import app.openschool.course.module.Module; import app.openschool.course.module.item.ModuleItem; +import app.openschool.course.module.quiz.EnrolledQuiz; import app.openschool.course.status.CourseStatus; import app.openschool.user.User; import app.openschool.user.api.mapper.MentorMapper; diff --git a/os-server/src/main/java/app/openschool/course/api/mapper/EnrolledModuleMapper.java b/os-server/src/main/java/app/openschool/course/api/mapper/EnrolledModuleMapper.java index 4f0d77c3..69a3e0e0 100644 --- a/os-server/src/main/java/app/openschool/course/api/mapper/EnrolledModuleMapper.java +++ b/os-server/src/main/java/app/openschool/course/api/mapper/EnrolledModuleMapper.java @@ -5,6 +5,8 @@ import app.openschool.course.module.EnrolledModule; import app.openschool.course.module.item.EnrolledModuleItem; import app.openschool.course.module.item.status.ModuleItemStatus; +import app.openschool.course.module.quiz.EnrolledQuiz; +import app.openschool.course.module.quiz.status.QuizStatus; import app.openschool.course.module.status.ModuleStatus; import java.util.Set; import java.util.stream.Collectors; @@ -19,10 +21,10 @@ public static Set toEnrolledModules( .collect(Collectors.toSet()); return enrolledModules.stream() - .map( + .peek( enrolledModule -> { enrolledModule.setEnrolledModuleItems(getEnrolledModuleItems(enrolledModule)); - return enrolledModule; + enrolledModule.setEnrolledQuizzes(getEnrolledQuiz(enrolledModule)); }) .collect(Collectors.toSet()); } @@ -34,4 +36,10 @@ private static Set getEnrolledModuleItems(EnrolledModule enr new EnrolledModuleItem(moduleItem, enrolledModule, ModuleItemStatus.inProgress())) .collect(Collectors.toSet()); } + + private static Set getEnrolledQuiz(EnrolledModule enrolledModule) { + return enrolledModule.getModule().getQuizzes().stream() + .map(quiz -> new EnrolledQuiz(QuizStatus.inProgress(), quiz, enrolledModule)) + .collect(Collectors.toSet()); + } } diff --git a/os-server/src/main/java/app/openschool/course/module/EnrolledModule.java b/os-server/src/main/java/app/openschool/course/module/EnrolledModule.java index 022a1cb6..632d0054 100644 --- a/os-server/src/main/java/app/openschool/course/module/EnrolledModule.java +++ b/os-server/src/main/java/app/openschool/course/module/EnrolledModule.java @@ -2,6 +2,7 @@ import app.openschool.course.EnrolledCourse; import app.openschool.course.module.item.EnrolledModuleItem; +import app.openschool.course.module.quiz.EnrolledQuiz; import app.openschool.course.module.status.ModuleStatus; import java.util.Set; import javax.persistence.CascadeType; @@ -39,6 +40,9 @@ public class EnrolledModule { @OneToMany(cascade = CascadeType.MERGE, mappedBy = "enrolledModule") private Set enrolledModuleItems; + @OneToMany(cascade = CascadeType.MERGE, mappedBy = "enrolledModule") + private Set enrolledQuizzes; + public EnrolledModule() {} public EnrolledModule(Module module, ModuleStatus moduleStatus, EnrolledCourse enrolledCourse) { @@ -52,12 +56,14 @@ public EnrolledModule( Module module, EnrolledCourse enrolledCourse, ModuleStatus moduleStatus, - Set enrolledModuleItems) { + Set enrolledModuleItems, + Set enrolledQuizzes) { this.id = id; this.module = module; this.enrolledCourse = enrolledCourse; this.moduleStatus = moduleStatus; this.enrolledModuleItems = enrolledModuleItems; + this.enrolledQuizzes = enrolledQuizzes; } public Long getId() { @@ -88,6 +94,14 @@ public ModuleStatus getModuleStatus() { return moduleStatus; } + public Set getEnrolledQuizzes() { + return enrolledQuizzes; + } + + public void setEnrolledQuizzes(Set enrolledQuizzes) { + this.enrolledQuizzes = enrolledQuizzes; + } + public void setModuleStatus(ModuleStatus moduleStatus) { this.moduleStatus = moduleStatus; } diff --git a/os-server/src/main/java/app/openschool/course/module/Module.java b/os-server/src/main/java/app/openschool/course/module/Module.java index f77ba900..0fbb9958 100644 --- a/os-server/src/main/java/app/openschool/course/module/Module.java +++ b/os-server/src/main/java/app/openschool/course/module/Module.java @@ -2,6 +2,7 @@ import app.openschool.course.Course; import app.openschool.course.module.item.ModuleItem; +import app.openschool.course.module.quiz.Quiz; import java.util.HashSet; import java.util.Set; import javax.persistence.CascadeType; @@ -37,16 +38,20 @@ public class Module { @OneToMany(cascade = CascadeType.ALL, mappedBy = "module") private Set moduleItems = new HashSet<>(); + @OneToMany(cascade = CascadeType.ALL, mappedBy = "module") + private Set quizzes = new HashSet<>(); + public Module() {} public Module(Course course) { this.course = course; } - public Module(Long id, Course course, Set moduleItems) { + public Module(Long id, Course course, Set moduleItems, Set quizzes) { this.id = id; this.course = course; this.moduleItems = moduleItems; + this.quizzes = quizzes; } public Long getId() { @@ -88,4 +93,12 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + + public Set getQuizzes() { + return quizzes; + } + + public void setQuizzes(Set quizzes) { + this.quizzes = quizzes; + } } diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java index 577d74bb..8ccd3858 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java @@ -1,6 +1,9 @@ package app.openschool.course.module.quiz; +import app.openschool.common.exceptionhandler.exception.PermissionDeniedException; import app.openschool.common.response.ResponseMessage; +import app.openschool.course.module.Module; +import app.openschool.course.module.ModuleRepository; import app.openschool.course.module.quiz.api.dto.CreateQuizDto; import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentRequestDto; import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; @@ -14,6 +17,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.security.Principal; import java.util.Locale; import javax.validation.Valid; import org.springframework.data.domain.Page; @@ -30,17 +34,16 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; - - - @RestController -@RequestMapping("/api/v1/quizzes") +@RequestMapping("/api/v1/{moduleId}/quizzes") public class QuizController { private final QuizService quizService; + private final ModuleRepository moduleRepository; - public QuizController(QuizService quizService) { + public QuizController(QuizService quizService, ModuleRepository moduleRepository) { this.quizService = quizService; + this.moduleRepository = moduleRepository; } @Operation( @@ -66,14 +69,16 @@ public QuizController(QuizService quizService) { content = @Content(schema = @Schema())) }) @PreAuthorize("hasAuthority('MENTOR')") - @PostMapping("/{moduleId}") + @PostMapping() public ResponseEntity createQuiz( @Parameter(description = "Id of the module for which the quiz will be created") @PathVariable Long moduleId, + Principal principal, @io.swagger.v3.oas.annotations.parameters.RequestBody( description = "Request object to create a quiz for a specific module") @RequestBody CreateQuizDto createQuizDto) { + checkPermission(moduleId, principal); return quizService .createQuiz(moduleId, createQuizDto) .map(quiz -> ResponseEntity.status(HttpStatus.CREATED).body(QuizMapper.quizToQuizDto(quiz))) @@ -100,10 +105,14 @@ public ResponseEntity createQuiz( content = @Content(schema = @Schema())) }) @PatchMapping(value = "/{id}") + @PreAuthorize("hasAuthority('MENTOR')") public ResponseEntity updateQuiz( @Parameter(description = "Id of quiz which fields will be modified") @PathVariable(value = "id") Long quizId, + @Parameter(description = "Id of the module to which this quiz belongs") @PathVariable() + Long moduleId, + Principal principal, @Parameter( description = "Request object for modifying quiz, " @@ -111,6 +120,7 @@ public ResponseEntity updateQuiz( @Valid @RequestBody ModifyQuizDataRequest request) { + checkPermission(moduleId, principal); return ResponseEntity.ok() .body(QuizMapper.quizToQuizDto(quizService.updateQuiz(quizId, request))); } @@ -135,7 +145,11 @@ public ResponseEntity updateQuiz( @PreAuthorize("hasAuthority('MENTOR')") @DeleteMapping("/{quizId}") public ResponseEntity deleteQuiz( - @Parameter(description = "Id of the quiz which will be deleted") @PathVariable Long quizId) { + @Parameter(description = "Id of the quiz which will be deleted") @PathVariable Long quizId, + @Parameter(description = "Id of the module to which this quiz belongs") @PathVariable + Long moduleId, + Principal principal) { + checkPermission(moduleId, principal); if (quizService.deleteQuiz(quizId)) { return ResponseEntity.noContent().build(); } else { @@ -143,16 +157,50 @@ public ResponseEntity deleteQuiz( } } - @GetMapping("/{id}") + @Operation(summary = "find quiz by quiz id", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Get details and information about the quiz"), + @ApiResponse( + responseCode = "400", + description = "Invalid quiz id ", + content = @Content(schema = @Schema(implementation = ResponseMessage.class))), + @ApiResponse( + responseCode = "401", + description = " Lacks valid authentication credentials for the requested resource", + content = @Content(schema = @Schema())) + }) + @GetMapping("/{quizId}") public ResponseEntity findById( - @Parameter(description = "Id of the quiz which will be find") @PathVariable Long id) { - return ResponseEntity.ok(quizService.findById(id)); + @Parameter(description = "Id of the quiz which will be find") @PathVariable Long quizId, + @Parameter(description = "Id of the module to which this quiz belongs") @PathVariable() + Long moduleId) { + return ResponseEntity.ok(quizService.findById(quizId)); } - @GetMapping("/byModule/{id}") + @Operation( + summary = "find quiz by module id", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Get details and information about the quiz"), + @ApiResponse( + responseCode = "400", + description = "Invalid module id ", + content = @Content(schema = @Schema(implementation = ResponseMessage.class))), + @ApiResponse( + responseCode = "401", + description = " Lacks valid authentication credentials for the requested resource", + content = @Content(schema = @Schema())) + }) + @GetMapping() public ResponseEntity> findAllByModuleId( @Parameter(description = "Module ID that belongs to the quizzes being searched") @PathVariable - Long id, + Long moduleId, @Parameter( description = "Includes parameters page, size, and sort which is not required. " @@ -163,13 +211,30 @@ public ResponseEntity> findAllByModuleId( + "Multiple sort criteria are supported.") Pageable pageable) { - return ResponseEntity.ok(quizService.findAllByModuleId(id, pageable)); + return ResponseEntity.ok(quizService.findAllByModuleId(moduleId, pageable)); } + @Operation( + summary = "complete enrolled quiz", + security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "Complete enrolled quiz"), + @ApiResponse( + responseCode = "400", + description = "Invalid enrolled quiz Id ", + content = @Content(schema = @Schema(implementation = ResponseMessage.class))), + @ApiResponse( + responseCode = "401", + description = " Lacks valid authentication credentials for the requested resource", + content = @Content(schema = @Schema())) + }) @PostMapping("/enrolledQuizzes/{enrolledQuizId}") public ResponseEntity completeEnrolledQuiz( @Parameter(description = "Id of the enrolled quiz which will be completed") @PathVariable Long enrolledQuizId, + @Parameter(description = "Id of the module to which this quiz belongs") @PathVariable() + Long moduleId, @io.swagger.v3.oas.annotations.parameters.RequestBody( description = "Request object which contains questions and chosen answer options") @RequestBody @@ -180,4 +245,11 @@ public ResponseEntity completeEnrolledQuiz( .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } + + private void checkPermission(Long moduleId, Principal principal) { + Module module = moduleRepository.findById(moduleId).orElseThrow(IllegalArgumentException::new); + if (!module.getCourse().getMentor().getEmail().equals(principal.getName())) { + throw new PermissionDeniedException("permission.denied"); + } + } } diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java index f1f9c30f..6481ece4 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizServiceImpl.java @@ -25,7 +25,6 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; - @Service public class QuizServiceImpl implements QuizService { @@ -51,7 +50,6 @@ public Optional createQuiz(Long moduleId, CreateQuizDto createQuizDto) { .findById(moduleId) .map( module -> { - checkIfTheModuleBelongsToCurrentMentor(module); Quiz quiz = QuizMapper.createQuizDtoToQuiz(createQuizDto, module); return quizRepository.save(quiz); }); @@ -63,7 +61,6 @@ public Boolean deleteQuiz(Long quizId) { .findById(quizId) .map( quiz -> { - checkIfTheQuizBelongsToCurrentMentor(quiz); quizRepository.delete(quiz); return true; }) @@ -95,10 +92,10 @@ public Optional completeEnrolledQuiz( @Override public Quiz updateQuiz(Long id, ModifyQuizDataRequest request) { Quiz quiz = quizRepository.findById(id).orElseThrow(IllegalArgumentException::new); - if (request.getTitle() != null) { + if (Objects.nonNull(request.getTitle())) { quiz.setTitle(request.getTitle()); } - if (request.getTitle() != null) { + if (Objects.nonNull(request.getDescription())) { quiz.setDescription(request.getDescription()); } if (request.getPassingScore() != -1) { @@ -185,18 +182,4 @@ private boolean checkQuestionAnswers(Question question, List chosenAnswerI }); return rightAnswers.get() == question.getRightAnswersCount(); } - - private void checkIfTheQuizBelongsToCurrentMentor(Quiz quiz) { - String username = SecurityContextHolder.getContext().getAuthentication().getName(); - if (!quiz.getModule().getCourse().getMentor().getEmail().equals(username)) { - throw new IllegalArgumentException(); - } - } - - private void checkIfTheModuleBelongsToCurrentMentor(Module module) { - String username = SecurityContextHolder.getContext().getAuthentication().getName(); - if (!module.getCourse().getMentor().getEmail().equals(username)) { - throw new PermissionDeniedException("permission.denied"); - } - } } diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java index a9bc9997..046f37e7 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java @@ -1,5 +1,8 @@ package app.openschool.course.module.quiz.question; +import app.openschool.common.exceptionhandler.exception.PermissionDeniedException; +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.QuizRepository; import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; import app.openschool.course.module.quiz.question.api.dto.QuestionDto; import io.swagger.v3.oas.annotations.Operation; @@ -9,6 +12,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.security.Principal; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; @@ -21,15 +25,16 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; - @RestController -@RequestMapping("/api/v1/questions") +@RequestMapping("/api/v1/{quizId}/questions") public class QuestionController { private final QuestionService questionService; + private final QuizRepository quizRepository; - public QuestionController(QuestionService questionService) { + public QuestionController(QuestionService questionService, QuizRepository quizRepository) { this.questionService = questionService; + this.quizRepository = quizRepository; } @Operation(summary = "delete question", security = @SecurityRequirement(name = "bearerAuth")) @@ -52,8 +57,12 @@ public QuestionController(QuestionService questionService) { @PreAuthorize("hasAuthority('MENTOR')") @DeleteMapping("/{questionId}") public ResponseEntity deleteQuestion( + @Parameter(description = "Id of the quiz to which this question belongs") @PathVariable() + Long quizId, @Parameter(description = "Id of the question which will be deleted") @PathVariable - Long questionId) { + Long questionId, + Principal principal) { + checkQuestionAuthor(quizId, principal); if (questionService.deleteQuestion(questionId)) { return ResponseEntity.noContent().build(); } else { @@ -81,12 +90,16 @@ public ResponseEntity deleteQuestion( @PreAuthorize("hasAuthority('MENTOR')") @PutMapping("/{questionId}") public ResponseEntity updateQuestion( + @Parameter(description = "Id of the quiz to which this question belongs") @PathVariable() + Long quizId, @Parameter(description = "Id of the question which will be updated") @PathVariable Long questionId, @io.swagger.v3.oas.annotations.parameters.RequestBody( description = "Request object to update question") @RequestBody - CreateQuestionDto createQuestionDto) { + CreateQuestionDto createQuestionDto, + Principal principal) { + checkQuestionAuthor(quizId, principal); if (questionService.updateQuestion(questionId, createQuestionDto)) { return ResponseEntity.noContent().build(); } else { @@ -94,7 +107,7 @@ public ResponseEntity updateQuestion( } } - @GetMapping("/{quizId}") + @GetMapping() public ResponseEntity> findAllByQuizId( @Parameter(description = "Quiz ID that belongs to the questions being searched") @PathVariable Long quizId, @@ -110,4 +123,11 @@ public ResponseEntity> findAllByQuizId( return ResponseEntity.ok(questionService.findAllByQuizId(quizId, pageable)); } + + private void checkQuestionAuthor(Long quizId, Principal principal) { + Quiz quiz = quizRepository.findById(quizId).orElseThrow(IllegalArgumentException::new); + if (!quiz.getModule().getCourse().getMentor().getEmail().equals(principal.getName())) { + throw new PermissionDeniedException("permission.denied"); + } + } } diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java index f85909ec..f9fe8650 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java @@ -23,7 +23,6 @@ public boolean deleteQuestion(Long questionId) { .findById(questionId) .map( question -> { - checkIfQuestionBelongsToCurrentMentor(question); questionRepository.delete(question); return true; }) @@ -36,7 +35,6 @@ public boolean updateQuestion(Long questionId, CreateQuestionDto createQuestionD .findById(questionId) .map( question -> { - checkIfQuestionBelongsToCurrentMentor(question); Question updatedQuestion = QuestionMapper.createQuestionDtoToQuestion(createQuestionDto, question.getQuiz()); updatedQuestion.setId(question.getId()); @@ -51,11 +49,4 @@ public Page findAllByQuizId(Long id, Pageable pageable) { return QuestionMapper.toQuestionDtoPage( questionRepository.findAllQuestionsByQuizId(id, pageable)); } - - private void checkIfQuestionBelongsToCurrentMentor(Question question) { - String username = SecurityContextHolder.getContext().getAuthentication().getName(); - if (!question.getQuiz().getModule().getCourse().getMentor().getEmail().equals(username)) { - throw new IllegalArgumentException(); - } - } } diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java index 58967243..61506fa0 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/dto/CreateQuestionDto.java @@ -13,7 +13,7 @@ public class CreateQuestionDto { private String question; @Schema(description = "The right answers count", example = "3") - private int rightAnswersCount; + private int rightAnswersCount = -1; @ArraySchema(schema = @Schema(implementation = CreateAnswerOptionDto.class)) private Set answerOptions; diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java b/os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java index c787b225..6d4acbe1 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/status/QuizStatus.java @@ -21,11 +21,19 @@ public class QuizStatus { public QuizStatus() {} + public QuizStatus(Long id) { + this.id = id; + } + public QuizStatus(Long id, String type) { this.id = id; this.type = type; } + public static QuizStatus inProgress() { + return new QuizStatus(1L); + } + public static QuizStatus isInProgress() { return new QuizStatus(1L, "IN_PROGRESS"); } diff --git a/os-server/src/main/resources/db/migration/h2/V1_0_17_updates.sql b/os-server/src/main/resources/db/migration/h2/V1_0_17__updates.sql similarity index 95% rename from os-server/src/main/resources/db/migration/h2/V1_0_17_updates.sql rename to os-server/src/main/resources/db/migration/h2/V1_0_17__updates.sql index 2e5c797c..d7e878be 100644 --- a/os-server/src/main/resources/db/migration/h2/V1_0_17_updates.sql +++ b/os-server/src/main/resources/db/migration/h2/V1_0_17__updates.sql @@ -48,13 +48,19 @@ CREATE TABLE IF NOT EXISTS enrolled_quiz ( PRIMARY KEY (id), CONSTRAINT fk_enrolled_quiz_quiz_status FOREIGN KEY (quiz_status_id) -REFERENCES quiz_status (id), +REFERENCES quiz_status (id) +ON DELETE CASCADE +ON UPDATE CASCADE, CONSTRAINT fk_enrolled_quiz_quiz FOREIGN KEY (quiz_id) -REFERENCES quiz (id), +REFERENCES quiz (id) +ON DELETE CASCADE +ON UPDATE CASCADE, CONSTRAINT fk_enrolled_quiz_enrolled_module FOREIGN KEY (enrolled_module_id) REFERENCES enrolled_module (id) +ON DELETE CASCADE +ON UPDATE CASCADE ); CREATE INDEX fk_enrolled_quiz_quiz_status_idx ON enrolled_quiz (quiz_status_id ASC); diff --git a/os-server/src/main/resources/db/migration/mysql/V1_0_17__update.sql b/os-server/src/main/resources/db/migration/mysql/V1_0_17__update.sql index 08feb694..7cf174cb 100644 --- a/os-server/src/main/resources/db/migration/mysql/V1_0_17__update.sql +++ b/os-server/src/main/resources/db/migration/mysql/V1_0_17__update.sql @@ -42,23 +42,29 @@ ENGINE = InnoDB; -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `open_school_db`.`enrolled_quiz` ( `id` BIGINT NOT NULL AUTO_INCREMENT, - `student_grade` INT NOT NULL, - `quiz_status_id` BIGINT NOT NULL, + `student_grade` INT NULL, `quiz_id` BIGINT NOT NULL, `enrolled_module_id` BIGINT NOT NULL, + `quiz_status_id` BIGINT NOT NULL, PRIMARY KEY (`id`), -INDEX `fk_enrolled_quiz_quiz_status_idx` (`quiz_status_id` ASC) VISIBLE, -INDEX `fk_enrolled_quiz_quiz_idx` (`quiz_id` ASC) VISIBLE, -INDEX `fk_enrolled_quiz_enrolled_module_idx` (`enrolled_module_id` ASC) VISIBLE, -CONSTRAINT `fk_enrolled_quiz_quiz_status` -FOREIGN KEY (`quiz_status_id`) -REFERENCES `open_school_db`.`quiz_status` (`id`), -CONSTRAINT `fk_enrolled_quiz_quiz` -FOREIGN KEY (`quiz_id`) -REFERENCES `open_school_db`.`quiz` (`id`), -CONSTRAINT `fk_enrolled_quiz_enrolled_module` -FOREIGN KEY (`enrolled_module_id`) -REFERENCES `open_school_db`.`enrolled_module` (`id`) + INDEX `fk_enrolled_quiz_quiz_idx` (`quiz_id` ASC) VISIBLE, + INDEX `fk_enrolled_quiz_enrolled_module_idx` (`enrolled_module_id` ASC) VISIBLE, + INDEX `fk_enrolled_quiz_quiz_status_idx` (`quiz_status_id` ASC) VISIBLE, + CONSTRAINT `fk_enrolled_quiz_quiz` + FOREIGN KEY (`quiz_id`) + REFERENCES `open_school_db`.`quiz` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT `fk_enrolled_quiz_enrolled_module` + FOREIGN KEY (`enrolled_module_id`) + REFERENCES `open_school_db`.`enrolled_module` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT `fk_enrolled_quiz_quiz_status` + FOREIGN KEY (`quiz_status_id`) + REFERENCES `open_school_db`.`quiz_status` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE ) ENGINE = InnoDB; diff --git a/os-server/src/main/resources/messages.properties b/os-server/src/main/resources/messages.properties index ea5f26ff..41cda3eb 100644 --- a/os-server/src/main/resources/messages.properties +++ b/os-server/src/main/resources/messages.properties @@ -38,3 +38,5 @@ exception.file.not.deleted=File was not deleted permission.denied=Permission is not allowed exception.persist=Unable to create new data, please check the input data. faq.length.max=Maximum 500 characters are allowed +quiz.failed=FAILED +quiz.completed=COMPLETED \ No newline at end of file diff --git a/os-server/src/test/java/app/openschool/course/api/CourseGenerator.java b/os-server/src/test/java/app/openschool/course/api/CourseGenerator.java index 1b721e3b..07ff3c61 100644 --- a/os-server/src/test/java/app/openschool/course/api/CourseGenerator.java +++ b/os-server/src/test/java/app/openschool/course/api/CourseGenerator.java @@ -1,5 +1,6 @@ package app.openschool.course.api; +import app.openschool.category.Category; import app.openschool.course.Course; import app.openschool.course.EnrolledCourse; import app.openschool.course.difficulty.Difficulty; @@ -7,6 +8,7 @@ import app.openschool.course.module.Module; import app.openschool.course.module.item.ModuleItem; import app.openschool.course.module.item.type.ModuleItemType; +import app.openschool.course.module.quiz.Quiz; import app.openschool.user.User; import java.util.HashSet; import java.util.Set; @@ -17,7 +19,7 @@ public static Course generateCourseWithEnrolledCourses() { Course course = new Course(); Set modules = new HashSet<>(); Set moduleItems = new HashSet<>(); - Module module = new Module(1L, course, moduleItems); + Module module = new Module(1L, course, moduleItems, new HashSet<>()); ModuleItemType moduleItemType = new ModuleItemType(); moduleItemType.setType("TheType"); ModuleItem moduleItem = new ModuleItem(1L, "String", moduleItemType, "kkk", 100L, module); @@ -44,7 +46,7 @@ public static Course generateCourse() { Course course = new Course(); Set modules = new HashSet<>(); Set moduleItems = new HashSet<>(); - Module module = new Module(1L, course, moduleItems); + Module module = new Module(1L, course, moduleItems, new HashSet<>()); ModuleItemType moduleItemType = new ModuleItemType(); moduleItemType.setType("TheType"); ModuleItem moduleItem = new ModuleItem(1L, "String", moduleItemType, "kkk", 100L, module); @@ -57,8 +59,16 @@ public static Course generateCourse() { course.setModules(modules); course.setMentor(new User(1L)); course.setRating(5.0); - course.setDifficulty(new Difficulty("Basic")); - course.setLanguage(new Language("English")); + Category category = new Category(); + category.setId(1L); + category.setTitle("category"); + course.setCategory(category); + Difficulty basic = new Difficulty("Basic"); + basic.setId(1); + course.setDifficulty(basic); + Language english = new Language("English"); + english.setId(1); + course.setLanguage(english); return course; } diff --git a/os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java b/os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java new file mode 100644 index 00000000..6a052212 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java @@ -0,0 +1,23 @@ +package app.openschool.course.module.api; + +import app.openschool.course.Course; +import app.openschool.course.module.Module; +import app.openschool.user.User; +import app.openschool.user.api.UserGenerator; +import app.openschool.user.role.Role; + +public class ModuleGenerator { + public static Module generateModule() { + Module module = new Module(); + Course course = new Course(); + course.setMentor(UserGenerator.generateMentor()); + module.setCourse(course); + return module; + } + + public static Module generateModuleWithAnotherUser(){ + Module module = ModuleGenerator.generateModule(); + module.getCourse().getMentor().setEmail("another@gmail"); + return module; + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java new file mode 100644 index 00000000..07ff54f0 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java @@ -0,0 +1,277 @@ +package app.openschool.course.module.quiz; + +import app.openschool.common.security.JwtTokenProvider; +import app.openschool.common.security.UserPrincipal; +import app.openschool.course.module.ModuleRepository; +import app.openschool.course.module.api.ModuleGenerator; +import app.openschool.course.module.quiz.api.mapper.QuizMapper; +import app.openschool.course.module.quiz.util.EnrolledQuizAssessmentResponseDtoGenerator; +import app.openschool.course.module.quiz.util.QuizGenerator; +import app.openschool.user.User; +import app.openschool.user.api.UserGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc +public class QuizControllerTest { + @Autowired private MockMvc mockMvc; + + @Autowired private JwtTokenProvider jwtTokenProvider; + + @MockBean private QuizServiceImpl quizService; + @MockBean ModuleRepository moduleRepository; + private static final String REQUEST_BODY = + "{\n" + + " \"maxGrade\": \"10\",\n" + + " \"passingScore\": \"7\",\n" + + " \"questions\": [\n" + + " {\n" + + " \"question\": \"What is Java bytecode\",\n" + + " \"answerOptions\": [\n" + + " {\n" + + " \"answerOption\": \"Java bytecode is the instruction set for the JVM\",\n" + + " \"rightAnswer\": \"true\"\n" + + " }\n" + + " ],\n" + + " \"questionType\": \"MULTIPLE_CHOICE\"\n" + + " }\n" + + " ]\n" + + "}"; + + private static final String AUTHORIZATION = "Authorization"; + + @Test + void createQuiz_withCorrectCredentials_isCreated() throws Exception { + Quiz quiz = QuizGenerator.generateQuiz(); + when(quizService.createQuiz(anyLong(), any())).thenReturn(Optional.of(quiz)); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + mockMvc + .perform( + post("/api/v1/1/quizzes") + .contentType(APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isCreated()) + .andExpect( + content().json(new ObjectMapper().writeValueAsString(QuizMapper.quizToQuizDto(quiz)))); + } + + @Test + void createQuiz_withIncorrectModuleId_returnNotFound() throws Exception { + when(quizService.createQuiz(anyLong(), any())).thenReturn(Optional.empty()); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + mockMvc + .perform( + post("/api/v1/1/quizzes") + .contentType(APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNotFound()); + } + + @Test + void createQuiz_withIncorrectMentor_isBadRequest() throws Exception { + when(quizService.createQuiz(anyLong(), any())).thenThrow(IllegalArgumentException.class); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + mockMvc + .perform( + post("/api/v1/1/quizzes") + .contentType(APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isBadRequest()); + } + + @Test + void createQuiz_withStudent_isForbidden() throws Exception { + mockMvc + .perform( + post("/api/v1/1/quizzes/1") + .contentType(APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) + .andExpect(status().isForbidden()); + } + + @Test + void createQuiz_withoutJwtToken_isUnauthorized() throws Exception { + + mockMvc + .perform(post("/api/v1/1/quizzes").contentType(APPLICATION_JSON).content(REQUEST_BODY)) + .andExpect(status().isUnauthorized()); + } + + @Test + void deleteQuiz_withCorrectCredentials_isNoContent() throws Exception { + when(quizService.deleteQuiz(anyLong())).thenReturn(true); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + mockMvc + .perform( + delete("/api/v1/1/quizzes/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNoContent()); + } + + @Test + void deleteQuiz_withIncorrectQuizId_returnNotFound() throws Exception { + when(quizService.deleteQuiz(anyLong())).thenReturn(false); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + + mockMvc + .perform( + delete("/api/v1/1/quizzes/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNotFound()); + } + + @Test + void deleteQuiz_withIncorrectMentor_isForbidden() throws Exception { + when(quizService.deleteQuiz(anyLong())).thenThrow(IllegalArgumentException.class); + + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModuleWithAnotherUser())); + + mockMvc + .perform( + delete("/api/v1/1/quizzes/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isForbidden()); + } + + @Test + void deleteQuiz_withStudent_isForbidden() throws Exception { + mockMvc + .perform( + delete("/api/v1/1/quizzes/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) + .andExpect(status().isForbidden()); + } + + @Test + void deleteQuiz_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(delete("/api/v1/1/quizzes/1")).andExpect(status().isUnauthorized()); + } + + @Test + void updateQuiz_withCorrectArg_returnOk() throws Exception { + when(quizService.updateQuiz(anyLong(), any())).thenReturn(QuizGenerator.generateQuiz()); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + mockMvc + .perform( + patch("/api/v1/1/quizzes/1") + .contentType(APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isOk()); + } + + @Test + void updateQuiz_withIncorrectMentor_isForbidden() throws Exception { + when(quizService.updateQuiz(anyLong(), any())).thenReturn(QuizGenerator.generateQuiz()); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModuleWithAnotherUser())); + mockMvc + .perform( + patch("/api/v1/1/quizzes/1") + .contentType(APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isForbidden()); + } + + @Test + void updateQuiz_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(patch("/api/v1/1/quizzes/1")).andExpect(status().isUnauthorized()); + } + + @Test + void findById_withCorrectArg_returnOk() throws Exception { + when(quizService.findById(anyLong())) + .thenReturn(QuizMapper.quizToQuizDto(QuizGenerator.generateQuiz())); + + mockMvc + .perform( + get("/api/v1/1/quizzes/1") + .contentType(APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isOk()); + } + + @Test + void findById_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(get("/api/v1/1/quizzes/1")).andExpect(status().isUnauthorized()); + } + + @Test + void findAllByModuleId_withCorrectArg_returnOk() throws Exception { + when(quizService.findAllByModuleId(anyLong(), any())) + .thenReturn(QuizGenerator.generateQuizDtoPage()); + mockMvc + .perform( + get("/api/v1/1/quizzes") + .contentType(APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isOk()); + } + + @Test + void findByModuleId_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(get("/api/v1/1/quizzes")).andExpect(status().isUnauthorized()); + } + + @Test + void completeEnrolledQuiz_withCorrectArg_returnOK() throws Exception { + when(quizService.completeEnrolledQuiz(anyLong(), any(), any())) + .thenReturn( + Optional.of( + EnrolledQuizAssessmentResponseDtoGenerator + .generateEnrolledQuizAssessmentResponseDto("FAILED"))); + mockMvc + .perform( + get("/api/v1/1/quizzes/enrolledQuizzes/1") + .contentType(APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isOk()); + } + + @Test + void completeEnrolledQuiz_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc + .perform(get("/api/v1/1/quizzes/enrolledQuizzes/1")) + .andExpect(status().isUnauthorized()); + } + + private String generateJwtToken(User user) { + return "Bearer " + jwtTokenProvider.generateJwtToken(new UserPrincipal(user)); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java new file mode 100644 index 00000000..397e3977 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java @@ -0,0 +1,116 @@ +package app.openschool.course.module.quiz; + +import app.openschool.category.Category; +import app.openschool.category.CategoryRepository; +import app.openschool.course.Course; +import app.openschool.course.CourseRepository; +import app.openschool.course.api.CourseGenerator; +import app.openschool.course.difficulty.Difficulty; +import app.openschool.course.difficulty.DifficultyRepository; +import app.openschool.course.language.Language; +import app.openschool.course.language.LanguageRepository; +import app.openschool.course.module.Module; +import app.openschool.course.module.ModuleRepository; +import app.openschool.course.module.api.ModuleGenerator; +import app.openschool.course.module.item.ModuleItemRepository; +import app.openschool.course.module.item.type.ModuleItemTypeRepository; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.QuestionRepository; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.type.QuestionType; +import app.openschool.course.module.quiz.question.util.QuestionGenerator; +import app.openschool.course.module.quiz.util.QuizGenerator; +import app.openschool.user.User; +import app.openschool.user.UserRepository; +import app.openschool.user.api.UserGenerator; +import app.openschool.user.company.CompanyRepository; +import app.openschool.user.role.RoleRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD; + +@DataJpaTest +@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) +public class QuizRepositoryTest { + @Autowired QuizRepository quizRepository; + @Autowired ModuleRepository moduleRepository; + @Autowired UserRepository userRepository; + @Autowired CourseRepository courseRepository; + @Autowired CategoryRepository categoryRepository; + @Autowired DifficultyRepository difficultyRepository; + @Autowired LanguageRepository languageRepository; + + @Autowired QuestionRepository questionRepository; + + @BeforeEach + public void setup() { + User user = UserGenerator.generateMentor(); + userRepository.save(user); + Category category = new Category(); + category.setTitle("category title"); + categoryRepository.save(category); + Difficulty difficulty = new Difficulty(); + difficulty.setTitle("difficulty title"); + difficultyRepository.save(difficulty); + Language language = new Language(); + language.setTitle("English"); + languageRepository.save(language); + Course course = new Course(); + course.setTitle("Stream"); + course.setRating(5.5); + course.setCategory(categoryRepository.getById(1L)); + course.setDifficulty(difficultyRepository.getById(1)); + course.setLanguage(languageRepository.getById(1)); + course.setMentor(user); + courseRepository.save(course); + Module module = ModuleGenerator.generateModule(); + module.setCourse(course); + module.setDescription("module description"); + moduleRepository.save(module); + } + + @Test + void saveQuiz() { + Quiz quiz = new Quiz(); + quiz.setTitle("quiz title"); + quiz.setDescription("quiz description"); + Question question = new Question(); + question.setQuestion("question"); + AnswerOption answerOption = new AnswerOption(); + answerOption.setAnswerOption("Answer1"); + answerOption.setRightAnswer(true); + answerOption.setQuestion(question); + Set answerOptions = new HashSet<>(); + answerOptions.add(answerOption); + question.setAnswerOptions(answerOptions); + question.setQuiz(quiz); + QuestionType questionType = new QuestionType(1L, "Question type"); + question.setQuestionType(questionType); + Set questions = new HashSet<>(); + questions.add(question); + quiz.setQuestions(questions); + quiz.setMaxGrade(7); + quiz.setPassingScore(5); + quiz.setModule(moduleRepository.getById(1L)); + Quiz savedQuiz = quizRepository.save(quiz); + assertEquals(quiz.getTitle(), savedQuiz.getTitle()); + assertEquals(quiz.getDescription(), savedQuiz.getDescription()); + assertEquals(quiz.getMaxGrade(), savedQuiz.getMaxGrade()); + assertEquals(quiz.getPassingScore(), savedQuiz.getPassingScore()); + assertEquals(quiz.getQuestions(), savedQuiz.getQuestions()); + assertEquals( + Objects.requireNonNull(quiz.getQuestions().stream().findFirst().orElse(null)).getAnswerOptions().stream().findFirst(), + Objects.requireNonNull(savedQuiz.getQuestions().stream().findFirst().orElse(null)).getAnswerOptions().stream().findFirst()); + assertEquals(quiz.getModule(), savedQuiz.getModule()); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java new file mode 100644 index 00000000..0b2191c9 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java @@ -0,0 +1,195 @@ +package app.openschool.course.module.quiz; + +import app.openschool.course.module.ModuleRepository; +import app.openschool.course.module.api.ModuleGenerator; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.util.*; +import app.openschool.user.User; +import app.openschool.user.api.UserGenerator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.MessageSource; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; + +import java.util.Locale; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) + class QuizServiceImplTest { + @Mock ModuleRepository moduleRepository; + + @Mock QuizRepository quizRepository; + @Mock EnrolledQuizRepository enrolledQuizRepository; + + @Mock MessageSource messageSource; + private QuizServiceImpl quizService; + + @BeforeEach + void setUp() { + quizService = + new QuizServiceImpl( + moduleRepository, quizRepository, enrolledQuizRepository, messageSource); + } + + @AfterEach + void cleanUp() { + SecurityContextHolder.clearContext(); + } + + @Test + void createQuiz_withCorrectArg_returnQuiz() { + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + when(quizRepository.save(any())).thenReturn(QuizGenerator.generateQuiz()); + Quiz actualQuiz = quizService.createQuiz(1L, QuizDtoGenerator.generateCreateQuizDto()).get(); + Quiz expectedQuiz = QuizGenerator.generateQuiz(); + assertEquals(actualQuiz.getDescription(), expectedQuiz.getDescription()); + assertEquals(actualQuiz.getTitle(), expectedQuiz.getTitle()); + assertEquals(actualQuiz.getMaxGrade(), expectedQuiz.getMaxGrade()); + assertEquals(actualQuiz.getPassingScore(), expectedQuiz.getPassingScore()); + } + + @Test + void createQuiz_withIncorrectModuleId_returnEmptyOptional() { + Long moduleId = 1L; + when(moduleRepository.findById(anyLong())).thenReturn(Optional.empty()); + + assertEquals(quizService.createQuiz(moduleId, any()), Optional.empty()); + } + + @Test + void createQuiz_withCorrectModuleId_returnOptionalOfQuiz() { + Quiz quiz = Quiz.getInstance(); + when(moduleRepository.findById(anyLong())) + .thenReturn(Optional.of(ModuleGenerator.generateModule())); + when(quizRepository.save(any())).thenReturn(quiz); + setAuthentication(UserGenerator.generateMentor()); + + Long moduleId = 1L; + assertEquals( + quizService.createQuiz(moduleId, QuizDtoGenerator.generateCreateQuizDto()), + Optional.of(quiz)); + verify(moduleRepository, times(1)).findById(anyLong()); + verify(quizRepository, times(1)).save(any()); + } + + @Test + void deleteQuiz_withInCorrectQuizId_returnFalse() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Long quizId = 1L; + assertEquals(quizService.deleteQuiz(quizId), false); + } + + @Test + void deleteQuiz_withCorrectQuizId_returnTrue() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + + setAuthentication(UserGenerator.generateMentor()); + + Long quizId = 1L; + assertEquals(quizService.deleteQuiz(quizId), true); + } + + @Test + void updateQuiz_withCorrectArg_returnQuiz() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + when(quizRepository.save(any())).thenReturn(QuizGenerator.generateQuiz()); + + Quiz actualQuiz = quizService.updateQuiz(1L, QuizDtoGenerator.generateModifyQuizDataRequest()); + Quiz expectedQuiz = QuizGenerator.generateQuiz(); + assertEquals(actualQuiz.getTitle(), expectedQuiz.getTitle()); + assertEquals(actualQuiz.getDescription(), expectedQuiz.getDescription()); + assertEquals(actualQuiz.getMaxGrade(), expectedQuiz.getMaxGrade()); + assertEquals(actualQuiz.getPassingScore(), expectedQuiz.getPassingScore()); + } + + @Test + void updateQuiz_withInCorrectQuizId_throwIllegalArgumentException() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.empty()); + assertThatThrownBy(() -> quizService.updateQuiz(1L, any())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void findById_withCorrectArg_returnQuiz() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + QuizDto actualQuizDto = quizService.findById(1L); + QuizDto quizDto = QuizDtoGenerator.generateQuizDto(); + assertEquals(actualQuizDto.getTitle(), quizDto.getTitle()); + assertEquals(actualQuizDto.getDescription(), quizDto.getDescription()); + assertEquals(actualQuizDto.getMaxGrade(), quizDto.getMaxGrade()); + assertEquals(actualQuizDto.getPassingScore(), quizDto.getPassingScore()); + } + + @Test + void findById_withInCorrectId_throwIllegalArgumentException() { + when(quizRepository.findById(anyLong())).thenReturn(Optional.empty()); + assertThatThrownBy(() -> quizService.findById(1L)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void findAllByModuleId_withCorrectArg_quizDtoPage() { + doReturn(QuizGenerator.generateQuizPage()) + .when(quizRepository) + .findAllByModuleId(anyLong(), any()); + PageRequest pageRequest = PageRequest.of(1, 1); + Page actualPageQuizDto = quizService.findAllByModuleId(1L, pageRequest); + Page quizDtoPage = QuizGenerator.generateQuizDtoPage(); + assertEquals( + actualPageQuizDto.stream().findFirst().orElseThrow().getTitle(), + quizDtoPage.stream().findFirst().orElseThrow().getTitle()); + assertEquals( + actualPageQuizDto.stream().findFirst().orElseThrow().getDescription(), + quizDtoPage.stream().findFirst().orElseThrow().getDescription()); + assertEquals( + actualPageQuizDto.stream().findFirst().orElseThrow().getMaxGrade(), + quizDtoPage.stream().findFirst().orElseThrow().getMaxGrade()); + assertEquals( + actualPageQuizDto.stream().findFirst().orElseThrow().getPassingScore(), + quizDtoPage.stream().findFirst().orElseThrow().getPassingScore()); + } + + @Test + void completeEnrolledQuiz_WithCorrectQuizId_returnOptionalOfEnrolledQuizAssessmentResponseDto() { + String quizStatus = "FAILED"; + when(enrolledQuizRepository.findById(anyLong())) + .thenReturn(Optional.of(EnrolledQuizGenerator.generateEnrolledQuiz())); + when(messageSource.getMessage(anyString(), any(), any())).thenReturn(quizStatus); + + assertEquals( + quizService.completeEnrolledQuiz( + 1L, + EnrolledQuizAssessmentRequestDtoGenerator.generateEnrolledQuizAssessmentRequestDto(), + Locale.ROOT), + Optional.of( + EnrolledQuizAssessmentResponseDtoGenerator.generateEnrolledQuizAssessmentResponseDto( + quizStatus))); + } + + @Test + void completeEnrolledQuiz_WithIncorrectQuizId_returnEmptyOptional() { + when(enrolledQuizRepository.findById(anyLong())).thenReturn(Optional.empty()); + + assertEquals(quizService.completeEnrolledQuiz(1L, null, null), Optional.empty()); + } + + private void setAuthentication(User user) { + SecurityContextHolder.setContext( + new SecurityContextImpl( + new UsernamePasswordAuthenticationToken(user.getEmail(), null, null))); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java new file mode 100644 index 00000000..94a35dd5 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java @@ -0,0 +1,21 @@ +package app.openschool.course.module.quiz.question.util; + +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.type.QuestionType; +import app.openschool.course.module.quiz.util.QuizGenerator; + +public class QuestionGenerator { + private static final Long ID = 1L; + private static final String QUESTION = "question"; + + private QuestionGenerator() {} + + public static Question generateQuestion() { + Question question = Question.getInstance(); + question.setId(ID); + question.setQuestion(QUESTION); + question.setQuestionType(QuestionType.isMultipleChoice()); + question.setQuiz(QuizGenerator.generateQuiz()); + return question; + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java new file mode 100644 index 00000000..43dcf02d --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java @@ -0,0 +1,32 @@ +package app.openschool.course.module.quiz.util; + +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentRequestDto; +import app.openschool.course.module.quiz.api.dto.QuestionWithChosenAnswerDto; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class EnrolledQuizAssessmentRequestDtoGenerator { + private static final long ID = 1L; + + private EnrolledQuizAssessmentRequestDtoGenerator() {} + + public static EnrolledQuizAssessmentRequestDto generateEnrolledQuizAssessmentRequestDto() { + EnrolledQuizAssessmentRequestDto enrolledQuizAssessmentRequestDto = + new EnrolledQuizAssessmentRequestDto(); + enrolledQuizAssessmentRequestDto.setQuestionWithChosenAnswerDtoSet( + generateQuestionWithChosenAnswerDtoSet()); + return enrolledQuizAssessmentRequestDto; + } + + private static Set generateQuestionWithChosenAnswerDtoSet() { + Set questionWithChosenAnswerDtoSet = new HashSet<>(); + QuestionWithChosenAnswerDto questionWithChosenAnswerDto = + QuestionWithChosenAnswerDto.getInstance(); + questionWithChosenAnswerDto.setQuestionId(ID); + questionWithChosenAnswerDto.setChosenAnswersIds(List.of(ID)); + questionWithChosenAnswerDtoSet.add(questionWithChosenAnswerDto); + return questionWithChosenAnswerDtoSet; + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentResponseDtoGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentResponseDtoGenerator.java new file mode 100644 index 00000000..337e320e --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentResponseDtoGenerator.java @@ -0,0 +1,17 @@ +package app.openschool.course.module.quiz.util; + +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; + +public class EnrolledQuizAssessmentResponseDtoGenerator { + private static final int MAX_GRADE = 10; + private static final int PASSING_SCORE = 7; + private static final int RIGHT_ANSWER_COUNT = 1; + + private EnrolledQuizAssessmentResponseDtoGenerator() {} + + public static EnrolledQuizAssessmentResponseDto generateEnrolledQuizAssessmentResponseDto( + String assessmentResult) { + return new EnrolledQuizAssessmentResponseDto( + RIGHT_ANSWER_COUNT, assessmentResult, PASSING_SCORE, MAX_GRADE); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizGenerator.java new file mode 100644 index 00000000..40eff57c --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizGenerator.java @@ -0,0 +1,14 @@ +package app.openschool.course.module.quiz.util; + +import app.openschool.course.module.EnrolledModule; +import app.openschool.course.module.quiz.EnrolledQuiz; +import app.openschool.course.module.quiz.status.QuizStatus; + +public class EnrolledQuizGenerator { + private EnrolledQuizGenerator() {} + + public static EnrolledQuiz generateEnrolledQuiz() { + return new EnrolledQuiz( + QuizStatus.isInProgress(), QuizGenerator.generateQuiz(), new EnrolledModule()); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java new file mode 100644 index 00000000..3e81b699 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java @@ -0,0 +1,87 @@ +package app.openschool.course.module.quiz.util; + +import app.openschool.course.module.quiz.api.dto.CreateQuizDto; +import app.openschool.course.module.quiz.api.dto.ModifyQuizDataRequest; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class QuizDtoGenerator { + private QuizDtoGenerator() {} + + private static final int MAX_GRADE = 10; + private static final int PASSING_SCORE = 7; + private static final String QUESTION = "question"; + + private static final String DESCRIPTION = "description"; + + private static final String TITLE = "title"; + private static final String MULTIPLE_CHOICE = "MULTIPLE_CHOICE"; + private static final String ANSWER_OPTION = "answerOption"; + private static final Boolean IS_RIGHT_ANSWER = true; + + public static CreateQuizDto generateCreateQuizDto() { + CreateQuizDto createQuizDto = CreateQuizDto.getInstance(); + createQuizDto.setTitle(TITLE); + createQuizDto.setDescription(DESCRIPTION); + createQuizDto.setMaxGrade(MAX_GRADE); + createQuizDto.setPassingScore(PASSING_SCORE); + createQuizDto.setQuestions(generateCreateQuestionDtoSet()); + return createQuizDto; + } + + public static Page generateQuizDtoPage() { + List quizDtoList = new ArrayList<>(); + quizDtoList.add(generateQuizDto()); + return new PageImpl<>(quizDtoList); + } + + public static QuizDto generateQuizDto() { + QuizDto quizDto = new QuizDto(); + quizDto.setTitle(TITLE); + quizDto.setDescription(DESCRIPTION); + quizDto.setMaxGrade(MAX_GRADE); + quizDto.setPassingScore(PASSING_SCORE); + quizDto.setQuestions(generateQuestionDtoSet()); + return quizDto; + } + + public static ModifyQuizDataRequest generateModifyQuizDataRequest() { + ModifyQuizDataRequest request = new ModifyQuizDataRequest(); + request.setTitle(TITLE); + request.setDescription(DESCRIPTION); + request.setMaxGrade(MAX_GRADE); + request.setPassingScore(PASSING_SCORE); + return request; + } + + private static Set generateCreateQuestionDtoSet() { + CreateQuestionDto createQuestionDto = CreateQuestionDto.getInstance(); + createQuestionDto.setQuestion(QUESTION); + createQuestionDto.setQuestionType(MULTIPLE_CHOICE); + createQuestionDto.setAnswerOptions(generateCreateAnswerOptionDtoSet()); + return Set.of(createQuestionDto); + } + + private static Set generateQuestionDtoSet() { + QuestionDto questionDto = QuestionDto.getInstance(); + questionDto.setQuestion(QUESTION); + questionDto.setQuestionType(MULTIPLE_CHOICE); + return Set.of(questionDto); + } + + private static Set generateCreateAnswerOptionDtoSet() { + CreateAnswerOptionDto createAnswerOptionDto = CreateAnswerOptionDto.getInstance(); + createAnswerOptionDto.setAnswerOption(ANSWER_OPTION); + createAnswerOptionDto.setRightAnswer(IS_RIGHT_ANSWER); + return Set.of(createAnswerOptionDto); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java new file mode 100644 index 00000000..a0478488 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java @@ -0,0 +1,74 @@ +package app.openschool.course.module.quiz.util; + +import app.openschool.course.module.api.ModuleGenerator; +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.type.QuestionType; +import app.openschool.course.module.quiz.api.mapper.QuizMapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class QuizGenerator { + private static final Long ID = 1L; + private static final int MAX_GRADE = 10; + + private static final String DESCRIPTION = "description"; + + private static final String TITLE = "title"; + private static final int PASSING_SCORE = 7; + private static final String QUESTION = "question"; + private static final String ANSWER_OPTION = "answerOption"; + private static final Boolean IS_RIGHT_ANSWER = true; + private static final int RIGHT_ANSWER_COUNT = 1; + + private QuizGenerator() {} + + public static Quiz generateQuiz() { + Quiz quiz = Quiz.getInstance(); + quiz.setId(ID); + quiz.setTitle(TITLE); + quiz.setDescription(DESCRIPTION); + quiz.setModule(ModuleGenerator.generateModule()); + quiz.setMaxGrade(MAX_GRADE); + quiz.setPassingScore(PASSING_SCORE); + quiz.setQuestions(generateQuestionSet()); + return quiz; + } + + public static Page generateQuizPage() { + List quizList = new ArrayList<>(); + quizList.add(generateQuiz()); + return new PageImpl<>(quizList); + } + + public static Page generateQuizDtoPage() { + List quizDtoList = new ArrayList<>(); + QuizDto quizDto = QuizMapper.quizToQuizDto(generateQuiz()); + quizDtoList.add(quizDto); + return new PageImpl<>(quizDtoList); + } + + private static Set generateQuestionSet() { + Question question = Question.getInstance(); + question.setId(ID); + question.setQuestion(QUESTION); + question.setRightAnswersCount(RIGHT_ANSWER_COUNT); + question.setQuestionType(QuestionType.isMultipleChoice()); + question.setAnswerOptions(generateAnswerOptionSet()); + return Set.of(question); + } + + private static Set generateAnswerOptionSet() { + AnswerOption answerOption = AnswerOption.getInstance(); + answerOption.setId(ID); + answerOption.setAnswerOption(ANSWER_OPTION); + answerOption.setRightAnswer(IS_RIGHT_ANSWER); + return Set.of(answerOption); + } +} diff --git a/os-server/src/test/java/app/openschool/user/api/UserGenerator.java b/os-server/src/test/java/app/openschool/user/api/UserGenerator.java index 19b8eae0..95a6bab3 100644 --- a/os-server/src/test/java/app/openschool/user/api/UserGenerator.java +++ b/os-server/src/test/java/app/openschool/user/api/UserGenerator.java @@ -34,4 +34,27 @@ public static User generateUserWithSavedMentors() { user.getMentors().add(generateUser()); return user; } + public static User generateMentor() { + + String email = "mentor"; + String password = "pass"; + User mentor = new User(email, password); + mentor.setName("Jon"); + mentor.setSurname("Smith"); + Role role = new Role(); + role.setId(3); + role.setType("MENTOR"); + mentor.setRole(role); + return mentor; + } + public static User generateStudent() { + String email = "student"; + String password = "pass"; + User mentor = new User(email, password); + Role role = new Role(); + role.setId(1); + role.setType("STUDENT"); + mentor.setRole(role); + return mentor; + } } From 2f641feaec62384c88456a55dc1f0ca807a6b26f Mon Sep 17 00:00:00 2001 From: DELL Date: Thu, 2 Mar 2023 19:22:54 +0400 Subject: [PATCH 3/8] =?UTF-8?q?Created=20tests=E2=80=A4The=20changes=20men?= =?UTF-8?q?tioned=20in=20the=20comments=20have=20been=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/SecurityConfiguration.java | 3 +- .../quiz/question/QuestionRepository.java | 5 +- .../quiz/question/QuestionServiceImpl.java | 5 +- .../question/api/mapper/QuestionMapper.java | 16 -- ...e_quiz_question_answer_option_tablies.sql} | 0 ...e_quiz_question_answer_option_tablies.sql} | 0 .../course/module/api/ModuleGenerator.java | 2 +- .../module/quiz/QuizControllerTest.java | 42 ++-- .../module/quiz/QuizRepositoryTest.java | 90 +++++--- .../module/quiz/QuizServiceImplTest.java | 30 ++- ...olledQuizAssessmentResponseMapperTest.java | 18 ++ .../quiz/api/mapper/QuizMapperTest.java | 55 +++++ .../quiz/question/QuestionControllerTest.java | 196 ++++++++++++++++++ .../question/QuestionServiceImplTest.java | 151 ++++++++++++++ .../api/mapper/QuestionMapperTest.java | 40 ++++ .../util/CreateQuestionDtoGenerator.java | 22 ++ .../quiz/question/util/QuestionGenerator.java | 30 +++ ...lledQuizAssessmentRequestDtoGenerator.java | 1 - .../module/quiz/util/QuizDtoGenerator.java | 8 +- .../module/quiz/util/QuizGenerator.java | 7 +- .../openschool/user/api/UserGenerator.java | 2 + 21 files changed, 635 insertions(+), 88 deletions(-) rename os-server/src/main/resources/db/migration/h2/{V1_0_17__updates.sql => V1_0_18__create_quiz_question_answer_option_tablies.sql} (100%) rename os-server/src/main/resources/db/migration/mysql/{V1_0_17__update.sql => V1_0_18__create_quiz_question_answer_option_tablies.sql} (100%) create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapperTest.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/QuizMapperTest.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionServiceImplTest.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapperTest.java create mode 100644 os-server/src/test/java/app/openschool/course/module/quiz/question/util/CreateQuestionDtoGenerator.java diff --git a/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java b/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java index 6ab9c1ad..65a312ed 100644 --- a/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java +++ b/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java @@ -31,7 +31,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { "/api/v1/categories/**", "/api/v1/mentors/**", "/api/v1/courses/**", - "/api/v1/*/quizzes/**" + "/api/v1/*/quizzes/**", + "/api/v1/*/questions/**" }; private final JwtAuthenticationFilter jwtAuthenticationFilter; diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java index 3bd6641b..411af516 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionRepository.java @@ -1,9 +1,12 @@ package app.openschool.course.module.quiz.question; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; + + public interface QuestionRepository extends JpaRepository { - Page findAllQuestionsByQuizId(Long id, Pageable pageable); + Optional> findAllQuestionsByQuizId(Long id, Pageable pageable); } diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java index f9fe8650..bff6556e 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionServiceImpl.java @@ -5,7 +5,6 @@ import app.openschool.course.module.quiz.question.api.mapper.QuestionMapper; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @Service @@ -47,6 +46,8 @@ public boolean updateQuestion(Long questionId, CreateQuestionDto createQuestionD @Override public Page findAllByQuizId(Long id, Pageable pageable) { return QuestionMapper.toQuestionDtoPage( - questionRepository.findAllQuestionsByQuizId(id, pageable)); + questionRepository + .findAllQuestionsByQuizId(id, pageable) + .orElseThrow(IllegalArgumentException::new)); } } diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java index 3ebfe547..23e50aed 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapper.java @@ -14,8 +14,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; - - public final class QuestionMapper { private static final String MULTIPLE_CHOICE = "MULTIPLE_CHOICE"; @@ -58,18 +56,4 @@ private static QuestionType createQuestionType(String questionType) { ? QuestionType.isMultipleChoice() : QuestionType.isMatching(); } - - private static Set createAnswerOptionDtoToAnswerOptions( - Set answerOptions, Question question) { - return answerOptions.stream() - .map( - createAnswerOptionDto -> { - AnswerOption answerOption = AnswerOption.getInstance(); - answerOption.setAnswerOption(createAnswerOptionDto.getAnswerOption()); - answerOption.setRightAnswer(createAnswerOptionDto.getRightAnswer()); - answerOption.setQuestion(question); - return answerOption; - }) - .collect(Collectors.toSet()); - } } diff --git a/os-server/src/main/resources/db/migration/h2/V1_0_17__updates.sql b/os-server/src/main/resources/db/migration/h2/V1_0_18__create_quiz_question_answer_option_tablies.sql similarity index 100% rename from os-server/src/main/resources/db/migration/h2/V1_0_17__updates.sql rename to os-server/src/main/resources/db/migration/h2/V1_0_18__create_quiz_question_answer_option_tablies.sql diff --git a/os-server/src/main/resources/db/migration/mysql/V1_0_17__update.sql b/os-server/src/main/resources/db/migration/mysql/V1_0_18__create_quiz_question_answer_option_tablies.sql similarity index 100% rename from os-server/src/main/resources/db/migration/mysql/V1_0_17__update.sql rename to os-server/src/main/resources/db/migration/mysql/V1_0_18__create_quiz_question_answer_option_tablies.sql diff --git a/os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java b/os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java index 6a052212..b1a6cb88 100644 --- a/os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java +++ b/os-server/src/test/java/app/openschool/course/module/api/ModuleGenerator.java @@ -15,7 +15,7 @@ public static Module generateModule() { return module; } - public static Module generateModuleWithAnotherUser(){ + public static Module generateModuleWithAnotherUser() { Module module = ModuleGenerator.generateModule(); module.getCourse().getMentor().setEmail("another@gmail"); return module; diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java index 07ff54f0..f06bb5ce 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java @@ -1,7 +1,19 @@ package app.openschool.course.module.quiz; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +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.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import app.openschool.common.security.JwtTokenProvider; import app.openschool.common.security.UserPrincipal; +import app.openschool.course.module.Module; import app.openschool.course.module.ModuleRepository; import app.openschool.course.module.api.ModuleGenerator; import app.openschool.course.module.quiz.api.mapper.QuizMapper; @@ -10,25 +22,21 @@ import app.openschool.user.User; import app.openschool.user.api.UserGenerator; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; 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.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; -import java.util.Optional; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + @ExtendWith(SpringExtension.class) @SpringBootTest @@ -86,25 +94,12 @@ void createQuiz_withIncorrectModuleId_returnNotFound() throws Exception { mockMvc .perform( post("/api/v1/1/quizzes") - .contentType(APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isNotFound()); } - @Test - void createQuiz_withIncorrectMentor_isBadRequest() throws Exception { - when(quizService.createQuiz(anyLong(), any())).thenThrow(IllegalArgumentException.class); - when(moduleRepository.findById(anyLong())) - .thenReturn(Optional.of(ModuleGenerator.generateModule())); - mockMvc - .perform( - post("/api/v1/1/quizzes") - .contentType(APPLICATION_JSON) - .content(REQUEST_BODY) - .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) - .andExpect(status().isBadRequest()); - } @Test void createQuiz_withStudent_isForbidden() throws Exception { @@ -195,6 +190,7 @@ void updateQuiz_withCorrectArg_returnOk() throws Exception { @Test void updateQuiz_withIncorrectMentor_isForbidden() throws Exception { when(quizService.updateQuiz(anyLong(), any())).thenReturn(QuizGenerator.generateQuiz()); + when(moduleRepository.findById(anyLong())) .thenReturn(Optional.of(ModuleGenerator.generateModuleWithAnotherUser())); mockMvc @@ -249,7 +245,7 @@ void findByModuleId_withoutJwtToken_isUnauthorized() throws Exception { } @Test - void completeEnrolledQuiz_withCorrectArg_returnOK() throws Exception { + void completeEnrolledQuiz_withCorrectArg_returnOk() throws Exception { when(quizService.completeEnrolledQuiz(anyLong(), any(), any())) .thenReturn( Optional.of( diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java index 397e3977..ee41eda2 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizRepositoryTest.java @@ -1,10 +1,13 @@ package app.openschool.course.module.quiz; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD; + import app.openschool.category.Category; import app.openschool.category.CategoryRepository; import app.openschool.course.Course; import app.openschool.course.CourseRepository; -import app.openschool.course.api.CourseGenerator; import app.openschool.course.difficulty.Difficulty; import app.openschool.course.difficulty.DifficultyRepository; import app.openschool.course.language.Language; @@ -12,33 +15,27 @@ import app.openschool.course.module.Module; import app.openschool.course.module.ModuleRepository; import app.openschool.course.module.api.ModuleGenerator; -import app.openschool.course.module.item.ModuleItemRepository; -import app.openschool.course.module.item.type.ModuleItemTypeRepository; import app.openschool.course.module.quiz.question.Question; import app.openschool.course.module.quiz.question.QuestionRepository; import app.openschool.course.module.quiz.question.answer.AnswerOption; import app.openschool.course.module.quiz.question.type.QuestionType; -import app.openschool.course.module.quiz.question.util.QuestionGenerator; -import app.openschool.course.module.quiz.util.QuizGenerator; import app.openschool.user.User; import app.openschool.user.UserRepository; import app.openschool.user.api.UserGenerator; -import app.openschool.user.company.CompanyRepository; -import app.openschool.user.role.RoleRepository; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.test.annotation.DirtiesContext; import org.springframework.transaction.annotation.Transactional; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD; - @DataJpaTest @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) public class QuizRepositoryTest { @@ -81,6 +78,60 @@ public void setup() { @Test void saveQuiz() { + Quiz quiz = createQuiz(); + Quiz savedQuiz = saveQuizInDb(); + assertEquals(quiz.getTitle(), savedQuiz.getTitle()); + assertEquals(quiz.getDescription(), savedQuiz.getDescription()); + assertEquals(quiz.getMaxGrade(), savedQuiz.getMaxGrade()); + assertEquals(quiz.getPassingScore(), savedQuiz.getPassingScore()); + assertEquals(quiz.getQuestions(), savedQuiz.getQuestions()); + assertEquals( + Objects.requireNonNull(quiz.getQuestions().stream().findFirst().orElse(null)) + .getAnswerOptions() + .stream() + .findFirst(), + Objects.requireNonNull(savedQuiz.getQuestions().stream().findFirst().orElse(null)) + .getAnswerOptions() + .stream() + .findFirst()); + assertEquals(quiz.getModule(), savedQuiz.getModule()); + } + + @Test + @Transactional + void deleteQuiz() { + Quiz quiz = saveQuizInDb(); + quizRepository.delete(quiz); + Optional byId = quizRepository.findById(quiz.getId()); + assertTrue(byId.isEmpty()); + } + + @Test + @Transactional + void findById() { + Quiz quiz = saveQuizInDb(); + Quiz findQuiz = quizRepository.findById(quiz.getId()).orElseThrow(); + assertEquals(findQuiz.getId(), quiz.getId()); + } + + @Test + @Transactional + void findAllByModuleId() { + Quiz quiz = saveQuizInDb(); + Pageable of = PageRequest.of(0, 1); + Page allByModuleId = quizRepository.findAllByModuleId(1L, of); + assertEquals(allByModuleId.getTotalElements(), 1); + assertEquals(allByModuleId.getTotalPages(), 1); + assertEquals( + allByModuleId.getContent().stream().findAny().orElse(new Quiz()).getId(), quiz.getId()); + } + + private Quiz saveQuizInDb() { + Quiz quiz = createQuiz(); + return quizRepository.save(quiz); + } + + private Quiz createQuiz() { Quiz quiz = new Quiz(); quiz.setTitle("quiz title"); quiz.setDescription("quiz description"); @@ -102,15 +153,6 @@ void saveQuiz() { quiz.setMaxGrade(7); quiz.setPassingScore(5); quiz.setModule(moduleRepository.getById(1L)); - Quiz savedQuiz = quizRepository.save(quiz); - assertEquals(quiz.getTitle(), savedQuiz.getTitle()); - assertEquals(quiz.getDescription(), savedQuiz.getDescription()); - assertEquals(quiz.getMaxGrade(), savedQuiz.getMaxGrade()); - assertEquals(quiz.getPassingScore(), savedQuiz.getPassingScore()); - assertEquals(quiz.getQuestions(), savedQuiz.getQuestions()); - assertEquals( - Objects.requireNonNull(quiz.getQuestions().stream().findFirst().orElse(null)).getAnswerOptions().stream().findFirst(), - Objects.requireNonNull(savedQuiz.getQuestions().stream().findFirst().orElse(null)).getAnswerOptions().stream().findFirst()); - assertEquals(quiz.getModule(), savedQuiz.getModule()); + return quiz; } } diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java index 0b2191c9..6cb3d08a 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizServiceImplTest.java @@ -1,11 +1,27 @@ package app.openschool.course.module.quiz; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import app.openschool.course.module.ModuleRepository; import app.openschool.course.module.api.ModuleGenerator; import app.openschool.course.module.quiz.api.dto.QuizDto; -import app.openschool.course.module.quiz.util.*; +import app.openschool.course.module.quiz.util.EnrolledQuizAssessmentRequestDtoGenerator; +import app.openschool.course.module.quiz.util.EnrolledQuizAssessmentResponseDtoGenerator; +import app.openschool.course.module.quiz.util.EnrolledQuizGenerator; +import app.openschool.course.module.quiz.util.QuizDtoGenerator; +import app.openschool.course.module.quiz.util.QuizGenerator; import app.openschool.user.User; import app.openschool.user.api.UserGenerator; +import java.util.Locale; +import java.util.Optional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,16 +35,8 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextImpl; -import java.util.Locale; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) - class QuizServiceImplTest { +class QuizServiceImplTest { @Mock ModuleRepository moduleRepository; @Mock QuizRepository quizRepository; @@ -146,7 +154,7 @@ void findAllByModuleId_withCorrectArg_quizDtoPage() { doReturn(QuizGenerator.generateQuizPage()) .when(quizRepository) .findAllByModuleId(anyLong(), any()); - PageRequest pageRequest = PageRequest.of(1, 1); + PageRequest pageRequest = PageRequest.of(0, 1); Page actualPageQuizDto = quizService.findAllByModuleId(1L, pageRequest); Page quizDtoPage = QuizGenerator.generateQuizDtoPage(); assertEquals( diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapperTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapperTest.java new file mode 100644 index 00000000..36ba5351 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/EnrolledQuizAssessmentResponseMapperTest.java @@ -0,0 +1,18 @@ +package app.openschool.course.module.quiz.api.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentResponseDto; +import org.junit.jupiter.api.Test; + +public class EnrolledQuizAssessmentResponseMapperTest { + @Test + void toEnrolledQuizAssessmentResponseDto() { + EnrolledQuizAssessmentResponseDto enrolledQuizAssessmentResponseDto = + EnrolledQuizAssessmentResponseMapper.toEnrolledQuizAssessmentResponseDto( + 2, "COMPLETED", 2, 3); + + assertThat(enrolledQuizAssessmentResponseDto) + .hasOnlyFields("rightAnswersCount", "assessmentResult", "passingScore", "maxGrade"); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/QuizMapperTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/QuizMapperTest.java new file mode 100644 index 00000000..3fddf76a --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/api/mapper/QuizMapperTest.java @@ -0,0 +1,55 @@ +package app.openschool.course.module.quiz.api.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import app.openschool.course.module.Module; +import app.openschool.course.module.quiz.EnrolledQuiz; +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.api.dto.EnrolledQuizDto; +import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.util.EnrolledQuizGenerator; +import app.openschool.course.module.quiz.util.QuizDtoGenerator; +import app.openschool.course.module.quiz.util.QuizGenerator; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; + +public class QuizMapperTest { + + @Test + void createQuizDtoToQuiz() { + Quiz quiz = + QuizMapper.createQuizDtoToQuiz(QuizDtoGenerator.generateCreateQuizDto(), new Module()); + + assertThat(quiz) + .hasOnlyFields( + "id", "description", "title", "maxGrade", "passingScore", "questions", "module"); + } + + @Test + void quizToQuizDto() { + QuizDto quizDto = QuizMapper.quizToQuizDto(QuizGenerator.generateQuiz()); + + assertThat(quizDto) + .hasOnlyFields( + "id", "description", "title", "moduleId", "maxGrade", "passingScore", "questions"); + } + + @Test + void toEnrolledQuizDtoSet() { + Set enrolledQuizSet = new HashSet<>(); + enrolledQuizSet.add(EnrolledQuizGenerator.generateEnrolledQuiz()); + Set enrolledQuizDtoSet = + QuizMapper.enrolledQuizzesToEnrolledQuizDtoSet(enrolledQuizSet); + + assertEquals(1, enrolledQuizDtoSet.size()); + } + + @Test + void toQuizDtoPage() { + Page quizDtoPage = QuizMapper.toQuizDtoPage(QuizGenerator.generateQuizPage()); + assertThat(quizDtoPage).hasOnlyElementsOfTypes(QuizDto.class); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java new file mode 100644 index 00000000..3ba2b17e --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java @@ -0,0 +1,196 @@ +package app.openschool.course.module.quiz.question; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +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.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import app.openschool.common.security.JwtTokenProvider; +import app.openschool.common.security.UserPrincipal; +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.QuizRepository; +import app.openschool.course.module.quiz.question.util.QuestionGenerator; +import app.openschool.course.module.quiz.util.QuizGenerator; +import app.openschool.user.User; +import app.openschool.user.api.UserGenerator; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc +public class QuestionControllerTest { + private static final String UPDATE_QUESTION_REQUEST = + "{\n" + + " \"question\": \"What is Java bytecode\",\n" + + " \"rightAnswersCount\": 3,\n" + + " \"answerOptions\": [\n" + + " {\n" + + " \"answerOption\": \"Java bytecode is the instruction\",\n" + + " \"rightAnswer\": true\n" + + " }\n" + + " ],\n" + + " \"questionType\": \"MULTIPLE_CHOICE\"\n" + + "}"; + + @Autowired private MockMvc mockMvc; + + @Autowired private JwtTokenProvider jwtTokenProvider; + + @MockBean QuestionServiceImpl questionService; + @MockBean QuizRepository quizRepository; + private static final String AUTHORIZATION = "Authorization"; + + @Test + void deleteQuestion_withCorrectCredentials_isNoContent() throws Exception { + when(questionService.deleteQuestion(anyLong())).thenReturn(true); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + mockMvc + .perform( + delete("/api/v1/1/questions/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNoContent()); + } + + @Test + void deleteQuestion_withIncorrectQuestionId_returnNotFound() throws Exception { + when(questionService.deleteQuestion(anyLong())).thenReturn(false); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + + mockMvc + .perform( + delete("/api/v1/1/questions/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNotFound()); + } + + @Test + void deleteQuestion_withIncorrectMentor_isBadRequest() throws Exception { + when(questionService.deleteQuestion(anyLong())).thenThrow(IllegalArgumentException.class); + Quiz quiz = QuizGenerator.generateQuiz(); + quiz.getModule().getCourse().getMentor().setEmail("anotherEmail"); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(quiz)); + + mockMvc + .perform( + delete("/api/v1/1/questions/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isForbidden()); + } + + @Test + void deleteQuestion_withStudent_isForbidden() throws Exception { + mockMvc + .perform( + delete("/api/v1/1/questions/1") + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) + .andExpect(status().isForbidden()); + } + + @Test + void deleteQuestion_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(delete("/api/v1/1/questions/1")).andExpect(status().isUnauthorized()); + } + + @Test + void updateQuestion_withCorrectCredentials_isNoContent() throws Exception { + when(questionService.updateQuestion(anyLong(), any())).thenReturn(true); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + + mockMvc + .perform( + put("/api/v1/1/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNoContent()); + } + + @Test + void updateQuestion_withIncorrectQuestionId_returnNotFound() throws Exception { + when(questionService.updateQuestion(anyLong(), any())).thenReturn(false); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); + + mockMvc + .perform( + put("/api/v1/1/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isNotFound()); + } + + @Test + void updateQuestion_withIncorrectMentor_isBadRequest() throws Exception { + when(questionService.updateQuestion(anyLong(), any())) + .thenThrow(IllegalArgumentException.class); + Quiz quiz = QuizGenerator.generateQuiz(); + quiz.getModule().getCourse().getMentor().setEmail("anotherEmail"); + when(quizRepository.findById(anyLong())).thenReturn(Optional.of(quiz)); + mockMvc + .perform( + put("/api/v1/1/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isForbidden()); + } + + @Test + void updateQuestion_withStudent_isForbidden() throws Exception { + mockMvc + .perform( + put("/api/v1/1/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) + .andExpect(status().isForbidden()); + } + + @Test + void updateQuestion_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc + .perform( + put("/api/v1/1/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST)) + .andExpect(status().isUnauthorized()); + } + + @Test + void findAllByQuizId_withCorrectArg_returnOk() throws Exception { + when(questionService.findAllByQuizId(anyLong(), any())) + .thenReturn(QuestionGenerator.generateQuestionDtoPage()); + mockMvc + .perform( + get("/api/v1/1/questions") + .contentType(APPLICATION_JSON) + .content(UPDATE_QUESTION_REQUEST) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isOk()); + } + + @Test + void findAllByQuizId_withoutJwtToken_isUnauthorized() throws Exception { + mockMvc.perform(get("/api/v1/1/questions")).andExpect(status().isUnauthorized()); + } + + private String generateJwtToken(User user) { + return "Bearer " + jwtTokenProvider.generateJwtToken(new UserPrincipal(user)); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionServiceImplTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionServiceImplTest.java new file mode 100644 index 00000000..2acc7cda --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionServiceImplTest.java @@ -0,0 +1,151 @@ +package app.openschool.course.module.quiz.question; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import app.openschool.course.module.quiz.question.answer.api.dto.AnswerOptionDto; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.util.CreateQuestionDtoGenerator; +import app.openschool.course.module.quiz.question.util.QuestionGenerator; +import app.openschool.user.User; +import app.openschool.user.api.UserGenerator; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; + +@ExtendWith(MockitoExtension.class) +public class QuestionServiceImplTest { + @Mock QuestionRepository questionRepository; + + private QuestionServiceImpl questionService; + + @BeforeEach + void setUp() { + questionService = new QuestionServiceImpl(questionRepository); + } + + @AfterEach + void cleanUp() { + SecurityContextHolder.clearContext(); + } + + @Test + void deleteQuestion_withCorrectQuestionId_returnTrue() { + when(questionRepository.findById(anyLong())) + .thenReturn(Optional.of(QuestionGenerator.generateQuestion())); + + setAuthentication(UserGenerator.generateMentor()); + + Long questionId = 1L; + assertTrue(questionService.deleteQuestion(questionId)); + } + + @Test + void deleteQuestion_withInCorrectQuestionId_returnFalse() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Long questionId = 1L; + assertFalse(questionService.deleteQuestion(questionId)); + } + + @Test + void deleteQuestion_withIncorrectMentor_throwIllegalArgumentException() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.empty()); + + setAuthentication(UserGenerator.generateStudent()); + + Long questionId = 1L; + assertFalse(questionService.deleteQuestion(1L)); + } + + @Test + void updateQuestion_withCorrectQuestionId_returnTrue() { + when(questionRepository.findById(anyLong())) + .thenReturn(Optional.of(QuestionGenerator.generateQuestion())); + + setAuthentication(UserGenerator.generateMentor()); + + Long questionId = 1L; + assertTrue( + questionService.updateQuestion( + questionId, CreateQuestionDtoGenerator.generateCreateQuestionDto())); + } + + @Test + void updateQuestion_withInCorrectQuestionId_returnFalse() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.empty()); + + Long questionId = 1L; + assertFalse(questionService.updateQuestion(questionId, CreateQuestionDto.getInstance())); + } + + @Test + void updateQuestion_withIncorrectMentor_throwIllegalArgumentException() { + when(questionRepository.findById(anyLong())).thenReturn(Optional.empty()); + + setAuthentication(UserGenerator.generateStudent()); + + Long questionId = 1L; + assertFalse(questionService.updateQuestion(questionId, CreateQuestionDto.getInstance())); + } + + @Test + void findAllByModuleId_withCorrectArg_quizDtoPage() { + doReturn(Optional.of(QuestionGenerator.generateQuestionPage())) + .when(questionRepository) + .findAllQuestionsByQuizId(anyLong(), any()); + PageRequest pageRequest = PageRequest.of(0, 1); + Page actualPageQuestionDto = questionService.findAllByQuizId(1L, pageRequest); + + Page questionDtoPage = QuestionGenerator.generateQuestionDtoPage(); + assertEquals( + actualPageQuestionDto.stream().findFirst().orElseThrow().getQuestion(), + questionDtoPage.stream().findFirst().orElseThrow().getQuestion()); + assertEquals( + actualPageQuestionDto.stream().findFirst().orElseThrow().getQuestionType(), + questionDtoPage.stream().findFirst().orElseThrow().getQuestionType()); + assertEquals( + actualPageQuestionDto.stream().findFirst().orElseThrow().getRightAnswersCount(), + questionDtoPage.stream().findFirst().orElseThrow().getRightAnswersCount()); + assertEquals( + actualPageQuestionDto.stream().findFirst().orElseThrow().getAnswerOptions().stream() + .findFirst() + .orElse(new AnswerOptionDto()) + .getAnswerOption(), + questionDtoPage.stream().findFirst().orElseThrow().getAnswerOptions().stream() + .findFirst() + .orElse(new AnswerOptionDto()) + .getAnswerOption()); + } + + @Test + void findAllByModuleId_withInCorrectModuleId_throwIllegalArgumentException() { + when(questionRepository.findAllQuestionsByQuizId(anyLong(), any())) + .thenReturn(Optional.empty()); + PageRequest pageRequest = PageRequest.of(0, 1); + assertThatThrownBy(() -> questionService.findAllByQuizId(1L, pageRequest)) + .isInstanceOf(IllegalArgumentException.class); + } + + private void setAuthentication(User user) { + SecurityContextHolder.setContext( + new SecurityContextImpl( + new UsernamePasswordAuthenticationToken(user.getEmail(), null, null))); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapperTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapperTest.java new file mode 100644 index 00000000..5b1a85fa --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/api/mapper/QuestionMapperTest.java @@ -0,0 +1,40 @@ +package app.openschool.course.module.quiz.question.api.mapper; + +import static org.assertj.core.api.Assertions.assertThat; + +import app.openschool.course.module.quiz.Quiz; +import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.util.CreateQuestionDtoGenerator; +import app.openschool.course.module.quiz.question.util.QuestionGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; + +public class QuestionMapperTest { + @Test + void createQuestionDtoToQuestion() { + Question question = + QuestionMapper.createQuestionDtoToQuestion( + CreateQuestionDtoGenerator.generateCreateQuestionDto(), Quiz.getInstance()); + + assertThat(question) + .hasOnlyFields( + "id", "question", "rightAnswersCount", "questionType", "answerOptions", "quiz"); + } + + @Test + void toQuestionDtoPage() { + + Page questionDtoPage = + QuestionMapper.toQuestionDtoPage(QuestionGenerator.generateQuestionPage()); + + assertThat(questionDtoPage).hasOnlyElementsOfTypes(QuestionDto.class); + } + + @Test + void toQuestionDto() { + QuestionDto questionDto = QuestionMapper.toQuestionDto(QuestionGenerator.generateQuestion()); + assertThat(questionDto) + .hasOnlyFields("id", "question", "rightAnswersCount", "questionType", "answerOptions"); + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/util/CreateQuestionDtoGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/util/CreateQuestionDtoGenerator.java new file mode 100644 index 00000000..75483525 --- /dev/null +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/util/CreateQuestionDtoGenerator.java @@ -0,0 +1,22 @@ +package app.openschool.course.module.quiz.question.util; + +import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; +import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; +import java.util.Set; + +public class CreateQuestionDtoGenerator { + private static final String QUESTION = "question"; + private static final int RIGHT_ANSWER_COUNT = 1; + private static final String MATCHING = "MATCHING"; + + private CreateQuestionDtoGenerator() {} + + public static CreateQuestionDto generateCreateQuestionDto() { + CreateQuestionDto createQuestionDto = CreateQuestionDto.getInstance(); + createQuestionDto.setQuestion(QUESTION); + createQuestionDto.setRightAnswersCount(RIGHT_ANSWER_COUNT); + createQuestionDto.setQuestionType(MATCHING); + createQuestionDto.setAnswerOptions(Set.of(CreateAnswerOptionDto.getInstance())); + return createQuestionDto; + } +} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java index 94a35dd5..14634365 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/util/QuestionGenerator.java @@ -1,8 +1,16 @@ package app.openschool.course.module.quiz.question.util; import app.openschool.course.module.quiz.question.Question; +import app.openschool.course.module.quiz.question.answer.AnswerOption; +import app.openschool.course.module.quiz.question.api.dto.QuestionDto; +import app.openschool.course.module.quiz.question.api.mapper.QuestionMapper; import app.openschool.course.module.quiz.question.type.QuestionType; import app.openschool.course.module.quiz.util.QuizGenerator; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; public class QuestionGenerator { private static final Long ID = 1L; @@ -16,6 +24,28 @@ public static Question generateQuestion() { question.setQuestion(QUESTION); question.setQuestionType(QuestionType.isMultipleChoice()); question.setQuiz(QuizGenerator.generateQuiz()); + question.setAnswerOptions(generateAnswerOption()); return question; } + + public static Page generateQuestionDtoPage() { + List questionDtoList = new ArrayList<>(); + QuestionDto questionDto = QuestionMapper.toQuestionDto(generateQuestion()); + questionDtoList.add(questionDto); + return new PageImpl<>(questionDtoList); + } + + public static Page generateQuestionPage() { + List questionList = new ArrayList<>(); + questionList.add(generateQuestion()); + return new PageImpl<>(questionList); + } + + private static Set generateAnswerOption() { + AnswerOption answerOption = new AnswerOption(); + answerOption.setAnswerOption("answer 1"); + answerOption.setRightAnswer(true); + + return Set.of(answerOption); + } } diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java index 43dcf02d..ab5ad229 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/EnrolledQuizAssessmentRequestDtoGenerator.java @@ -2,7 +2,6 @@ import app.openschool.course.module.quiz.api.dto.EnrolledQuizAssessmentRequestDto; import app.openschool.course.module.quiz.api.dto.QuestionWithChosenAnswerDto; - import java.util.HashSet; import java.util.List; import java.util.Set; diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java index 3e81b699..03ff217a 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizDtoGenerator.java @@ -3,16 +3,16 @@ import app.openschool.course.module.quiz.api.dto.CreateQuizDto; import app.openschool.course.module.quiz.api.dto.ModifyQuizDataRequest; import app.openschool.course.module.quiz.api.dto.QuizDto; -import app.openschool.course.module.quiz.question.Question; import app.openschool.course.module.quiz.question.answer.api.dto.CreateAnswerOptionDto; import app.openschool.course.module.quiz.question.api.dto.CreateQuestionDto; import app.openschool.course.module.quiz.question.api.dto.QuestionDto; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; - import java.util.ArrayList; import java.util.List; import java.util.Set; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + + public class QuizDtoGenerator { private QuizDtoGenerator() {} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java index a0478488..e67f27a6 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/util/QuizGenerator.java @@ -3,16 +3,15 @@ import app.openschool.course.module.api.ModuleGenerator; import app.openschool.course.module.quiz.Quiz; import app.openschool.course.module.quiz.api.dto.QuizDto; +import app.openschool.course.module.quiz.api.mapper.QuizMapper; import app.openschool.course.module.quiz.question.Question; import app.openschool.course.module.quiz.question.answer.AnswerOption; import app.openschool.course.module.quiz.question.type.QuestionType; -import app.openschool.course.module.quiz.api.mapper.QuizMapper; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; - import java.util.ArrayList; import java.util.List; import java.util.Set; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; public class QuizGenerator { private static final Long ID = 1L; diff --git a/os-server/src/test/java/app/openschool/user/api/UserGenerator.java b/os-server/src/test/java/app/openschool/user/api/UserGenerator.java index 95a6bab3..5cb6da78 100644 --- a/os-server/src/test/java/app/openschool/user/api/UserGenerator.java +++ b/os-server/src/test/java/app/openschool/user/api/UserGenerator.java @@ -34,6 +34,7 @@ public static User generateUserWithSavedMentors() { user.getMentors().add(generateUser()); return user; } + public static User generateMentor() { String email = "mentor"; @@ -47,6 +48,7 @@ public static User generateMentor() { mentor.setRole(role); return mentor; } + public static User generateStudent() { String email = "student"; String password = "pass"; From d899c299c547927a9123685997a991c4941607b5 Mon Sep 17 00:00:00 2001 From: DELL Date: Sat, 4 Mar 2023 21:02:39 +0400 Subject: [PATCH 4/8] =?UTF-8?q?Created=20tests=E2=80=A4The=20changes=20men?= =?UTF-8?q?tioned=20in=20the=20comments=20have=20been=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../course/module/quiz/QuizController.java | 3 + .../module/quiz/QuizControllerTest.java | 81 +++++++++++-------- .../quiz/question/QuestionControllerTest.java | 14 ++-- 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java index 8ccd3858..b610c5fb 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java @@ -149,6 +149,7 @@ public ResponseEntity deleteQuiz( @Parameter(description = "Id of the module to which this quiz belongs") @PathVariable Long moduleId, Principal principal) { + System.out.println(principal.getName()); checkPermission(moduleId, principal); if (quizService.deleteQuiz(quizId)) { return ResponseEntity.noContent().build(); @@ -247,8 +248,10 @@ public ResponseEntity completeEnrolledQuiz( } private void checkPermission(Long moduleId, Principal principal) { + System.out.println("started"); Module module = moduleRepository.findById(moduleId).orElseThrow(IllegalArgumentException::new); if (!module.getCourse().getMentor().getEmail().equals(principal.getName())) { + System.out.println(principal.getName()); throw new PermissionDeniedException("permission.denied"); } } diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java index f06bb5ce..b706f001 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java @@ -3,7 +3,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; -import static org.springframework.http.MediaType.APPLICATION_JSON; 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.patch; @@ -34,38 +33,36 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; - - - - @ExtendWith(SpringExtension.class) @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc public class QuizControllerTest { + + private static final String REQUEST_BODY = + "{\n" + + " \"maxGrade\": \"10\",\n" + + " \"passingScore\": \"7\",\n" + + " \"questions\": [\n" + + " {\n" + + " \"question\": \"What is Java bytecode\",\n" + + " \"answerOptions\": [\n" + + " {\n" + + " \"answerOption\": \"Java bytecode is the instruction set for the JVM\",\n" + + " \"rightAnswer\": \"true\"\n" + + " }\n" + + " ],\n" + + " \"questionType\": \"MULTIPLE_CHOICE\"\n" + + " }\n" + + " ]\n" + + "}"; + @Autowired private MockMvc mockMvc; @Autowired private JwtTokenProvider jwtTokenProvider; @MockBean private QuizServiceImpl quizService; - @MockBean ModuleRepository moduleRepository; - private static final String REQUEST_BODY = - "{\n" - + " \"maxGrade\": \"10\",\n" - + " \"passingScore\": \"7\",\n" - + " \"questions\": [\n" - + " {\n" - + " \"question\": \"What is Java bytecode\",\n" - + " \"answerOptions\": [\n" - + " {\n" - + " \"answerOption\": \"Java bytecode is the instruction set for the JVM\",\n" - + " \"rightAnswer\": \"true\"\n" - + " }\n" - + " ],\n" - + " \"questionType\": \"MULTIPLE_CHOICE\"\n" - + " }\n" - + " ]\n" - + "}"; + @MockBean private ModuleRepository moduleRepository; private static final String AUTHORIZATION = "Authorization"; @@ -78,7 +75,7 @@ void createQuiz_withCorrectCredentials_isCreated() throws Exception { mockMvc .perform( post("/api/v1/1/quizzes") - .contentType(APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isCreated()) @@ -100,13 +97,27 @@ void createQuiz_withIncorrectModuleId_returnNotFound() throws Exception { .andExpect(status().isNotFound()); } + @Test + void createQuiz_withIncorrectMentor_isForbidden() throws Exception { + when(quizService.createQuiz(anyLong(), any())).thenThrow(IllegalArgumentException.class); + Module module = ModuleGenerator.generateModule(); + module.getCourse().getMentor().setEmail("anotherEmail"); + when(moduleRepository.findById(anyLong())).thenReturn(Optional.of(module)); + mockMvc + .perform( + post("/api/v1/1/quizzes") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY) + .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) + .andExpect(status().isForbidden()); + } @Test void createQuiz_withStudent_isForbidden() throws Exception { mockMvc .perform( post("/api/v1/1/quizzes/1") - .contentType(APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) .andExpect(status().isForbidden()); @@ -116,7 +127,8 @@ void createQuiz_withStudent_isForbidden() throws Exception { void createQuiz_withoutJwtToken_isUnauthorized() throws Exception { mockMvc - .perform(post("/api/v1/1/quizzes").contentType(APPLICATION_JSON).content(REQUEST_BODY)) + .perform( + post("/api/v1/1/quizzes").contentType(MediaType.APPLICATION_JSON).content(REQUEST_BODY)) .andExpect(status().isUnauthorized()); } @@ -147,10 +159,9 @@ void deleteQuiz_withIncorrectQuizId_returnNotFound() throws Exception { @Test void deleteQuiz_withIncorrectMentor_isForbidden() throws Exception { - when(quizService.deleteQuiz(anyLong())).thenThrow(IllegalArgumentException.class); - - when(moduleRepository.findById(anyLong())) - .thenReturn(Optional.of(ModuleGenerator.generateModuleWithAnotherUser())); + Module module = ModuleGenerator.generateModule(); + module.getCourse().getMentor().setEmail("anotherEmail@gmail.com"); + when(moduleRepository.findById(anyLong())).thenReturn(Optional.of(module)); mockMvc .perform( @@ -181,7 +192,7 @@ void updateQuiz_withCorrectArg_returnOk() throws Exception { mockMvc .perform( patch("/api/v1/1/quizzes/1") - .contentType(APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isOk()); @@ -196,7 +207,7 @@ void updateQuiz_withIncorrectMentor_isForbidden() throws Exception { mockMvc .perform( patch("/api/v1/1/quizzes/1") - .contentType(APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isForbidden()); @@ -215,7 +226,7 @@ void findById_withCorrectArg_returnOk() throws Exception { mockMvc .perform( get("/api/v1/1/quizzes/1") - .contentType(APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isOk()); @@ -233,7 +244,7 @@ void findAllByModuleId_withCorrectArg_returnOk() throws Exception { mockMvc .perform( get("/api/v1/1/quizzes") - .contentType(APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isOk()); @@ -254,7 +265,7 @@ void completeEnrolledQuiz_withCorrectArg_returnOk() throws Exception { mockMvc .perform( get("/api/v1/1/quizzes/enrolledQuizzes/1") - .contentType(APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isOk()); diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java index 3ba2b17e..5260a422 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java @@ -3,7 +3,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; -import static org.springframework.http.MediaType.APPLICATION_JSON; 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.put; @@ -52,8 +51,8 @@ public class QuestionControllerTest { @Autowired private JwtTokenProvider jwtTokenProvider; - @MockBean QuestionServiceImpl questionService; - @MockBean QuizRepository quizRepository; + @MockBean private QuestionServiceImpl questionService; + @MockBean private QuizRepository quizRepository; private static final String AUTHORIZATION = "Authorization"; @Test @@ -80,8 +79,7 @@ void deleteQuestion_withIncorrectQuestionId_returnNotFound() throws Exception { } @Test - void deleteQuestion_withIncorrectMentor_isBadRequest() throws Exception { - when(questionService.deleteQuestion(anyLong())).thenThrow(IllegalArgumentException.class); + void deleteQuestion_withIncorrectMentor_isForbidden() throws Exception { Quiz quiz = QuizGenerator.generateQuiz(); quiz.getModule().getCourse().getMentor().setEmail("anotherEmail"); when(quizRepository.findById(anyLong())).thenReturn(Optional.of(quiz)); @@ -136,9 +134,7 @@ void updateQuestion_withIncorrectQuestionId_returnNotFound() throws Exception { } @Test - void updateQuestion_withIncorrectMentor_isBadRequest() throws Exception { - when(questionService.updateQuestion(anyLong(), any())) - .thenThrow(IllegalArgumentException.class); + void updateQuestion_withIncorrectMentor_isForbidden() throws Exception { Quiz quiz = QuizGenerator.generateQuiz(); quiz.getModule().getCourse().getMentor().setEmail("anotherEmail"); when(quizRepository.findById(anyLong())).thenReturn(Optional.of(quiz)); @@ -179,7 +175,7 @@ void findAllByQuizId_withCorrectArg_returnOk() throws Exception { mockMvc .perform( get("/api/v1/1/questions") - .contentType(APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_QUESTION_REQUEST) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isOk()); From a8ff76096e62e1bb9d11bad8b55829720b80f7af Mon Sep 17 00:00:00 2001 From: DELL Date: Thu, 9 Mar 2023 10:14:39 +0400 Subject: [PATCH 5/8] =?UTF-8?q?Created=20tests=E2=80=A4The=20changes=20men?= =?UTF-8?q?tioned=20in=20the=20comments=20have=20been=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/openschool/course/module/Module.java | 4 +-- .../module/quiz/QuizControllerTest.java | 34 ++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/os-server/src/main/java/app/openschool/course/module/Module.java b/os-server/src/main/java/app/openschool/course/module/Module.java index 0fbb9958..0d39b69c 100644 --- a/os-server/src/main/java/app/openschool/course/module/Module.java +++ b/os-server/src/main/java/app/openschool/course/module/Module.java @@ -38,8 +38,8 @@ public class Module { @OneToMany(cascade = CascadeType.ALL, mappedBy = "module") private Set moduleItems = new HashSet<>(); - @OneToMany(cascade = CascadeType.ALL, mappedBy = "module") - private Set quizzes = new HashSet<>(); + @OneToMany(mappedBy = "module", cascade = CascadeType.ALL) + private Set quizzes; public Module() {} diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java index b706f001..0b27a2ec 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java @@ -22,6 +22,7 @@ import app.openschool.user.api.UserGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Optional; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -37,25 +38,26 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc +@Disabled public class QuizControllerTest { private static final String REQUEST_BODY = - "{\n" - + " \"maxGrade\": \"10\",\n" - + " \"passingScore\": \"7\",\n" - + " \"questions\": [\n" - + " {\n" - + " \"question\": \"What is Java bytecode\",\n" - + " \"answerOptions\": [\n" - + " {\n" - + " \"answerOption\": \"Java bytecode is the instruction set for the JVM\",\n" - + " \"rightAnswer\": \"true\"\n" - + " }\n" - + " ],\n" - + " \"questionType\": \"MULTIPLE_CHOICE\"\n" - + " }\n" - + " ]\n" - + "}"; + "{\n" + + " \"maxGrade\": \"10\",\n" + + " \"passingScore\": \"7\",\n" + + " \"questions\": [\n" + + " {\n" + + " \"question\": \"What is Java bytecode\",\n" + + " \"answerOptions\": [\n" + + " {\n" + + " \"answerOption\": \"Java bytecode is the instruction set for the JVM\",\n" + + " \"rightAnswer\": \"true\"\n" + + " }\n" + + " ],\n" + + " \"questionType\": \"MULTIPLE_CHOICE\"\n" + + " }\n" + + " ]\n" + + "}"; @Autowired private MockMvc mockMvc; From 927f4decf4718abc6a909cf67dfb03663739fc54 Mon Sep 17 00:00:00 2001 From: DELL Date: Thu, 9 Mar 2023 12:13:30 +0400 Subject: [PATCH 6/8] =?UTF-8?q?Created=20tests=E2=80=A4The=20changes=20men?= =?UTF-8?q?tioned=20in=20the=20comments=20have=20been=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/security/SecurityConfiguration.java | 10 +++------- .../openschool/course/module/quiz/QuizController.java | 2 +- .../module/quiz/question/QuestionController.java | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java b/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java index 65a312ed..f010d00c 100644 --- a/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java +++ b/os-server/src/main/java/app/openschool/common/security/SecurityConfiguration.java @@ -18,11 +18,7 @@ @Configuration @EnableWebSecurity -@EnableGlobalMethodSecurity( - securedEnabled = true, - jsr250Enabled = true, - prePostEnabled = true -) +@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private static final String[] PRIVATE_URLS = { @@ -31,8 +27,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { "/api/v1/categories/**", "/api/v1/mentors/**", "/api/v1/courses/**", - "/api/v1/*/quizzes/**", - "/api/v1/*/questions/**" + "/api/v1/module/*/quizzes/**", + "/api/v1/quiz/*/questions/**" }; private final JwtAuthenticationFilter jwtAuthenticationFilter; diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java b/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java index b610c5fb..017e5cac 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/QuizController.java @@ -35,7 +35,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/v1/{moduleId}/quizzes") +@RequestMapping("/api/v1/module/{moduleId}/quizzes") public class QuizController { private final QuizService quizService; diff --git a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java index 046f37e7..d08db786 100644 --- a/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java +++ b/os-server/src/main/java/app/openschool/course/module/quiz/question/QuestionController.java @@ -26,7 +26,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/v1/{quizId}/questions") +@RequestMapping("/api/v1/quiz/{quizId}/questions") public class QuestionController { private final QuestionService questionService; From a471d1c2d63939d74276603f273540ae5d018791 Mon Sep 17 00:00:00 2001 From: DELL Date: Thu, 9 Mar 2023 12:33:22 +0400 Subject: [PATCH 7/8] =?UTF-8?q?Created=20tests=E2=80=A4The=20changes=20men?= =?UTF-8?q?tioned=20in=20the=20comments=20have=20been=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/quiz/QuizControllerTest.java | 40 ++++++++++--------- .../quiz/question/QuestionControllerTest.java | 24 +++++------ 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java index 0b27a2ec..59569490 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/QuizControllerTest.java @@ -76,7 +76,7 @@ void createQuiz_withCorrectCredentials_isCreated() throws Exception { .thenReturn(Optional.of(ModuleGenerator.generateModule())); mockMvc .perform( - post("/api/v1/1/quizzes") + post("/api/v1/module/1/quizzes") .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -92,7 +92,7 @@ void createQuiz_withIncorrectModuleId_returnNotFound() throws Exception { .thenReturn(Optional.of(ModuleGenerator.generateModule())); mockMvc .perform( - post("/api/v1/1/quizzes") + post("/api/v1/module/1/quizzes") .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -107,7 +107,7 @@ void createQuiz_withIncorrectMentor_isForbidden() throws Exception { when(moduleRepository.findById(anyLong())).thenReturn(Optional.of(module)); mockMvc .perform( - post("/api/v1/1/quizzes") + post("/api/v1/module/1/quizzes") .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -118,7 +118,7 @@ void createQuiz_withIncorrectMentor_isForbidden() throws Exception { void createQuiz_withStudent_isForbidden() throws Exception { mockMvc .perform( - post("/api/v1/1/quizzes/1") + post("/api/v1/module/1/quizzes/1") .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) @@ -130,7 +130,9 @@ void createQuiz_withoutJwtToken_isUnauthorized() throws Exception { mockMvc .perform( - post("/api/v1/1/quizzes").contentType(MediaType.APPLICATION_JSON).content(REQUEST_BODY)) + post("/api/v1/module/1/quizzes") + .contentType(MediaType.APPLICATION_JSON) + .content(REQUEST_BODY)) .andExpect(status().isUnauthorized()); } @@ -141,7 +143,7 @@ void deleteQuiz_withCorrectCredentials_isNoContent() throws Exception { .thenReturn(Optional.of(ModuleGenerator.generateModule())); mockMvc .perform( - delete("/api/v1/1/quizzes/1") + delete("/api/v1/module/1/quizzes/1") .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isNoContent()); } @@ -154,7 +156,7 @@ void deleteQuiz_withIncorrectQuizId_returnNotFound() throws Exception { mockMvc .perform( - delete("/api/v1/1/quizzes/1") + delete("/api/v1/module/1/quizzes/1") .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isNotFound()); } @@ -167,7 +169,7 @@ void deleteQuiz_withIncorrectMentor_isForbidden() throws Exception { mockMvc .perform( - delete("/api/v1/1/quizzes/1") + delete("/api/v1/module/1/quizzes/1") .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isForbidden()); } @@ -176,14 +178,14 @@ void deleteQuiz_withIncorrectMentor_isForbidden() throws Exception { void deleteQuiz_withStudent_isForbidden() throws Exception { mockMvc .perform( - delete("/api/v1/1/quizzes/1") + delete("/api/v1/module/1/quizzes/1") .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) .andExpect(status().isForbidden()); } @Test void deleteQuiz_withoutJwtToken_isUnauthorized() throws Exception { - mockMvc.perform(delete("/api/v1/1/quizzes/1")).andExpect(status().isUnauthorized()); + mockMvc.perform(delete("/api/v1/module/1/quizzes/1")).andExpect(status().isUnauthorized()); } @Test @@ -193,7 +195,7 @@ void updateQuiz_withCorrectArg_returnOk() throws Exception { .thenReturn(Optional.of(ModuleGenerator.generateModule())); mockMvc .perform( - patch("/api/v1/1/quizzes/1") + patch("/api/v1/module/1/quizzes/1") .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -208,7 +210,7 @@ void updateQuiz_withIncorrectMentor_isForbidden() throws Exception { .thenReturn(Optional.of(ModuleGenerator.generateModuleWithAnotherUser())); mockMvc .perform( - patch("/api/v1/1/quizzes/1") + patch("/api/v1/module/1/quizzes/1") .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -217,7 +219,7 @@ void updateQuiz_withIncorrectMentor_isForbidden() throws Exception { @Test void updateQuiz_withoutJwtToken_isUnauthorized() throws Exception { - mockMvc.perform(patch("/api/v1/1/quizzes/1")).andExpect(status().isUnauthorized()); + mockMvc.perform(patch("/api/v1/module/1/quizzes/1")).andExpect(status().isUnauthorized()); } @Test @@ -227,7 +229,7 @@ void findById_withCorrectArg_returnOk() throws Exception { mockMvc .perform( - get("/api/v1/1/quizzes/1") + get("/api/v1/module/1/quizzes/1") .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -236,7 +238,7 @@ void findById_withCorrectArg_returnOk() throws Exception { @Test void findById_withoutJwtToken_isUnauthorized() throws Exception { - mockMvc.perform(get("/api/v1/1/quizzes/1")).andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/v1/module/1/quizzes/1")).andExpect(status().isUnauthorized()); } @Test @@ -245,7 +247,7 @@ void findAllByModuleId_withCorrectArg_returnOk() throws Exception { .thenReturn(QuizGenerator.generateQuizDtoPage()); mockMvc .perform( - get("/api/v1/1/quizzes") + get("/api/v1/module/1/quizzes") .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -254,7 +256,7 @@ void findAllByModuleId_withCorrectArg_returnOk() throws Exception { @Test void findByModuleId_withoutJwtToken_isUnauthorized() throws Exception { - mockMvc.perform(get("/api/v1/1/quizzes")).andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/v1/module/1/quizzes")).andExpect(status().isUnauthorized()); } @Test @@ -266,7 +268,7 @@ void completeEnrolledQuiz_withCorrectArg_returnOk() throws Exception { .generateEnrolledQuizAssessmentResponseDto("FAILED"))); mockMvc .perform( - get("/api/v1/1/quizzes/enrolledQuizzes/1") + get("/api/v1/module/1/quizzes/enrolledQuizzes/1") .contentType(MediaType.APPLICATION_JSON) .content(REQUEST_BODY) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -276,7 +278,7 @@ void completeEnrolledQuiz_withCorrectArg_returnOk() throws Exception { @Test void completeEnrolledQuiz_withoutJwtToken_isUnauthorized() throws Exception { mockMvc - .perform(get("/api/v1/1/quizzes/enrolledQuizzes/1")) + .perform(get("/api/v1/module/1/quizzes/enrolledQuizzes/1")) .andExpect(status().isUnauthorized()); } diff --git a/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java index 5260a422..87a28362 100644 --- a/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java +++ b/os-server/src/test/java/app/openschool/course/module/quiz/question/QuestionControllerTest.java @@ -61,7 +61,7 @@ void deleteQuestion_withCorrectCredentials_isNoContent() throws Exception { when(quizRepository.findById(anyLong())).thenReturn(Optional.of(QuizGenerator.generateQuiz())); mockMvc .perform( - delete("/api/v1/1/questions/1") + delete("/api/v1/quiz/1/questions/1") .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isNoContent()); } @@ -73,7 +73,7 @@ void deleteQuestion_withIncorrectQuestionId_returnNotFound() throws Exception { mockMvc .perform( - delete("/api/v1/1/questions/1") + delete("/api/v1/quiz/1/questions/1") .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isNotFound()); } @@ -86,7 +86,7 @@ void deleteQuestion_withIncorrectMentor_isForbidden() throws Exception { mockMvc .perform( - delete("/api/v1/1/questions/1") + delete("/api/v1/quiz/1/questions/1") .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) .andExpect(status().isForbidden()); } @@ -95,14 +95,14 @@ void deleteQuestion_withIncorrectMentor_isForbidden() throws Exception { void deleteQuestion_withStudent_isForbidden() throws Exception { mockMvc .perform( - delete("/api/v1/1/questions/1") + delete("/api/v1/quiz/1/questions/1") .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) .andExpect(status().isForbidden()); } @Test void deleteQuestion_withoutJwtToken_isUnauthorized() throws Exception { - mockMvc.perform(delete("/api/v1/1/questions/1")).andExpect(status().isUnauthorized()); + mockMvc.perform(delete("/api/v1/quiz/1/questions/1")).andExpect(status().isUnauthorized()); } @Test @@ -112,7 +112,7 @@ void updateQuestion_withCorrectCredentials_isNoContent() throws Exception { mockMvc .perform( - put("/api/v1/1/questions/1") + put("/api/v1/quiz/1/questions/1") .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_QUESTION_REQUEST) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -126,7 +126,7 @@ void updateQuestion_withIncorrectQuestionId_returnNotFound() throws Exception { mockMvc .perform( - put("/api/v1/1/questions/1") + put("/api/v1/quiz/1/questions/1") .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_QUESTION_REQUEST) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -140,7 +140,7 @@ void updateQuestion_withIncorrectMentor_isForbidden() throws Exception { when(quizRepository.findById(anyLong())).thenReturn(Optional.of(quiz)); mockMvc .perform( - put("/api/v1/1/questions/1") + put("/api/v1/quiz/1/questions/1") .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_QUESTION_REQUEST) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -151,7 +151,7 @@ void updateQuestion_withIncorrectMentor_isForbidden() throws Exception { void updateQuestion_withStudent_isForbidden() throws Exception { mockMvc .perform( - put("/api/v1/1/questions/1") + put("/api/v1/quiz/1/questions/1") .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_QUESTION_REQUEST) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateStudent()))) @@ -162,7 +162,7 @@ void updateQuestion_withStudent_isForbidden() throws Exception { void updateQuestion_withoutJwtToken_isUnauthorized() throws Exception { mockMvc .perform( - put("/api/v1/1/questions/1") + put("/api/v1/quiz/1/questions/1") .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_QUESTION_REQUEST)) .andExpect(status().isUnauthorized()); @@ -174,7 +174,7 @@ void findAllByQuizId_withCorrectArg_returnOk() throws Exception { .thenReturn(QuestionGenerator.generateQuestionDtoPage()); mockMvc .perform( - get("/api/v1/1/questions") + get("/api/v1/quiz/1/questions") .contentType(MediaType.APPLICATION_JSON) .content(UPDATE_QUESTION_REQUEST) .header(AUTHORIZATION, generateJwtToken(UserGenerator.generateMentor()))) @@ -183,7 +183,7 @@ void findAllByQuizId_withCorrectArg_returnOk() throws Exception { @Test void findAllByQuizId_withoutJwtToken_isUnauthorized() throws Exception { - mockMvc.perform(get("/api/v1/1/questions")).andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/v1/quiz/1/questions")).andExpect(status().isUnauthorized()); } private String generateJwtToken(User user) { From c4468810489d9ec11fc1ea98dd9ba3f259c06edc Mon Sep 17 00:00:00 2001 From: DELL Date: Wed, 15 Mar 2023 12:13:12 +0400 Subject: [PATCH 8/8] Merge conflict correction. --- ...ql => V1_0_19__create_quiz_question_answer_option_tablies.sql} | 0 ...ql => V1_0_19__create_quiz_question_answer_option_tablies.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename os-server/src/main/resources/db/migration/h2/{V1_0_18__create_quiz_question_answer_option_tablies.sql => V1_0_19__create_quiz_question_answer_option_tablies.sql} (100%) rename os-server/src/main/resources/db/migration/mysql/{V1_0_18__create_quiz_question_answer_option_tablies.sql => V1_0_19__create_quiz_question_answer_option_tablies.sql} (100%) diff --git a/os-server/src/main/resources/db/migration/h2/V1_0_18__create_quiz_question_answer_option_tablies.sql b/os-server/src/main/resources/db/migration/h2/V1_0_19__create_quiz_question_answer_option_tablies.sql similarity index 100% rename from os-server/src/main/resources/db/migration/h2/V1_0_18__create_quiz_question_answer_option_tablies.sql rename to os-server/src/main/resources/db/migration/h2/V1_0_19__create_quiz_question_answer_option_tablies.sql diff --git a/os-server/src/main/resources/db/migration/mysql/V1_0_18__create_quiz_question_answer_option_tablies.sql b/os-server/src/main/resources/db/migration/mysql/V1_0_19__create_quiz_question_answer_option_tablies.sql similarity index 100% rename from os-server/src/main/resources/db/migration/mysql/V1_0_18__create_quiz_question_answer_option_tablies.sql rename to os-server/src/main/resources/db/migration/mysql/V1_0_19__create_quiz_question_answer_option_tablies.sql