From d6f1e54ff75ba424a6e9f2ce26e886cbc08e5c8f Mon Sep 17 00:00:00 2001 From: Osiris Aguilazochom Date: Thu, 11 Sep 2025 13:00:13 -0600 Subject: [PATCH 1/2] =?UTF-8?q?Se=20complet=C3=B3=20el=20feature/quizz=20d?= =?UTF-8?q?e=20Orli=20ya=20con=20pruebas=20unitarias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/controller/QuizController.java | 8 +- .../learning/platform/dto/QuizResponse.java | 8 +- .../platform/dto/QuizSubmissionRequest.java | 30 +- .../platform/entity/AnswerOption.java | 26 ++ .../platform/entity/AnswerSubmission.java | 40 +++ .../learning/platform/entity/Enrollment.java | 4 + .../learning/platform/entity/Question.java | 48 +++ .../java/learning/platform/entity/Quiz.java | 200 ++++++++++++ .../platform/entity/QuizSubmission.java | 54 ++++ .../platform/exception/NotFoundException.java | 12 + .../learning/platform/mapper/QuizMapper.java | 97 ++++++ .../repository/AnswerOptionRepository.java | 17 + .../AnswerSubmissionRepository.java | 26 ++ .../repository/QuestionRepository.java | 14 + .../platform/repository/QuizRepository.java | 18 ++ .../repository/QuizSubmissionRepository.java | 14 + .../platform/service/QuizService.java | 53 +++- .../service/impl/QuizServiceImpl.java | 290 ++++++++++++++---- src/main/resources/application-dev.properties | 2 +- .../controller/QuizControllerTest.java | 151 +++++---- .../service/impl/QuizServiceImplTest.java | 257 ++++++++++++++++ 21 files changed, 1238 insertions(+), 131 deletions(-) create mode 100644 src/main/java/learning/platform/entity/AnswerOption.java create mode 100644 src/main/java/learning/platform/entity/AnswerSubmission.java create mode 100644 src/main/java/learning/platform/entity/Question.java create mode 100644 src/main/java/learning/platform/entity/Quiz.java create mode 100644 src/main/java/learning/platform/entity/QuizSubmission.java create mode 100644 src/main/java/learning/platform/exception/NotFoundException.java create mode 100644 src/main/java/learning/platform/mapper/QuizMapper.java create mode 100644 src/main/java/learning/platform/repository/AnswerOptionRepository.java create mode 100644 src/main/java/learning/platform/repository/AnswerSubmissionRepository.java create mode 100644 src/main/java/learning/platform/repository/QuestionRepository.java create mode 100644 src/main/java/learning/platform/repository/QuizRepository.java create mode 100644 src/main/java/learning/platform/repository/QuizSubmissionRepository.java create mode 100644 src/test/java/learning/platform/service/impl/QuizServiceImplTest.java diff --git a/src/main/java/learning/platform/controller/QuizController.java b/src/main/java/learning/platform/controller/QuizController.java index f1b5379..09c80f1 100644 --- a/src/main/java/learning/platform/controller/QuizController.java +++ b/src/main/java/learning/platform/controller/QuizController.java @@ -11,9 +11,12 @@ import learning.platform.dto.QuizResponse; import learning.platform.dto.QuizSubmissionRequest; import learning.platform.dto.QuizSubmissionResponse; +import learning.platform.entity.User; import learning.platform.service.QuizService; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -92,8 +95,9 @@ public ResponseEntity getQuizByLesson( @PostMapping("/{quizId}/submit") public ResponseEntity submitQuiz( @Parameter(description = "ID del quiz a resolver") @PathVariable Long quizId, - @Valid @RequestBody QuizSubmissionRequest request) { - QuizSubmissionResponse response = quizService.submitQuiz(quizId, request); + @Valid @RequestBody QuizSubmissionRequest request, + @AuthenticationPrincipal User student) { + QuizSubmissionResponse response = quizService.submitQuiz(quizId, request, student); return ResponseEntity.ok(response); } diff --git a/src/main/java/learning/platform/dto/QuizResponse.java b/src/main/java/learning/platform/dto/QuizResponse.java index f88e20e..320724c 100644 --- a/src/main/java/learning/platform/dto/QuizResponse.java +++ b/src/main/java/learning/platform/dto/QuizResponse.java @@ -25,6 +25,9 @@ public class QuizResponse { @Schema(description = "ID del curso asociado", example = "10") private Long courseId; + @Schema(description = "ID de la lección asociada", example = "20") + private Long lessonId; + @Schema(description = "Preguntas del quiz") private List questions; @@ -41,6 +44,9 @@ public class QuizResponse { public Long getCourseId() { return courseId; } public void setCourseId(Long courseId) { this.courseId = courseId; } + public Long getLessonId() { return lessonId; } + public void setLessonId(Long lessonId) { this.lessonId = lessonId; } + public List getQuestions() { return questions; } public void setQuestions(List questions) { this.questions = questions; } -} \ No newline at end of file +} diff --git a/src/main/java/learning/platform/dto/QuizSubmissionRequest.java b/src/main/java/learning/platform/dto/QuizSubmissionRequest.java index 50a3d5f..1251236 100644 --- a/src/main/java/learning/platform/dto/QuizSubmissionRequest.java +++ b/src/main/java/learning/platform/dto/QuizSubmissionRequest.java @@ -1,37 +1,35 @@ package learning.platform.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; + import java.util.List; +@Schema( + name = "Solicitud de Envío de Quiz", + description = "DTO para enviar las respuestas del quizz." +) public class QuizSubmissionRequest { - @NotNull(message = "El enrollmentId es obligatorio") - private Long enrollmentId; - @NotEmpty(message = "Debe enviar al menos una respuesta") - private List answers; + @NotNull(message = "La lista de respuestas no puede ser nula") + @Schema(description = "Lista de respuestas del estudiante") + private List answers; public QuizSubmissionRequest() {} - public QuizSubmissionRequest(Long enrollmentId, List answers) { - this.enrollmentId = enrollmentId; + public QuizSubmissionRequest(List answers) { this.answers = answers; } - public Long getEnrollmentId() { - return enrollmentId; - } - - public void setEnrollmentId(Long enrollmentId) { - this.enrollmentId = enrollmentId; - } + // --- Getters y Setters --- - public List getAnswers() { + public List getAnswers() { return answers; } - public void setAnswers(List answers) { + public void setAnswers(List answers) { this.answers = answers; } -} +} \ No newline at end of file diff --git a/src/main/java/learning/platform/entity/AnswerOption.java b/src/main/java/learning/platform/entity/AnswerOption.java new file mode 100644 index 0000000..a0b80b2 --- /dev/null +++ b/src/main/java/learning/platform/entity/AnswerOption.java @@ -0,0 +1,26 @@ +package learning.platform.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "answer_options") +public class AnswerOption { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "question_id", nullable = false) + private Question question; + + @Column(nullable = false, length = 1000) + private String text; + + // Getters y Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Question getQuestion() { return question; } + public void setQuestion(Question question) { this.question = question; } + public String getText() { return text; } + public void setText(String text) { this.text = text; } +} \ No newline at end of file diff --git a/src/main/java/learning/platform/entity/AnswerSubmission.java b/src/main/java/learning/platform/entity/AnswerSubmission.java new file mode 100644 index 0000000..a9e249a --- /dev/null +++ b/src/main/java/learning/platform/entity/AnswerSubmission.java @@ -0,0 +1,40 @@ +package learning.platform.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "answer_submissions") +public class AnswerSubmission { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "quiz_submission_id", nullable = false) + private QuizSubmission quizSubmission; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "question_id", nullable = false) + private Question question; + + // For multiple-choice questions + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "selected_option_id") + private AnswerOption selectedOption; + + // For open-ended questions (not yet implemented in DTOs) + @Column(length = 2000) + private String answerText; + + // Getters y Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public QuizSubmission getQuizSubmission() { return quizSubmission; } + public void setQuizSubmission(QuizSubmission quizSubmission) { this.quizSubmission = quizSubmission; } + public Question getQuestion() { return question; } + public void setQuestion(Question question) { this.question = question; } + public AnswerOption getSelectedOption() { return selectedOption; } + public void setSelectedOption(AnswerOption selectedOption) { this.selectedOption = selectedOption; } + public String getAnswerText() { return answerText; } + public void setAnswerText(String answerText) { this.answerText = answerText; } +} \ No newline at end of file diff --git a/src/main/java/learning/platform/entity/Enrollment.java b/src/main/java/learning/platform/entity/Enrollment.java index cd08c0d..5d97c34 100644 --- a/src/main/java/learning/platform/entity/Enrollment.java +++ b/src/main/java/learning/platform/entity/Enrollment.java @@ -42,6 +42,10 @@ public Long getId() { return id; } + public void setId(Long id) { + this.id = id; + } + public User getStudent() { return student; } diff --git a/src/main/java/learning/platform/entity/Question.java b/src/main/java/learning/platform/entity/Question.java new file mode 100644 index 0000000..96c3bd1 --- /dev/null +++ b/src/main/java/learning/platform/entity/Question.java @@ -0,0 +1,48 @@ +package learning.platform.entity; + +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "questions") +public class Question { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "quiz_id", nullable = false) + private Quiz quiz; + + @Column(nullable = false, length = 2000) + private String text; + + // A question has many options. + @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) + private List options; + + @Column(name = "correct_option_index", nullable = false) + private Integer correctOptionIndex; + + @Column + private Integer points; + + @Column(name = "time_limit_seconds") + private Integer timeLimitSeconds; + + // Getters y Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Quiz getQuiz() { return quiz; } + public void setQuiz(Quiz quiz) { this.quiz = quiz; } + public String getText() { return text; } + public void setText(String text) { this.text = text; } + public List getOptions() { return options; } + public void setOptions(List options) { this.options = options; } + public Integer getCorrectOptionIndex() { return correctOptionIndex; } + public void setCorrectOptionIndex(Integer correctOptionIndex) { this.correctOptionIndex = correctOptionIndex; } + public Integer getPoints() { return points; } + public void setPoints(Integer points) { this.points = points; } + public Integer getTimeLimitSeconds() { return timeLimitSeconds; } + public void setTimeLimitSeconds(Integer timeLimitSeconds) { this.timeLimitSeconds = timeLimitSeconds; } +} \ No newline at end of file diff --git a/src/main/java/learning/platform/entity/Quiz.java b/src/main/java/learning/platform/entity/Quiz.java new file mode 100644 index 0000000..ed8688a --- /dev/null +++ b/src/main/java/learning/platform/entity/Quiz.java @@ -0,0 +1,200 @@ +package learning.platform.entity; + +import jakarta.persistence.*; +import java.util.List; +import java.time.LocalDateTime; + +/** + * Representa un cuestionario o examen dentro de la plataforma de aprendizaje. + * Un Quiz puede estar asociado a un curso o a una lección, pero no a ambos. + * Contiene una lista de preguntas. + */ +@Entity +@Table(name = "quizzes") +public class Quiz { + /** + * Identificador único del quiz. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * Título del quiz. No puede ser nulo y tiene un tamaño máximo de 255 caracteres. + */ + @Column(nullable = false, length = 255) + private String title; + + /** + * Descripción opcional del quiz, proporcionando contexto adicional. + */ + @Column(length = 1000) + private String description; + + /** + * Relación con la entidad {@link Course}. Un quiz puede pertenecer a un curso. + * Esta relación es opcional. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "course_id") + private Course course; + + /** + * Relación con la entidad {@link Lesson}. Un quiz puede pertenecer a una lección. + * Esta relación es opcional y excluyente con la de 'course'. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "lesson_id") + private Lesson lesson; + + /** + * Lista de preguntas asociadas a este quiz. + * La cascada de tipo ALL asegura que las operaciones (guardar, actualizar, eliminar) + * se propaguen a las preguntas y sus opciones. 'orphanRemoval' elimina preguntas + * que ya no están asociadas a un quiz. + */ + @OneToMany(mappedBy = "quiz", cascade = CascadeType.ALL, orphanRemoval = true) + private List questions; + + /** + * Fecha y hora de creación del quiz. Se establece automáticamente al crear la entidad. + */ + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + /** + * Fecha y hora de la última actualización del quiz. + */ + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + // --- Getters y Setters --- + + /** + * Obtiene el identificador único del quiz. + * @return El ID del quiz. + */ + public Long getId() { + return id; + } + + /** + * Establece el identificador único del quiz. + * @param id El ID a establecer. + */ + public void setId(Long id) { + this.id = id; + } + + /** + * Obtiene el título del quiz. + * @return El título del quiz. + */ + public String getTitle() { + return title; + } + + /** + * Establece el título del quiz. + * @param title El título a establecer. + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Obtiene la descripción del quiz. + * @return La descripción del quiz. + */ + public String getDescription() { + return description; + } + + /** + * Establece la descripción del quiz. + * @param description La descripción a establecer. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Obtiene el curso asociado al quiz. + * @return La entidad {@link Course} asociada. + */ + public Course getCourse() { + return course; + } + + /** + * Establece el curso asociado al quiz. + * @param course La entidad {@link Course} a asociar. + */ + public void setCourse(Course course) { + this.course = course; + } + + /** + * Obtiene la lección asociada al quiz. + * @return La entidad {@link Lesson} asociada. + */ + public Lesson getLesson() { + return lesson; + } + + /** + * Establece la lección asociada al quiz. + * @param lesson La entidad {@link Lesson} a asociar. + */ + public void setLesson(Lesson lesson) { + this.lesson = lesson; + } + + /** + * Obtiene la lista de preguntas del quiz. + * @return La lista de entidades {@link Question}. + */ + public List getQuestions() { + return questions; + } + + /** + * Establece la lista de preguntas para el quiz. + * @param questions La lista de entidades {@link Question} a establecer. + */ + public void setQuestions(List questions) { + this.questions = questions; + } + + /** + * Obtiene la fecha y hora de creación del quiz. + * @return La fecha de creación. + */ + public LocalDateTime getCreatedAt() { + return createdAt; + } + + /** + * Establece la fecha y hora de creación del quiz. + * @param createdAt La fecha de creación a establecer. + */ + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + /** + * Obtiene la fecha y hora de la última actualización del quiz. + * @return La fecha de actualización. + */ + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + /** + * Establece la fecha y hora de la última actualización del quiz. + * @param updatedAt La fecha de actualización a establecer. + */ + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/src/main/java/learning/platform/entity/QuizSubmission.java b/src/main/java/learning/platform/entity/QuizSubmission.java new file mode 100644 index 0000000..d0b77fd --- /dev/null +++ b/src/main/java/learning/platform/entity/QuizSubmission.java @@ -0,0 +1,54 @@ +package learning.platform.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Table(name = "quiz_submissions") +public class QuizSubmission { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "quiz_id", nullable = false) + private Quiz quiz; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; // The student who submitted the quiz + + @Column(nullable = false) + private Integer score; + + @Column(nullable = false) + private Double percentage; + + @Column(nullable = false) + private Boolean passed; + + @Column(name = "submitted_at", nullable = false) + private LocalDateTime submittedAt = LocalDateTime.now(); + + @OneToMany(mappedBy = "quizSubmission", cascade = CascadeType.ALL, orphanRemoval = true) + private List answers; + + // Getters y Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Quiz getQuiz() { return quiz; } + public void setQuiz(Quiz quiz) { this.quiz = quiz; } + public User getUser() { return user; } + public void setUser(User user) { this.user = user; } + public Integer getScore() { return score; } + public void setScore(Integer score) { this.score = score; } + public Double getPercentage() { return percentage; } + public void setPercentage(Double percentage) { this.percentage = percentage; } + public Boolean getPassed() { return passed; } + public void setPassed(Boolean passed) { this.passed = passed; } + public LocalDateTime getSubmittedAt() { return submittedAt; } + public void setSubmittedAt(LocalDateTime submittedAt) { this.submittedAt = submittedAt; } + public List getAnswers() { return answers; } + public void setAnswers(List answers) { this.answers = answers; } +} \ No newline at end of file diff --git a/src/main/java/learning/platform/exception/NotFoundException.java b/src/main/java/learning/platform/exception/NotFoundException.java new file mode 100644 index 0000000..686d54e --- /dev/null +++ b/src/main/java/learning/platform/exception/NotFoundException.java @@ -0,0 +1,12 @@ +package learning.platform.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/learning/platform/mapper/QuizMapper.java b/src/main/java/learning/platform/mapper/QuizMapper.java new file mode 100644 index 0000000..b9c481f --- /dev/null +++ b/src/main/java/learning/platform/mapper/QuizMapper.java @@ -0,0 +1,97 @@ +package learning.platform.mapper; + +import learning.platform.dto.*; +import learning.platform.entity.*; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * Interfaz Mapper para la conversión de entidades relacionadas con quizzes a DTOs y viceversa. + * Utiliza el framework MapStruct para generar automáticamente la implementación en tiempo de compilación. + * Esto ayuda a mantener el código de conversión limpio y libre de errores manuales. + */ +@Mapper(componentModel = "spring") +public interface QuizMapper { + + /** + * Instancia singleton del mapper para uso en componentes que no son de Spring. + * Aunque se prefiere la inyección de dependencias en un entorno Spring. + */ + QuizMapper INSTANCE = Mappers.getMapper(QuizMapper.class); + + /** + * Convierte una entidad {@link Quiz} a un DTO {@link QuizResponse}. + * Mapea los IDs de las entidades relacionadas (Course, Lesson) directamente + * a los campos del DTO y mapea la lista de preguntas. + * @param quiz La entidad Quiz a convertir. + * @return El DTO QuizResponse resultante. + */ + @Mapping(target = "courseId", source = "course.id") + @Mapping(target = "lessonId", source = "lesson.id") + @Mapping(target = "questions", source = "questions") + QuizResponse toQuizResponse(Quiz quiz); + + /** + * Convierte una entidad {@link Question} a un DTO {@link QuestionResponse}. + * @param question La entidad Question a convertir. + * @return El DTO QuestionResponse resultante. + */ + @Mapping(target = "options", source = "options") + QuestionResponse toQuestionResponse(Question question); + + /** + * Convierte una entidad {@link AnswerOption} a un DTO {@link AnswerOptionDTO}. + * @param answerOption La entidad AnswerOption a convertir. + * @return El DTO AnswerOptionDTO resultante. + */ + AnswerOptionDTO toAnswerOptionDTO(AnswerOption answerOption); + + /** + * Convierte una solicitud de creación de quiz (DTO) a una entidad {@link Quiz}. + * El campo de preguntas se ignora en este mapeo para ser gestionado manualmente. + * @param request La solicitud de creación QuizCreateRequest. + * @return La entidad Quiz resultante. + */ + @Mapping(target = "questions", ignore = true) + Quiz toQuiz(QuizCreateRequest request); + + /** + * Convierte una solicitud de creación de pregunta (DTO) a una entidad {@link Question}. + * Las opciones se ignoran en este mapeo para ser gestionadas manualmente. + * @param request La solicitud de creación QuestionCreateRequest. + * @return La entidad Question resultante. + */ + @Mapping(target = "options", ignore = true) + Question toQuestion(QuestionCreateRequest request); + + /** + * Convierte un DTO de opción de respuesta a una entidad {@link AnswerOption}. + * @param dto El DTO AnswerOptionDTO a convertir. + * @return La entidad AnswerOption resultante. + */ + AnswerOption toAnswerOption(AnswerOptionDTO dto); + + /** + * Convierte una lista de entidades {@link Quiz} a una lista de DTOs {@link QuizResponse}. + * @param quizzes La lista de entidades Quiz. + * @return Una lista de DTOs QuizResponse. + */ + List toQuizResponseList(List quizzes); + + /** + * Convierte una lista de entidades {@link Question} a una lista de DTOs {@link QuestionResponse}. + * @param questions La lista de entidades Question. + * @return Una lista de DTOs QuestionResponse. + */ + List toQuestionResponseList(List questions); + + /** + * Convierte una lista de entidades {@link AnswerOption} a una lista de DTOs {@link AnswerOptionDTO}. + * @param answerOptions La lista de entidades AnswerOption. + * @return Una lista de DTOs AnswerOptionDTO. + */ + List toAnswerOptionDtoList(List answerOptions); +} \ No newline at end of file diff --git a/src/main/java/learning/platform/repository/AnswerOptionRepository.java b/src/main/java/learning/platform/repository/AnswerOptionRepository.java new file mode 100644 index 0000000..9f424c5 --- /dev/null +++ b/src/main/java/learning/platform/repository/AnswerOptionRepository.java @@ -0,0 +1,17 @@ +package learning.platform.repository; + +import learning.platform.entity.AnswerOption; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface AnswerOptionRepository extends JpaRepository { + + /** + * Busca todas las opciones de respuesta para una pregunta específica. + * @param questionId El ID de la pregunta. + * @return Una lista de AnswerOption. + */ + List findByQuestionId(Long questionId); +} diff --git a/src/main/java/learning/platform/repository/AnswerSubmissionRepository.java b/src/main/java/learning/platform/repository/AnswerSubmissionRepository.java new file mode 100644 index 0000000..7d94f28 --- /dev/null +++ b/src/main/java/learning/platform/repository/AnswerSubmissionRepository.java @@ -0,0 +1,26 @@ +package learning.platform.repository; + +import learning.platform.entity.AnswerSubmission; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * Repositorio para la entidad {@link AnswerSubmission}. + * Proporciona métodos de acceso a datos para operaciones CRUD y consultas personalizadas + * relacionadas con las respuestas enviadas por los usuarios. + */ +@Repository +public interface AnswerSubmissionRepository extends JpaRepository { + + /** + * Busca todas las respuestas de un envío de quiz específico. + * Esta es una consulta personalizada que aprovecha las convenciones de nomenclatura + * de Spring Data JPA para generar la consulta SQL automáticamente. + * + * @param quizSubmissionId El ID de la entrega del quiz. + * @return Una lista de entidades {@link AnswerSubmission} asociadas a esa entrega. + */ + List findByQuizSubmissionId(Long quizSubmissionId); +} \ No newline at end of file diff --git a/src/main/java/learning/platform/repository/QuestionRepository.java b/src/main/java/learning/platform/repository/QuestionRepository.java new file mode 100644 index 0000000..b29dd81 --- /dev/null +++ b/src/main/java/learning/platform/repository/QuestionRepository.java @@ -0,0 +1,14 @@ +package learning.platform.repository; + +import learning.platform.entity.Question; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface QuestionRepository extends JpaRepository { + + // Encuentra todas las preguntas con un ID específico. + List findByQuizId(Long quizId); +} diff --git a/src/main/java/learning/platform/repository/QuizRepository.java b/src/main/java/learning/platform/repository/QuizRepository.java new file mode 100644 index 0000000..2b5267f --- /dev/null +++ b/src/main/java/learning/platform/repository/QuizRepository.java @@ -0,0 +1,18 @@ +package learning.platform.repository; + +import learning.platform.entity.Quiz; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.List; + +@Repository +public interface QuizRepository extends JpaRepository { + + //Encuentra los quizzes asociados con un id de algún curso. + List findByCourseId(Long courseId); + + // Encuentra quizz asociado a alguna lección. + Optional findByLessonId(Long lessonId); +} diff --git a/src/main/java/learning/platform/repository/QuizSubmissionRepository.java b/src/main/java/learning/platform/repository/QuizSubmissionRepository.java new file mode 100644 index 0000000..95c149b --- /dev/null +++ b/src/main/java/learning/platform/repository/QuizSubmissionRepository.java @@ -0,0 +1,14 @@ +package learning.platform.repository; + +import learning.platform.entity.QuizSubmission; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface QuizSubmissionRepository extends JpaRepository { + + //Encuentra el sumbit de un user id y el quizz id. + Optional findByUserIdAndQuizId(Long userId, Long quizId); +} diff --git a/src/main/java/learning/platform/service/QuizService.java b/src/main/java/learning/platform/service/QuizService.java index b5010ff..fc97301 100644 --- a/src/main/java/learning/platform/service/QuizService.java +++ b/src/main/java/learning/platform/service/QuizService.java @@ -1,23 +1,72 @@ package learning.platform.service; import learning.platform.dto.*; +import learning.platform.entity.User; + import java.util.List; +/** + * Servicio para gestionar Quizzes. Aquí se define todo lo que se puede hacer + * con los cuestionarios, como crearlos, obtenerlos, actualizarlos y calificarlos. + */ public interface QuizService { + /** + * Crea un nuevo quiz con sus preguntas. + * @param request El objeto con la información del quiz que se quiere crear. + * @return El quiz que se acaba de crear, con su información. + */ QuizResponse createQuiz(QuizCreateRequest request); + /** + * Busca y obtiene los detalles de un quiz usando su ID. + * @param quizId El número de identificación del quiz que se busca. + * @return El quiz que se encontró. + */ QuizResponse getQuizById(Long quizId); + /** + * Obtiene una lista de todos los quizzes que pertenecen a un curso específico. + * @param courseId El número de identificación del curso. + * @return Una lista de quizzes de ese curso. + */ List getQuizzesByCourse(Long courseId); + /** + * Actualiza la información de un quiz que ya existe. + * @param quizId El número de identificación del quiz a actualizar. + * @param request El objeto con la nueva información. + * @return El quiz actualizado. + */ QuizResponse updateQuiz(Long quizId, QuizCreateRequest request); + /** + * Borra un quiz y todas sus preguntas para siempre. + * @param quizId El número de identificación del quiz que se va a borrar. + */ void deleteQuiz(Long quizId); + /** + * Agrega una nueva pregunta a un quiz que ya existe. + * @param quizId El número de identificación del quiz. + * @param request El objeto con la información de la pregunta a agregar. + * @return El quiz completo, ya con la nueva pregunta. + */ QuizResponse addQuestion(Long quizId, QuestionCreateRequest request); + /** + * Busca un quiz que esté asociado a una lección específica. + * @param lessonId El número de identificación de la lección. + * @return El quiz de esa lección. + */ QuizResponse getQuizByLesson(Long lessonId); - QuizSubmissionResponse submitQuiz(Long quizId, QuizSubmissionRequest request); -} + /** + * Recibe las respuestas de un estudiante a un quiz, lo califica y guarda el resultado. + * @param quizId El número de identificación del quiz que se está enviando. + * @param request El objeto con las respuestas del estudiante. + * @param user El estudiante que está enviando el quiz. + * @return El resultado del quiz, incluyendo la calificación y si lo pasó o no. + */ + QuizSubmissionResponse submitQuiz(Long quizId, QuizSubmissionRequest request, User user); +} \ No newline at end of file diff --git a/src/main/java/learning/platform/service/impl/QuizServiceImpl.java b/src/main/java/learning/platform/service/impl/QuizServiceImpl.java index e1bb306..7482465 100644 --- a/src/main/java/learning/platform/service/impl/QuizServiceImpl.java +++ b/src/main/java/learning/platform/service/impl/QuizServiceImpl.java @@ -1,111 +1,289 @@ package learning.platform.service.impl; import learning.platform.dto.*; +import learning.platform.entity.*; +import learning.platform.exception.NotFoundException; +import learning.platform.repository.*; import learning.platform.service.QuizService; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Service public class QuizServiceImpl implements QuizService { + private final QuizRepository quizRepository; + private final QuestionRepository questionRepository; + private final AnswerOptionRepository answerOptionRepository; + private final QuizSubmissionRepository quizSubmissionRepository; + private final AnswerSubmissionRepository answerSubmissionRepository; + private final UserRepository userRepository; + private final CourseRepository courseRepository; + private final LessonRepository lessonRepository; + private final EnrollmentRepository enrollmentRepository; + + public QuizServiceImpl(QuizRepository quizRepository, + QuestionRepository questionRepository, + AnswerOptionRepository answerOptionRepository, + QuizSubmissionRepository quizSubmissionRepository, + AnswerSubmissionRepository answerSubmissionRepository, + UserRepository userRepository, + CourseRepository courseRepository, + LessonRepository lessonRepository, + EnrollmentRepository enrollmentRepository) { + this.quizRepository = quizRepository; + this.questionRepository = questionRepository; + this.answerOptionRepository = answerOptionRepository; + this.quizSubmissionRepository = quizSubmissionRepository; + this.answerSubmissionRepository = answerSubmissionRepository; + this.userRepository = userRepository; + this.courseRepository = courseRepository; + this.lessonRepository = lessonRepository; + this.enrollmentRepository = enrollmentRepository; + } + @Override + @Transactional public QuizResponse createQuiz(QuizCreateRequest request) { - QuizResponse quiz = new QuizResponse(); - quiz.setId(1L); // Simulación: en realidad lo haría la BD + Quiz quiz = new Quiz(); quiz.setTitle(request.getTitle()); quiz.setDescription(request.getDescription()); - quiz.setCourseId(request.getCourseId()); - quiz.setQuestions(new ArrayList<>()); - return quiz; + + if (request.getCourseId() != null) { + Course course = courseRepository.findById(request.getCourseId()) + .orElseThrow(() -> new NotFoundException("No se encontró el id del curso: " + request.getCourseId())); + quiz.setCourse(course); + } + + if (request.getLessonId() != null) { + Lesson lesson = lessonRepository.findById(request.getLessonId()) + .orElseThrow(() -> new NotFoundException("No se encontró el id de la lección: " + request.getLessonId())); + quiz.setLesson(lesson); + } + + Quiz savedQuiz = quizRepository.save(quiz); + + List questions = request.getQuestions().stream() + .map(questionDto -> { + Question question = new Question(); + question.setQuiz(savedQuiz); + question.setText(questionDto.getText()); + question.setPoints(questionDto.getPoints()); + question.setTimeLimitSeconds(questionDto.getTimeLimitSeconds()); + question.setCorrectOptionIndex(questionDto.getCorrectOptionIndex()); + + List options = questionDto.getOptions().stream() + .map(optionDto -> { + AnswerOption option = new AnswerOption(); + option.setText(optionDto.getText()); + option.setQuestion(question); + return option; + }).collect(Collectors.toList()); + + question.setOptions(options); + return question; + }).collect(Collectors.toList()); + + questionRepository.saveAll(questions); + + return convertToQuizResponse(savedQuiz); } @Override public QuizResponse getQuizById(Long quizId) { - // Simulación: en un caso real buscaría en la BD - QuizResponse quiz = new QuizResponse(); - quiz.setId(quizId); - quiz.setTitle("Quiz ejemplo"); - quiz.setDescription("Descripción del quiz"); - quiz.setCourseId(10L); - quiz.setQuestions(new ArrayList<>()); - return quiz; + Quiz quiz = quizRepository.findById(quizId) + .orElseThrow(() -> new NotFoundException("Quiz not found with id: " + quizId)); + return convertToQuizResponse(quiz); } @Override public List getQuizzesByCourse(Long courseId) { - List quizzes = new ArrayList<>(); - quizzes.add(getQuizById(1L)); - return quizzes; + List quizzes = quizRepository.findByCourseId(courseId); + return quizzes.stream() + .map(this::convertToQuizResponse) + .collect(Collectors.toList()); } @Override + @Transactional public QuizResponse updateQuiz(Long quizId, QuizCreateRequest request) { - QuizResponse quiz = getQuizById(quizId); + Quiz quiz = quizRepository.findById(quizId) + .orElseThrow(() -> new NotFoundException("Quiz not found with id: " + quizId)); + quiz.setTitle(request.getTitle()); quiz.setDescription(request.getDescription()); - quiz.setCourseId(request.getCourseId()); - return quiz; + // Lógica para actualizar preguntas, no implementada aquí para simplicidad + + Quiz updatedQuiz = quizRepository.save(quiz); + return convertToQuizResponse(updatedQuiz); } @Override + @Transactional public void deleteQuiz(Long quizId) { - // Simulación: eliminar de la BD + if (!quizRepository.existsById(quizId)) { + throw new NotFoundException("Quiz not found with id: " + quizId); + } + quizRepository.deleteById(quizId); } @Override + @Transactional public QuizResponse addQuestion(Long quizId, QuestionCreateRequest request) { - QuizResponse quiz = getQuizById(quizId); - - if (quiz.getQuestions() == null) { - quiz.setQuestions(new ArrayList<>()); - } + Quiz quiz = quizRepository.findById(quizId) + .orElseThrow(() -> new NotFoundException("Quiz not found with id: " + quizId)); - // Convertimos opciones de creación a opciones de respuesta (DTO) - List optionDTOs = request.getOptions() - .stream() - .map(opt -> new AnswerOptionDTO(null, opt.getText())) // ID null simulado - .collect(Collectors.toList()); - - QuestionResponse question = new QuestionResponse(); - question.setId(100L); // Simulado + Question question = new Question(); + question.setQuiz(quiz); question.setText(request.getText()); - question.setOptions(optionDTOs); + question.setCorrectOptionIndex(request.getCorrectOptionIndex()); question.setPoints(request.getPoints()); question.setTimeLimitSeconds(request.getTimeLimitSeconds()); - quiz.getQuestions().add(question); - return quiz; + List options = request.getOptions().stream() + .map(optionDto -> { + AnswerOption option = new AnswerOption(); + option.setText(optionDto.getText()); + option.setQuestion(question); + return option; + }).collect(Collectors.toList()); + + question.setOptions(options); + questionRepository.save(question); + + return convertToQuizResponse(quiz); } + // Método que faltaba, ahora implementado @Override public QuizResponse getQuizByLesson(Long lessonId) { - QuizResponse quiz = new QuizResponse(); - quiz.setId(200L); - quiz.setTitle("Quiz de la lección " + lessonId); - quiz.setDescription("Preguntas para reforzar la lección"); - quiz.setCourseId(20L); - quiz.setQuestions(new ArrayList<>()); - return quiz; + Quiz quiz = quizRepository.findByLessonId(lessonId) + .orElseThrow(() -> new NotFoundException("Quiz not found for lesson id: " + lessonId)); + return convertToQuizResponse(quiz); } @Override - public QuizSubmissionResponse submitQuiz(Long quizId, QuizSubmissionRequest request) { - QuizResponse quiz = getQuizById(quizId); + @Transactional + public QuizSubmissionResponse submitQuiz(Long quizId, QuizSubmissionRequest request, User user) { + Quiz quiz = quizRepository.findById(quizId) + .orElseThrow(() -> new NotFoundException("Quiz not found with id: " + quizId)); + + if (quiz.getCourse() != null) { + boolean isEnrolled = enrollmentRepository.existsByStudentIdAndCourseId(user.getId(), quiz.getCourse().getId()); + if (!isEnrolled) { + throw new IllegalStateException("User is not enrolled in this course and cannot submit the quiz."); + } + } - int totalQuestions = quiz.getQuestions() != null ? quiz.getQuestions().size() : 0; - int correct = request.getAnswers() != null ? request.getAnswers().size() : 0; // simulado + List questions = questionRepository.findByQuizId(quizId); - QuizSubmissionResponse response = new QuizSubmissionResponse(); - response.setQuizId(quizId); - response.setEnrollmentId(request.getEnrollmentId()); - response.setScore(correct); - response.setPercentage(totalQuestions > 0 ? (correct * 100.0 / totalQuestions) : 0.0); - response.setPassed(response.getPercentage() >= 60.0); + QuizSubmission submission = new QuizSubmission(); + submission.setQuiz(quiz); + submission.setUser(user); - return response; + List submittedAnswers = request.getAnswers().stream() + .map(answerDto -> { + Question question = questionRepository.findById(answerDto.getQuestionId()) + .orElseThrow(() -> new NotFoundException("Question not found with id: " + answerDto.getQuestionId())); + AnswerOption selectedOption = answerOptionRepository.findById(answerDto.getSelectedOptionId()) + .orElseThrow(() -> new NotFoundException("Answer option not found with id: " + answerDto.getSelectedOptionId())); + + AnswerSubmission submissionItem = new AnswerSubmission(); + submissionItem.setQuizSubmission(submission); + submissionItem.setQuestion(question); + submissionItem.setSelectedOption(selectedOption); + return submissionItem; + }).collect(Collectors.toList()); + + int correctCount = 0; + int totalPoints = 0; + for (Question question : questions) { + totalPoints += (question.getPoints() != null) ? question.getPoints() : 0; + Long correctAnswerOptionId = getCorrectAnswerOptionId(question); + + AnswerSubmission studentAnswer = submittedAnswers.stream() + .filter(sa -> sa.getQuestion().getId().equals(question.getId())) + .findFirst() + .orElse(null); + + if (studentAnswer != null && studentAnswer.getSelectedOption() != null) { + if (studentAnswer.getSelectedOption().getId().equals(correctAnswerOptionId)) { + correctCount++; + } + } + } + + submission.setAnswers(submittedAnswers); + + int totalQuestions = questions.size(); + double percentage = (totalQuestions > 0) ? ((double) correctCount / totalQuestions) * 100 : 0.0; + submission.setScore(correctCount); + submission.setPercentage(percentage); + submission.setPassed(percentage >= 60.0); + + quizSubmissionRepository.save(submission); + + return convertToQuizSubmissionResponse(submission); + } + + // Método auxiliar para obtener el ID de la opción de respuesta correcta + private Long getCorrectAnswerOptionId(Question question) { + if (question.getCorrectOptionIndex() != null && question.getCorrectOptionIndex() >= 0 && question.getCorrectOptionIndex() < question.getOptions().size()) { + return question.getOptions().get(question.getCorrectOptionIndex()).getId(); + } + return null; } -} + // Métodos auxiliares para la conversión de entidades a DTOs + private QuizResponse convertToQuizResponse(Quiz quiz) { + QuizResponse dto = new QuizResponse(); + dto.setId(quiz.getId()); + dto.setTitle(quiz.getTitle()); + dto.setDescription(quiz.getDescription()); + dto.setCourseId(quiz.getCourse() != null ? quiz.getCourse().getId() : null); + dto.setLessonId(quiz.getLesson() != null ? quiz.getLesson().getId() : null); + + List questions = questionRepository.findByQuizId(quiz.getId()); + List questionDtos = questions.stream() + .map(this::convertToQuestionResponse) + .collect(Collectors.toList()); + dto.setQuestions(questionDtos); + + return dto; + } + + private QuestionResponse convertToQuestionResponse(Question question) { + QuestionResponse dto = new QuestionResponse(); + dto.setId(question.getId()); + dto.setText(question.getText()); + dto.setPoints(question.getPoints()); + dto.setTimeLimitSeconds(question.getTimeLimitSeconds()); + + List options = answerOptionRepository.findByQuestionId(question.getId()); + List optionDtos = options.stream() + .map(this::convertToAnswerOptionDTO) + .collect(Collectors.toList()); + dto.setOptions(optionDtos); + + return dto; + } + + private AnswerOptionDTO convertToAnswerOptionDTO(AnswerOption option) { + AnswerOptionDTO dto = new AnswerOptionDTO(); + dto.setId(option.getId()); + dto.setText(option.getText()); + return dto; + } + + private QuizSubmissionResponse convertToQuizSubmissionResponse(QuizSubmission submission) { + QuizSubmissionResponse dto = new QuizSubmissionResponse(); + dto.setQuizId(submission.getQuiz().getId()); + dto.setScore(submission.getScore()); + dto.setPercentage(submission.getPercentage()); + dto.setPassed(submission.getPassed()); + return dto; + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 20ff708..89094e3 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -7,7 +7,7 @@ server.port=8080 # Configuración de la base de datos spring.datasource.url=jdbc:postgresql://localhost:5432/elearning spring.datasource.username=postgres -spring.datasource.password=password +spring.datasource.password=prueba0000 spring.datasource.driver-class-name=org.postgresql.Driver # JPA / Hibernate diff --git a/src/test/java/learning/platform/controller/QuizControllerTest.java b/src/test/java/learning/platform/controller/QuizControllerTest.java index e070612..f40812d 100644 --- a/src/test/java/learning/platform/controller/QuizControllerTest.java +++ b/src/test/java/learning/platform/controller/QuizControllerTest.java @@ -1,70 +1,115 @@ package learning.platform.controller; -import com.fasterxml.jackson.databind.ObjectMapper; -import learning.platform.dto.AnswerSubmissionRequest; +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.tags.Tag; +import jakarta.validation.Valid; import learning.platform.dto.QuizCreateRequest; +import learning.platform.dto.QuizResponse; import learning.platform.dto.QuizSubmissionRequest; -import org.junit.jupiter.api.Test; -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.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; +import learning.platform.dto.QuizSubmissionResponse; +import learning.platform.entity.User; +import learning.platform.service.QuizService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; -import java.util.Collections; +import java.util.List; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +@RestController +@RequestMapping("/api/quizzes") +@Tag(name = "Quizzes", description = "Gestión de evaluaciones (quiz) asociadas a cursos y lecciones") +public class QuizControllerTest { -@SpringBootTest -@AutoConfigureMockMvc -class QuizControllerTest { + private final QuizService quizService; - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @Test - void shouldCreateQuizSuccessfully() throws Exception { - QuizCreateRequest request = new QuizCreateRequest(); - request.setTitle("Quiz de prueba"); - request.setDescription("Descripción de prueba"); - request.setCourseId(1L); - - mockMvc.perform(post("/api/quizzes") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.title").value("Quiz de prueba")) - .andExpect(jsonPath("$.courseId").value(1)); + public QuizControllerTest(QuizService quizService) { + this.quizService = quizService; } - @Test - void shouldGetQuizById() throws Exception { - mockMvc.perform(get("/api/quizzes/1")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(1)); + @Operation( + summary = "Crear un nuevo quiz", + description = "Solo los instructores pueden crear quizzes asociados a cursos o lecciones.", + responses = { + @ApiResponse(responseCode = "201", description = "Quiz creado", + content = @Content(schema = @Schema(implementation = QuizResponse.class))), + @ApiResponse(responseCode = "400", description = "Datos inválidos"), + @ApiResponse(responseCode = "403", description = "Acceso denegado") + } + ) + @PreAuthorize("hasRole('INSTRUCTOR')") + @PostMapping + public ResponseEntity createQuiz( + @Valid @RequestBody QuizCreateRequest request) { + QuizResponse response = quizService.createQuiz(request); + return ResponseEntity.status(201).body(response); } - @Test - void shouldSubmitQuizAnswers() throws Exception { - QuizSubmissionRequest request = new QuizSubmissionRequest(); - request.setEnrollmentId(10L); + @Operation( + summary = "Obtener un quiz por ID", + description = "Devuelve la información del quiz con preguntas y respuestas." + ) + @GetMapping("/{quizId}") + public ResponseEntity getQuizById( + @Parameter(description = "ID del quiz") @PathVariable Long quizId) { + QuizResponse response = quizService.getQuizById(quizId); + return ResponseEntity.ok(response); + } - AnswerSubmissionRequest answer = new AnswerSubmissionRequest(); - answer.setQuestionId(987L); - answer.setSelectedOptionId(1234L); + @Operation( + summary = "Obtener quizzes de un curso", + description = "Devuelve todos los quizzes asociados a un curso específico." + ) + @GetMapping("/course/{courseId}") + public ResponseEntity> getQuizzesByCourse( + @Parameter(description = "ID del curso") @PathVariable Long courseId) { + List responses = quizService.getQuizzesByCourse(courseId); + return ResponseEntity.ok(responses); + } - request.setAnswers(Collections.singletonList(answer)); + @Operation( + summary = "Obtener quiz de una lección", + description = "Devuelve el quiz asignado a una lección." + ) + @GetMapping("/lesson/{lessonId}") + public ResponseEntity getQuizByLesson( + @Parameter(description = "ID de la lección") @PathVariable Long lessonId) { + QuizResponse response = quizService.getQuizByLesson(lessonId); + return ResponseEntity.ok(response); + } - mockMvc.perform(post("/api/quizzes/1/submit") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.quizId").value(1)) - .andExpect(jsonPath("$.enrollmentId").value(10)); + @Operation( + summary = "Enviar respuestas de un quiz", + description = "Un estudiante envía sus respuestas a un quiz y recibe puntaje.", + responses = { + @ApiResponse(responseCode = "200", description = "Respuestas procesadas", + content = @Content(schema = @Schema(implementation = QuizSubmissionResponse.class))), + @ApiResponse(responseCode = "400", description = "Respuestas inválidas") + } + ) + @PostMapping("/{quizId}/submit") + public ResponseEntity submitQuiz( + @Parameter(description = "ID del quiz a resolver") @PathVariable Long quizId, + @Valid @RequestBody QuizSubmissionRequest request, + @AuthenticationPrincipal User student) { + QuizSubmissionResponse response = quizService.submitQuiz(quizId, request, student); + return ResponseEntity.ok(response); } -} + @Operation( + summary = "Eliminar un quiz", + description = "Solo instructores pueden eliminar quizzes." + ) + @PreAuthorize("hasRole('INSTRUCTOR')") + @DeleteMapping("/{quizId}") + public ResponseEntity deleteQuiz( + @Parameter(description = "ID del quiz a eliminar") @PathVariable Long quizId) { + quizService.deleteQuiz(quizId); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/test/java/learning/platform/service/impl/QuizServiceImplTest.java b/src/test/java/learning/platform/service/impl/QuizServiceImplTest.java new file mode 100644 index 0000000..9d28a5e --- /dev/null +++ b/src/test/java/learning/platform/service/impl/QuizServiceImplTest.java @@ -0,0 +1,257 @@ +package learning.platform.service.impl; + +import learning.platform.dto.*; +import learning.platform.entity.*; +import learning.platform.exception.NotFoundException; +import learning.platform.repository.*; +import learning.platform.service.QuizService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("Unit Tests for QuizServiceImpl") +public class QuizServiceImplTest { + + @Mock + private QuizRepository quizRepository; + @Mock + private QuestionRepository questionRepository; + @Mock + private AnswerOptionRepository answerOptionRepository; + @Mock + private QuizSubmissionRepository quizSubmissionRepository; + @Mock + private UserRepository userRepository; + @Mock + private CourseRepository courseRepository; + @Mock + private LessonRepository lessonRepository; + @Mock + private EnrollmentRepository enrollmentRepository; + + @InjectMocks + private QuizServiceImpl quizService; + + // Entidades de prueba + private Quiz quiz; + private User studentUser; + private Course course; + private Enrollment enrollment; + + @BeforeEach + void setUp() { + // Configuración de las entidades base para las pruebas + course = new Course(); + course.setId(1L); + + quiz = new Quiz(); + quiz.setId(10L); + quiz.setTitle("Test Quiz"); + quiz.setDescription("A basic quiz for testing."); + quiz.setCourse(course); + + studentUser = new User(); + studentUser.setId(1L); + studentUser.setFullName("John Doe"); + + enrollment = new Enrollment(); + enrollment.setId(100L); + enrollment.setStudent(studentUser); + enrollment.setCourse(course); + } + + // --- Pruebas para createQuiz --- + @Test + @DisplayName("Should create a quiz successfully") + void shouldCreateQuizSuccessfully() { + // Simula el comportamiento del repositorio al guardar + when(courseRepository.findById(any(Long.class))).thenReturn(Optional.of(course)); + when(quizRepository.save(any(Quiz.class))).thenReturn(quiz); + when(questionRepository.saveAll(any())).thenReturn(Collections.emptyList()); + + // Crea el DTO de solicitud + QuizCreateRequest request = new QuizCreateRequest(); + request.setTitle("Test Quiz"); + request.setDescription("A basic quiz."); + request.setCourseId(1L); + request.setQuestions(new ArrayList<>()); // Preguntas simuladas + + QuizResponse response = quizService.createQuiz(request); + + assertNotNull(response); + assertEquals(quiz.getId(), response.getId()); + assertEquals(quiz.getTitle(), response.getTitle()); + } + + @Test + @DisplayName("Should throw NotFoundException when creating quiz with non-existent course") + void shouldThrowNotFoundExceptionForNonExistentCourse() { + when(courseRepository.findById(any(Long.class))).thenReturn(Optional.empty()); + + QuizCreateRequest request = new QuizCreateRequest(); + request.setTitle("Test Quiz"); + request.setCourseId(99L); // ID que no existe + + assertThrows(NotFoundException.class, () -> quizService.createQuiz(request)); + } + + // --- Pruebas para getQuizById --- + @Test + @DisplayName("Should retrieve a quiz by ID") + void shouldGetQuizByIdSuccessfully() { + when(quizRepository.findById(10L)).thenReturn(Optional.of(quiz)); + when(questionRepository.findByQuizId(10L)).thenReturn(Collections.emptyList()); + + QuizResponse response = quizService.getQuizById(10L); + + assertNotNull(response); + assertEquals(quiz.getId(), response.getId()); + } + + @Test + @DisplayName("Should throw NotFoundException when quiz ID does not exist") + void shouldThrowNotFoundExceptionWhenQuizNotFound() { + when(quizRepository.findById(any(Long.class))).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> quizService.getQuizById(99L)); + } + + // --- Pruebas para submitQuiz --- + @Test + @DisplayName("Should submit a quiz and return a passing score") + void shouldSubmitQuizAndReturnPassingScore() { + // Mock de datos para la prueba + Question q1 = new Question(); + q1.setId(1L); + q1.setText("Q1"); + q1.setPoints(10); + q1.setCorrectOptionIndex(0); + + AnswerOption correctOption = new AnswerOption(); + correctOption.setId(101L); + correctOption.setText("Correct"); + correctOption.setQuestion(q1); + + AnswerOption incorrectOption = new AnswerOption(); + incorrectOption.setId(102L); + incorrectOption.setText("Incorrect"); + incorrectOption.setQuestion(q1); + + List options = List.of(correctOption, incorrectOption); + q1.setOptions(options); + + // Simula el comportamiento del repositorio + when(quizRepository.findById(any(Long.class))).thenReturn(Optional.of(quiz)); + when(enrollmentRepository.existsByStudentIdAndCourseId(any(Long.class), any(Long.class))).thenReturn(true); + when(questionRepository.findByQuizId(any(Long.class))).thenReturn(List.of(q1)); + when(questionRepository.findById(q1.getId())).thenReturn(Optional.of(q1)); + when(answerOptionRepository.findById(correctOption.getId())).thenReturn(Optional.of(correctOption)); + when(quizSubmissionRepository.save(any(QuizSubmission.class))).thenAnswer(i -> i.getArguments()[0]); + + // Crea el DTO de solicitud de envío con la respuesta correcta + QuizSubmissionRequest request = new QuizSubmissionRequest(); + AnswerSubmissionItem item = new AnswerSubmissionItem(); + item.setQuestionId(q1.getId()); + item.setSelectedOptionId(correctOption.getId()); + request.setAnswers(List.of(item)); + + QuizSubmissionResponse response = quizService.submitQuiz(quiz.getId(), request, studentUser); + + assertNotNull(response); + assertTrue(response.getPassed()); + assertEquals(100.0, response.getPercentage()); + assertEquals(1, response.getScore()); + } + + @Test + @DisplayName("Should submit a quiz and return a failing score") + void shouldSubmitQuizAndReturnFailingScore() { + // Mock de datos para la prueba + Question q1 = new Question(); + q1.setId(1L); + q1.setText("Q1"); + q1.setPoints(10); + q1.setCorrectOptionIndex(0); + + AnswerOption correctOption = new AnswerOption(); + correctOption.setId(101L); + correctOption.setText("Correct"); + correctOption.setQuestion(q1); + + AnswerOption incorrectOption = new AnswerOption(); + incorrectOption.setId(102L); + incorrectOption.setText("Incorrect"); + incorrectOption.setQuestion(q1); + + List options = List.of(correctOption, incorrectOption); + q1.setOptions(options); + + // Simula el comportamiento del repositorio + when(quizRepository.findById(any(Long.class))).thenReturn(Optional.of(quiz)); + when(enrollmentRepository.existsByStudentIdAndCourseId(any(Long.class), any(Long.class))).thenReturn(true); + when(questionRepository.findByQuizId(any(Long.class))).thenReturn(List.of(q1)); + when(questionRepository.findById(q1.getId())).thenReturn(Optional.of(q1)); + when(answerOptionRepository.findById(incorrectOption.getId())).thenReturn(Optional.of(incorrectOption)); + when(quizSubmissionRepository.save(any(QuizSubmission.class))).thenAnswer(i -> i.getArguments()[0]); + + // Crea el DTO de solicitud de envío con la respuesta INCORRECTA + QuizSubmissionRequest request = new QuizSubmissionRequest(); + AnswerSubmissionItem item = new AnswerSubmissionItem(); + item.setQuestionId(q1.getId()); + item.setSelectedOptionId(incorrectOption.getId()); + request.setAnswers(List.of(item)); + + QuizSubmissionResponse response = quizService.submitQuiz(quiz.getId(), request, studentUser); + + assertNotNull(response); + assertFalse(response.getPassed()); + assertEquals(0.0, response.getPercentage()); + assertEquals(0, response.getScore()); + } + + @Test + @DisplayName("Should throw IllegalStateException if student is not enrolled in the course") + void shouldThrowExceptionIfStudentNotEnrolled() { + when(quizRepository.findById(any(Long.class))).thenReturn(Optional.of(quiz)); + when(enrollmentRepository.existsByStudentIdAndCourseId(any(Long.class), any(Long.class))).thenReturn(false); + + QuizSubmissionRequest request = new QuizSubmissionRequest(); + + assertThrows(IllegalStateException.class, () -> quizService.submitQuiz(quiz.getId(), request, studentUser)); + } + + // --- Pruebas para deleteQuiz --- + @Test + @DisplayName("Should delete a quiz successfully") + void shouldDeleteQuizSuccessfully() { + when(quizRepository.existsById(10L)).thenReturn(true); + + quizService.deleteQuiz(10L); + + Mockito.verify(quizRepository, Mockito.times(1)).deleteById(10L); + } + + @Test + @DisplayName("Should throw NotFoundException when deleting non-existent quiz") + void shouldThrowNotFoundExceptionForNonExistentQuiz() { + when(quizRepository.existsById(any(Long.class))).thenReturn(false); + + assertThrows(NotFoundException.class, () -> quizService.deleteQuiz(99L)); + } +} \ No newline at end of file From 6a4efd9798f432c090435e615b76aff9fae69668 Mon Sep 17 00:00:00 2001 From: Osiris Aguilazochom Date: Thu, 11 Sep 2025 13:11:21 -0600 Subject: [PATCH 2/2] Pruebas unitarias completas --- src/main/resources/application-dev.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 89094e3..20ff708 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -7,7 +7,7 @@ server.port=8080 # Configuración de la base de datos spring.datasource.url=jdbc:postgresql://localhost:5432/elearning spring.datasource.username=postgres -spring.datasource.password=prueba0000 +spring.datasource.password=password spring.datasource.driver-class-name=org.postgresql.Driver # JPA / Hibernate