diff --git a/src/main/java/com/example/spot/domain/Region.java b/src/main/java/com/example/spot/domain/Region.java index 57d4b8ab..34168b2e 100644 --- a/src/main/java/com/example/spot/domain/Region.java +++ b/src/main/java/com/example/spot/domain/Region.java @@ -15,6 +15,9 @@ @Entity @Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor public class Region extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -28,25 +31,15 @@ public class Region extends BaseEntity { private String neighborhood; + @Builder.Default @OneToMany(mappedBy = "region") private List regionStudyList = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "region") private List prefferedRegionList = new ArrayList<>(); -/* ----------------------------- 생성자 ------------------------------------- */ - - protected Region() {} - - @Builder - public Region(String code, String province, String district, String neighborhood) { - this.code = code; - this.province = province; - this.district = district; - this.neighborhood = neighborhood; - } - /* ----------------------------- 연관관계 메소드 ------------------------------------- */ public void addRegionStudy(RegionStudy regionStudy) { diff --git a/src/main/java/com/example/spot/domain/Theme.java b/src/main/java/com/example/spot/domain/Theme.java index 76881e9c..396de520 100644 --- a/src/main/java/com/example/spot/domain/Theme.java +++ b/src/main/java/com/example/spot/domain/Theme.java @@ -10,6 +10,7 @@ import lombok.Builder; import lombok.Getter; +import java.util.ArrayList; import java.util.List; import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicInsert; @@ -34,12 +35,14 @@ public class Theme extends BaseEntity { private ThemeType studyTheme; //== 해당 테마를 선호하는 멤버 목록 ==// + @Builder.Default @OneToMany(mappedBy = "theme", cascade = CascadeType.ALL) - private List memberThemeList; + private List memberThemeList = new ArrayList<>(); //== 테마별 스터디 목록 ==// + @Builder.Default @OneToMany(mappedBy = "theme", cascade = CascadeType.ALL) - private List studyThemeList; + private List studyThemeList = new ArrayList<>(); /* ----------------------------- 연관관계 메소드 ------------------------------------- */ diff --git a/src/main/java/com/example/spot/web/controller/StudyController.java b/src/main/java/com/example/spot/web/controller/StudyController.java index 641cf04c..7ccbfa40 100644 --- a/src/main/java/com/example/spot/web/controller/StudyController.java +++ b/src/main/java/com/example/spot/web/controller/StudyController.java @@ -49,7 +49,6 @@ public ApiResponse getStudyInfo( ## [스터디 생성/참여] 스터디 페이지 > 신청하기 클릭, 로그인한 회원이 스터디에 신청합니다. 로그인한 회원이 member_study에 application_status = APPLIED 상태로 추가됩니다. """) - @Parameter(name = "memberId", description = "스터디에 참여하는 회원의 id를 입력 받습니다.", required = true) @Parameter(name = "studyId", description = "참여할 스터디의 id를 입력 받습니다.", required = true) @PostMapping("/studies/{studyId}") public ApiResponse applyToStudy( @@ -67,7 +66,6 @@ public ApiResponse applyToStudy( regions에는 지역 코드를 입력해야 합니다. """) - @Parameter(name = "memberId", description = "스터디를 생성할 회원의 id를 입력 받습니다.", required = true) @PostMapping("/studies") public ApiResponse registerStudy( @RequestBody @Valid StudyRegisterRequestDTO.RegisterDTO studyRegisterRequestDTO) { diff --git a/src/main/java/com/example/spot/web/dto/study/request/StudyJoinRequestDTO.java b/src/main/java/com/example/spot/web/dto/study/request/StudyJoinRequestDTO.java index d06e918a..8a0c38e0 100644 --- a/src/main/java/com/example/spot/web/dto/study/request/StudyJoinRequestDTO.java +++ b/src/main/java/com/example/spot/web/dto/study/request/StudyJoinRequestDTO.java @@ -2,6 +2,7 @@ import com.example.spot.validation.annotation.TextLength; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -9,6 +10,7 @@ public class StudyJoinRequestDTO { @Getter + @Builder @NoArgsConstructor @AllArgsConstructor public static class StudyJoinDTO { diff --git a/src/main/java/com/example/spot/web/dto/study/request/StudyRegisterRequestDTO.java b/src/main/java/com/example/spot/web/dto/study/request/StudyRegisterRequestDTO.java index f44a6c8f..5f7d4064 100644 --- a/src/main/java/com/example/spot/web/dto/study/request/StudyRegisterRequestDTO.java +++ b/src/main/java/com/example/spot/web/dto/study/request/StudyRegisterRequestDTO.java @@ -6,6 +6,7 @@ import com.example.spot.validation.annotation.LongSize; import com.example.spot.validation.annotation.TextLength; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -15,6 +16,7 @@ public class StudyRegisterRequestDTO { @Getter + @Builder @NoArgsConstructor @AllArgsConstructor public static class RegisterDTO { diff --git a/src/main/java/com/example/spot/web/dto/study/response/StudyJoinResponseDTO.java b/src/main/java/com/example/spot/web/dto/study/response/StudyJoinResponseDTO.java index 178e9d96..fc00eefb 100644 --- a/src/main/java/com/example/spot/web/dto/study/response/StudyJoinResponseDTO.java +++ b/src/main/java/com/example/spot/web/dto/study/response/StudyJoinResponseDTO.java @@ -30,7 +30,7 @@ public static JoinDTO toDTO(Member member, Study study) { } @Getter - private static class TitleDTO { + public static class TitleDTO { private final Long studyId; private final String title; diff --git a/src/test/java/com/example/spot/service/study/StudyCommandServiceTest.java b/src/test/java/com/example/spot/service/study/StudyCommandServiceTest.java index 8afb7389..0c018c3f 100644 --- a/src/test/java/com/example/spot/service/study/StudyCommandServiceTest.java +++ b/src/test/java/com/example/spot/service/study/StudyCommandServiceTest.java @@ -1,20 +1,347 @@ package com.example.spot.service.study; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import com.example.spot.api.exception.handler.StudyHandler; +import com.example.spot.domain.Member; +import com.example.spot.domain.Region; +import com.example.spot.domain.Theme; +import com.example.spot.domain.enums.*; +import com.example.spot.domain.mapping.MemberStudy; +import com.example.spot.domain.mapping.RegionStudy; +import com.example.spot.domain.mapping.StudyTheme; +import com.example.spot.domain.study.Study; +import com.example.spot.repository.*; +import com.example.spot.web.dto.study.request.StudyJoinRequestDTO; +import com.example.spot.web.dto.study.request.StudyRegisterRequestDTO; +import com.example.spot.web.dto.study.response.StudyJoinResponseDTO; +import com.example.spot.web.dto.study.response.StudyRegisterResponseDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) class StudyCommandServiceTest { + @Mock + private MemberRepository memberRepository; + + @Mock + private StudyRepository studyRepository; + @Mock + private MemberStudyRepository memberStudyRepository; + + @Mock + private RegionRepository regionRepository; + @Mock + private RegionStudyRepository regionStudyRepository; + + @Mock + private ThemeRepository themeRepository; + @Mock + private StudyThemeRepository studyThemeRepository; + + @InjectMocks + private StudyCommandServiceImpl studyCommandService; + + private static Study study; + private static Member member1; + private static Member member2; + private static Member owner; + private static MemberStudy member1Study; + private static MemberStudy ownerStudy; + private static Region region; + private static Theme theme; + + @BeforeEach + void setUp() { + initMember(); + initStudy(); + initMemberStudy(); + initRegion(); + initTheme(); + + when(memberRepository.findById(1L)).thenReturn(Optional.of(member1)); + when(memberRepository.findById(2L)).thenReturn(Optional.of(member2)); + when(memberRepository.findById(3L)).thenReturn(Optional.of(owner)); + + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + when(regionRepository.findByCode("123456")).thenReturn(Optional.of(region)); + when(themeRepository.findByStudyTheme(ThemeType.자격증)).thenReturn(Optional.of(theme)); + + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(1L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(member1Study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(3L, 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(ownerStudy)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndIsOwned(3L, 1L, true)) + .thenReturn(Optional.of(ownerStudy)); + } + @Test - void applyToStudy() { + @DisplayName("스터디 신청 - (성공)") + void applyToStudy_Success() { + + // given + Long memberId = 2L; + Long studyId = 1L; + + getAuthentication(memberId); + + StudyJoinRequestDTO.StudyJoinDTO studyJoinRequestDTO = StudyJoinRequestDTO.StudyJoinDTO.builder() + .introduction("Hi") + .build(); + + MemberStudy memberStudy = MemberStudy.builder() + .id(3L) + .member(member2) + .study(study) + .status(ApplicationStatus.APPLIED) + .isOwned(false) + .introduction(studyJoinRequestDTO.getIntroduction()) + .build(); + + when(memberStudyRepository.countByStatusAndStudyId(ApplicationStatus.APPROVED, studyId)) + .thenReturn(2L); + when(memberStudyRepository.findByMemberIdAndStatusNot(memberId, ApplicationStatus.REJECTED)) + .thenReturn(List.of()); + when(memberStudyRepository.save(any(MemberStudy.class))) + .thenReturn(memberStudy); + + // when + StudyJoinResponseDTO.JoinDTO result = studyCommandService.applyToStudy(studyId, studyJoinRequestDTO); + + // then + assertThat(result).isNotNull(); + assertThat(result.getMemberId()).isEqualTo(memberId); + verify(memberStudyRepository, times(1)).save(any(MemberStudy.class)); } @Test + @DisplayName("스터디 신청 - 이미 스터디에 신청했거나 스터디 회원인 경우(실패)") + void applyToStudy_StudyMember_Fail() { + + // given + Long memberId = 1L; + Long studyId = 1L; + + getAuthentication(memberId); + + StudyJoinRequestDTO.StudyJoinDTO studyJoinRequestDTO = StudyJoinRequestDTO.StudyJoinDTO.builder() + .introduction("Hi") + .build(); + + MemberStudy memberStudy = MemberStudy.builder() + .id(3L) + .member(member1) + .study(study) + .status(ApplicationStatus.APPLIED) + .isOwned(false) + .introduction(studyJoinRequestDTO.getIntroduction()) + .build(); + + when(memberStudyRepository.countByStatusAndStudyId(ApplicationStatus.APPROVED, studyId)) + .thenReturn(2L); + when(memberStudyRepository.findByMemberIdAndStatusNot(memberId, ApplicationStatus.REJECTED)) + .thenReturn(List.of(member1Study)); + when(memberStudyRepository.save(any(MemberStudy.class))) + .thenReturn(memberStudy); + + // when & then + assertThrows(StudyHandler.class, () -> studyCommandService.applyToStudy(studyId, studyJoinRequestDTO)); + } + + @Test + @DisplayName("스터디 신청 - 모집중인 스터디가 아닌 경우(실패)") + void applyToStudy_NotRecruitingStudy_Fail() { + + // given + Long memberId = 2L; + Long studyId = 2L; + + getAuthentication(memberId); + + Study study = Study.builder() + .title("마감된 스터디") + .maxPeople(1L) + .build(); + + StudyJoinRequestDTO.StudyJoinDTO studyJoinRequestDTO = StudyJoinRequestDTO.StudyJoinDTO.builder() + .introduction("Hi") + .build(); + + MemberStudy memberStudy = MemberStudy.builder() + .id(3L) + .member(member2) + .study(study) + .status(ApplicationStatus.APPLIED) + .isOwned(false) + .introduction(studyJoinRequestDTO.getIntroduction()) + .build(); + + when(memberStudyRepository.countByStatusAndStudyId(ApplicationStatus.APPROVED, studyId)) + .thenReturn(1L); + when(memberStudyRepository.findByMemberIdAndStatusNot(memberId, ApplicationStatus.REJECTED)) + .thenReturn(List.of()); + when(memberStudyRepository.save(any(MemberStudy.class))) + .thenReturn(memberStudy); + + // when & then + assertThrows(StudyHandler.class, () -> studyCommandService.applyToStudy(studyId, studyJoinRequestDTO)); + } + + @Test + @DisplayName("스터디 등록 - (성공)") void registerStudy() { + + // given + Long memberId = 1L; + + getAuthentication(memberId); + + StudyRegisterRequestDTO.RegisterDTO registerDTO = StudyRegisterRequestDTO.RegisterDTO.builder() + .themes(List.of(ThemeType.자격증)) + .title("새로운 스터디") + .goal("목표") + .introduction("소개") + .isOnline(false) + .profileImage("profileImage") + .regions(List.of("123456")) + .maxPeople(5L) + .gender(Gender.UNKNOWN) + .minAge(1) + .maxAge(100) + .fee(0) + .hasFee(false) + .build(); + + Study study = Study.builder() + .title("새로운 스터디") + .maxPeople(10L) + .build(); + + MemberStudy memberStudy = MemberStudy.builder() + .member(member1) + .study(study) + .build(); + + RegionStudy regionStudy = RegionStudy.builder() + .region(region) + .study(study) + .build(); + + StudyTheme studyTheme = StudyTheme.builder() + .theme(theme) + .study(study) + .build(); + + when(studyRepository.save(any(Study.class))).thenReturn(study); + when(memberStudyRepository.save(any(MemberStudy.class))).thenReturn(memberStudy); + when(regionStudyRepository.save(any(RegionStudy.class))).thenReturn(regionStudy); + when(studyThemeRepository.save(any(StudyTheme.class))).thenReturn(studyTheme); + + // when + StudyRegisterResponseDTO.RegisterDTO result = studyCommandService.registerStudy(registerDTO); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTitle()).isEqualTo("새로운 스터디"); + verify(studyRepository, times(2)).save(any(Study.class)); + verify(memberStudyRepository, times(1)).save(any(MemberStudy.class)); + verify(regionStudyRepository, times(1)).save(any(RegionStudy.class)); + verify(studyThemeRepository, times(1)).save(any(StudyTheme.class)); } @Test void likeStudy() { } + +/*-------------------------------------------------------- Utils ------------------------------------------------------------------------*/ + + private static void initMember() { + member1 = Member.builder() + .id(1L) + .scheduleList(new ArrayList<>()) + .build(); + member2 = Member.builder() + .id(2L) + .scheduleList(new ArrayList<>()) + .build(); + owner = Member.builder() + .id(3L) + .scheduleList(new ArrayList<>()) + .build(); + } + + private static void initStudy() { + study = Study.builder() + .gender(Gender.MALE) + .minAge(20) + .maxAge(29) + .fee(10000) + .profileImage("a.jpg") + .hasFee(true) + .isOnline(true) + .goal("SQLD") + .introduction("SQLD 자격증 스터디") + .title("SQLD Master") + .maxPeople(10L) + .build(); + } + + private static void initMemberStudy() { + ownerStudy = MemberStudy.builder() + .id(1L) + .status(ApplicationStatus.APPROVED) + .isOwned(true) + .introduction("Hi") + .member(owner) + .study(study) + .build(); + member1Study = MemberStudy.builder() + .id(2L) + .status(ApplicationStatus.APPROVED) + .isOwned(false) + .introduction("Hi") + .member(member1) + .study(study) + .build(); + } + + private static void initRegion() { + region = Region.builder() + .code("123456") + .build(); + } + + private static void initTheme() { + theme = Theme.builder() + .studyTheme(ThemeType.자격증) + .build(); + } + + private static void getAuthentication(Long memberId) { + String idString = String.valueOf(memberId); + Authentication authentication = new UsernamePasswordAuthenticationToken(idString, null, Collections.emptyList()); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + } } \ No newline at end of file