diff --git a/src/test/java/com/aloc/aloc/course/service/CourseServiceTest.java b/src/test/java/com/aloc/aloc/course/service/CourseServiceTest.java new file mode 100644 index 00000000..6bef9d78 --- /dev/null +++ b/src/test/java/com/aloc/aloc/course/service/CourseServiceTest.java @@ -0,0 +1,345 @@ +package com.aloc.aloc.course.service; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThat; +import java.util.Optional; +import java.util.NoSuchElementException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import com.aloc.aloc.algorithm.entity.Algorithm; +import com.aloc.aloc.course.entity.CourseProblem; +import com.aloc.aloc.problem.entity.Problem; +import com.aloc.aloc.problem.entity.ProblemAlgorithm; +import org.mockito.ArgumentCaptor; +import java.io.IOException; +import java.util.Optional; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.aloc.aloc.course.dto.request.CourseRequestDto; +import com.aloc.aloc.course.dto.response.CourseResponseDto; +import com.aloc.aloc.course.entity.Course; +import com.aloc.aloc.course.enums.CourseType; +import com.aloc.aloc.course.enums.UserCourseState; +import com.aloc.aloc.course.repository.CourseRepository; +import com.aloc.aloc.scraper.ProblemScrapingService; +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.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Objects; + +@ExtendWith(MockitoExtension.class) +public class CourseServiceTest { + @Mock private CourseRepository courseRepository; + @Mock private ProblemScrapingService problemScrapingService; + + @InjectMocks private CourseService courseService; + + private Course stubCourseWithOf(Long id, String title, CourseType type, + int problemCnt, int minRank, int maxRank, int duration) { + // 1) 요청 DTO 채우기 + CourseRequestDto req = new CourseRequestDto(); + setFieldQuiet(req, "title", title); + setFieldQuiet(req, "description", "test desc"); + setFieldQuiet(req, "type", type); + setFieldQuiet(req, "problemCnt", problemCnt); + setFieldQuiet(req, "minRank", minRank); + setFieldQuiet(req, "maxRank", maxRank); + setFieldQuiet(req, "duration", duration); + setFieldQuiet(req, "algorithmIdList", java.util.List.of()); + + // 2) 실제 팩토리로 Course 생성 + Course course = Course.of(req); + + // 3) averageRank 보장: calculateAverageRank() 있으면 호출, 없으면 직접 세팅 + try { + var m = Course.class.getDeclaredMethod("calculateAverageRank"); + m.setAccessible(true); + m.invoke(course); + } catch (NoSuchMethodException e) { + // 메소드가 없으면 (min+max)/2로 직접 주입 + int avg = (minRank + maxRank) / 2; + setFieldQuiet(course, "averageRank", Integer.valueOf(avg)); + } catch (ReflectiveOperationException e) { + throw new AssertionError("averageRank 계산 호출 실패", e); + } + + // 4) 테스트용 id 주입 + setFieldQuiet(course, "id", id); + return course; + } + + private static void setFieldQuiet(Object target, String name, Object value) { + try { + java.lang.reflect.Field f = target.getClass().getDeclaredField(name); + f.setAccessible(true); + f.set(target, value); + } catch (ReflectiveOperationException e) { + throw new AssertionError("테스트 필드 주입 실패: " + name, e); + } + } + + @Test + void getCourseNormalCase(){ + //given + Pageable pageable = PageRequest.of(0, 2); + + Course course1 = stubCourseWithOf(1L, "test1", CourseType.DEADLINE, + 10, 800, 1200, 30); + Course course2 = stubCourseWithOf(2L, "test2", CourseType.DAILY, + 12, 900, 1300, 28); + + + Page mockCoursePage = new PageImpl<>(List.of(course1, course2),pageable, 2); + when(courseRepository.findAll(pageable)).thenReturn(mockCoursePage); + + //when + Page result = courseService.getCourses(pageable, null); + + //then + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent().get(0).getId()).isEqualTo(1L); + assertThat(result.getContent().get(1).getId()).isEqualTo(2L); + assertThat(result.getContent().get(0).getStatus()).isEqualTo(UserCourseState.NOT_STARTED); + assertThat(result.getContent().get(1).getStatus()).isEqualTo(UserCourseState.NOT_STARTED); + } + + @Test + void getCourseEmpryCase(){ + //given + Pageable pageable = PageRequest.of(0, 2); + Page emptyPage = Page.empty(pageable); + when(courseRepository.findAll(pageable)).thenReturn(emptyPage); + + //when + Page result = courseService.getCourses(pageable, null); + + // then + assertThat(result).isNotNull(); + assertThat(result.getContent()).isEmpty(); + assertThat(result.getTotalElements()).isZero(); + } + + @Test + void updateCourseNormalCase(){ + //given + Long id = 1L; + Course mockCourse = mock(Course.class); + when(courseRepository.findById(id)).thenReturn(Optional.of(mockCourse)); + when(courseRepository.save(mockCourse)).thenReturn(mockCourse); + + //when + Course updated = courseService.updateCourse(id); + + //then + assertThat(updated).isSameAs(mockCourse); + } + + @Test + void updateCourseEmptyCase(){ + Long id = 1L; + when(courseRepository.findById(id)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> courseService.updateCourse(id)) + .isInstanceOf(NoSuchElementException.class) + .hasMessageContaining("해당 코스 아이디로 된 코스가 존재하지 않습니다."); + + verify(courseRepository, times(1)).findById(id); + verify(courseRepository, never()).save(any()); + } + + @Test + void getCoursePageByCourseTypeNullCase(){ + //given + Pageable pageable = PageRequest.of(0, 2); + Course course1 = new Course(); + Course course2 = new Course(); + Page mockCoursePage = new PageImpl<>(List.of(course1, course2), pageable, 2); + + when(courseRepository.findAll(pageable)).thenReturn(mockCoursePage); + + //when + Page result = courseService.getCoursePageByCourseType(pageable, null); + + //then + assertThat(result).isSameAs(mockCoursePage); + assertThat(result.getContent()).containsExactly(course1, course2); + verify(courseRepository, times(1)).findAll(pageable); + verify(courseRepository, never()).findAllByCourseType(any(), any()); + } + + @Test + void getCoursePageByCourseTypeNotNullCase(){ + Pageable pageable = PageRequest.of(1, 2); + CourseType type = CourseType.DAILY; + Course course1 = new Course(); + Page mockPage = new PageImpl<>(List.of(course1), pageable, 5); + + when(courseRepository.findAllByCourseType(type, pageable)).thenReturn(mockPage); + + // when + Page result = courseService.getCoursePageByCourseType(pageable, type); + + //then + assertThat(result).isSameAs(mockPage); + assertThat(result.getTotalElements()).isEqualTo(5); + verify(courseRepository, times(1)).findAllByCourseType(type, pageable); + verify(courseRepository, never()).findAll(any(Pageable.class)); + } + + @Test + void createCourseNormalCase() throws IOException{ + //given + CourseRequestDto req = new CourseRequestDto(); + setFieldQuiet(req, "title", "title-1"); + setFieldQuiet(req, "description", "desc"); + setFieldQuiet(req, "type", CourseType.DAILY); + setFieldQuiet(req, "problemCnt", 10); + setFieldQuiet(req, "minRank", 800); + setFieldQuiet(req, "maxRank", 1200); + setFieldQuiet(req, "duration", 30); + setFieldQuiet(req, "algorithmIdList", java.util.List.of()); + + when(courseRepository.save(any(Course.class))) + .thenAnswer(inv -> inv.getArgument(0)); + + //when + Course created = courseService.createCourse(req); + + //then + ArgumentCaptor captor = ArgumentCaptor.forClass(Course.class); + verify(courseRepository, times(1)).save(captor.capture()); + Course saved = captor.getValue(); + + assertThat(created).isSameAs(saved); + verify(problemScrapingService, times(1)).createProblemsByCourse(saved, req); + verifyNoMoreInteractions(problemScrapingService); + //코스를 생성하여 save 한 후 순차적으로 스크래핑이 이어져야함. + } + + @Test + void CreateEmptyCourseNormalCase(){ + var emptyReq = new com.aloc.aloc.admin.dto.request.EmptyCourseRequestDto(); + setFieldQuiet(emptyReq, "title", "empty-course"); + setFieldQuiet(emptyReq, "description", "empty-desc"); + setFieldQuiet(emptyReq, "type", CourseType.DAILY); + setFieldQuiet(emptyReq, "duration", 25); + + when(courseRepository.save(any(Course.class))) + .thenAnswer(inv -> inv.getArgument(0)); + + // when + Course created = courseService.createEmptyCourse(emptyReq); + + // then + ArgumentCaptor courseCaptor = ArgumentCaptor.forClass(Course.class); + verify(courseRepository, times(1)).save(courseCaptor.capture()); + Course saved = courseCaptor.getValue(); + + // 동일 인스턴스 반환 확인 + assertThat(created).isSameAs(saved); + + // 스크래핑 서비스는 호출되지 않아야 함 + verifyNoInteractions(problemScrapingService); + } + + @Test + void getCourseByIdSuccessCase(){ + //given + Long id = 1L; + Course mockCourse = new Course(); + when(courseRepository.findById(id)).thenReturn(Optional.of(mockCourse)); + + //when + Course result = courseService.getCourseById(id); + + //then + assertThat(result).isEqualTo(mockCourse); + verify(courseRepository, times(1)).findById(id); + } + + @Test + void getCourseByIdFailCase() { + // given + Long id = 1L; + when(courseRepository.findById(id)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> courseService.getCourseById(id)) + .isInstanceOf(NoSuchElementException.class) + .hasMessage("해당 코스 아이디로 된 코스가 존재하지 않습니다."); + + verify(courseRepository, times(1)).findById(id); + } + + @Test + void getRecommendedCourseSuccessCase(){ + // given + Algorithm algorithm = mock(Algorithm.class); + when(algorithm.getId()).thenReturn(1L); + + ProblemAlgorithm problemAlgorithm = mock(ProblemAlgorithm.class); + when(problemAlgorithm.getAlgorithm()).thenReturn(algorithm); + + Problem problem = mock(Problem.class); + when(problem.getProblemAlgorithmList()).thenReturn(List.of(problemAlgorithm)); + + CourseProblem courseProblem = mock(CourseProblem.class); + when(courseProblem.getProblem()).thenReturn(problem); + + Course course = mock(Course.class); + when(course.getCourseProblemList()).thenReturn(List.of(courseProblem)); + + List expectedCourses = List.of(mock(Course.class)); + when(courseRepository.findCoursesByAlgorithmIds(List.of(1L))).thenReturn(expectedCourses); + + // when + List result = courseService.getRecommendedCourses(course); + + // then + assertThat(result).isEqualTo(expectedCourses); + verify(courseRepository).findCoursesByAlgorithmIds(List.of(1L)); + } + + @Test + void getRecommendedCourseEmptyCase() { + // given + Algorithm algorithm = mock(Algorithm.class); + when(algorithm.getId()).thenReturn(1L); + + ProblemAlgorithm problemAlgorithm = mock(ProblemAlgorithm.class); + when(problemAlgorithm.getAlgorithm()).thenReturn(algorithm); + + Problem problem = mock(Problem.class); + when(problem.getProblemAlgorithmList()).thenReturn(List.of(problemAlgorithm)); + + CourseProblem courseProblem = mock(CourseProblem.class); + when(courseProblem.getProblem()).thenReturn(problem); + + Course course = mock(Course.class); + when(course.getCourseProblemList()).thenReturn(List.of(courseProblem)); + + // repository가 빈 리스트 반환하도록 설정 + when(courseRepository.findCoursesByAlgorithmIds(List.of(1L))).thenReturn(List.of()); + + // when + List result = courseService.getRecommendedCourses(course); + + // then + assertThat(result).isEmpty(); + verify(courseRepository, times(1)).findCoursesByAlgorithmIds(List.of(1L)); + } + +}