diff --git a/back/src/test/java/com/back/domain/mentoring/mentoring/controller/MentoringControllerTest.java b/back/src/test/java/com/back/domain/mentoring/mentoring/controller/MentoringControllerTest.java index ad0959e4..78a14784 100644 --- a/back/src/test/java/com/back/domain/mentoring/mentoring/controller/MentoringControllerTest.java +++ b/back/src/test/java/com/back/domain/mentoring/mentoring/controller/MentoringControllerTest.java @@ -2,17 +2,15 @@ import com.back.domain.member.member.entity.Member; import com.back.domain.member.member.service.AuthTokenService; -import com.back.domain.member.mentee.entity.Mentee; import com.back.domain.member.mentor.entity.Mentor; import com.back.domain.mentoring.mentoring.dto.request.MentoringRequest; import com.back.domain.mentoring.mentoring.entity.Mentoring; import com.back.domain.mentoring.mentoring.error.MentoringErrorCode; import com.back.domain.mentoring.mentoring.repository.MentoringRepository; import com.back.domain.mentoring.reservation.repository.ReservationRepository; -import com.back.domain.mentoring.slot.entity.MentorSlot; import com.back.domain.mentoring.slot.repository.MentorSlotRepository; import com.back.fixture.MemberTestFixture; -import com.back.fixture.MentoringTestFixture; +import com.back.fixture.mentoring.MentoringTestFixture; import com.back.global.exception.ServiceException; import com.back.standard.util.Ut; import jakarta.servlet.http.Cookie; @@ -53,7 +51,6 @@ class MentoringControllerTest { private static final String MENTORING_URL = "/mentorings"; private Mentor mentor; - private Mentee mentee; private String mentorToken; private String menteeToken; @@ -65,7 +62,7 @@ void setUp() { // Mentee Member menteeMember = memberFixture.createMenteeMember(); - mentee = memberFixture.createMentee(menteeMember); + memberFixture.createMentee(menteeMember); // JWT 발급 mentorToken = authTokenService.genAccessToken(mentorMember); @@ -222,17 +219,6 @@ void createMentoringFailNotMentor() throws Exception { .andExpect(jsonPath("$.msg").value("멘토를 찾을 수 없습니다.")); } - @Test - @DisplayName("멘토링 생성 실패 - 멘토당 멘토링 1개 제한") - void createMentoringFailDuplicate() throws Exception { - mentoringFixture.createMentoring(mentor); - - performCreateMentoring(mentorToken) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.resultCode").value("409-1")) - .andExpect(jsonPath("$.msg").value("이미 멘토링 정보가 존재합니다.")); - } - // ===== 멘토링 수정 ===== @@ -283,22 +269,6 @@ void updateMentoringFailNotMentoring() throws Exception { .andExpect(jsonPath("$.msg").value("멘토링을 찾을 수 없습니다.")); } - @Test - @DisplayName("멘토링 수정 실패 - 멘토링 소유자가 아닌 경우") - void updateMentoringFailNotOwner() throws Exception { - Mentoring mentoring = mentoringFixture.createMentoring(mentor); - - // 다른 멘토 - Member otherMentor = memberFixture.createMentorMember(); - memberFixture.createMentor(otherMentor); - String token = authTokenService.genAccessToken(otherMentor); - - performUpdateMentoring(mentoring.getId(), token) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.resultCode").value("403-1")) - .andExpect(jsonPath("$.msg").value("해당 멘토링에 대한 권한이 없습니다.")); - } - // ===== 멘토링 삭제 ===== @@ -374,36 +344,6 @@ void deleteMentoringFailNotMentoring() throws Exception { .andExpect(jsonPath("$.msg").value("멘토링을 찾을 수 없습니다.")); } - @Test - @DisplayName("멘토링 삭제 실패 - 멘토링 소유자가 아닌 경우") - void deleteMentoringFailNotOwner() throws Exception { - Mentoring mentoring = mentoringFixture.createMentoring(mentor); - - // 다른 멘토 - Member otherMentor = memberFixture.createMentorMember(); - memberFixture.createMentor(otherMentor); - String token = authTokenService.genAccessToken(otherMentor); - - performDeleteMentoring(mentoring.getId(), token) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.resultCode").value("403-1")) - .andExpect(jsonPath("$.msg").value("해당 멘토링에 대한 권한이 없습니다.")); - } - - @Test - @DisplayName("멘토링 삭제 실패 - 예약 정보가 있는 경우") - void deleteMentoringFailExistsReservation() throws Exception { - Mentoring mentoring = mentoringFixture.createMentoring(mentor); - MentorSlot mentorSlot = mentoringFixture.createMentorSlot(mentor); - mentoringFixture.createReservation(mentoring, mentee, mentorSlot); - - performDeleteMentoring(mentoring.getId(), mentorToken) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.resultCode").value("400-1")) - .andExpect(jsonPath("$.msg").value("예약 이력이 있는 멘토링은 삭제할 수 없습니다.")); - - } - // ===== perform ===== diff --git a/back/src/test/java/com/back/domain/mentoring/mentoring/service/MentoringServiceTest.java b/back/src/test/java/com/back/domain/mentoring/mentoring/service/MentoringServiceTest.java new file mode 100644 index 00000000..007bb28d --- /dev/null +++ b/back/src/test/java/com/back/domain/mentoring/mentoring/service/MentoringServiceTest.java @@ -0,0 +1,328 @@ +package com.back.domain.mentoring.mentoring.service; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.member.mentor.entity.Mentor; +import com.back.domain.mentoring.mentoring.dto.MentoringWithTagsDto; +import com.back.domain.mentoring.mentoring.dto.request.MentoringRequest; +import com.back.domain.mentoring.mentoring.dto.response.MentoringResponse; +import com.back.domain.mentoring.mentoring.entity.Mentoring; +import com.back.domain.mentoring.mentoring.error.MentoringErrorCode; +import com.back.domain.mentoring.mentoring.repository.MentoringRepository; +import com.back.fixture.MemberFixture; +import com.back.fixture.MentorFixture; +import com.back.fixture.mentoring.MentoringFixture; +import com.back.global.exception.ServiceException; +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.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +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.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MentoringServiceTest { + + @InjectMocks + private MentoringService mentoringService; + + @Mock + private MentoringRepository mentoringRepository; + + @Mock + private MentoringStorage mentoringStorage; + + private Mentor mentor1, mentor2; + private Mentoring mentoring1; + private MentoringRequest request; + + @BeforeEach + void setUp() { + Member member1 = MemberFixture.create("mentor1@test.com", "Mentor1", "pass123"); + mentor1 = MentorFixture.create(1L, member1); + mentoring1 = MentoringFixture.create(1L, mentor1); + + Member member2 = MemberFixture.create("mentor2@test.com", "Mentor2", "pass123"); + mentor2 = MentorFixture.create(2L, member2); + + request = new MentoringRequest( + "Spring Boot 멘토링", + List.of("Spring", "Java"), + "Spring Boot를 활용한 백엔드 개발 입문", + "https://example.com/thumb.jpg" + ); + } + + @Nested + @DisplayName("멘토링 다건 조회") + class Describe_getMentorings { + + @Test + @DisplayName("검색어 없이 일반 조회") + void getMentorings() { + // given + Mentoring mentoring2 = MentoringFixture.create(2L, mentor2); + + Member member3 = MemberFixture.create("mentor3@test.com", "Mentor3", "pass123"); + Mentor mentor3 = MentorFixture.create(3L, member3); + Mentoring mentoring3 = MentoringFixture.create(3L, mentor3); + + String keyword = ""; + Pageable pageable = PageRequest.of(0, 10); + + List mentorings = List.of(mentoring1, mentoring2, mentoring3); + Page mentoringPage = new PageImpl<>(mentorings, pageable, mentorings.size()); + + when(mentoringRepository.searchMentorings(keyword, pageable)) + .thenReturn(mentoringPage); + + // when + Page result = mentoringService.getMentorings(keyword, 0, 10); + + // then + assertThat(result.getContent()).hasSize(3); + verify(mentoringRepository).searchMentorings(keyword, pageable); + } + + @Test + @DisplayName("멘토명 검색") + void searchByKeyword() { + // given + String keyword = "Mentor2"; + Pageable pageable = PageRequest.of(0, 10); + + List mentorings = List.of(mentoring1); + Page mentoringPage = new PageImpl<>(mentorings, pageable, 1); + + when(mentoringRepository.searchMentorings(keyword, pageable)) + .thenReturn(mentoringPage); + + // when + Page result = mentoringService.getMentorings(keyword, 0, 10); + + // then + assertThat(result.getContent()).hasSize(1); + verify(mentoringRepository).searchMentorings(keyword, pageable); + } + + @Test + @DisplayName("검색 결과 없을 시 빈 페이지 반환") + void returnEmptyPage() { + // given + String keyword = "NoMatch"; + Pageable pageable = PageRequest.of(0, 10); + Page emptyPage = new PageImpl<>(List.of(), pageable, 0); + + when(mentoringRepository.searchMentorings(keyword, pageable)) + .thenReturn(emptyPage); + + // when + Page result = mentoringService.getMentorings(keyword, 0, 10); + + // then + assertThat(result.getContent()).isEmpty(); + verify(mentoringRepository).searchMentorings(keyword, pageable); + } + } + + @Nested + @DisplayName("멘토링 조회") + class Describe_getMentoring { + + @Test + @DisplayName("조회 성공") + void getMentoring() { + // given + Long mentoringId = 1L; + + when(mentoringStorage.findMentoring(mentoringId)) + .thenReturn(mentoring1); + + // when + MentoringResponse result = mentoringService.getMentoring(mentoringId); + + // then + assertThat(result).isNotNull(); + verify(mentoringStorage).findMentoring(mentoringId); + } + } + + @Nested + @DisplayName("멘토링 생성") + class Describe_createMentoring { + + @Test + @DisplayName("생성 성공") + void createMentoring() { + when(mentoringRepository.existsByMentorId(mentor1.getId())) + .thenReturn(false); + + // when + MentoringResponse result = mentoringService.createMentoring(request, mentor1); + + // then + assertThat(result).isNotNull(); + assertThat(result.mentoring().title()).isEqualTo(request.title()); + assertThat(result.mentoring().bio()).isEqualTo(request.bio()); + assertThat(result.mentoring().tags()).isEqualTo(request.tags()); + assertThat(result.mentoring().thumb()).isEqualTo(request.thumb()); + verify(mentoringRepository).existsByMentorId(mentor1.getId()); + verify(mentoringRepository).save(any(Mentoring.class)); + } + + @Test + @DisplayName("이미 존재하면 예외 (멘토당 멘토링 1개 제한)") + void throwExceptionWhenAlreadyExists() { + // given + when(mentoringRepository.existsByMentorId(mentor1.getId())) + .thenReturn(true); + + // when & then + assertThatThrownBy(() -> mentoringService.createMentoring(request, mentor1)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentoringErrorCode.ALREADY_EXISTS_MENTORING.getCode()); + verify(mentoringRepository).existsByMentorId(mentor1.getId()); + verify(mentoringRepository, never()).save(any(Mentoring.class)); + } + } + + @Nested + @DisplayName("멘토링 수정") + class Describe_updateMentoring { + + @Test + @DisplayName("수정 성공") + void updateMentoring() { + // given + Long mentoringId = 1L; + + when(mentoringStorage.findMentoring(mentoringId)) + .thenReturn(mentoring1); + + // when + MentoringResponse result = mentoringService.updateMentoring(mentoringId, request, mentor1); + + // then + assertThat(result).isNotNull(); + assertThat(result.mentoring().title()).isEqualTo(request.title()); + assertThat(result.mentoring().bio()).isEqualTo(request.bio()); + assertThat(result.mentoring().tags()).isEqualTo(request.tags()); + assertThat(result.mentoring().thumb()).isEqualTo(request.thumb()); + verify(mentoringStorage).findMentoring(mentoringId); + } + + @Test + @DisplayName("소유자가 아니면 예외") + void throwExceptionWhenNotOwner() { + // given + Long mentoringId = 1L; + + when(mentoringStorage.findMentoring(mentoringId)) + .thenReturn(mentoring1); + + // when & then + assertThatThrownBy(() -> mentoringService.updateMentoring(mentoringId, request, mentor2)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentoringErrorCode.FORBIDDEN_NOT_OWNER.getCode()); + } + } + + @Nested + @DisplayName("멘토링 삭제") + class Describe_deleteMentoring { + + @Test + @DisplayName("관련 엔티티 없을 시 삭제 성공") + void deleteMentoring() { + // given + Long mentoringId = 1L; + + when(mentoringStorage.findMentoring(mentoringId)) + .thenReturn(mentoring1); + when(mentoringStorage.hasReservationsForMentoring(mentoringId)) + .thenReturn(false); + when(mentoringStorage.hasMentorSlotsForMentor(mentor1.getId())) + .thenReturn(false); + + // when + mentoringService.deleteMentoring(mentoringId, mentor1); + + // then + verify(mentoringStorage).findMentoring(mentoringId); + verify(mentoringStorage).hasReservationsForMentoring(mentoringId); + verify(mentoringStorage).hasMentorSlotsForMentor(mentor1.getId()); + verify(mentoringRepository).delete(mentoring1); + } + + @Test + @DisplayName("멘토 슬롯 있으면 함께 삭제") + void deleteWithMentorSlots() { + // given + Long mentoringId = 1L; + + when(mentoringStorage.findMentoring(mentoringId)) + .thenReturn(mentoring1); + when(mentoringStorage.hasReservationsForMentoring(mentoringId)) + .thenReturn(false); + when(mentoringStorage.hasMentorSlotsForMentor(mentor1.getId())) + .thenReturn(true); + + // when + mentoringService.deleteMentoring(mentoringId, mentor1); + + // then + verify(mentoringStorage).deleteMentorSlotsData(mentor1.getId()); + verify(mentoringRepository).delete(mentoring1); + } + + @Test + @DisplayName("소유자가 아니면 예외") + void throwExceptionWhenNotOwner() { + // given + Long mentoringId = 1L; + + when(mentoringStorage.findMentoring(mentoringId)) + .thenReturn(mentoring1); + + // when & then + assertThatThrownBy(() -> mentoringService.deleteMentoring(mentoringId, mentor2)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentoringErrorCode.FORBIDDEN_NOT_OWNER.getCode()); + + verify(mentoringRepository, never()).delete(any(Mentoring.class)); + } + + @Test + @DisplayName("예약 존재하면 예외") + void throwExceptionWhenReservationsExist() { + // given + Long mentoringId = 1L; + + when(mentoringStorage.findMentoring(mentoringId)) + .thenReturn(mentoring1); + when(mentoringStorage.hasReservationsForMentoring(mentoringId)) + .thenReturn(true); + + // when & then + assertThatThrownBy(() -> mentoringService.deleteMentoring(mentoringId, mentor1)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentoringErrorCode.CANNOT_DELETE_MENTORING.getCode()); + + verify(mentoringStorage).findMentoring(mentoringId); + verify(mentoringStorage).hasReservationsForMentoring(mentoringId); + verify(mentoringRepository, never()).delete(any()); + } + } +} \ No newline at end of file diff --git a/back/src/test/java/com/back/domain/mentoring/reservation/controller/ReservationControllerTest.java b/back/src/test/java/com/back/domain/mentoring/reservation/controller/ReservationControllerTest.java index ea2052be..d6f01588 100644 --- a/back/src/test/java/com/back/domain/mentoring/reservation/controller/ReservationControllerTest.java +++ b/back/src/test/java/com/back/domain/mentoring/reservation/controller/ReservationControllerTest.java @@ -10,7 +10,7 @@ import com.back.domain.mentoring.reservation.repository.ReservationRepository; import com.back.domain.mentoring.slot.entity.MentorSlot; import com.back.fixture.MemberTestFixture; -import com.back.fixture.MentoringTestFixture; +import com.back.fixture.mentoring.MentoringTestFixture; import com.back.global.exception.ServiceException; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; @@ -91,30 +91,6 @@ void createReservationSuccess() throws Exception { .andExpect(jsonPath("$.data.reservation.endDateTime").value(mentorSlot.getEndDateTime().format(formatter))); } - @Test - @DisplayName("멘티가 멘토에게 예약 신청 실패 - 예약 가능한 상태가 아닌 경우") - void createReservationFailNotAvailable() throws Exception { - Member menteeMember = memberFixture.createMenteeMember(); - Mentee mentee2 = memberFixture.createMentee(menteeMember); - mentoringFixture.createReservation(mentoring, mentee2, mentorSlot); - - performCreateReservation() - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.resultCode").value("409-1")) - .andExpect(jsonPath("$.msg").value("이미 예약이 완료된 시간대입니다.")); - } - - @Test - @DisplayName("멘티가 멘토에게 예약 신청 실패 - 이미 예약한 경우") - void createReservationFailAlreadyReservation() throws Exception { - mentoringFixture.createReservation(mentoring, mentee, mentorSlot); - - performCreateReservation() - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.resultCode").value("409-2")) - .andExpect(jsonPath("$.msg").value("이미 예약한 시간대입니다. 예약 목록을 확인해 주세요.")); - } - // ===== perform ===== diff --git a/back/src/test/java/com/back/domain/mentoring/reservation/service/ReservationServiceTest.java b/back/src/test/java/com/back/domain/mentoring/reservation/service/ReservationServiceTest.java new file mode 100644 index 00000000..d4e65152 --- /dev/null +++ b/back/src/test/java/com/back/domain/mentoring/reservation/service/ReservationServiceTest.java @@ -0,0 +1,212 @@ +package com.back.domain.mentoring.reservation.service; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.member.mentee.entity.Mentee; +import com.back.domain.member.mentor.entity.Mentor; +import com.back.domain.mentoring.mentoring.entity.Mentoring; +import com.back.domain.mentoring.mentoring.service.MentoringStorage; +import com.back.domain.mentoring.reservation.constant.ReservationStatus; +import com.back.domain.mentoring.reservation.dto.request.ReservationRequest; +import com.back.domain.mentoring.reservation.dto.response.ReservationResponse; +import com.back.domain.mentoring.reservation.entity.Reservation; +import com.back.domain.mentoring.reservation.error.ReservationErrorCode; +import com.back.domain.mentoring.reservation.repository.ReservationRepository; +import com.back.domain.mentoring.slot.entity.MentorSlot; +import com.back.domain.mentoring.slot.error.MentorSlotErrorCode; +import com.back.fixture.MemberFixture; +import com.back.fixture.MenteeFixture; +import com.back.fixture.MentorFixture; +import com.back.fixture.mentoring.MentorSlotFixture; +import com.back.fixture.mentoring.MentoringFixture; +import com.back.global.exception.ServiceException; +import jakarta.persistence.OptimisticLockException; +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 java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ReservationServiceTest { + + @InjectMocks + private ReservationService reservationService; + + @Mock + private ReservationRepository reservationRepository; + + @Mock + private MentoringStorage mentoringStorage; + + private Mentor mentor; + private Mentee mentee, mentee2; + private Mentoring mentoring; + private MentorSlot mentorSlot; + + @BeforeEach + void setUp() { + Member mentorMember = MemberFixture.create("mentor@test.com", "Mentor", "pass123"); + mentor = MentorFixture.create(1L, mentorMember); + + Member menteeMember = MemberFixture.create("mentee@test.com", "Mentee", "pass123"); + mentee = MenteeFixture.create(1L, menteeMember); + + Member menteeMember2 = MemberFixture.create("mentee2@test.com", "Mentee2", "pass123"); + mentee2 = MenteeFixture.create(2L, menteeMember2); + + mentoring = MentoringFixture.create(1L, mentor); + mentorSlot = MentorSlotFixture.create(1L, mentor); + } + + @Nested + @DisplayName("멘토링 예약 생성") + class Describe_createReservation { + + private ReservationRequest request; + + @BeforeEach + void setUp() { + request = new ReservationRequest( + mentor.getId(), + mentorSlot.getId(), + mentoring.getId(), + "사전 질문입니다." + ); + } + + @Test + @DisplayName("생성 성공") + void createReservation() { + // given + when(mentoringStorage.findMentoring(request.mentoringId())) + .thenReturn(mentoring); + when(mentoringStorage.findMentorSlot(request.mentorSlotId())) + .thenReturn(mentorSlot); + when(reservationRepository.findByMentorSlotIdAndStatusIn(mentorSlot.getId(), + List.of(ReservationStatus.PENDING, ReservationStatus.APPROVED, ReservationStatus.COMPLETED))) + .thenReturn(Optional.empty()); + + // when + ReservationResponse response = reservationService.createReservation(mentee, request); + + // then + assertThat(response.mentoring().mentoringId()).isEqualTo(mentoring.getId()); + assertThat(response.mentee().menteeId()).isEqualTo(mentee.getId()); + assertThat(response.mentor().mentorId()).isEqualTo(mentor.getId()); + assertThat(response.reservation().mentorSlotId()).isEqualTo(mentorSlot.getId()); + assertThat(response.reservation().preQuestion()).isEqualTo(request.preQuestion()); + assertThat(response.reservation().status()).isEqualTo(ReservationStatus.PENDING); + + verify(reservationRepository).save(any(Reservation.class)); + } + + @Test + @DisplayName("이미 해당 멘티가 예약한 슬롯이면 예외") + void throwExceptionWhenAlreadyReservedBySameMentee() { + // given + Reservation existingReservation = Reservation.builder() + .mentee(mentee) + .mentorSlot(mentorSlot) + .mentoring(mentoring) + .build(); + + when(mentoringStorage.findMentoring(request.mentoringId())) + .thenReturn(mentoring); + when(mentoringStorage.findMentorSlot(request.mentorSlotId())) + .thenReturn(mentorSlot); + when(reservationRepository.findByMentorSlotIdAndStatusIn(mentorSlot.getId(), + List.of(ReservationStatus.PENDING, ReservationStatus.APPROVED, ReservationStatus.COMPLETED))) + .thenReturn(Optional.of(existingReservation)); + + // when & then + assertThatThrownBy(() -> reservationService.createReservation(mentee, request)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.ALREADY_RESERVED_SLOT.getCode()); + } + + @Test + @DisplayName("다른 멘티가 이미 예약한 슬롯이면 예외") + void throwExceptionWhenSlotNotAvailable() { + // given + Reservation existingReservation = Reservation.builder() + .mentee(mentee2) // 다른 멘티 + .mentorSlot(mentorSlot) + .mentoring(mentoring) + .build(); + + when(mentoringStorage.findMentoring(request.mentoringId())) + .thenReturn(mentoring); + when(mentoringStorage.findMentorSlot(request.mentorSlotId())) + .thenReturn(mentorSlot); + when(reservationRepository.findByMentorSlotIdAndStatusIn(mentorSlot.getId(), + List.of(ReservationStatus.PENDING, ReservationStatus.APPROVED, ReservationStatus.COMPLETED))) + .thenReturn(Optional.of(existingReservation)); + + // when & then + assertThatThrownBy(() -> reservationService.createReservation(mentee, request)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.NOT_AVAILABLE_SLOT.getCode()); + } + + @Test + @DisplayName("예약 시간이 과거이면 예외") + void throwExceptionWhenStartTimeInPast() { + // given + MentorSlot pastSlot = MentorSlotFixture.create(2L, mentor, + LocalDateTime.now().minusDays(1), LocalDateTime.now().minusDays(1).plusHours(1)); + + ReservationRequest pastRequest = new ReservationRequest( + mentor.getId(), + pastSlot.getId(), + mentoring.getId(), + "사전 질문입니다." + ); + + when(mentoringStorage.findMentoring(pastRequest.mentoringId())) + .thenReturn(mentoring); + when(mentoringStorage.findMentorSlot(pastRequest.mentorSlotId())) + .thenReturn(pastSlot); + when(reservationRepository.findByMentorSlotIdAndStatusIn(pastSlot.getId(), + List.of(ReservationStatus.PENDING, ReservationStatus.APPROVED, ReservationStatus.COMPLETED))) + .thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> reservationService.createReservation(mentee, pastRequest)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentorSlotErrorCode.START_TIME_IN_PAST.getCode()); + } + + @Test + @DisplayName("동시성 충돌 발생 시 예외") + void throwExceptionOnConcurrentReservation() { + // given + when(mentoringStorage.findMentoring(request.mentoringId())) + .thenReturn(mentoring); + when(mentoringStorage.findMentorSlot(request.mentorSlotId())) + .thenReturn(mentorSlot); + when(reservationRepository.findByMentorSlotIdAndStatusIn(mentorSlot.getId(), + List.of(ReservationStatus.PENDING, ReservationStatus.APPROVED, ReservationStatus.COMPLETED))) + .thenReturn(Optional.empty()); + + // OptimisticLockException 테스트 위해 save 호출 시 예외 설정 + doThrow(new OptimisticLockException()).when(reservationRepository).save(any(Reservation.class)); + + // when & then + assertThatThrownBy(() -> reservationService.createReservation(mentee, request)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.CONCURRENT_RESERVATION_CONFLICT.getCode()); + } + } +} diff --git a/back/src/test/java/com/back/domain/mentoring/slot/controller/MentorSlotControllerTest.java b/back/src/test/java/com/back/domain/mentoring/slot/controller/MentorSlotControllerTest.java index 8141a557..3412ecaf 100644 --- a/back/src/test/java/com/back/domain/mentoring/slot/controller/MentorSlotControllerTest.java +++ b/back/src/test/java/com/back/domain/mentoring/slot/controller/MentorSlotControllerTest.java @@ -2,16 +2,13 @@ import com.back.domain.member.member.entity.Member; import com.back.domain.member.member.service.AuthTokenService; -import com.back.domain.member.mentee.entity.Mentee; import com.back.domain.member.mentor.entity.Mentor; import com.back.domain.mentoring.mentoring.entity.Mentoring; -import com.back.domain.mentoring.reservation.constant.ReservationStatus; -import com.back.domain.mentoring.reservation.entity.Reservation; import com.back.domain.mentoring.slot.entity.MentorSlot; import com.back.domain.mentoring.slot.error.MentorSlotErrorCode; import com.back.domain.mentoring.slot.repository.MentorSlotRepository; import com.back.fixture.MemberTestFixture; -import com.back.fixture.MentoringTestFixture; +import com.back.fixture.mentoring.MentoringTestFixture; import com.back.global.exception.ServiceException; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; @@ -219,15 +216,6 @@ void createMentorSlotFailInValidDate() throws Exception { .andExpect(jsonPath("$.msg").value("종료 일시는 시작 일시보다 이후여야 합니다.")); } - @Test - @DisplayName("멘토 슬롯 생성 실패 - 기존 슬롯과 시간 겹치는 경우") - void createMentorSlotFailOverlappingSlots() throws Exception { - performCreateMentorSlot(mentor.getId(), mentorToken, "2025-10-01T11:00:00", "2025-10-01T11:20:00") - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.resultCode").value("409-1")) - .andExpect(jsonPath("$.msg").value("선택한 시간은 이미 예약된 시간대입니다.")); - } - // ===== 슬롯 반복 생성 ===== @Test @@ -308,83 +296,6 @@ void updateMentorSlotSuccess() throws Exception { .andExpect(jsonPath("$.data.mentorSlotStatus").value("AVAILABLE")); } - @Test - @DisplayName("멘토 슬롯 수정 성공 - 비활성화된 예약이 있는 경우") - void updateMentorSlotSuccessReserved() throws Exception { - MentorSlot mentorSlot = mentorSlots.getFirst(); - - // 예약 생성 및 취소 - Member menteeMember = memberFixture.createMenteeMember(); - Mentee mentee = memberFixture.createMentee(menteeMember); - Reservation reservation = mentoringFixture.createReservation(mentoring, mentee, mentorSlot); - reservation.updateStatus(ReservationStatus.CANCELED); - - // 수정 API - LocalDateTime updateEndDate = mentorSlot.getEndDateTime().minusMinutes(10); - ResultActions resultActions = performUpdateMentorSlot(mentor.getId(), mentorToken, mentorSlot, updateEndDate); - - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); - String expectedEndDate = updateEndDate.format(formatter); - - resultActions - .andExpect(status().isOk()) - .andExpect(jsonPath("$.resultCode").value("200")) - .andExpect(jsonPath("$.msg").value("멘토의 예약 가능 일정이 수정되었습니다.")) - .andExpect(jsonPath("$.data.mentorSlotId").value(mentorSlot.getId())) - .andExpect(jsonPath("$.data.mentorId").value(mentorSlot.getMentor().getId())) - .andExpect(jsonPath("$.data.mentoringId").value(mentoring.getId())) - .andExpect(jsonPath("$.data.mentoringTitle").value(mentoring.getTitle())) - .andExpect(jsonPath("$.data.endDateTime").value(expectedEndDate)) - .andExpect(jsonPath("$.data.mentorSlotStatus").value("AVAILABLE")); - } - - @Test - @DisplayName("멘토 슬롯 수정 실패 - 작성자가 아닌 경우") - void updateMentorSlotFailNotOwner() throws Exception { - Member mentorMember2 = memberFixture.createMentorMember(); - Mentor mentor2 = memberFixture.createMentor(mentorMember2); - mentoringFixture.createMentoring(mentor2); - String token = authTokenService.genAccessToken(mentorMember2); - - MentorSlot mentorSlot = mentorSlots.getFirst(); - LocalDateTime updateEndDate = mentorSlots.get(1).getEndDateTime(); - - performUpdateMentorSlot(mentor2.getId(), token, mentorSlot, updateEndDate) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.resultCode").value("403-1")) - .andExpect(jsonPath("$.msg").value("접근 권한이 없습니다.")); - } - - @Test - @DisplayName("멘토 슬롯 수정 실패 - 기존 슬롯과 겹치는지 검사") - void updateMentorSlotFailOverlapping() throws Exception { - MentorSlot mentorSlot = mentorSlots.getFirst(); - LocalDateTime updateEndDate = mentorSlots.get(1).getEndDateTime(); - - performUpdateMentorSlot(mentor.getId(), mentorToken, mentorSlot, updateEndDate) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.resultCode").value("409-1")) - .andExpect(jsonPath("$.msg").value("선택한 시간은 이미 예약된 시간대입니다.")); - } - - @Test - @DisplayName("멘토 슬롯 수정 실패 - 활성화된 예약이 있는 경우") - void updateMentorSlotFailReserved() throws Exception { - MentorSlot mentorSlot = mentorSlots.getFirst(); - - // 예약 생성 - Member menteeMember = memberFixture.createMenteeMember(); - Mentee mentee = memberFixture.createMentee(menteeMember); - mentoringFixture.createReservation(mentoring, mentee, mentorSlot); - - LocalDateTime updateEndDate = mentorSlot.getEndDateTime().minusMinutes(10); - - performUpdateMentorSlot(mentor.getId(), mentorToken, mentorSlot, updateEndDate) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.resultCode").value("400-6")) - .andExpect(jsonPath("$.msg").value("예약된 슬롯은 수정할 수 없습니다.")); - } - // ===== delete ===== @@ -407,36 +318,6 @@ void deleteMentorSlotSuccess() throws Exception { assertThat(afterCnt).isEqualTo(beforeCnt - 1); } - @Test - @DisplayName("멘토 슬롯 삭제 실패 - 작성자가 아닌 경우") - void deleteMentorSlotFailNotOwner() throws Exception { - Member mentorMember2 = memberFixture.createMentorMember(); - memberFixture.createMentor(mentorMember2); - String token = authTokenService.genAccessToken(mentorMember2); - - performDeleteMentorSlot(mentorSlots.getFirst(), token) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.resultCode").value("403-1")) - .andExpect(jsonPath("$.msg").value("접근 권한이 없습니다.")); - } - - @Test - @DisplayName("멘토 슬롯 삭제 실패 - 예약이 있는 경우") - void deleteMentorSlotFailReserved() throws Exception { - MentorSlot mentorSlot = mentorSlots.getFirst(); - - // 예약 생성 및 취소 - Member menteeMember = memberFixture.createMenteeMember(); - Mentee mentee = memberFixture.createMentee(menteeMember); - Reservation reservation = mentoringFixture.createReservation(mentoring, mentee, mentorSlot); - reservation.updateStatus(ReservationStatus.CANCELED); - - performDeleteMentorSlot(mentorSlot, mentorToken) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.resultCode").value("400-7")) - .andExpect(jsonPath("$.msg").value("예약된 슬롯은 삭제할 수 없습니다.")); - } - // ===== perform ===== diff --git a/back/src/test/java/com/back/domain/mentoring/slot/service/MentorSlotServiceTest.java b/back/src/test/java/com/back/domain/mentoring/slot/service/MentorSlotServiceTest.java new file mode 100644 index 00000000..e93c1f2f --- /dev/null +++ b/back/src/test/java/com/back/domain/mentoring/slot/service/MentorSlotServiceTest.java @@ -0,0 +1,478 @@ +package com.back.domain.mentoring.slot.service; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.member.mentee.entity.Mentee; +import com.back.domain.member.mentor.entity.Mentor; +import com.back.domain.mentoring.mentoring.entity.Mentoring; +import com.back.domain.mentoring.mentoring.service.MentoringStorage; +import com.back.domain.mentoring.reservation.entity.Reservation; +import com.back.domain.mentoring.slot.constant.MentorSlotStatus; +import com.back.domain.mentoring.slot.dto.request.MentorSlotRepetitionRequest; +import com.back.domain.mentoring.slot.dto.request.MentorSlotRequest; +import com.back.domain.mentoring.slot.dto.response.MentorSlotDto; +import com.back.domain.mentoring.slot.dto.response.MentorSlotResponse; +import com.back.domain.mentoring.slot.entity.MentorSlot; +import com.back.domain.mentoring.slot.error.MentorSlotErrorCode; +import com.back.domain.mentoring.slot.repository.MentorSlotRepository; +import com.back.fixture.MemberFixture; +import com.back.fixture.MenteeFixture; +import com.back.fixture.MentorFixture; +import com.back.fixture.mentoring.MentorSlotFixture; +import com.back.fixture.mentoring.MentoringFixture; +import com.back.fixture.mentoring.ReservationFixture; +import com.back.global.exception.ServiceException; +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 java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MentorSlotServiceTest { + + @InjectMocks + private MentorSlotService mentorSlotService; + + @Mock + private MentorSlotRepository mentorSlotRepository; + + @Mock + private MentoringStorage mentoringStorage; + + private Mentor mentor1, mentor2; + private Mentoring mentoring1; + private MentorSlot mentorSlot1; + private Mentee mentee1; + + @BeforeEach + void setUp() { + Member mentorMember1 = MemberFixture.create("mentor1@test.com", "Mentor1", "pass123"); + mentor1 = MentorFixture.create(1L, mentorMember1); + mentoring1 = MentoringFixture.create(1L, mentor1); + mentorSlot1 = MentorSlotFixture.create(1L, mentor1); + + Member mentorMember2 = MemberFixture.create("mentor2@test.com", "Mentor2", "pass123"); + mentor2 = MentorFixture.create(2L, mentorMember2); + + Member menteeMember1 = MemberFixture.create("mentee1@test.com", "Mentee1", "pass123"); + mentee1 = MenteeFixture.create(1L, menteeMember1); + } + + @Nested + @DisplayName("나의 멘토 슬롯 목록 조회") + class Describe_getMyMentorSlots { + + @Test + @DisplayName("조회 성공") + void getMyMentorSlots() { + // given + LocalDateTime startDate = LocalDateTime.of(2025, 10, 1, 0, 0); + LocalDateTime endDate = LocalDateTime.of(2025, 10, 31, 23, 59); + + MentorSlot slot2 = MentorSlotFixture.create(2L, mentor1, + LocalDateTime.of(2025, 10, 2, 10, 0), + LocalDateTime.of(2025, 10, 2, 12, 0)); + MentorSlot slot3 = MentorSlotFixture.create(3L, mentor1, + LocalDateTime.of(2025, 10, 3, 14, 0), + LocalDateTime.of(2025, 10, 3, 16, 0)); + + List slots = List.of(mentorSlot1, slot2, slot3); + + when(mentorSlotRepository.findMySlots(mentor1.getId(), startDate, endDate)) + .thenReturn(slots); + + // when + List result = mentorSlotService.getMyMentorSlots(mentor1, startDate, endDate); + + // then + assertThat(result).hasSize(3); + verify(mentorSlotRepository).findMySlots(mentor1.getId(), startDate, endDate); + } + + @Test + @DisplayName("조회 결과 없을 시 빈 리스트 반환") + void returnEmptyList() { + // given + LocalDateTime startDate = LocalDateTime.of(2025, 11, 1, 0, 0); + LocalDateTime endDate = LocalDateTime.of(2025, 11, 30, 23, 59); + + when(mentorSlotRepository.findMySlots(mentor1.getId(), startDate, endDate)) + .thenReturn(List.of()); + + // when + List result = mentorSlotService.getMyMentorSlots(mentor1, startDate, endDate); + + // then + assertThat(result).isEmpty(); + verify(mentorSlotRepository).findMySlots(mentor1.getId(), startDate, endDate); + } + } + + @Nested + @DisplayName("예약 가능한 멘토 슬롯 목록 조회") + class Describe_getAvailableMentorSlots { + + @Test + @DisplayName("조회 성공") + void getAvailableMentorSlots() { + // given + LocalDateTime startDate = LocalDateTime.of(2025, 10, 1, 0, 0); + LocalDateTime endDate = LocalDateTime.of(2025, 10, 31, 23, 59); + + MentorSlot slot2 = MentorSlotFixture.create(2L, mentor1, + LocalDateTime.of(2025, 10, 2, 10, 0), + LocalDateTime.of(2025, 10, 2, 12, 0)); + + List availableSlots = List.of(mentorSlot1, slot2); + + when(mentorSlotRepository.findAvailableSlots(mentor1.getId(), startDate, endDate)) + .thenReturn(availableSlots); + + // when + List result = mentorSlotService.getAvailableMentorSlots(mentor1.getId(), startDate, endDate); + + // then + assertThat(result).hasSize(2); + verify(mentorSlotRepository).findAvailableSlots(mentor1.getId(), startDate, endDate); + } + } + + @Nested + @DisplayName("멘토 슬롯 단건 조회") + class Describe_getMentorSlot { + + @Test + @DisplayName("조회 성공") + void getMentorSlot() { + // given + Long slotId = 1L; + + when(mentoringStorage.findMentorSlot(slotId)) + .thenReturn(mentorSlot1); + when(mentoringStorage.findMentoringByMentor(mentor1)) + .thenReturn(mentoring1); + + // when + MentorSlotResponse result = mentorSlotService.getMentorSlot(slotId); + + // then + assertThat(result).isNotNull(); + assertThat(result.mentorSlotId()).isEqualTo(slotId); + assertThat(result.mentorId()).isEqualTo(mentor1.getId()); + verify(mentoringStorage).findMentorSlot(slotId); + verify(mentoringStorage).findMentoringByMentor(mentor1); + } + } + + @Nested + @DisplayName("멘토 슬롯 생성") + class Describe_createMentorSlot { + + private MentorSlotRequest request; + + @BeforeEach + void setUp() { + request = new MentorSlotRequest( + mentor1.getId(), + LocalDateTime.of(2025, 10, 5, 10, 0), + LocalDateTime.of(2025, 10, 5, 12, 0) + ); + } + + @Test + @DisplayName("생성 성공") + void createMentorSlot() { + // given + when(mentoringStorage.findMentoringByMentor(mentor1)) + .thenReturn(mentoring1); + when(mentorSlotRepository.existsOverlappingSlot( + mentor1.getId(), request.startDateTime(), request.endDateTime())) + .thenReturn(false); + + // when + MentorSlotResponse result = mentorSlotService.createMentorSlot(request, mentor1); + + // then + assertThat(result).isNotNull(); + assertThat(result.mentorId()).isEqualTo(mentor1.getId()); + assertThat(result.mentoringId()).isEqualTo(mentoring1.getId()); + assertThat(result.mentoringTitle()).isEqualTo(mentoring1.getTitle()); + assertThat(result.startDateTime()).isEqualTo(request.startDateTime()); + assertThat(result.endDateTime()).isEqualTo(request.endDateTime()); + assertThat(result.mentorSlotStatus()).isEqualTo(MentorSlotStatus.AVAILABLE); + + verify(mentoringStorage).findMentoringByMentor(mentor1); + verify(mentorSlotRepository).existsOverlappingSlot(mentor1.getId(), request.startDateTime(), request.endDateTime()); + verify(mentorSlotRepository).save(any(MentorSlot.class)); + } + + @Test + @DisplayName("기존 슬롯과 시간 겹치면 예외") + void throwExceptionWhenOverlapping() { + // given + when(mentoringStorage.findMentoringByMentor(mentor1)) + .thenReturn(mentoring1); + when(mentorSlotRepository.existsOverlappingSlot( + mentor1.getId(), request.startDateTime(), request.endDateTime())) + .thenReturn(true); + + // when & then + assertThatThrownBy(() -> mentorSlotService.createMentorSlot(request, mentor1)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentorSlotErrorCode.OVERLAPPING_SLOT.getCode()); + verify(mentorSlotRepository, never()).save(any()); + } + } + + @Nested + @DisplayName("멘토 슬롯 반복 생성") + class Describe_createMentorSlotRepetition { + + @Test + @DisplayName("반복 생성 성공") + void createMentorSlotRepetition() { + // given + MentorSlotRepetitionRequest request = new MentorSlotRepetitionRequest( + LocalDate.of(2025, 11, 1), + LocalDate.of(2025, 11, 30), + List.of(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY), + LocalTime.of(10, 0), + LocalTime.of(11, 0) + ); + + when(mentorSlotRepository.existsOverlappingSlot(any(), any(), any())) + .thenReturn(false); + + // when + mentorSlotService.createMentorSlotRepetition(request, mentor1); + + // then + verify(mentorSlotRepository).saveAll(argThat(slots -> { + List slotList = new ArrayList<>(); + slots.forEach(slotList::add); + + // 개수 검증 + if (slotList.size() != 12) { + return false; + } + + // 요일 검증 + Set daysOfWeek = slotList.stream() + .map(slot -> slot.getStartDateTime().getDayOfWeek()) + .collect(Collectors.toSet()); + if (!daysOfWeek.equals(Set.of(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY))) { + return false; + } + + // 시간 검증 + return slotList.stream().allMatch(slot -> + slot.getStartDateTime().toLocalTime().equals(LocalTime.of(10, 0)) && + slot.getEndDateTime().toLocalTime().equals(LocalTime.of(11, 0)) && + slot.getMentor().equals(mentor1) + ); + })); + } + + @Test + @DisplayName("반복 생성 중 겹치는 슬롯 있으면 예외") + void throwExceptionWhenOverlapping() { + // given + MentorSlotRepetitionRequest request = new MentorSlotRepetitionRequest( + LocalDate.of(2025, 11, 1), + LocalDate.of(2025, 11, 7), + List.of(DayOfWeek.MONDAY), + LocalTime.of(10, 0), + LocalTime.of(11, 0) + ); + + when(mentorSlotRepository.existsOverlappingSlot(any(), any(), any())) + .thenReturn(true); + + // when & then + assertThatThrownBy(() -> mentorSlotService.createMentorSlotRepetition(request, mentor1)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentorSlotErrorCode.OVERLAPPING_SLOT.getCode()); + } + } + + @Nested + @DisplayName("멘토 슬롯 수정") + class Describe_updateMentorSlot { + + private MentorSlotRequest request; + + @BeforeEach + void setUp() { + request = new MentorSlotRequest( + mentor1.getId(), + LocalDateTime.of(2025, 10, 1, 14, 0), + LocalDateTime.of(2025, 10, 1, 15, 30) + ); + } + + @Test + @DisplayName("수정 성공") + void updateMentorSlot() { + // given + Long slotId = 1L; + + when(mentoringStorage.findMentoringByMentor(mentor1)) + .thenReturn(mentoring1); + when(mentoringStorage.findMentorSlot(slotId)) + .thenReturn(mentorSlot1); + when(mentorSlotRepository.existsOverlappingExcept(mentor1.getId(), slotId, request.startDateTime(), request.endDateTime())) + .thenReturn(false); + + // when + MentorSlotResponse result = mentorSlotService.updateMentorSlot(slotId, request, mentor1); + + // then + assertThat(result).isNotNull(); + assertThat(result.mentorSlotId()).isEqualTo(slotId); + assertThat(result.mentorId()).isEqualTo(mentor1.getId()); + assertThat(result.mentoringId()).isEqualTo(mentoring1.getId()); + assertThat(result.mentoringTitle()).isEqualTo(mentoring1.getTitle()); + assertThat(result.startDateTime()).isEqualTo(request.startDateTime()); + assertThat(result.endDateTime()).isEqualTo(request.endDateTime()); + assertThat(result.mentorSlotStatus()).isEqualTo(MentorSlotStatus.AVAILABLE); + + verify(mentoringStorage).findMentorSlot(slotId); + verify(mentorSlotRepository).existsOverlappingExcept( + mentor1.getId(), slotId, request.startDateTime(), request.endDateTime()); + } + + @Test + @DisplayName("소유자가 아니면 예외") + void throwExceptionWhenNotOwner() { + // given + Long slotId = 1L; + + when(mentoringStorage.findMentoringByMentor(mentor2)) + .thenReturn(mentoring1); + when(mentoringStorage.findMentorSlot(slotId)) + .thenReturn(mentorSlot1); + + // when & then + assertThatThrownBy(() -> mentorSlotService.updateMentorSlot(slotId, request, mentor2)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentorSlotErrorCode.NOT_OWNER.getCode()); + } + + @Test + @DisplayName("활성화된 예약이 있으면 예외") + void throwExceptionWhenReserved() { + // given + Long slotId = 1L; + Reservation reservation = ReservationFixture.create(1L, mentoring1, mentee1, mentorSlot1); + mentorSlot1.setReservation(reservation); + + when(mentoringStorage.findMentoringByMentor(mentor1)) + .thenReturn(mentoring1); + when(mentoringStorage.findMentorSlot(slotId)) + .thenReturn(mentorSlot1); + + // when & then + assertThatThrownBy(() -> mentorSlotService.updateMentorSlot(slotId, request, mentor1)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentorSlotErrorCode.CANNOT_UPDATE_RESERVED_SLOT.getCode()); + } + + @Test + @DisplayName("다른 슬롯과 시간 겹치면 예외") + void throwExceptionWhenOverlapping() { + // given + Long slotId = 1L; + + when(mentoringStorage.findMentoringByMentor(mentor1)) + .thenReturn(mentoring1); + when(mentoringStorage.findMentorSlot(slotId)) + .thenReturn(mentorSlot1); + when(mentorSlotRepository.existsOverlappingExcept( + mentor1.getId(), slotId, request.startDateTime(), request.endDateTime())) + .thenReturn(true); + + // when & then + assertThatThrownBy(() -> mentorSlotService.updateMentorSlot(slotId, request, mentor1)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentorSlotErrorCode.OVERLAPPING_SLOT.getCode()); + } + } + + @Nested + @DisplayName("멘토 슬롯 삭제") + class Describe_deleteMentorSlot { + + @Test + @DisplayName("삭제 성공") + void deleteMentorSlot() { + // given + Long slotId = 1L; + + when(mentoringStorage.findMentorSlot(slotId)) + .thenReturn(mentorSlot1); + when(mentoringStorage.hasReservationForMentorSlot(slotId)) + .thenReturn(false); + + // when + mentorSlotService.deleteMentorSlot(slotId, mentor1); + + // then + verify(mentoringStorage).findMentorSlot(slotId); + verify(mentoringStorage).hasReservationForMentorSlot(slotId); + verify(mentorSlotRepository).delete(mentorSlot1); + } + + @Test + @DisplayName("소유자가 아니면 예외") + void throwExceptionWhenNotOwner() { + // given + Long slotId = 1L; + + when(mentoringStorage.findMentorSlot(slotId)) + .thenReturn(mentorSlot1); + + // when & then + assertThatThrownBy(() -> mentorSlotService.deleteMentorSlot(slotId, mentor2)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentorSlotErrorCode.NOT_OWNER.getCode()); + verify(mentorSlotRepository, never()).delete(any()); + } + + @Test + @DisplayName("예약 기록이 있으면 예외") + void throwExceptionWhenReservationHistoryExists() { + // given + Long slotId = 1L; + + when(mentoringStorage.findMentorSlot(slotId)) + .thenReturn(mentorSlot1); + when(mentoringStorage.hasReservationForMentorSlot(slotId)) + .thenReturn(true); + + // when & then + assertThatThrownBy(() -> mentorSlotService.deleteMentorSlot(slotId, mentor1)) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", MentorSlotErrorCode.CANNOT_DELETE_RESERVED_SLOT.getCode()); + verify(mentoringStorage).findMentorSlot(slotId); + verify(mentoringStorage).hasReservationForMentorSlot(slotId); + verify(mentorSlotRepository, never()).delete(any()); + } + } +} \ No newline at end of file diff --git a/back/src/test/java/com/back/fixture/MenteeFixture.java b/back/src/test/java/com/back/fixture/MenteeFixture.java new file mode 100644 index 00000000..83164a17 --- /dev/null +++ b/back/src/test/java/com/back/fixture/MenteeFixture.java @@ -0,0 +1,37 @@ +package com.back.fixture; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.member.mentee.entity.Mentee; +import org.springframework.test.util.ReflectionTestUtils; + +public class MenteeFixture { + + private static final Long DEFAULT_JOB_ID = 1L; + + public static Mentee create(Member member) { + return Mentee.builder() + .member(member) + .jobId(DEFAULT_JOB_ID) + .build(); + } + + public static Mentee create(Long id, Member member) { + Mentee mentee = Mentee.builder() + .member(member) + .jobId(DEFAULT_JOB_ID) + .build(); + + ReflectionTestUtils.setField(mentee, "id", id); + return mentee; + } + + public static Mentee create(Long id, Member member, Long jobId) { + Mentee mentee = Mentee.builder() + .member(member) + .jobId(jobId) + .build(); + + ReflectionTestUtils.setField(mentee, "id", id); + return mentee; + } +} diff --git a/back/src/test/java/com/back/fixture/MentorFixture.java b/back/src/test/java/com/back/fixture/MentorFixture.java new file mode 100644 index 00000000..7dcdc0f5 --- /dev/null +++ b/back/src/test/java/com/back/fixture/MentorFixture.java @@ -0,0 +1,45 @@ +package com.back.fixture; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.member.mentor.entity.Mentor; +import org.springframework.test.util.ReflectionTestUtils; + +public class MentorFixture { + + private static final Long DEFAULT_JOB_ID = 1L; + private static final Double DEFAULT_RATE = 4.5; + private static final Integer DEFAULT_CAREER_YEARS = 5; + + public static Mentor create(Member member) { + return Mentor.builder() + .member(member) + .jobId(DEFAULT_JOB_ID) + .rate(DEFAULT_RATE) + .careerYears(DEFAULT_CAREER_YEARS) + .build(); + } + + public static Mentor create(Long id, Member member) { + Mentor mentor = Mentor.builder() + .member(member) + .jobId(DEFAULT_JOB_ID) + .rate(DEFAULT_RATE) + .careerYears(DEFAULT_CAREER_YEARS) + .build(); + + ReflectionTestUtils.setField(mentor, "id", id); + return mentor; + } + + public static Mentor create(Long id, Member member, Long jobId, Double rate, Integer careerYears) { + Mentor mentor = Mentor.builder() + .member(member) + .jobId(jobId) + .rate(rate) + .careerYears(careerYears) + .build(); + + ReflectionTestUtils.setField(mentor, "id", id); + return mentor; + } +} diff --git a/back/src/test/java/com/back/fixture/mentoring/MentorSlotFixture.java b/back/src/test/java/com/back/fixture/mentoring/MentorSlotFixture.java new file mode 100644 index 00000000..e45cc471 --- /dev/null +++ b/back/src/test/java/com/back/fixture/mentoring/MentorSlotFixture.java @@ -0,0 +1,43 @@ +package com.back.fixture.mentoring; + +import com.back.domain.member.mentor.entity.Mentor; +import com.back.domain.mentoring.slot.entity.MentorSlot; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; + +public class MentorSlotFixture { + + private static final LocalDateTime DEFAULT_START_TIME = LocalDateTime.of(2025, 10, 1, 14, 0); + private static final LocalDateTime DEFAULT_END_TIME = LocalDateTime.of(2025, 10, 1, 16, 0); + + public static MentorSlot create(Mentor mentor) { + return MentorSlot.builder() + .mentor(mentor) + .startDateTime(DEFAULT_START_TIME) + .endDateTime(DEFAULT_END_TIME) + .build(); + } + + public static MentorSlot create(Long id, Mentor mentor) { + MentorSlot slot = MentorSlot.builder() + .mentor(mentor) + .startDateTime(DEFAULT_START_TIME) + .endDateTime(DEFAULT_END_TIME) + .build(); + + ReflectionTestUtils.setField(slot, "id", id); + return slot; + } + + public static MentorSlot create(Long id, Mentor mentor, LocalDateTime startDateTime, LocalDateTime endDateTime) { + MentorSlot slot = MentorSlot.builder() + .mentor(mentor) + .startDateTime(startDateTime) + .endDateTime(endDateTime) + .build(); + + ReflectionTestUtils.setField(slot, "id", id); + return slot; + } +} diff --git a/back/src/test/java/com/back/fixture/mentoring/MentoringFixture.java b/back/src/test/java/com/back/fixture/mentoring/MentoringFixture.java new file mode 100644 index 00000000..05881bfb --- /dev/null +++ b/back/src/test/java/com/back/fixture/mentoring/MentoringFixture.java @@ -0,0 +1,53 @@ +package com.back.fixture.mentoring; + +import com.back.domain.member.mentor.entity.Mentor; +import com.back.domain.mentoring.mentoring.entity.Mentoring; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +public class MentoringFixture { + + private static final String DEFAULT_TITLE = "테스트 멘토링"; + private static final String DEFAULT_BIO = "테스트 설명"; + private static final List DEFAULT_TAGS = List.of("Spring", "Java"); + private static final String DEFAULT_THUMB = "https://example.com/thumb.jpg"; + + public static Mentoring create(Mentor mentor) { + return Mentoring.builder() + .mentor(mentor) + .title(DEFAULT_TITLE) + .bio(DEFAULT_BIO) + .tags(DEFAULT_TAGS) + .thumb(DEFAULT_THUMB) + .build(); + } + + public static Mentoring create(Long id, Mentor mentor) { + Mentoring mentoring = Mentoring.builder() + .mentor(mentor) + .title(DEFAULT_TITLE) + .bio(DEFAULT_BIO) + .tags(DEFAULT_TAGS) + .thumb(DEFAULT_THUMB) + .build(); + + ReflectionTestUtils.setField(mentoring, "id", id); + return mentoring; + } + + public static Mentoring create(Long id, Mentor mentor, String title, String bio, List tags) { + Mentoring mentoring = Mentoring.builder() + .mentor(mentor) + .title(title) + .bio(bio) + .tags(tags) + .thumb(DEFAULT_THUMB) + .build(); + + if (id != null) { + ReflectionTestUtils.setField(mentoring, "id", id); + } + return mentoring; + } +} diff --git a/back/src/test/java/com/back/fixture/MentoringTestFixture.java b/back/src/test/java/com/back/fixture/mentoring/MentoringTestFixture.java similarity index 99% rename from back/src/test/java/com/back/fixture/MentoringTestFixture.java rename to back/src/test/java/com/back/fixture/mentoring/MentoringTestFixture.java index 4d540bf0..56efe895 100644 --- a/back/src/test/java/com/back/fixture/MentoringTestFixture.java +++ b/back/src/test/java/com/back/fixture/mentoring/MentoringTestFixture.java @@ -1,4 +1,4 @@ -package com.back.fixture; +package com.back.fixture.mentoring; import com.back.domain.member.mentee.entity.Mentee; import com.back.domain.member.mentor.entity.Mentor; diff --git a/back/src/test/java/com/back/fixture/mentoring/ReservationFixture.java b/back/src/test/java/com/back/fixture/mentoring/ReservationFixture.java new file mode 100644 index 00000000..265ea767 --- /dev/null +++ b/back/src/test/java/com/back/fixture/mentoring/ReservationFixture.java @@ -0,0 +1,33 @@ +package com.back.fixture.mentoring; + +import com.back.domain.member.mentee.entity.Mentee; +import com.back.domain.mentoring.mentoring.entity.Mentoring; +import com.back.domain.mentoring.reservation.entity.Reservation; +import com.back.domain.mentoring.slot.entity.MentorSlot; +import org.springframework.test.util.ReflectionTestUtils; + +public class ReservationFixture { + + private static final String DEFAULT_PRE_QUESTION = "테스트 사전 질문입니다."; + + public static Reservation create(Mentoring mentoring, Mentee mentee, MentorSlot mentorSlot) { + return Reservation.builder() + .mentoring(mentoring) + .mentee(mentee) + .mentorSlot(mentorSlot) + .preQuestion(DEFAULT_PRE_QUESTION) + .build(); + } + + public static Reservation create(Long id, Mentoring mentoring, Mentee mentee, MentorSlot mentorSlot) { + Reservation reservation = Reservation.builder() + .mentoring(mentoring) + .mentee(mentee) + .mentorSlot(mentorSlot) + .preQuestion(DEFAULT_PRE_QUESTION) + .build(); + + ReflectionTestUtils.setField(reservation, "id", id); + return reservation; + } +}