From 51494eddfa8de6b3b66d80891490f926c1ec0f4b Mon Sep 17 00:00:00 2001 From: teklit_tewolde Date: Fri, 23 Aug 2024 14:40:18 +0200 Subject: [PATCH] fix [back] [QCMPLUS-40] update Questions endpoints Signed-off-by: teklit_tewolde --- qcmplusweb/src/services/QuestionService.jsx | 10 ++ .../controller/QuestionController.java | 39 ++-- .../exception/QuestionNotFoundException.java | 7 + .../pmn/qcmplus/service/QuestionService.java | 6 +- .../service/impl/QuestionServiceImpl.java | 53 ++++-- .../controller/QuestionControllerTest.java | 55 ++++-- .../repository/QuestionRepositoryTest.java | 21 ++- .../service/impl/QuestionServiceImplTest.java | 169 ++++++++++++++++++ 8 files changed, 311 insertions(+), 49 deletions(-) create mode 100644 qcmplusweb/src/services/QuestionService.jsx create mode 100644 src/main/java/com/pmn/qcmplus/exception/QuestionNotFoundException.java create mode 100644 src/test/java/com/pmn/qcmplus/service/impl/QuestionServiceImplTest.java diff --git a/qcmplusweb/src/services/QuestionService.jsx b/qcmplusweb/src/services/QuestionService.jsx new file mode 100644 index 0000000..bee7de9 --- /dev/null +++ b/qcmplusweb/src/services/QuestionService.jsx @@ -0,0 +1,10 @@ +import axiosInstance, {API_BASE_URL} from './AxiosInstance'; + +const QUESTION_REST_API_URL = `${API_BASE_URL}/api/questions`; + +export const getQuestionsByQuizId = (quizId) => axiosInstance.get(`${QUESTION_REST_API_URL}/quizzes/${quizId}`); +export const getQuestionById = (quizId, questionId) => axiosInstance.get(`${QUESTION_REST_API_URL}/${questionId}/quizzes/${quizId}`); +export const createQuestion = (quizId, questionData) => axiosInstance.post(`${QUESTION_REST_API_URL}/quizzes/${quizId}`, questionData); +export const updateQuestion = (quizId, questionId, questionData) => axiosInstance.put(`${QUESTION_REST_API_URL}/${questionId}/quizzes/${quizId}`, questionData); +export const deleteQuestion = (quizId, questionId) => axiosInstance.delete(`${QUESTION_REST_API_URL}/${questionId}/quizzes/${quizId}`); +export const getAllQuestions = () => axiosInstance.get(`${QUESTION_REST_API_URL}/all`); \ No newline at end of file diff --git a/src/main/java/com/pmn/qcmplus/controller/QuestionController.java b/src/main/java/com/pmn/qcmplus/controller/QuestionController.java index 7f90bbf..1053eb6 100644 --- a/src/main/java/com/pmn/qcmplus/controller/QuestionController.java +++ b/src/main/java/com/pmn/qcmplus/controller/QuestionController.java @@ -4,6 +4,7 @@ import com.pmn.qcmplus.model.Quiz; import com.pmn.qcmplus.service.QuestionService; import com.pmn.qcmplus.service.QuizService; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -11,15 +12,14 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +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; -import java.util.List; - @RestController @PreAuthorize("hasRole('ADMIN')") -@RequestMapping("/api/quizzes/{quizId}/questions") +@RequestMapping("/api/questions") public class QuestionController { private final QuestionService questionService; @@ -31,13 +31,13 @@ public QuestionController(QuestionService questionService, QuizService quizServi this.quizService = quizService; } - @GetMapping + @GetMapping("/quizzes/{quizId}") public ResponseEntity> getQuestionsByQuizId(@PathVariable Integer quizId) { List questions = questionService.getQuestionsByQuizId(quizId); return ResponseEntity.ok(questions); } - @PostMapping + @PostMapping("/quizzes/{quizId}") public ResponseEntity createQuestion(@RequestBody Question question, @PathVariable Integer quizId) { Quiz quiz = quizService.getQuizById(quizId); question.setQuiz(quiz); @@ -45,15 +45,30 @@ public ResponseEntity createQuestion(@RequestBody Question question, @ return ResponseEntity.ok(createdQuestion); } - @DeleteMapping("/{id}") - public ResponseEntity deleteQuestion(@PathVariable Integer quizId, @PathVariable Integer id) { - questionService.deleteQuestion(id); + @PutMapping("/{questionId}/quizzes/{quizId}") + public ResponseEntity updateQuestion(@RequestBody Question question, @PathVariable Integer quizId, @PathVariable Integer questionId) { + Quiz quiz = quizService.getQuizById(quizId); + question.setQuiz(quiz); + question.setQuestionId(questionId); + Question updatedQuestion = questionService.updateQuestion(question); + return ResponseEntity.ok(updatedQuestion); + } + + @DeleteMapping("/{questionId}/quizzes/{quizId}") + public ResponseEntity deleteQuestion(@PathVariable Integer quizId, @PathVariable Integer questionId) { + questionService.deleteQuestion(questionId); return ResponseEntity.noContent().build(); } - @GetMapping("/{id}") - public ResponseEntity getQuestionById(@PathVariable Integer quizId, @PathVariable Integer id) { - Question question = questionService.getQuestionById(id); + @GetMapping("/{questionId}/quizzes/{quizId}") + public ResponseEntity getQuestionById(@PathVariable Integer quizId, @PathVariable Integer questionId) { + Question question = questionService.getQuestionById(questionId); return ResponseEntity.ok(question); } -} + + @GetMapping("/all") + public ResponseEntity> getAllQuestions() { + List questions = questionService.getAllQuestions(); + return ResponseEntity.ok(questions); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmn/qcmplus/exception/QuestionNotFoundException.java b/src/main/java/com/pmn/qcmplus/exception/QuestionNotFoundException.java new file mode 100644 index 0000000..a55b7a9 --- /dev/null +++ b/src/main/java/com/pmn/qcmplus/exception/QuestionNotFoundException.java @@ -0,0 +1,7 @@ +package com.pmn.qcmplus.exception; + +public class QuestionNotFoundException extends RuntimeException { + public QuestionNotFoundException(Integer id) { + super("Question with id " + id + " not found."); + } +} \ No newline at end of file diff --git a/src/main/java/com/pmn/qcmplus/service/QuestionService.java b/src/main/java/com/pmn/qcmplus/service/QuestionService.java index 5ca96ef..b623fbb 100644 --- a/src/main/java/com/pmn/qcmplus/service/QuestionService.java +++ b/src/main/java/com/pmn/qcmplus/service/QuestionService.java @@ -11,5 +11,9 @@ public interface QuestionService { Question saveQuestion(Question question); + Question updateQuestion(Question question); + void deleteQuestion(Integer id); -} + + List getAllQuestions(); // New method +} \ No newline at end of file diff --git a/src/main/java/com/pmn/qcmplus/service/impl/QuestionServiceImpl.java b/src/main/java/com/pmn/qcmplus/service/impl/QuestionServiceImpl.java index 332d7a5..f881459 100644 --- a/src/main/java/com/pmn/qcmplus/service/impl/QuestionServiceImpl.java +++ b/src/main/java/com/pmn/qcmplus/service/impl/QuestionServiceImpl.java @@ -1,15 +1,15 @@ package com.pmn.qcmplus.service.impl; +import com.pmn.qcmplus.exception.QuestionNotFoundException; import com.pmn.qcmplus.model.Question; import com.pmn.qcmplus.model.Quiz; import com.pmn.qcmplus.repository.QuestionRepository; import com.pmn.qcmplus.repository.QuizRepository; import com.pmn.qcmplus.service.QuestionService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - import java.util.List; import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; @Service public class QuestionServiceImpl implements QuestionService { @@ -31,31 +31,60 @@ public List getQuestionsByQuizId(Integer quizId) { @Override public Question getQuestionById(Integer id) { return questionRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("Question not found with id: " + id)); + .orElseThrow(() -> new QuestionNotFoundException(id)); } - @Override public Question saveQuestion(Question question) { - // Validate that the quiz exists + if (question == null) { + throw new IllegalArgumentException("Question cannot be null."); + } + if (question.getQuiz() == null || question.getQuiz().getQuizId() == null) { + throw new IllegalArgumentException("Quiz or Quiz ID cannot be null."); + } + + Optional quiz = quizRepository.findById(question.getQuiz().getQuizId()); + if (quiz.isEmpty()) { + throw new IllegalArgumentException("Quiz does not exist."); + } + + return questionRepository.save(question); + } + + + @Override + public Question updateQuestion(Question question) { + if (question == null) { + throw new IllegalArgumentException("Question cannot be null."); + } + if (question.getQuiz() == null || question.getQuiz().getQuizId() == null) { + throw new IllegalArgumentException("Quiz or Quiz ID cannot be null."); + } + Optional quiz = quizRepository.findById(question.getQuiz().getQuizId()); if (quiz.isEmpty()) { throw new IllegalArgumentException("Quiz does not exist."); } - // Save the question + Optional existingQuestion = questionRepository.findById(question.getQuestionId()); + if (existingQuestion.isEmpty()) { + throw new QuestionNotFoundException(question.getQuestionId()); + } + return questionRepository.save(question); } @Override public void deleteQuestion(Integer id) { - // Validate that the question exists Optional question = questionRepository.findById(id); if (question.isEmpty()) { - throw new IllegalArgumentException("Question does not exist."); + throw new QuestionNotFoundException(id); } - - // Delete the question questionRepository.deleteById(id); } -} + + @Override + public List getAllQuestions() { + return questionRepository.findAll(); + } +} \ No newline at end of file diff --git a/src/test/java/com/pmn/qcmplus/controller/QuestionControllerTest.java b/src/test/java/com/pmn/qcmplus/controller/QuestionControllerTest.java index d9664e5..6b4098c 100644 --- a/src/test/java/com/pmn/qcmplus/controller/QuestionControllerTest.java +++ b/src/test/java/com/pmn/qcmplus/controller/QuestionControllerTest.java @@ -4,24 +4,22 @@ import com.pmn.qcmplus.model.Quiz; import com.pmn.qcmplus.service.QuestionService; import com.pmn.qcmplus.service.QuizService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.util.Arrays; +import java.util.Collections; import java.util.List; - import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import org.mockito.InjectMocks; +import org.mockito.Mock; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; class QuestionControllerTest { @@ -47,7 +45,7 @@ void setUp() { @Test void testGetQuestionsByQuizId() { - when(questionService.getQuestionsByQuizId(eq(1))).thenReturn(Arrays.asList(question)); + when(questionService.getQuestionsByQuizId(eq(1))).thenReturn(Collections.singletonList(question)); ResponseEntity> response = questionController.getQuestionsByQuizId(1); @@ -60,7 +58,6 @@ void testGetQuestionsByQuizId() { void testGetQuestionById() { when(questionService.getQuestionById(eq(1))).thenReturn(question); - // Update: Include quizId as a parameter in the method call ResponseEntity response = questionController.getQuestionById(1, 1); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -80,14 +77,46 @@ void testCreateQuestion() { verify(questionService, times(1)).saveQuestion(any(Question.class)); } + @Test + void testUpdateQuestion() { + Quiz mockQuiz = new Quiz(); + mockQuiz.setQuizId(1); + Question mockQuestion = new Question(); + mockQuestion.setQuiz(mockQuiz); + mockQuestion.setQuestionId(1); + + when(quizService.getQuizById(eq(1))).thenReturn(mockQuiz); + when(questionService.updateQuestion(any(Question.class))).thenReturn(mockQuestion); + + ResponseEntity response = questionController.updateQuestion(mockQuestion, 1, 1); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(mockQuestion, response.getBody()); + verify(quizService, times(1)).getQuizById(eq(1)); + verify(questionService, times(1)).updateQuestion(any(Question.class)); + } + @Test void testDeleteQuestion() { doNothing().when(questionService).deleteQuestion(eq(1)); - // Update: Include quizId as a parameter in the method call ResponseEntity response = questionController.deleteQuestion(1, 1); assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode()); verify(questionService, times(1)).deleteQuestion(eq(1)); } + + @Test + void testGetAllQuestions() { + when(questionService.getAllQuestions()).thenReturn(Collections.singletonList(question)); + + ResponseEntity> response = questionController.getAllQuestions(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(1, response.getBody().size()); + verify(questionService, times(1)).getAllQuestions(); + } + + + } diff --git a/src/test/java/com/pmn/qcmplus/repository/QuestionRepositoryTest.java b/src/test/java/com/pmn/qcmplus/repository/QuestionRepositoryTest.java index 24d4170..c1730af 100644 --- a/src/test/java/com/pmn/qcmplus/repository/QuestionRepositoryTest.java +++ b/src/test/java/com/pmn/qcmplus/repository/QuestionRepositoryTest.java @@ -1,25 +1,24 @@ package com.pmn.qcmplus.repository; +import com.pmn.qcmplus.exception.QuestionNotFoundException; import com.pmn.qcmplus.model.Question; import com.pmn.qcmplus.model.Quiz; import com.pmn.qcmplus.service.impl.QuestionServiceImpl; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - import java.util.List; import java.util.Optional; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import static org.mockito.ArgumentMatchers.any; +import org.mockito.InjectMocks; +import org.mockito.Mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -116,13 +115,13 @@ void testDeleteQuestion() { void testDeleteQuestion_NotFound() { when(questionRepository.findById(1)).thenReturn(Optional.empty()); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + QuestionNotFoundException exception = assertThrows(QuestionNotFoundException.class, () -> { questionService.deleteQuestion(1); }); - assertEquals("Question does not exist.", exception.getMessage()); + assertEquals("Question with id 1 not found.", exception.getMessage()); verify(questionRepository, times(1)).findById(1); verify(questionRepository, times(0)).deleteById(1); } -} +} \ No newline at end of file diff --git a/src/test/java/com/pmn/qcmplus/service/impl/QuestionServiceImplTest.java b/src/test/java/com/pmn/qcmplus/service/impl/QuestionServiceImplTest.java new file mode 100644 index 0000000..ef721e1 --- /dev/null +++ b/src/test/java/com/pmn/qcmplus/service/impl/QuestionServiceImplTest.java @@ -0,0 +1,169 @@ +package com.pmn.qcmplus.service.impl; + +import com.pmn.qcmplus.exception.QuestionNotFoundException; +import com.pmn.qcmplus.model.Question; +import com.pmn.qcmplus.model.Quiz; +import com.pmn.qcmplus.repository.QuestionRepository; +import com.pmn.qcmplus.repository.QuizRepository; +import java.util.List; +import java.util.Optional; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.mockito.ArgumentMatchers.any; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.MockitoAnnotations; + +class QuestionServiceImplTest { + + @Mock + private QuestionRepository questionRepository; + + @Mock + private QuizRepository quizRepository; + + @InjectMocks + private QuestionServiceImpl questionService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testGetQuestionsByQuizId() { + int quizId = 1; + List mockQuestions = List.of(new Question()); + when(questionRepository.findByQuiz_QuizId(quizId)).thenReturn(mockQuestions); + + List questions = questionService.getQuestionsByQuizId(quizId); + + assertEquals(mockQuestions, questions); + verify(questionRepository, times(1)).findByQuiz_QuizId(quizId); + } + + @Test + void testGetQuestionById() { + int questionId = 1; + Question mockQuestion = new Question(); + when(questionRepository.findById(questionId)).thenReturn(Optional.of(mockQuestion)); + + Question question = questionService.getQuestionById(questionId); + + assertEquals(mockQuestion, question); + verify(questionRepository, times(1)).findById(questionId); + } + + @Test + void testGetQuestionByIdThrowsExceptionWhenNotFound() { + int questionId = 1; + when(questionRepository.findById(questionId)).thenReturn(Optional.empty()); + + assertThrows(QuestionNotFoundException.class, () -> questionService.getQuestionById(questionId)); + verify(questionRepository, times(1)).findById(questionId); + } + + @Test + void testSaveQuestion() { + Quiz mockQuiz = new Quiz(); + mockQuiz.setQuizId(1); + Question mockQuestion = new Question(); + mockQuestion.setQuiz(mockQuiz); + + when(quizRepository.findById(any(Integer.class))).thenReturn(Optional.of(mockQuiz)); + when(questionRepository.save(any(Question.class))).thenReturn(mockQuestion); + + Question savedQuestion = questionService.saveQuestion(mockQuestion); + + assertEquals(mockQuestion, savedQuestion); + verify(questionRepository, times(1)).save(mockQuestion); + } + + @Test + void testSaveQuestionThrowsExceptionWhenQuizNotFound() { + + Quiz mockQuiz = new Quiz(); + mockQuiz.setQuizId(1); + Question mockQuestion = new Question(); + mockQuestion.setQuiz(mockQuiz); + when(quizRepository.findById(any(Integer.class))).thenReturn(Optional.empty()); + + assertThrows(IllegalArgumentException.class, () -> questionService.saveQuestion(mockQuestion)); + verify(questionRepository, never()).save(any(Question.class)); + } + + @Test + void testUpdateQuestion() { + Quiz mockQuiz = new Quiz(); + mockQuiz.setQuizId(1); + Question mockQuestion = new Question(); + mockQuestion.setQuiz(mockQuiz); + mockQuestion.setQuestionId(1); + + when(quizRepository.findById(any(Integer.class))).thenReturn(Optional.of(mockQuiz)); + when(questionRepository.findById(any(Integer.class))).thenReturn(Optional.of(mockQuestion)); + when(questionRepository.save(any(Question.class))).thenReturn(mockQuestion); + + Question updatedQuestion = questionService.updateQuestion(mockQuestion); + + assertEquals(mockQuestion, updatedQuestion); + verify(quizRepository, times(1)).findById(any(Integer.class)); + verify(questionRepository, times(1)).findById(any(Integer.class)); + verify(questionRepository, times(1)).save(any(Question.class)); + } + + @Test + void testUpdateQuestionThrowsExceptionWhenNotFound() { + Quiz mockQuiz = new Quiz(); + mockQuiz.setQuizId(1); + Question mockQuestion = new Question(); + mockQuestion.setQuiz(mockQuiz); + mockQuestion.setQuestionId(1); + + when(quizRepository.findById(any(Integer.class))).thenReturn(Optional.of(mockQuiz)); + when(questionRepository.findById(any(Integer.class))).thenReturn(Optional.empty()); + + assertThrows(QuestionNotFoundException.class, () -> questionService.updateQuestion(mockQuestion)); + verify(quizRepository, times(1)).findById(any(Integer.class)); + verify(questionRepository, times(1)).findById(any(Integer.class)); + verify(questionRepository, never()).save(any(Question.class)); + } + + + @Test + void testDeleteQuestion() { + int questionId = 1; + Question mockQuestion = new Question(); + when(questionRepository.findById(questionId)).thenReturn(Optional.of(mockQuestion)); + + questionService.deleteQuestion(questionId); + + verify(questionRepository, times(1)).deleteById(questionId); + } + + @Test + void testDeleteQuestionThrowsExceptionWhenNotFound() { + int questionId = 1; + when(questionRepository.findById(questionId)).thenReturn(Optional.empty()); + + assertThrows(QuestionNotFoundException.class, () -> questionService.deleteQuestion(questionId)); + verify(questionRepository, never()).deleteById(questionId); + } + + @Test + void testGetAllQuestions() { + List mockQuestions = List.of(new Question()); + when(questionRepository.findAll()).thenReturn(mockQuestions); + + List questions = questionService.getAllQuestions(); + + assertEquals(mockQuestions, questions); + verify(questionRepository, times(1)).findAll(); + } +}