From 1c23a2aa77f71e0e87243dcdb9a4f9e12def8d73 Mon Sep 17 00:00:00 2001 From: Subin Lee Date: Fri, 8 Aug 2025 18:51:05 +0900 Subject: [PATCH] =?UTF-8?q?Revert=20"chore=20:=20AI=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EA=B8=B8=EC=9D=B4=20=EA=B2=80=EC=A6=9D=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=A3=BC=EC=84=9D=20=EC=B2=98=EB=A6=AC=20(#359)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b26cd78bfdda6bea44cf77aa6bfaf7dc16981b5c. --- .../ai/service/AiFeedbackStreamProcessor.java | 11 +- .../example/cs25service/ai/AiServiceTest.java | 340 +++---- .../FallbackAiChatClientIntegrationTest.java | 220 ++-- .../admin/service/QuizAdminServiceTest.java | 960 +++++++++--------- .../service/UserQuizAnswerServiceTest.java | 678 ++++++------- 5 files changed, 1104 insertions(+), 1105 deletions(-) diff --git a/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/AiFeedbackStreamProcessor.java b/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/AiFeedbackStreamProcessor.java index 0ace826b..d46196bb 100644 --- a/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/AiFeedbackStreamProcessor.java +++ b/cs25-service/src/main/java/com/example/cs25service/domain/ai/service/AiFeedbackStreamProcessor.java @@ -4,8 +4,8 @@ import com.example.cs25entity.domain.user.repository.UserRepository; import com.example.cs25entity.domain.userQuizAnswer.repository.UserQuizAnswerRepository; import com.example.cs25service.domain.ai.client.AiChatClient; -//import com.example.cs25service.domain.ai.exception.AiException; -//import com.example.cs25service.domain.ai.exception.AiExceptionCode; +import com.example.cs25service.domain.ai.exception.AiException; +import com.example.cs25service.domain.ai.exception.AiExceptionCode; import com.example.cs25service.domain.ai.prompt.AiPromptProvider; import com.fasterxml.jackson.databind.JsonNode; import java.io.IOException; @@ -91,10 +91,9 @@ public void stream(Long answerId, SseEmitter emitter) { send(emitter, "[종료]"); String feedback = fullFeedbackBuffer.toString(); - //서비스 흐름 상 예외를 던지는 유효성 검증이 옳은지 논의 필요 -// if (feedback == null || feedback.isEmpty()) { -// throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR); -// } + if (feedback == null || feedback.isEmpty()) { + throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR); + } boolean isCorrect = isCorrect(feedback); diff --git a/cs25-service/src/test/java/com/example/cs25service/ai/AiServiceTest.java b/cs25-service/src/test/java/com/example/cs25service/ai/AiServiceTest.java index 7a17022e..bcb820c2 100644 --- a/cs25-service/src/test/java/com/example/cs25service/ai/AiServiceTest.java +++ b/cs25-service/src/test/java/com/example/cs25service/ai/AiServiceTest.java @@ -1,170 +1,170 @@ -//package com.example.cs25service.ai; -// -//import static org.assertj.core.api.Assertions.assertThat; -// -//import com.example.cs25entity.domain.quiz.entity.Quiz; -//import com.example.cs25entity.domain.quiz.entity.QuizCategory; -//import com.example.cs25entity.domain.quiz.enums.QuizFormatType; -//import com.example.cs25entity.domain.quiz.enums.QuizLevel; -//import com.example.cs25entity.domain.quiz.repository.QuizRepository; -//import com.example.cs25entity.domain.subscription.entity.Subscription; -//import com.example.cs25entity.domain.subscription.repository.SubscriptionRepository; -//import com.example.cs25entity.domain.user.repository.UserRepository; -//import com.example.cs25entity.domain.userQuizAnswer.entity.UserQuizAnswer; -//import com.example.cs25entity.domain.userQuizAnswer.repository.UserQuizAnswerRepository; -//import com.example.cs25service.domain.ai.client.AiChatClient; -//import com.example.cs25service.domain.ai.dto.response.AiFeedbackResponse; -//import com.example.cs25service.domain.ai.prompt.AiPromptProvider; -//import com.example.cs25service.domain.ai.service.AiFeedbackQueueService; -//import com.example.cs25service.domain.ai.service.AiFeedbackStreamWorker; -//import com.example.cs25service.domain.ai.service.AiService; -//import com.example.cs25service.domain.ai.service.RagService; -//import jakarta.persistence.EntityManager; -//import jakarta.persistence.PersistenceContext; -//import java.time.LocalDate; -// -//import org.junit.jupiter.api.*; -//import org.springframework.ai.chat.client.ChatClient; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.test.annotation.DirtiesContext; -//import org.springframework.transaction.annotation.Transactional; -//import static org.mockito.Mockito.mock; -// -//@SpringBootTest -//@Transactional -//@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) // 스프링 컨텍스트 리프레시 -//@Disabled -//public class AiServiceTest { -// @Autowired -// private AiService aiService; -// -// @Autowired -// private QuizRepository quizRepository; -// -// @Autowired -// private UserQuizAnswerRepository userQuizAnswerRepository; -// -// @Autowired -// private SubscriptionRepository subscriptionRepository; -// -// @Autowired -// private AiFeedbackStreamWorker aiFeedbackStreamWorker; -// -// @PersistenceContext -// private EntityManager em; -// -// private Quiz quiz; -// private Subscription memberSubscription; -// private Subscription guestSubscription; -// private UserQuizAnswer answerWithMember; -// private UserQuizAnswer answerWithGuest; -// -// @BeforeEach -// void setUp() { -// // 카테고리 생성 -// QuizCategory quizCategory = new QuizCategory("BACKEND", null); -// em.persist(quizCategory); -// -// // 퀴즈 생성 -// quiz = Quiz.builder() -// .type(QuizFormatType.SUBJECTIVE) -// .question("HTTP와 HTTPS의 차이점을 설명하세요.") -// .answer("HTTPS는 암호화, HTTP는 암호화X") -// .commentary("HTTPS는 SSL/TLS로 암호화되어 보안성이 높다.") -// .choice(null) -// .category(quizCategory) -// .level(QuizLevel.EASY) -// .build(); -// quizRepository.save(quiz); -// -// // 구독 생성 (회원, 비회원) -// memberSubscription = Subscription.builder() -// .email("test@example.com") -// .startDate(LocalDate.now()) -// .endDate(LocalDate.now().plusDays(30)) -// .subscriptionType(Subscription.decodeDays(0b1111111)) -// .build(); -// subscriptionRepository.save(memberSubscription); -// -// guestSubscription = Subscription.builder() -// .email("guest@example.com") -// .startDate(LocalDate.now()) -// .endDate(LocalDate.now().plusDays(7)) -// .subscriptionType(Subscription.decodeDays(0b1111111)) -// .build(); -// subscriptionRepository.save(guestSubscription); -// -// // 사용자 답변 생성 -// answerWithMember = UserQuizAnswer.builder() -// .userAnswer("HTTP는 암호화가 없고, HTTPS는 암호화로 보안성이 높아요.") -// .subscription(memberSubscription) -// .isCorrect(null) -// .quiz(quiz) -// .build(); -// userQuizAnswerRepository.save(answerWithMember); -// -// answerWithGuest = UserQuizAnswer.builder() -// .userAnswer("HTTP는 암호화가 없고, HTTPS는 암호화로 보안성이 높아요.") -// .subscription(guestSubscription) -// .isCorrect(null) -// .quiz(quiz) -// .build(); -// userQuizAnswerRepository.save(answerWithGuest); -// -// } -// -// @Test -// void testGetFeedbackForMember() { -// AiFeedbackResponse response = aiService.getFeedback(answerWithMember.getId()); -// -// assertThat(response).isNotNull(); -// assertThat(response.getQuizId()).isEqualTo(quiz.getId()); -// assertThat(response.getQuizAnswerId()).isEqualTo(answerWithMember.getId()); -// assertThat(response.getAiFeedback()).isNotBlank(); -// -// var updated = userQuizAnswerRepository.findById(answerWithMember.getId()).orElseThrow(); -// assertThat(updated.getAiFeedback()).isEqualTo(response.getAiFeedback()); -// assertThat(updated.getIsCorrect()).isNotNull(); -// -// System.out.println("[회원 구독] AI 피드백:\n" + response.getAiFeedback()); -// } -// -// @Test -// void testGetFeedbackForGuest() { -// AiFeedbackResponse response = aiService.getFeedback(answerWithGuest.getId()); -// -// assertThat(response).isNotNull(); -// assertThat(response.getQuizId()).isEqualTo(quiz.getId()); -// assertThat(response.getQuizAnswerId()).isEqualTo(answerWithGuest.getId()); -// assertThat(response.getAiFeedback()).isNotBlank(); -// -// var updated = userQuizAnswerRepository.findById(answerWithGuest.getId()).orElseThrow(); -// assertThat(updated.getAiFeedback()).isEqualTo(response.getAiFeedback()); -// assertThat(updated.getIsCorrect()).isNotNull(); -// -// System.out.println("[비회원 구독] AI 피드백:\n" + response.getAiFeedback()); -// } -// -// @Test -// @DisplayName("6글자 이내에 정답이 포함된 경우 true 반환") -// void testIfAiFeedbackIsCorrectThenReturnTrue(){ -// assertThat(aiService.isCorrect("- 정답 : 당신의 답은 완벽합니다.")).isTrue(); -// assertThat(aiService.isCorrect("정답 : 당신의 답은 완벽합니다.")).isTrue(); -// assertThat(aiService.isCorrect("정답입니다. 당신의 답은 완벽합니다.")).isTrue(); -// } -// -// @Test -// @DisplayName("오답인 경우 false 반환") -// void testIfAiFeedbackIsWrongThenReturnfalse(){ -// assertThat(aiService.isCorrect("- 오답 : 당신의 답은 완벽합니다.")).isFalse(); -// assertThat(aiService.isCorrect("오답 : 당신의 답은 완벽합니다.")).isFalse(); -// assertThat(aiService.isCorrect("오답입니다. 당신의 답은 완벽합니다.")).isFalse(); -// assertThat(aiService.isCorrect("오답: 정답이라고 하기에는 부족합니다.")).isFalse(); -// } -// -// @AfterEach -// void tearDown() { -// aiFeedbackStreamWorker.stop(); -// } -//} +package com.example.cs25service.ai; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.cs25entity.domain.quiz.entity.Quiz; +import com.example.cs25entity.domain.quiz.entity.QuizCategory; +import com.example.cs25entity.domain.quiz.enums.QuizFormatType; +import com.example.cs25entity.domain.quiz.enums.QuizLevel; +import com.example.cs25entity.domain.quiz.repository.QuizRepository; +import com.example.cs25entity.domain.subscription.entity.Subscription; +import com.example.cs25entity.domain.subscription.repository.SubscriptionRepository; +import com.example.cs25entity.domain.user.repository.UserRepository; +import com.example.cs25entity.domain.userQuizAnswer.entity.UserQuizAnswer; +import com.example.cs25entity.domain.userQuizAnswer.repository.UserQuizAnswerRepository; +import com.example.cs25service.domain.ai.client.AiChatClient; +import com.example.cs25service.domain.ai.dto.response.AiFeedbackResponse; +import com.example.cs25service.domain.ai.prompt.AiPromptProvider; +import com.example.cs25service.domain.ai.service.AiFeedbackQueueService; +import com.example.cs25service.domain.ai.service.AiFeedbackStreamWorker; +import com.example.cs25service.domain.ai.service.AiService; +import com.example.cs25service.domain.ai.service.RagService; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.time.LocalDate; + +import org.junit.jupiter.api.*; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.transaction.annotation.Transactional; +import static org.mockito.Mockito.mock; + +@SpringBootTest +@Transactional +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) // 스프링 컨텍스트 리프레시 +@Disabled +public class AiServiceTest { + @Autowired + private AiService aiService; + + @Autowired + private QuizRepository quizRepository; + + @Autowired + private UserQuizAnswerRepository userQuizAnswerRepository; + + @Autowired + private SubscriptionRepository subscriptionRepository; + + @Autowired + private AiFeedbackStreamWorker aiFeedbackStreamWorker; + + @PersistenceContext + private EntityManager em; + + private Quiz quiz; + private Subscription memberSubscription; + private Subscription guestSubscription; + private UserQuizAnswer answerWithMember; + private UserQuizAnswer answerWithGuest; + + @BeforeEach + void setUp() { + // 카테고리 생성 + QuizCategory quizCategory = new QuizCategory("BACKEND", null); + em.persist(quizCategory); + + // 퀴즈 생성 + quiz = Quiz.builder() + .type(QuizFormatType.SUBJECTIVE) + .question("HTTP와 HTTPS의 차이점을 설명하세요.") + .answer("HTTPS는 암호화, HTTP는 암호화X") + .commentary("HTTPS는 SSL/TLS로 암호화되어 보안성이 높다.") + .choice(null) + .category(quizCategory) + .level(QuizLevel.EASY) + .build(); + quizRepository.save(quiz); + + // 구독 생성 (회원, 비회원) + memberSubscription = Subscription.builder() + .email("test@example.com") + .startDate(LocalDate.now()) + .endDate(LocalDate.now().plusDays(30)) + .subscriptionType(Subscription.decodeDays(0b1111111)) + .build(); + subscriptionRepository.save(memberSubscription); + + guestSubscription = Subscription.builder() + .email("guest@example.com") + .startDate(LocalDate.now()) + .endDate(LocalDate.now().plusDays(7)) + .subscriptionType(Subscription.decodeDays(0b1111111)) + .build(); + subscriptionRepository.save(guestSubscription); + + // 사용자 답변 생성 + answerWithMember = UserQuizAnswer.builder() + .userAnswer("HTTP는 암호화가 없고, HTTPS는 암호화로 보안성이 높아요.") + .subscription(memberSubscription) + .isCorrect(null) + .quiz(quiz) + .build(); + userQuizAnswerRepository.save(answerWithMember); + + answerWithGuest = UserQuizAnswer.builder() + .userAnswer("HTTP는 암호화가 없고, HTTPS는 암호화로 보안성이 높아요.") + .subscription(guestSubscription) + .isCorrect(null) + .quiz(quiz) + .build(); + userQuizAnswerRepository.save(answerWithGuest); + + } + + @Test + void testGetFeedbackForMember() { + AiFeedbackResponse response = aiService.getFeedback(answerWithMember.getId()); + + assertThat(response).isNotNull(); + assertThat(response.getQuizId()).isEqualTo(quiz.getId()); + assertThat(response.getQuizAnswerId()).isEqualTo(answerWithMember.getId()); + assertThat(response.getAiFeedback()).isNotBlank(); + + var updated = userQuizAnswerRepository.findById(answerWithMember.getId()).orElseThrow(); + assertThat(updated.getAiFeedback()).isEqualTo(response.getAiFeedback()); + assertThat(updated.getIsCorrect()).isNotNull(); + + System.out.println("[회원 구독] AI 피드백:\n" + response.getAiFeedback()); + } + + @Test + void testGetFeedbackForGuest() { + AiFeedbackResponse response = aiService.getFeedback(answerWithGuest.getId()); + + assertThat(response).isNotNull(); + assertThat(response.getQuizId()).isEqualTo(quiz.getId()); + assertThat(response.getQuizAnswerId()).isEqualTo(answerWithGuest.getId()); + assertThat(response.getAiFeedback()).isNotBlank(); + + var updated = userQuizAnswerRepository.findById(answerWithGuest.getId()).orElseThrow(); + assertThat(updated.getAiFeedback()).isEqualTo(response.getAiFeedback()); + assertThat(updated.getIsCorrect()).isNotNull(); + + System.out.println("[비회원 구독] AI 피드백:\n" + response.getAiFeedback()); + } + + @Test + @DisplayName("6글자 이내에 정답이 포함된 경우 true 반환") + void testIfAiFeedbackIsCorrectThenReturnTrue(){ + assertThat(aiService.isCorrect("- 정답 : 당신의 답은 완벽합니다.")).isTrue(); + assertThat(aiService.isCorrect("정답 : 당신의 답은 완벽합니다.")).isTrue(); + assertThat(aiService.isCorrect("정답입니다. 당신의 답은 완벽합니다.")).isTrue(); + } + + @Test + @DisplayName("오답인 경우 false 반환") + void testIfAiFeedbackIsWrongThenReturnfalse(){ + assertThat(aiService.isCorrect("- 오답 : 당신의 답은 완벽합니다.")).isFalse(); + assertThat(aiService.isCorrect("오답 : 당신의 답은 완벽합니다.")).isFalse(); + assertThat(aiService.isCorrect("오답입니다. 당신의 답은 완벽합니다.")).isFalse(); + assertThat(aiService.isCorrect("오답: 정답이라고 하기에는 부족합니다.")).isFalse(); + } + + @AfterEach + void tearDown() { + aiFeedbackStreamWorker.stop(); + } +} diff --git a/cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientIntegrationTest.java b/cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientIntegrationTest.java index 46f8de81..87095b4a 100644 --- a/cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientIntegrationTest.java +++ b/cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientIntegrationTest.java @@ -1,110 +1,110 @@ -//package com.example.cs25service.ai; -// -//import static org.assertj.core.api.Assertions.assertThat; -// -//import com.example.cs25entity.domain.quiz.entity.Quiz; -//import com.example.cs25entity.domain.quiz.entity.QuizCategory; -//import com.example.cs25entity.domain.quiz.enums.QuizFormatType; -//import com.example.cs25entity.domain.quiz.enums.QuizLevel; -//import com.example.cs25entity.domain.subscription.entity.Subscription; -//import com.example.cs25entity.domain.user.entity.Role; -//import com.example.cs25entity.domain.user.entity.SocialType; -//import com.example.cs25entity.domain.user.entity.User; -//import com.example.cs25entity.domain.userQuizAnswer.entity.UserQuizAnswer; -//import com.example.cs25entity.domain.userQuizAnswer.repository.UserQuizAnswerRepository; -//import com.example.cs25service.domain.ai.service.AiFeedbackStreamWorker; -//import com.example.cs25service.domain.ai.service.AiService; -//import jakarta.persistence.EntityManager; -//import jakarta.persistence.PersistenceContext; -//import java.time.LocalDate; -//import java.util.Set; -// -//import org.junit.jupiter.api.*; -//import org.springframework.ai.chat.client.ChatClient; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.test.annotation.DirtiesContext; -//import org.springframework.transaction.annotation.Transactional; -// -//@SpringBootTest -//@Transactional -//@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -//@Disabled -//class FallbackAiChatClientIntegrationTest { -// -// @Autowired -// private AiService aiService; -// -// @Autowired -// private UserQuizAnswerRepository userQuizAnswerRepository; -// -// @PersistenceContext -// private EntityManager em; -// -// @Autowired -// private AiFeedbackStreamWorker aiFeedbackStreamWorker; -// -// @Test -// @DisplayName("OpenAI 호출 실패 시 Claude로 폴백하여 피드백 생성한다") -// void openAiFail_thenUseClaudeFeedback() { -// // given - 기본 퀴즈, 사용자, 정답 생성 -// QuizCategory category = QuizCategory.builder() -// .categoryType("네트워크") -// .parent(null) -// .build(); -// em.persist(category); -// -// Quiz quiz = Quiz.builder() -// .type(QuizFormatType.SUBJECTIVE) -// .question("HTTP와 HTTPS의 차이를 설명하시오.") -// .answer("HTTPS는 보안이 강화된 프로토콜이다.") -// .commentary("HTTPS는 SSL/TLS를 통해 데이터 암호화를 제공한다.") -// .category(category) -// .level(QuizLevel.NORMAL) -// .build(); -// em.persist(quiz); -// -// Subscription subscription = Subscription.builder() -// .category(category) -// .email("fallback@test.com") -// .startDate(LocalDate.now().minusDays(1)) -// .endDate(LocalDate.now().plusDays(30)) -// .subscriptionType(Set.of()) -// .build(); -// em.persist(subscription); -// -// User user = User.builder() -// .email("fallback@test.com") -// .name("fallback_user") -// .socialType(SocialType.KAKAO) -// .role(Role.USER) -// .subscription(subscription) -// .build(); -// em.persist(user); -// -// UserQuizAnswer answer = UserQuizAnswer.builder() -// .user(user) -// .quiz(quiz) -// .userAnswer("HTTPS는 HTTP보다 빠르다.") -// .aiFeedback(null) -// .isCorrect(null) -// .subscription(subscription) -// .build(); -// em.persist(answer); -// -// // when - AI 피드백 호출 -// var response = aiService.getFeedback(answer.getId()); -// -// // then - Claude로부터 받은 피드백이 저장됨 -// UserQuizAnswer updated = userQuizAnswerRepository.findById(answer.getId()).orElseThrow(); -// -// assertThat(updated.getAiFeedback()).isNotBlank(); -// assertThat(updated.getIsCorrect()).isNotNull(); -// System.out.println("📢 Claude 기반 피드백: " + updated.getAiFeedback()); -// } -// -// @AfterEach -// void tearDown() { -// aiFeedbackStreamWorker.stop(); -// } -//} \ No newline at end of file +package com.example.cs25service.ai; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.cs25entity.domain.quiz.entity.Quiz; +import com.example.cs25entity.domain.quiz.entity.QuizCategory; +import com.example.cs25entity.domain.quiz.enums.QuizFormatType; +import com.example.cs25entity.domain.quiz.enums.QuizLevel; +import com.example.cs25entity.domain.subscription.entity.Subscription; +import com.example.cs25entity.domain.user.entity.Role; +import com.example.cs25entity.domain.user.entity.SocialType; +import com.example.cs25entity.domain.user.entity.User; +import com.example.cs25entity.domain.userQuizAnswer.entity.UserQuizAnswer; +import com.example.cs25entity.domain.userQuizAnswer.repository.UserQuizAnswerRepository; +import com.example.cs25service.domain.ai.service.AiFeedbackStreamWorker; +import com.example.cs25service.domain.ai.service.AiService; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.time.LocalDate; +import java.util.Set; + +import org.junit.jupiter.api.*; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@Disabled +class FallbackAiChatClientIntegrationTest { + + @Autowired + private AiService aiService; + + @Autowired + private UserQuizAnswerRepository userQuizAnswerRepository; + + @PersistenceContext + private EntityManager em; + + @Autowired + private AiFeedbackStreamWorker aiFeedbackStreamWorker; + + @Test + @DisplayName("OpenAI 호출 실패 시 Claude로 폴백하여 피드백 생성한다") + void openAiFail_thenUseClaudeFeedback() { + // given - 기본 퀴즈, 사용자, 정답 생성 + QuizCategory category = QuizCategory.builder() + .categoryType("네트워크") + .parent(null) + .build(); + em.persist(category); + + Quiz quiz = Quiz.builder() + .type(QuizFormatType.SUBJECTIVE) + .question("HTTP와 HTTPS의 차이를 설명하시오.") + .answer("HTTPS는 보안이 강화된 프로토콜이다.") + .commentary("HTTPS는 SSL/TLS를 통해 데이터 암호화를 제공한다.") + .category(category) + .level(QuizLevel.NORMAL) + .build(); + em.persist(quiz); + + Subscription subscription = Subscription.builder() + .category(category) + .email("fallback@test.com") + .startDate(LocalDate.now().minusDays(1)) + .endDate(LocalDate.now().plusDays(30)) + .subscriptionType(Set.of()) + .build(); + em.persist(subscription); + + User user = User.builder() + .email("fallback@test.com") + .name("fallback_user") + .socialType(SocialType.KAKAO) + .role(Role.USER) + .subscription(subscription) + .build(); + em.persist(user); + + UserQuizAnswer answer = UserQuizAnswer.builder() + .user(user) + .quiz(quiz) + .userAnswer("HTTPS는 HTTP보다 빠르다.") + .aiFeedback(null) + .isCorrect(null) + .subscription(subscription) + .build(); + em.persist(answer); + + // when - AI 피드백 호출 + var response = aiService.getFeedback(answer.getId()); + + // then - Claude로부터 받은 피드백이 저장됨 + UserQuizAnswer updated = userQuizAnswerRepository.findById(answer.getId()).orElseThrow(); + + assertThat(updated.getAiFeedback()).isNotBlank(); + assertThat(updated.getIsCorrect()).isNotNull(); + System.out.println("📢 Claude 기반 피드백: " + updated.getAiFeedback()); + } + + @AfterEach + void tearDown() { + aiFeedbackStreamWorker.stop(); + } +} \ No newline at end of file diff --git a/cs25-service/src/test/java/com/example/cs25service/domain/admin/service/QuizAdminServiceTest.java b/cs25-service/src/test/java/com/example/cs25service/domain/admin/service/QuizAdminServiceTest.java index 33da1fda..56c6f274 100644 --- a/cs25-service/src/test/java/com/example/cs25service/domain/admin/service/QuizAdminServiceTest.java +++ b/cs25-service/src/test/java/com/example/cs25service/domain/admin/service/QuizAdminServiceTest.java @@ -1,480 +1,480 @@ -//package com.example.cs25service.domain.admin.service; -// -//import static org.assertj.core.api.Assertions.assertThat; -//import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -//import static org.mockito.ArgumentMatchers.any; -//import static org.mockito.ArgumentMatchers.anyList; -//import static org.mockito.ArgumentMatchers.eq; -//import static org.mockito.BDDMockito.given; -//import static org.mockito.BDDMockito.then; -//import static org.mockito.Mockito.mock; -//import static org.mockito.Mockito.times; -// -//import com.example.cs25entity.domain.quiz.entity.Quiz; -//import com.example.cs25entity.domain.quiz.entity.QuizCategory; -//import com.example.cs25entity.domain.quiz.enums.QuizFormatType; -//import com.example.cs25entity.domain.quiz.exception.QuizException; -//import com.example.cs25entity.domain.quiz.exception.QuizExceptionCode; -//import com.example.cs25entity.domain.quiz.repository.QuizCategoryRepository; -//import com.example.cs25entity.domain.quiz.repository.QuizRepository; -//import com.example.cs25entity.domain.userQuizAnswer.repository.UserQuizAnswerRepository; -//import com.example.cs25service.domain.admin.dto.request.CreateQuizDto; -//import com.example.cs25service.domain.admin.dto.request.QuizCreateRequestDto; -//import com.example.cs25service.domain.admin.dto.request.QuizUpdateRequestDto; -//import com.example.cs25service.domain.admin.dto.response.QuizDetailDto; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import jakarta.validation.ConstraintViolation; -//import jakarta.validation.Validator; -//import java.io.IOException; -//import java.io.InputStream; -//import java.util.Collections; -//import java.util.List; -//import java.util.Set; -//import org.junit.jupiter.api.BeforeEach; -//import org.junit.jupiter.api.DisplayName; -//import org.junit.jupiter.api.Nested; -//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 org.springframework.data.domain.Page; -//import org.springframework.data.domain.PageImpl; -//import org.springframework.data.domain.Pageable; -//import org.springframework.mock.web.MockMultipartFile; -//import org.springframework.test.util.ReflectionTestUtils; -// -//@ExtendWith(MockitoExtension.class) -//class QuizAdminServiceTest { -// -// @InjectMocks -// private QuizAdminService quizAdminService; -// -// @Mock -// private QuizRepository quizRepository; -// -// @Mock -// private UserQuizAnswerRepository quizAnswerRepository; -// -// @Mock -// private QuizCategoryRepository quizCategoryRepository; -// -// @Mock -// private ObjectMapper objectMapper; -// -// @Mock -// private Validator validator; -// -// QuizCategory parentCategory; -// QuizCategory subCategory1; -// -// @BeforeEach -// void setUp() { -// // 상위 카테고리와 하위 카테고리 mock -// parentCategory = QuizCategory.builder() -// .categoryType("Backend") -// .build(); -// -// subCategory1 = QuizCategory.builder() -// .categoryType("InformationSystemManagement") -// .parent(parentCategory) -// .build(); -// -// ReflectionTestUtils.setField(parentCategory, "children", List.of(subCategory1)); -// } -// -// -// @Nested -// @DisplayName("uploadQuizJson 함수는") -// class inUploadQuizJson { -// -// @Test -// @DisplayName("정상작동_시_퀴즈가저장된다") -// void uploadQuizJson_success() throws Exception { -// // given -// String categoryType = "Backend"; -// QuizFormatType formatType = QuizFormatType.MULTIPLE_CHOICE; -// -// // JSON을 담은 가짜 파일 생성 -// String json = """ -// [ -// { -// "question": "HTTP는 상태를 유지한다.", -// "choice": "1.예/2.아니오", -// "answer": "2", -// "commentary": "HTTP는 무상태 프로토콜입니다.", -// "category": "InformationSystemManagement", -// "level": "EASY" -// } -// ] -// """; -// -// MockMultipartFile file = new MockMultipartFile("file", "quiz.json", "application/json", -// json.getBytes()); -// -// // CreateQuizDto mock -// CreateQuizDto quizDto = CreateQuizDto.builder() -// .question("HTTP는 상태를 유지한다.") -// .choice("1.예/2.아니오") -// .answer("2") -// .commentary("HTTP는 무상태 프로토콜입니다.") -// .category("InformationSystemManagement") -// .level("EASY") -// .build(); -// -// CreateQuizDto[] quizDtos = {quizDto}; -// -// given(quizCategoryRepository.findByCategoryTypeOrElseThrow("Backend")) -// .willReturn(parentCategory); -// -// given(objectMapper.readValue(any(InputStream.class), eq(CreateQuizDto[].class))) -// .willReturn(quizDtos); -// -// given(validator.validate(any(CreateQuizDto.class))) -// .willReturn(Collections.emptySet()); -// -// // when -// quizAdminService.uploadQuizJson(file, categoryType, formatType); -// -// // then -// then(quizRepository).should(times(1)).saveAll(anyList()); -// } -// -// @Test -// @DisplayName("JSON_파싱_실패_시_예외발생") -// void uploadQuizJson_JSON_PARSING_FAILED_ERROR() throws Exception { -// // given -// MockMultipartFile file = new MockMultipartFile("file", "quiz.json", "application/json", -// "invalid".getBytes()); -// -// given(quizCategoryRepository.findByCategoryTypeOrElseThrow("Backend")) -// .willReturn(parentCategory); -// -// given(objectMapper.readValue(any(InputStream.class), eq(CreateQuizDto[].class))) -// .willThrow(new IOException("파싱 오류")); -// -// // when & then -// assertThatThrownBy(() -> -// quizAdminService.uploadQuizJson(file, "Backend", QuizFormatType.MULTIPLE_CHOICE) -// ).isInstanceOf(QuizException.class) -// .hasMessageContaining("JSON 파싱 실패"); -// } -// -// @Test -// @DisplayName("유효성 검증 실패 시 예외발생 한다") -// void uploadQuizJson_QUIZ_VALIDATION_FAILED_ERROR() throws Exception { -// // given -// CreateQuizDto quizDto = CreateQuizDto.builder() -// .question(null) // 필수값 빠짐 -// .choice("1.예/2.아니오") -// .answer("2") -// .category("Infra") -// .level("EASY") -// .build(); -// -// CreateQuizDto[] quizDtos = {quizDto}; -// -// MockMultipartFile file = new MockMultipartFile("file", "quiz.json", "application/json", -// "any".getBytes()); -// -// given(quizCategoryRepository.findByCategoryTypeOrElseThrow("Backend")) -// .willReturn(parentCategory); -// given(objectMapper.readValue(any(InputStream.class), eq(CreateQuizDto[].class))) -// .willReturn(quizDtos); -// -// // 검증 실패 set -// Set> violations = Set.of( -// mock(ConstraintViolation.class)); -// given(validator.validate(any(CreateQuizDto.class))) -// .willReturn(violations); -// -// // when & then -// assertThatThrownBy(() -> -// quizAdminService.uploadQuizJson(file, "Backend", QuizFormatType.MULTIPLE_CHOICE) -// ).isInstanceOf(QuizException.class) -// .hasMessageContaining("Quiz 유효성 검증 실패"); -// } -// } -// -// @Nested -// @DisplayName("getAdminQuizDetails 함수는") -// class inGetAdminQuizDetails { -// -// @Test -// @DisplayName("정상 작동 시 퀴즈리스트를 반환한다") -// void getAdminQuizDetails_success() { -// // given -// Quiz quiz = Quiz.builder() -// .question("Spring이란?") -// .answer("프레임워크") -// .commentary("스프링은 프레임워크입니다.") -// .choice(null) -// .type(QuizFormatType.MULTIPLE_CHOICE) -// .category(QuizCategory.builder().categoryType("SoftwareDevelopment") -// .parent(parentCategory).build()) -// .build(); -// ReflectionTestUtils.setField(quiz, "id", 1L); -// -// Page quizPage = new PageImpl<>(List.of(quiz)); -// -// given(quizRepository.findAllOrderByCreatedAtDesc(any(Pageable.class))) -// .willReturn(quizPage); -// given(quizAnswerRepository.countByQuizId(1L)) -// .willReturn(3L); -// -// // when -// Page result = quizAdminService.getAdminQuizDetails(1, 10); -// -// // then -// assertThat(result).hasSize(1); -// QuizDetailDto dto = result.getContent().get(0); -// assertThat(dto.getQuestion()).isEqualTo("Spring이란?"); -// assertThat(dto.getAnswer()).isEqualTo("프레임워크"); -// assertThat(dto.getSolvedCnt()).isEqualTo(3L); -// } -// } -// -// @Nested -// @DisplayName("getAdminQuizDetail 함수는") -// class inGetAdminQuizDetail { -// -// @Test -// @DisplayName("정상 작동 시 퀴즈리스트를 반환한다") -// void getAdminQuizDetail_success() { -// // given -// Long quizId = 1L; -// -// Quiz quiz = Quiz.builder() -// .question("REST란?") -// .answer("자원 기반 아키텍처") -// .commentary("HTTP URI를 통해 자원을 명확히 구분합니다.") -// .choice(null) -// .type(QuizFormatType.MULTIPLE_CHOICE) -// .category(QuizCategory.builder().categoryType("SoftwareDevelopment") -// .parent(parentCategory).build()) -// .build(); -// ReflectionTestUtils.setField(quiz, "id", 1L); -// -// given(quizRepository.findByIdOrElseThrow(quizId)).willReturn(quiz); -// given(quizAnswerRepository.countByQuizId(quizId)).willReturn(5L); -// -// // when -// QuizDetailDto result = quizAdminService.getAdminQuizDetail(quizId); -// -// // then -// assertThat(result.getQuizId()).isEqualTo(quizId); -// assertThat(result.getQuestion()).isEqualTo("REST란?"); -// assertThat(result.getAnswer()).isEqualTo("자원 기반 아키텍처"); -// assertThat(result.getSolvedCnt()).isEqualTo(5L); -// } -// -// @Test -// @DisplayName("없는_id면_예외가 발생한다.") -// void getAdminQuizDetail_NOT_FOUND_ERROR() { -// // given -// Long quizId = 999L; -// -// given(quizRepository.findByIdOrElseThrow(quizId)) -// .willThrow(new QuizException(QuizExceptionCode.NOT_FOUND_ERROR)); -// -// // when & then -// assertThatThrownBy(() -> quizAdminService.getAdminQuizDetail(quizId)) -// .isInstanceOf(QuizException.class) -// .hasMessageContaining("해당 퀴즈를 찾을 수 없습니다"); -// } -// } -// -// @Nested -// @DisplayName("createQuiz 함수는") -// class inCreateQuiz { -// -// QuizCreateRequestDto requestDto = new QuizCreateRequestDto(); -// -// @BeforeEach -// void setUp() { -// ReflectionTestUtils.setField(requestDto, "question", "REST란?"); -// ReflectionTestUtils.setField(requestDto, "category", subCategory1.getCategoryType()); -// ReflectionTestUtils.setField(requestDto, "choice", null); -// ReflectionTestUtils.setField(requestDto, "answer", "자원 기반 아키텍처"); -// ReflectionTestUtils.setField(requestDto, "commentary", "HTTP URI를 통해 자원을 명확히 구분합니다."); -// ReflectionTestUtils.setField(requestDto, "quizType", QuizFormatType.SUBJECTIVE); -// } -// -// @Test -// @DisplayName("정상 작동 시 퀴즈ID를 반환 한다") -// void createQuiz_success() { -// // given -// -// Quiz savedQuiz = Quiz.builder() -// .category(subCategory1) -// .question(requestDto.getQuestion()) -// .answer(requestDto.getAnswer()) -// .choice(requestDto.getChoice()) -// .commentary(requestDto.getCommentary()) -// .build(); -// ReflectionTestUtils.setField(savedQuiz, "id", 1L); -// -// given( -// quizCategoryRepository.findByCategoryTypeOrElseThrow("InformationSystemManagement")) -// .willReturn(subCategory1); -// -// given(quizRepository.save(any(Quiz.class))) -// .willReturn(savedQuiz); -// -// // when -// Long resultId = quizAdminService.createQuiz(requestDto); -// -// // then -// assertThat(resultId).isEqualTo(1L); -// } -// -// @Test -// @DisplayName("카테고리가 없으면 예외가 발생한다") -// void createQuiz_QUIZ_CATEGORY_NOT_FOUND_ERROR() { -// // given -// ReflectionTestUtils.setField(requestDto, "category", "NonExist"); -// -// given(quizCategoryRepository.findByCategoryTypeOrElseThrow("NonExist")) -// .willThrow(new QuizException(QuizExceptionCode.QUIZ_CATEGORY_NOT_FOUND_ERROR)); -// -// // when & then -// assertThatThrownBy(() -> quizAdminService.createQuiz(requestDto)) -// .isInstanceOf(QuizException.class) -// .hasMessageContaining("QuizCategory 를 찾을 수 없습니다"); -// } -// } -// -// @Nested -// @DisplayName("updateQuiz 함수는") -// class inUpdateQuiz { -// -// QuizUpdateRequestDto requestDto = new QuizUpdateRequestDto(); -// -// @Test -// @DisplayName("모든 필드를 정상적으로 업데이트하면 DTO를 반환한다") -// void updateQuiz_success() { -// // given -// Long quizId = 1L; -// Quiz quiz = createSampleQuiz(); -// ReflectionTestUtils.setField(quiz, "id", quizId); -// -// ReflectionTestUtils.setField(requestDto, "question", "기존 문제"); -// ReflectionTestUtils.setField(requestDto, "category", subCategory1.getCategoryType()); -// ReflectionTestUtils.setField(requestDto, "choice", null); -// ReflectionTestUtils.setField(requestDto, "answer", "1"); -// ReflectionTestUtils.setField(requestDto, "commentary", "기존 해설"); -// ReflectionTestUtils.setField(requestDto, "quizType", QuizFormatType.SUBJECTIVE); -// -// given(quizRepository.findByIdOrElseThrow(quizId)).willReturn(quiz); -// given(quizCategoryRepository.findByCategoryTypeOrElseThrow( -// "InformationSystemManagement")).willReturn(subCategory1); -// given(quizAnswerRepository.countByQuizId(quizId)).willReturn(5L); -// -// // when -// QuizDetailDto result = quizAdminService.updateQuiz(quizId, requestDto); -// -// // then -// assertThat(result.getQuestion()).isEqualTo("기존 문제"); -// assertThat(result.getCommentary()).isEqualTo("기존 해설"); -// assertThat(result.getCategory()).isEqualTo("InformationSystemManagement"); -// assertThat(result.getChoice()).isEqualTo(null); -// assertThat(result.getType()).isEqualTo("SUBJECTIVE"); -// assertThat(result.getSolvedCnt()).isEqualTo(5L); -// } -// -// @Test -// @DisplayName("카테고리만 변경되면 category 만 업데이트된다") -// void updateQuiz_category_success() { -// // given -// Long quizId = 1L; -// Quiz quiz = createSampleQuiz(); -// ReflectionTestUtils.setField(quiz, "id", quizId); -// ReflectionTestUtils.setField(requestDto, "category", "Programming"); -// -// QuizCategory newCategory = QuizCategory.builder() -// .categoryType("Programming") -// .parent(parentCategory) -// .build(); -// -// ReflectionTestUtils.setField(parentCategory, "children", -// List.of(subCategory1, newCategory)); -// -// given(quizRepository.findByIdOrElseThrow(quizId)).willReturn(quiz); -// given(quizCategoryRepository.findByCategoryTypeOrElseThrow("Programming")).willReturn( -// newCategory); -// given(quizAnswerRepository.countByQuizId(quizId)).willReturn(0L); -// -// // when -// QuizDetailDto result = quizAdminService.updateQuiz(quizId, requestDto); -// -// // then -// assertThat(result.getCategory()).isEqualTo("Programming"); -// } -// -// @Test -// @DisplayName("존재하지 않는 퀴즈 ID면 예외가 발생한다") -// void updateQuiz_NOT_FOUND_ERROR() { -// // given -// Long quizId = 999L; -// -// ReflectionTestUtils.setField(requestDto, "question", "변경된 질문121"); -// -// given(quizRepository.findByIdOrElseThrow(quizId)) -// .willThrow(new QuizException(QuizExceptionCode.NOT_FOUND_ERROR)); -// -// // when & then -// assertThatThrownBy(() -> quizAdminService.updateQuiz(quizId, requestDto)) -// .isInstanceOf(QuizException.class) -// .hasMessageContaining("해당 퀴즈를 찾을 수 없습니다"); -// } -// -// @Test -// @DisplayName("존재하지 않는 카테고리면 예외가 발생한다") -// void updateQuiz_QUIZ_CATEGORY_NOT_FOUND_ERROR() { -// // given -// Long quizId = 1L; -// Quiz quiz = createSampleQuiz(); -// ReflectionTestUtils.setField(quiz, "id", quizId); -// ReflectionTestUtils.setField(requestDto, "category", "NonExist"); -// -// given(quizRepository.findByIdOrElseThrow(quizId)).willReturn(quiz); -// given(quizCategoryRepository.findByCategoryTypeOrElseThrow("NonExist")) -// .willThrow(new QuizException(QuizExceptionCode.QUIZ_CATEGORY_NOT_FOUND_ERROR)); -// -// // when & then -// assertThatThrownBy(() -> quizAdminService.updateQuiz(quizId, requestDto)) -// .isInstanceOf(QuizException.class) -// .hasMessageContaining("QuizCategory 를 찾을 수 없습니다"); -// } -// -// @Test -// @DisplayName("퀴즈 타입을 MULTIPLE_CHOICE로 변경하려는데 choice가 없으면 예외 발생") -// void updateQuiz_MULTIPLE_CHOICE_REQUIRE_ERROR() { -// // given -// Long quizId = 1L; -// Quiz quiz = createSampleQuiz(); -// ReflectionTestUtils.setField(quiz, "id", quizId); -// ReflectionTestUtils.setField(requestDto, "quizType", QuizFormatType.MULTIPLE_CHOICE); -// -// given(quizRepository.findByIdOrElseThrow(quizId)).willReturn(quiz); -// -// // when & then -// assertThatThrownBy(() -> quizAdminService.updateQuiz(quizId, requestDto)) -// .isInstanceOf(QuizException.class) -// .hasMessageContaining("객관식 문제에는 선택지가 필요합니다."); -// } -// -// // 헬퍼 메서드 -// private Quiz createSampleQuiz() { -// return Quiz.builder() -// .question("기존 문제") -// .answer("1") -// .commentary("기존 해설") -// .choice(null) -// .type(QuizFormatType.SUBJECTIVE) -// .category(subCategory1) -// .build(); -// } -// } -// -//} \ No newline at end of file +package com.example.cs25service.domain.admin.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; + +import com.example.cs25entity.domain.quiz.entity.Quiz; +import com.example.cs25entity.domain.quiz.entity.QuizCategory; +import com.example.cs25entity.domain.quiz.enums.QuizFormatType; +import com.example.cs25entity.domain.quiz.exception.QuizException; +import com.example.cs25entity.domain.quiz.exception.QuizExceptionCode; +import com.example.cs25entity.domain.quiz.repository.QuizCategoryRepository; +import com.example.cs25entity.domain.quiz.repository.QuizRepository; +import com.example.cs25entity.domain.userQuizAnswer.repository.UserQuizAnswerRepository; +import com.example.cs25service.domain.admin.dto.request.CreateQuizDto; +import com.example.cs25service.domain.admin.dto.request.QuizCreateRequestDto; +import com.example.cs25service.domain.admin.dto.request.QuizUpdateRequestDto; +import com.example.cs25service.domain.admin.dto.response.QuizDetailDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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 org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class QuizAdminServiceTest { + + @InjectMocks + private QuizAdminService quizAdminService; + + @Mock + private QuizRepository quizRepository; + + @Mock + private UserQuizAnswerRepository quizAnswerRepository; + + @Mock + private QuizCategoryRepository quizCategoryRepository; + + @Mock + private ObjectMapper objectMapper; + + @Mock + private Validator validator; + + QuizCategory parentCategory; + QuizCategory subCategory1; + + @BeforeEach + void setUp() { + // 상위 카테고리와 하위 카테고리 mock + parentCategory = QuizCategory.builder() + .categoryType("Backend") + .build(); + + subCategory1 = QuizCategory.builder() + .categoryType("InformationSystemManagement") + .parent(parentCategory) + .build(); + + ReflectionTestUtils.setField(parentCategory, "children", List.of(subCategory1)); + } + + + @Nested + @DisplayName("uploadQuizJson 함수는") + class inUploadQuizJson { + + @Test + @DisplayName("정상작동_시_퀴즈가저장된다") + void uploadQuizJson_success() throws Exception { + // given + String categoryType = "Backend"; + QuizFormatType formatType = QuizFormatType.MULTIPLE_CHOICE; + + // JSON을 담은 가짜 파일 생성 + String json = """ + [ + { + "question": "HTTP는 상태를 유지한다.", + "choice": "1.예/2.아니오", + "answer": "2", + "commentary": "HTTP는 무상태 프로토콜입니다.", + "category": "InformationSystemManagement", + "level": "EASY" + } + ] + """; + + MockMultipartFile file = new MockMultipartFile("file", "quiz.json", "application/json", + json.getBytes()); + + // CreateQuizDto mock + CreateQuizDto quizDto = CreateQuizDto.builder() + .question("HTTP는 상태를 유지한다.") + .choice("1.예/2.아니오") + .answer("2") + .commentary("HTTP는 무상태 프로토콜입니다.") + .category("InformationSystemManagement") + .level("EASY") + .build(); + + CreateQuizDto[] quizDtos = {quizDto}; + + given(quizCategoryRepository.findByCategoryTypeOrElseThrow("Backend")) + .willReturn(parentCategory); + + given(objectMapper.readValue(any(InputStream.class), eq(CreateQuizDto[].class))) + .willReturn(quizDtos); + + given(validator.validate(any(CreateQuizDto.class))) + .willReturn(Collections.emptySet()); + + // when + quizAdminService.uploadQuizJson(file, categoryType, formatType); + + // then + then(quizRepository).should(times(1)).saveAll(anyList()); + } + + @Test + @DisplayName("JSON_파싱_실패_시_예외발생") + void uploadQuizJson_JSON_PARSING_FAILED_ERROR() throws Exception { + // given + MockMultipartFile file = new MockMultipartFile("file", "quiz.json", "application/json", + "invalid".getBytes()); + + given(quizCategoryRepository.findByCategoryTypeOrElseThrow("Backend")) + .willReturn(parentCategory); + + given(objectMapper.readValue(any(InputStream.class), eq(CreateQuizDto[].class))) + .willThrow(new IOException("파싱 오류")); + + // when & then + assertThatThrownBy(() -> + quizAdminService.uploadQuizJson(file, "Backend", QuizFormatType.MULTIPLE_CHOICE) + ).isInstanceOf(QuizException.class) + .hasMessageContaining("JSON 파싱 실패"); + } + + @Test + @DisplayName("유효성 검증 실패 시 예외발생 한다") + void uploadQuizJson_QUIZ_VALIDATION_FAILED_ERROR() throws Exception { + // given + CreateQuizDto quizDto = CreateQuizDto.builder() + .question(null) // 필수값 빠짐 + .choice("1.예/2.아니오") + .answer("2") + .category("Infra") + .level("EASY") + .build(); + + CreateQuizDto[] quizDtos = {quizDto}; + + MockMultipartFile file = new MockMultipartFile("file", "quiz.json", "application/json", + "any".getBytes()); + + given(quizCategoryRepository.findByCategoryTypeOrElseThrow("Backend")) + .willReturn(parentCategory); + given(objectMapper.readValue(any(InputStream.class), eq(CreateQuizDto[].class))) + .willReturn(quizDtos); + + // 검증 실패 set + Set> violations = Set.of( + mock(ConstraintViolation.class)); + given(validator.validate(any(CreateQuizDto.class))) + .willReturn(violations); + + // when & then + assertThatThrownBy(() -> + quizAdminService.uploadQuizJson(file, "Backend", QuizFormatType.MULTIPLE_CHOICE) + ).isInstanceOf(QuizException.class) + .hasMessageContaining("Quiz 유효성 검증 실패"); + } + } + + @Nested + @DisplayName("getAdminQuizDetails 함수는") + class inGetAdminQuizDetails { + + @Test + @DisplayName("정상 작동 시 퀴즈리스트를 반환한다") + void getAdminQuizDetails_success() { + // given + Quiz quiz = Quiz.builder() + .question("Spring이란?") + .answer("프레임워크") + .commentary("스프링은 프레임워크입니다.") + .choice(null) + .type(QuizFormatType.MULTIPLE_CHOICE) + .category(QuizCategory.builder().categoryType("SoftwareDevelopment") + .parent(parentCategory).build()) + .build(); + ReflectionTestUtils.setField(quiz, "id", 1L); + + Page quizPage = new PageImpl<>(List.of(quiz)); + + given(quizRepository.findAllOrderByCreatedAtDesc(any(Pageable.class))) + .willReturn(quizPage); + given(quizAnswerRepository.countByQuizId(1L)) + .willReturn(3L); + + // when + Page result = quizAdminService.getAdminQuizDetails(1, 10); + + // then + assertThat(result).hasSize(1); + QuizDetailDto dto = result.getContent().get(0); + assertThat(dto.getQuestion()).isEqualTo("Spring이란?"); + assertThat(dto.getAnswer()).isEqualTo("프레임워크"); + assertThat(dto.getSolvedCnt()).isEqualTo(3L); + } + } + + @Nested + @DisplayName("getAdminQuizDetail 함수는") + class inGetAdminQuizDetail { + + @Test + @DisplayName("정상 작동 시 퀴즈리스트를 반환한다") + void getAdminQuizDetail_success() { + // given + Long quizId = 1L; + + Quiz quiz = Quiz.builder() + .question("REST란?") + .answer("자원 기반 아키텍처") + .commentary("HTTP URI를 통해 자원을 명확히 구분합니다.") + .choice(null) + .type(QuizFormatType.MULTIPLE_CHOICE) + .category(QuizCategory.builder().categoryType("SoftwareDevelopment") + .parent(parentCategory).build()) + .build(); + ReflectionTestUtils.setField(quiz, "id", 1L); + + given(quizRepository.findByIdOrElseThrow(quizId)).willReturn(quiz); + given(quizAnswerRepository.countByQuizId(quizId)).willReturn(5L); + + // when + QuizDetailDto result = quizAdminService.getAdminQuizDetail(quizId); + + // then + assertThat(result.getQuizId()).isEqualTo(quizId); + assertThat(result.getQuestion()).isEqualTo("REST란?"); + assertThat(result.getAnswer()).isEqualTo("자원 기반 아키텍처"); + assertThat(result.getSolvedCnt()).isEqualTo(5L); + } + + @Test + @DisplayName("없는_id면_예외가 발생한다.") + void getAdminQuizDetail_NOT_FOUND_ERROR() { + // given + Long quizId = 999L; + + given(quizRepository.findByIdOrElseThrow(quizId)) + .willThrow(new QuizException(QuizExceptionCode.NOT_FOUND_ERROR)); + + // when & then + assertThatThrownBy(() -> quizAdminService.getAdminQuizDetail(quizId)) + .isInstanceOf(QuizException.class) + .hasMessageContaining("해당 퀴즈를 찾을 수 없습니다"); + } + } + + @Nested + @DisplayName("createQuiz 함수는") + class inCreateQuiz { + + QuizCreateRequestDto requestDto = new QuizCreateRequestDto(); + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(requestDto, "question", "REST란?"); + ReflectionTestUtils.setField(requestDto, "category", subCategory1.getCategoryType()); + ReflectionTestUtils.setField(requestDto, "choice", null); + ReflectionTestUtils.setField(requestDto, "answer", "자원 기반 아키텍처"); + ReflectionTestUtils.setField(requestDto, "commentary", "HTTP URI를 통해 자원을 명확히 구분합니다."); + ReflectionTestUtils.setField(requestDto, "quizType", QuizFormatType.SUBJECTIVE); + } + + @Test + @DisplayName("정상 작동 시 퀴즈ID를 반환 한다") + void createQuiz_success() { + // given + + Quiz savedQuiz = Quiz.builder() + .category(subCategory1) + .question(requestDto.getQuestion()) + .answer(requestDto.getAnswer()) + .choice(requestDto.getChoice()) + .commentary(requestDto.getCommentary()) + .build(); + ReflectionTestUtils.setField(savedQuiz, "id", 1L); + + given( + quizCategoryRepository.findByCategoryTypeOrElseThrow("InformationSystemManagement")) + .willReturn(subCategory1); + + given(quizRepository.save(any(Quiz.class))) + .willReturn(savedQuiz); + + // when + Long resultId = quizAdminService.createQuiz(requestDto); + + // then + assertThat(resultId).isEqualTo(1L); + } + + @Test + @DisplayName("카테고리가 없으면 예외가 발생한다") + void createQuiz_QUIZ_CATEGORY_NOT_FOUND_ERROR() { + // given + ReflectionTestUtils.setField(requestDto, "category", "NonExist"); + + given(quizCategoryRepository.findByCategoryTypeOrElseThrow("NonExist")) + .willThrow(new QuizException(QuizExceptionCode.QUIZ_CATEGORY_NOT_FOUND_ERROR)); + + // when & then + assertThatThrownBy(() -> quizAdminService.createQuiz(requestDto)) + .isInstanceOf(QuizException.class) + .hasMessageContaining("QuizCategory 를 찾을 수 없습니다"); + } + } + + @Nested + @DisplayName("updateQuiz 함수는") + class inUpdateQuiz { + + QuizUpdateRequestDto requestDto = new QuizUpdateRequestDto(); + + @Test + @DisplayName("모든 필드를 정상적으로 업데이트하면 DTO를 반환한다") + void updateQuiz_success() { + // given + Long quizId = 1L; + Quiz quiz = createSampleQuiz(); + ReflectionTestUtils.setField(quiz, "id", quizId); + + ReflectionTestUtils.setField(requestDto, "question", "기존 문제"); + ReflectionTestUtils.setField(requestDto, "category", subCategory1.getCategoryType()); + ReflectionTestUtils.setField(requestDto, "choice", null); + ReflectionTestUtils.setField(requestDto, "answer", "1"); + ReflectionTestUtils.setField(requestDto, "commentary", "기존 해설"); + ReflectionTestUtils.setField(requestDto, "quizType", QuizFormatType.SUBJECTIVE); + + given(quizRepository.findByIdOrElseThrow(quizId)).willReturn(quiz); + given(quizCategoryRepository.findByCategoryTypeOrElseThrow( + "InformationSystemManagement")).willReturn(subCategory1); + given(quizAnswerRepository.countByQuizId(quizId)).willReturn(5L); + + // when + QuizDetailDto result = quizAdminService.updateQuiz(quizId, requestDto); + + // then + assertThat(result.getQuestion()).isEqualTo("기존 문제"); + assertThat(result.getCommentary()).isEqualTo("기존 해설"); + assertThat(result.getCategory()).isEqualTo("InformationSystemManagement"); + assertThat(result.getChoice()).isEqualTo(null); + assertThat(result.getType()).isEqualTo("SUBJECTIVE"); + assertThat(result.getSolvedCnt()).isEqualTo(5L); + } + + @Test + @DisplayName("카테고리만 변경되면 category 만 업데이트된다") + void updateQuiz_category_success() { + // given + Long quizId = 1L; + Quiz quiz = createSampleQuiz(); + ReflectionTestUtils.setField(quiz, "id", quizId); + ReflectionTestUtils.setField(requestDto, "category", "Programming"); + + QuizCategory newCategory = QuizCategory.builder() + .categoryType("Programming") + .parent(parentCategory) + .build(); + + ReflectionTestUtils.setField(parentCategory, "children", + List.of(subCategory1, newCategory)); + + given(quizRepository.findByIdOrElseThrow(quizId)).willReturn(quiz); + given(quizCategoryRepository.findByCategoryTypeOrElseThrow("Programming")).willReturn( + newCategory); + given(quizAnswerRepository.countByQuizId(quizId)).willReturn(0L); + + // when + QuizDetailDto result = quizAdminService.updateQuiz(quizId, requestDto); + + // then + assertThat(result.getCategory()).isEqualTo("Programming"); + } + + @Test + @DisplayName("존재하지 않는 퀴즈 ID면 예외가 발생한다") + void updateQuiz_NOT_FOUND_ERROR() { + // given + Long quizId = 999L; + + ReflectionTestUtils.setField(requestDto, "question", "변경된 질문121"); + + given(quizRepository.findByIdOrElseThrow(quizId)) + .willThrow(new QuizException(QuizExceptionCode.NOT_FOUND_ERROR)); + + // when & then + assertThatThrownBy(() -> quizAdminService.updateQuiz(quizId, requestDto)) + .isInstanceOf(QuizException.class) + .hasMessageContaining("해당 퀴즈를 찾을 수 없습니다"); + } + + @Test + @DisplayName("존재하지 않는 카테고리면 예외가 발생한다") + void updateQuiz_QUIZ_CATEGORY_NOT_FOUND_ERROR() { + // given + Long quizId = 1L; + Quiz quiz = createSampleQuiz(); + ReflectionTestUtils.setField(quiz, "id", quizId); + ReflectionTestUtils.setField(requestDto, "category", "NonExist"); + + given(quizRepository.findByIdOrElseThrow(quizId)).willReturn(quiz); + given(quizCategoryRepository.findByCategoryTypeOrElseThrow("NonExist")) + .willThrow(new QuizException(QuizExceptionCode.QUIZ_CATEGORY_NOT_FOUND_ERROR)); + + // when & then + assertThatThrownBy(() -> quizAdminService.updateQuiz(quizId, requestDto)) + .isInstanceOf(QuizException.class) + .hasMessageContaining("QuizCategory 를 찾을 수 없습니다"); + } + + @Test + @DisplayName("퀴즈 타입을 MULTIPLE_CHOICE로 변경하려는데 choice가 없으면 예외 발생") + void updateQuiz_MULTIPLE_CHOICE_REQUIRE_ERROR() { + // given + Long quizId = 1L; + Quiz quiz = createSampleQuiz(); + ReflectionTestUtils.setField(quiz, "id", quizId); + ReflectionTestUtils.setField(requestDto, "quizType", QuizFormatType.MULTIPLE_CHOICE); + + given(quizRepository.findByIdOrElseThrow(quizId)).willReturn(quiz); + + // when & then + assertThatThrownBy(() -> quizAdminService.updateQuiz(quizId, requestDto)) + .isInstanceOf(QuizException.class) + .hasMessageContaining("객관식 문제에는 선택지가 필요합니다."); + } + + // 헬퍼 메서드 + private Quiz createSampleQuiz() { + return Quiz.builder() + .question("기존 문제") + .answer("1") + .commentary("기존 해설") + .choice(null) + .type(QuizFormatType.SUBJECTIVE) + .category(subCategory1) + .build(); + } + } + +} \ No newline at end of file diff --git a/cs25-service/src/test/java/com/example/cs25service/domain/userQuizAnswer/service/UserQuizAnswerServiceTest.java b/cs25-service/src/test/java/com/example/cs25service/domain/userQuizAnswer/service/UserQuizAnswerServiceTest.java index 7ecbb189..829a2228 100644 --- a/cs25-service/src/test/java/com/example/cs25service/domain/userQuizAnswer/service/UserQuizAnswerServiceTest.java +++ b/cs25-service/src/test/java/com/example/cs25service/domain/userQuizAnswer/service/UserQuizAnswerServiceTest.java @@ -1,339 +1,339 @@ -//package com.example.cs25service.domain.userQuizAnswer.service; -// -//import com.example.cs25entity.domain.quiz.entity.Quiz; -//import com.example.cs25entity.domain.quiz.entity.QuizCategory; -//import com.example.cs25entity.domain.quiz.enums.QuizFormatType; -//import com.example.cs25entity.domain.quiz.enums.QuizLevel; -//import com.example.cs25entity.domain.quiz.exception.QuizException; -//import com.example.cs25entity.domain.quiz.exception.QuizExceptionCode; -//import com.example.cs25entity.domain.quiz.repository.QuizRepository; -//import com.example.cs25entity.domain.subscription.entity.DayOfWeek; -//import com.example.cs25entity.domain.subscription.entity.Subscription; -//import com.example.cs25entity.domain.subscription.exception.SubscriptionException; -//import com.example.cs25entity.domain.subscription.exception.SubscriptionExceptionCode; -//import com.example.cs25entity.domain.subscription.repository.SubscriptionRepository; -//import com.example.cs25entity.domain.user.entity.Role; -//import com.example.cs25entity.domain.user.entity.User; -//import com.example.cs25entity.domain.user.repository.UserRepository; -//import com.example.cs25entity.domain.userQuizAnswer.dto.UserAnswerDto; -//import com.example.cs25entity.domain.userQuizAnswer.entity.UserQuizAnswer; -//import com.example.cs25entity.domain.userQuizAnswer.exception.UserQuizAnswerException; -//import com.example.cs25entity.domain.userQuizAnswer.exception.UserQuizAnswerExceptionCode; -//import com.example.cs25entity.domain.userQuizAnswer.repository.UserQuizAnswerRepository; -//import com.example.cs25service.domain.userQuizAnswer.dto.SelectionRateResponseDto; -//import com.example.cs25service.domain.userQuizAnswer.dto.UserQuizAnswerRequestDto; -//import com.example.cs25service.domain.userQuizAnswer.dto.UserQuizAnswerResponseDto; -//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 org.springframework.test.util.ReflectionTestUtils; -// -//import java.time.LocalDate; -//import java.util.EnumSet; -//import java.util.Optional; -//import java.util.*; -// -//import static org.assertj.core.api.Assertions.assertThat; -//import static org.assertj.core.api.Assertions.assertThatThrownBy; -//import static org.junit.jupiter.api.Assertions.assertEquals; -//import static org.mockito.Mockito.*; -// -//@ExtendWith(MockitoExtension.class) -//class UserQuizAnswerServiceTest { -// -// @InjectMocks -// private UserQuizAnswerService userQuizAnswerService; -// -// @Mock -// private UserQuizAnswerRepository userQuizAnswerRepository; -// -// @Mock -// private QuizRepository quizRepository; -// -// @Mock -// private UserRepository userRepository; -// -// @Mock -// private SubscriptionRepository subscriptionRepository; -// -// private Subscription subscription; -// private UserQuizAnswer userQuizAnswer; -// private Quiz shortAnswerQuiz; -// private Quiz choiceQuiz; -// private User user; -// private UserQuizAnswerRequestDto requestDto; -// -// @BeforeEach -// void setUp() { -// QuizCategory category = QuizCategory.builder() -// .categoryType("BACKEND") -// .build(); -// -// subscription = Subscription.builder() -// .category(category) -// .email("test@naver.com") -// .startDate(LocalDate.now()) -// .endDate(LocalDate.now().plusMonths(1)) -// .subscriptionType(EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY)) -// .build(); -// ReflectionTestUtils.setField(subscription, "id", 1L); -// ReflectionTestUtils.setField(subscription, "serialId", "uuid_subscription"); -// -// // 객관식 퀴즈 -// choiceQuiz = Quiz.builder() -// .type(QuizFormatType.MULTIPLE_CHOICE) -// .question("Java is?") -// .answer("1. Programming") -// .commentary("Java is a language.") -// .choice("1. Programming/2. Coffee/3. iceCream/4. latte") -// .category(category) -// .level(QuizLevel.EASY) -// .build(); -// ReflectionTestUtils.setField(choiceQuiz, "id", 1L); -// ReflectionTestUtils.setField(choiceQuiz, "serialId", "uuid_quiz"); -// -// -// // 주관식 퀴즈 -// shortAnswerQuiz = Quiz.builder() -// .type(QuizFormatType.SHORT_ANSWER) -// .question("Java is?") -// .answer("java") -// .commentary("Java is a language.") -// .category(category) -// .level(QuizLevel.EASY) -// .build(); -// ReflectionTestUtils.setField(shortAnswerQuiz, "id", 1L); -// ReflectionTestUtils.setField(shortAnswerQuiz, "serialId", "uuid_quiz_1"); -// -// userQuizAnswer = UserQuizAnswer.builder() -// .userAnswer("1") -// .isCorrect(true) -// .build(); -// ReflectionTestUtils.setField(userQuizAnswer, "id", 1L); -// -// user = User.builder() -// .email("test@naver.com") -// .name("test") -// .role(Role.USER) -// .build(); -// ReflectionTestUtils.setField(user, "id", 1L); -// -// requestDto = new UserQuizAnswerRequestDto("1", subscription.getSerialId()); -// } -// -// @Test -// void submitAnswer_정상_저장된다() { -// // given -// String subscriptionSerialId = "uuid_subscription"; -// String quizSerialId = "uuid_quiz"; -// -// when(subscriptionRepository.findBySerialIdOrElseThrow(subscriptionSerialId)).thenReturn(subscription); -// when(quizRepository.findBySerialIdOrElseThrow(quizSerialId)).thenReturn(choiceQuiz); -// when(userQuizAnswerRepository.existsByQuizIdAndSubscriptionId(choiceQuiz.getId(), subscription.getId())).thenReturn(false); -// when(userQuizAnswerRepository.save(any())).thenReturn(userQuizAnswer); -// -// // when -// UserQuizAnswerResponseDto userQuizAnswerResponseDto = userQuizAnswerService.submitAnswer(choiceQuiz.getSerialId(), requestDto); -// -// // then -// assertThat(userQuizAnswer.getId()).isEqualTo(userQuizAnswerResponseDto.getUserQuizAnswerId()); -// assertThat(userQuizAnswer.getUserAnswer()).isEqualTo(userQuizAnswerResponseDto.getUserAnswer()); -// assertThat(userQuizAnswer.getAiFeedback()).isEqualTo(userQuizAnswerResponseDto.getAiFeedback()); -// } -// -// @Test -// void submitAnswer_구독없음_예외() { -// // given -// String subscriptionSerialId = "uuid_subscription"; -// -// when(subscriptionRepository.findBySerialIdOrElseThrow(subscriptionSerialId)) -// .thenThrow(new SubscriptionException(SubscriptionExceptionCode.NOT_FOUND_SUBSCRIPTION_ERROR)); -// -// // when & then -// assertThatThrownBy(() -> userQuizAnswerService.submitAnswer(choiceQuiz.getSerialId(), requestDto)) -// .isInstanceOf(SubscriptionException.class) -// .hasMessageContaining("구독 정보를 불러올 수 없습니다."); -// } -// -// @Test -// void submitAnswer_구독_비활성_예외(){ -// //given -// String subscriptionSerialId = "uuid_subscription"; -// -// Subscription subscription = mock(Subscription.class); -// when(subscriptionRepository.findBySerialIdOrElseThrow(subscriptionSerialId)).thenReturn(subscription); -// when(subscription.isActive()).thenReturn(false); -// -// // when & then -// assertThatThrownBy(() -> userQuizAnswerService.submitAnswer(choiceQuiz.getSerialId(), requestDto)) -// .isInstanceOf(SubscriptionException.class) -// .hasMessageContaining("비활성화된 구독자 입니다."); -// } -// -// @Test -// void submitAnswer_중복답변_예외(){ -// //give -// String subscriptionSerialId = "uuid_subscription"; -// String quizSerialId = "uuid_quiz"; -// -// when(subscriptionRepository.findBySerialIdOrElseThrow(subscriptionSerialId)).thenReturn(subscription); -// when(quizRepository.findBySerialIdOrElseThrow(quizSerialId)).thenReturn(choiceQuiz); -// when(userQuizAnswerRepository.existsByQuizIdAndSubscriptionId(choiceQuiz.getId(), subscription.getId())).thenReturn(true); -// when(userQuizAnswerRepository.findUserQuizAnswerBySerialIds(quizSerialId, subscriptionSerialId)) -// .thenThrow(new UserQuizAnswerException(UserQuizAnswerExceptionCode.NOT_FOUND_ANSWER)); -// -// //when & then -// assertThatThrownBy(() -> userQuizAnswerService.submitAnswer(choiceQuiz.getSerialId(), requestDto)) -// .isInstanceOf(UserQuizAnswerException.class) -// .hasMessageContaining("해당 답변을 찾을 수 없습니다"); -// } -// -// @Test -// void submitAnswer_퀴즈없음_예외() { -// // given -// String subscriptionSerialId = "uuid_subscription"; -// String quizSerialId = "uuid_quiz"; -// -// when(subscriptionRepository.findBySerialIdOrElseThrow(subscriptionSerialId)).thenReturn(subscription); -// when(quizRepository.findBySerialIdOrElseThrow(quizSerialId)) -// .thenThrow(new QuizException(QuizExceptionCode.NOT_FOUND_ERROR)); -// -// // when & then -// assertThatThrownBy(() -> userQuizAnswerService.submitAnswer(choiceQuiz.getSerialId(), requestDto)) -// .isInstanceOf(QuizException.class) -// .hasMessageContaining("해당 퀴즈를 찾을 수 없습니다"); -// } -// -// @Test -// void evaluateAnswer_비회원_객관식_정답(){ -// //given -// UserQuizAnswer choiceAnswer = UserQuizAnswer.builder() -// .userAnswer("1. Programming") -// .quiz(choiceQuiz) -// .subscription(subscription) -// .build(); -// -// when(userQuizAnswerRepository.findWithQuizAndUserByIdOrElseThrow(choiceAnswer.getId())).thenReturn(choiceAnswer); -// -// //when -// UserQuizAnswerResponseDto userQuizAnswerResponseDto = userQuizAnswerService.evaluateAnswer(choiceAnswer.getId()); -// -// //then -// assertThat(userQuizAnswerResponseDto.isCorrect()).isTrue(); -// } -// -// @Test -// void evaluateAnswer_비회원_주관식_정답(){ -// //given -// UserQuizAnswer shortAnswer = UserQuizAnswer.builder() -// .subscription(subscription) -// .userAnswer("java") -// .quiz(shortAnswerQuiz) -// .build(); -// -// when(userQuizAnswerRepository.findWithQuizAndUserByIdOrElseThrow(shortAnswer.getId())).thenReturn(shortAnswer); -// -// //when -// UserQuizAnswerResponseDto userQuizAnswerResponseDto = userQuizAnswerService.evaluateAnswer(shortAnswer.getId()); -// -// //then -// assertThat(userQuizAnswerResponseDto.isCorrect()).isTrue(); -// } -// -// @Test -// void evaluateAnswer_회원_객관식_정답_점수부여(){ -// //given -// UserQuizAnswer choiceAnswer = UserQuizAnswer.builder() -// .userAnswer("1. Programming") -// .quiz(choiceQuiz) -// .user(user) -// .subscription(subscription) -// .build(); -// -// when(userQuizAnswerRepository.findWithQuizAndUserByIdOrElseThrow(choiceAnswer.getId())).thenReturn(choiceAnswer); -// -// //when -// UserQuizAnswerResponseDto userQuizAnswerResponseDto = userQuizAnswerService.evaluateAnswer(choiceAnswer.getId()); -// -// //then -// assertThat(userQuizAnswerResponseDto.isCorrect()).isTrue(); -// assertThat(user.getScore()).isEqualTo(3); -// } -// -// @Test -// void evaluateAnswer_회원_주관식_정답_점수부여(){ -// //given -// UserQuizAnswer shortAnswer = UserQuizAnswer.builder() -// .subscription(subscription) -// .userAnswer("java") -// .user(user) -// .quiz(shortAnswerQuiz) -// .build(); -// -// when(userQuizAnswerRepository.findWithQuizAndUserByIdOrElseThrow(shortAnswer.getId())).thenReturn(shortAnswer); -// -// //when -// UserQuizAnswerResponseDto checkSimpleAnswerResponseDto = userQuizAnswerService.evaluateAnswer(shortAnswer.getId()); -// -// //then -// assertThat(checkSimpleAnswerResponseDto.isCorrect()).isTrue(); -// assertThat(user.getScore()).isEqualTo(9); -// } -// -// @Test -// void evaluateAnswer_오답(){ -// //given -// UserQuizAnswer shortAnswer = UserQuizAnswer.builder() -// .subscription(subscription) -// .userAnswer("python") -// .quiz(shortAnswerQuiz) -// .build(); -// -// when(userQuizAnswerRepository.findWithQuizAndUserByIdOrElseThrow(shortAnswer.getId())).thenReturn(shortAnswer); -// -// //when -// UserQuizAnswerResponseDto userQuizAnswerResponseDto = userQuizAnswerService.evaluateAnswer(shortAnswer.getId()); -// -// //then -// assertThat(userQuizAnswerResponseDto.isCorrect()).isFalse(); -// } -// -// -// @Test -// void calculateSelectionRateByOption_조회_성공(){ -// //given -// String quizSerialId = "uuid_quiz"; -// -// List answers = List.of( -// new UserAnswerDto("1. Programming"), -// new UserAnswerDto("1. Programming"), -// new UserAnswerDto("2. Coffee"), -// new UserAnswerDto("2. Coffee"), -// new UserAnswerDto("2. Coffee"), -// new UserAnswerDto("3. iceCream"), -// new UserAnswerDto("3. iceCream"), -// new UserAnswerDto("3. iceCream"), -// new UserAnswerDto("4. latte"), -// new UserAnswerDto("4. latte") -// ); -// -// when(quizRepository.findBySerialIdOrElseThrow(quizSerialId)).thenReturn(choiceQuiz); -// when(userQuizAnswerRepository.findUserAnswerByQuizId(choiceQuiz.getId())).thenReturn(answers); -// -// //when -// SelectionRateResponseDto selectionRateByOption = userQuizAnswerService.calculateSelectionRateByOption(choiceQuiz.getSerialId()); -// -// //then -// assertThat(selectionRateByOption.getTotalCount()).isEqualTo(10); -// Map selectionRates = Map.of( -// "1. Programming", 0.2, -// "2. Coffee", 0.3, -// "3. iceCream", 0.3, -// "4. latte", 0.2 -// ); -// assertThat(selectionRateByOption.getSelectionRates()).isEqualTo(selectionRates); -// } -//} \ No newline at end of file +package com.example.cs25service.domain.userQuizAnswer.service; + +import com.example.cs25entity.domain.quiz.entity.Quiz; +import com.example.cs25entity.domain.quiz.entity.QuizCategory; +import com.example.cs25entity.domain.quiz.enums.QuizFormatType; +import com.example.cs25entity.domain.quiz.enums.QuizLevel; +import com.example.cs25entity.domain.quiz.exception.QuizException; +import com.example.cs25entity.domain.quiz.exception.QuizExceptionCode; +import com.example.cs25entity.domain.quiz.repository.QuizRepository; +import com.example.cs25entity.domain.subscription.entity.DayOfWeek; +import com.example.cs25entity.domain.subscription.entity.Subscription; +import com.example.cs25entity.domain.subscription.exception.SubscriptionException; +import com.example.cs25entity.domain.subscription.exception.SubscriptionExceptionCode; +import com.example.cs25entity.domain.subscription.repository.SubscriptionRepository; +import com.example.cs25entity.domain.user.entity.Role; +import com.example.cs25entity.domain.user.entity.User; +import com.example.cs25entity.domain.user.repository.UserRepository; +import com.example.cs25entity.domain.userQuizAnswer.dto.UserAnswerDto; +import com.example.cs25entity.domain.userQuizAnswer.entity.UserQuizAnswer; +import com.example.cs25entity.domain.userQuizAnswer.exception.UserQuizAnswerException; +import com.example.cs25entity.domain.userQuizAnswer.exception.UserQuizAnswerExceptionCode; +import com.example.cs25entity.domain.userQuizAnswer.repository.UserQuizAnswerRepository; +import com.example.cs25service.domain.userQuizAnswer.dto.SelectionRateResponseDto; +import com.example.cs25service.domain.userQuizAnswer.dto.UserQuizAnswerRequestDto; +import com.example.cs25service.domain.userQuizAnswer.dto.UserQuizAnswerResponseDto; +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 org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDate; +import java.util.EnumSet; +import java.util.Optional; +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserQuizAnswerServiceTest { + + @InjectMocks + private UserQuizAnswerService userQuizAnswerService; + + @Mock + private UserQuizAnswerRepository userQuizAnswerRepository; + + @Mock + private QuizRepository quizRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private SubscriptionRepository subscriptionRepository; + + private Subscription subscription; + private UserQuizAnswer userQuizAnswer; + private Quiz shortAnswerQuiz; + private Quiz choiceQuiz; + private User user; + private UserQuizAnswerRequestDto requestDto; + + @BeforeEach + void setUp() { + QuizCategory category = QuizCategory.builder() + .categoryType("BACKEND") + .build(); + + subscription = Subscription.builder() + .category(category) + .email("test@naver.com") + .startDate(LocalDate.now()) + .endDate(LocalDate.now().plusMonths(1)) + .subscriptionType(EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY)) + .build(); + ReflectionTestUtils.setField(subscription, "id", 1L); + ReflectionTestUtils.setField(subscription, "serialId", "uuid_subscription"); + + // 객관식 퀴즈 + choiceQuiz = Quiz.builder() + .type(QuizFormatType.MULTIPLE_CHOICE) + .question("Java is?") + .answer("1. Programming") + .commentary("Java is a language.") + .choice("1. Programming/2. Coffee/3. iceCream/4. latte") + .category(category) + .level(QuizLevel.EASY) + .build(); + ReflectionTestUtils.setField(choiceQuiz, "id", 1L); + ReflectionTestUtils.setField(choiceQuiz, "serialId", "uuid_quiz"); + + + // 주관식 퀴즈 + shortAnswerQuiz = Quiz.builder() + .type(QuizFormatType.SHORT_ANSWER) + .question("Java is?") + .answer("java") + .commentary("Java is a language.") + .category(category) + .level(QuizLevel.EASY) + .build(); + ReflectionTestUtils.setField(shortAnswerQuiz, "id", 1L); + ReflectionTestUtils.setField(shortAnswerQuiz, "serialId", "uuid_quiz_1"); + + userQuizAnswer = UserQuizAnswer.builder() + .userAnswer("1") + .isCorrect(true) + .build(); + ReflectionTestUtils.setField(userQuizAnswer, "id", 1L); + + user = User.builder() + .email("test@naver.com") + .name("test") + .role(Role.USER) + .build(); + ReflectionTestUtils.setField(user, "id", 1L); + + requestDto = new UserQuizAnswerRequestDto("1", subscription.getSerialId()); + } + + @Test + void submitAnswer_정상_저장된다() { + // given + String subscriptionSerialId = "uuid_subscription"; + String quizSerialId = "uuid_quiz"; + + when(subscriptionRepository.findBySerialIdOrElseThrow(subscriptionSerialId)).thenReturn(subscription); + when(quizRepository.findBySerialIdOrElseThrow(quizSerialId)).thenReturn(choiceQuiz); + when(userQuizAnswerRepository.existsByQuizIdAndSubscriptionId(choiceQuiz.getId(), subscription.getId())).thenReturn(false); + when(userQuizAnswerRepository.save(any())).thenReturn(userQuizAnswer); + + // when + UserQuizAnswerResponseDto userQuizAnswerResponseDto = userQuizAnswerService.submitAnswer(choiceQuiz.getSerialId(), requestDto); + + // then + assertThat(userQuizAnswer.getId()).isEqualTo(userQuizAnswerResponseDto.getUserQuizAnswerId()); + assertThat(userQuizAnswer.getUserAnswer()).isEqualTo(userQuizAnswerResponseDto.getUserAnswer()); + assertThat(userQuizAnswer.getAiFeedback()).isEqualTo(userQuizAnswerResponseDto.getAiFeedback()); + } + + @Test + void submitAnswer_구독없음_예외() { + // given + String subscriptionSerialId = "uuid_subscription"; + + when(subscriptionRepository.findBySerialIdOrElseThrow(subscriptionSerialId)) + .thenThrow(new SubscriptionException(SubscriptionExceptionCode.NOT_FOUND_SUBSCRIPTION_ERROR)); + + // when & then + assertThatThrownBy(() -> userQuizAnswerService.submitAnswer(choiceQuiz.getSerialId(), requestDto)) + .isInstanceOf(SubscriptionException.class) + .hasMessageContaining("구독 정보를 불러올 수 없습니다."); + } + + @Test + void submitAnswer_구독_비활성_예외(){ + //given + String subscriptionSerialId = "uuid_subscription"; + + Subscription subscription = mock(Subscription.class); + when(subscriptionRepository.findBySerialIdOrElseThrow(subscriptionSerialId)).thenReturn(subscription); + when(subscription.isActive()).thenReturn(false); + + // when & then + assertThatThrownBy(() -> userQuizAnswerService.submitAnswer(choiceQuiz.getSerialId(), requestDto)) + .isInstanceOf(SubscriptionException.class) + .hasMessageContaining("비활성화된 구독자 입니다."); + } + + @Test + void submitAnswer_중복답변_예외(){ + //give + String subscriptionSerialId = "uuid_subscription"; + String quizSerialId = "uuid_quiz"; + + when(subscriptionRepository.findBySerialIdOrElseThrow(subscriptionSerialId)).thenReturn(subscription); + when(quizRepository.findBySerialIdOrElseThrow(quizSerialId)).thenReturn(choiceQuiz); + when(userQuizAnswerRepository.existsByQuizIdAndSubscriptionId(choiceQuiz.getId(), subscription.getId())).thenReturn(true); + when(userQuizAnswerRepository.findUserQuizAnswerBySerialIds(quizSerialId, subscriptionSerialId)) + .thenThrow(new UserQuizAnswerException(UserQuizAnswerExceptionCode.NOT_FOUND_ANSWER)); + + //when & then + assertThatThrownBy(() -> userQuizAnswerService.submitAnswer(choiceQuiz.getSerialId(), requestDto)) + .isInstanceOf(UserQuizAnswerException.class) + .hasMessageContaining("해당 답변을 찾을 수 없습니다"); + } + + @Test + void submitAnswer_퀴즈없음_예외() { + // given + String subscriptionSerialId = "uuid_subscription"; + String quizSerialId = "uuid_quiz"; + + when(subscriptionRepository.findBySerialIdOrElseThrow(subscriptionSerialId)).thenReturn(subscription); + when(quizRepository.findBySerialIdOrElseThrow(quizSerialId)) + .thenThrow(new QuizException(QuizExceptionCode.NOT_FOUND_ERROR)); + + // when & then + assertThatThrownBy(() -> userQuizAnswerService.submitAnswer(choiceQuiz.getSerialId(), requestDto)) + .isInstanceOf(QuizException.class) + .hasMessageContaining("해당 퀴즈를 찾을 수 없습니다"); + } + + @Test + void evaluateAnswer_비회원_객관식_정답(){ + //given + UserQuizAnswer choiceAnswer = UserQuizAnswer.builder() + .userAnswer("1. Programming") + .quiz(choiceQuiz) + .subscription(subscription) + .build(); + + when(userQuizAnswerRepository.findWithQuizAndUserByIdOrElseThrow(choiceAnswer.getId())).thenReturn(choiceAnswer); + + //when + UserQuizAnswerResponseDto userQuizAnswerResponseDto = userQuizAnswerService.evaluateAnswer(choiceAnswer.getId()); + + //then + assertThat(userQuizAnswerResponseDto.isCorrect()).isTrue(); + } + + @Test + void evaluateAnswer_비회원_주관식_정답(){ + //given + UserQuizAnswer shortAnswer = UserQuizAnswer.builder() + .subscription(subscription) + .userAnswer("java") + .quiz(shortAnswerQuiz) + .build(); + + when(userQuizAnswerRepository.findWithQuizAndUserByIdOrElseThrow(shortAnswer.getId())).thenReturn(shortAnswer); + + //when + UserQuizAnswerResponseDto userQuizAnswerResponseDto = userQuizAnswerService.evaluateAnswer(shortAnswer.getId()); + + //then + assertThat(userQuizAnswerResponseDto.isCorrect()).isTrue(); + } + + @Test + void evaluateAnswer_회원_객관식_정답_점수부여(){ + //given + UserQuizAnswer choiceAnswer = UserQuizAnswer.builder() + .userAnswer("1. Programming") + .quiz(choiceQuiz) + .user(user) + .subscription(subscription) + .build(); + + when(userQuizAnswerRepository.findWithQuizAndUserByIdOrElseThrow(choiceAnswer.getId())).thenReturn(choiceAnswer); + + //when + UserQuizAnswerResponseDto userQuizAnswerResponseDto = userQuizAnswerService.evaluateAnswer(choiceAnswer.getId()); + + //then + assertThat(userQuizAnswerResponseDto.isCorrect()).isTrue(); + assertThat(user.getScore()).isEqualTo(3); + } + + @Test + void evaluateAnswer_회원_주관식_정답_점수부여(){ + //given + UserQuizAnswer shortAnswer = UserQuizAnswer.builder() + .subscription(subscription) + .userAnswer("java") + .user(user) + .quiz(shortAnswerQuiz) + .build(); + + when(userQuizAnswerRepository.findWithQuizAndUserByIdOrElseThrow(shortAnswer.getId())).thenReturn(shortAnswer); + + //when + UserQuizAnswerResponseDto checkSimpleAnswerResponseDto = userQuizAnswerService.evaluateAnswer(shortAnswer.getId()); + + //then + assertThat(checkSimpleAnswerResponseDto.isCorrect()).isTrue(); + assertThat(user.getScore()).isEqualTo(9); + } + + @Test + void evaluateAnswer_오답(){ + //given + UserQuizAnswer shortAnswer = UserQuizAnswer.builder() + .subscription(subscription) + .userAnswer("python") + .quiz(shortAnswerQuiz) + .build(); + + when(userQuizAnswerRepository.findWithQuizAndUserByIdOrElseThrow(shortAnswer.getId())).thenReturn(shortAnswer); + + //when + UserQuizAnswerResponseDto userQuizAnswerResponseDto = userQuizAnswerService.evaluateAnswer(shortAnswer.getId()); + + //then + assertThat(userQuizAnswerResponseDto.isCorrect()).isFalse(); + } + + + @Test + void calculateSelectionRateByOption_조회_성공(){ + //given + String quizSerialId = "uuid_quiz"; + + List answers = List.of( + new UserAnswerDto("1. Programming"), + new UserAnswerDto("1. Programming"), + new UserAnswerDto("2. Coffee"), + new UserAnswerDto("2. Coffee"), + new UserAnswerDto("2. Coffee"), + new UserAnswerDto("3. iceCream"), + new UserAnswerDto("3. iceCream"), + new UserAnswerDto("3. iceCream"), + new UserAnswerDto("4. latte"), + new UserAnswerDto("4. latte") + ); + + when(quizRepository.findBySerialIdOrElseThrow(quizSerialId)).thenReturn(choiceQuiz); + when(userQuizAnswerRepository.findUserAnswerByQuizId(choiceQuiz.getId())).thenReturn(answers); + + //when + SelectionRateResponseDto selectionRateByOption = userQuizAnswerService.calculateSelectionRateByOption(choiceQuiz.getSerialId()); + + //then + assertThat(selectionRateByOption.getTotalCount()).isEqualTo(10); + Map selectionRates = Map.of( + "1. Programming", 0.2, + "2. Coffee", 0.3, + "3. iceCream", 0.3, + "4. latte", 0.2 + ); + assertThat(selectionRateByOption.getSelectionRates()).isEqualTo(selectionRates); + } +} \ No newline at end of file