diff --git a/src/main/java/umc/cockple/demo/domain/exercise/service/ExerciseQueryService.java b/src/main/java/umc/cockple/demo/domain/exercise/service/ExerciseQueryService.java index 6876cee7a..adf7ae4b5 100644 --- a/src/main/java/umc/cockple/demo/domain/exercise/service/ExerciseQueryService.java +++ b/src/main/java/umc/cockple/demo/domain/exercise/service/ExerciseQueryService.java @@ -391,7 +391,7 @@ private void validatePartyIsActive(Party party) { private boolean checkManagerPermission(Party party, Member member) { return memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), member.getId(), Role.party_MANAGER); + party.getId(), member.getId(), Role.PARTY_MANAGER); } private ExerciseDetailDTO.ExerciseInfo createExerciseInfo(Exercise exercise) { diff --git a/src/main/java/umc/cockple/demo/domain/exercise/service/ExerciseValidator.java b/src/main/java/umc/cockple/demo/domain/exercise/service/ExerciseValidator.java index a81174b3f..fabda2ec8 100644 --- a/src/main/java/umc/cockple/demo/domain/exercise/service/ExerciseValidator.java +++ b/src/main/java/umc/cockple/demo/domain/exercise/service/ExerciseValidator.java @@ -90,9 +90,9 @@ private void validatePartyIsActive(Party party) { private void validateSubManagerPermission(Long memberId, Party party) { boolean isOwner = party.getOwnerId().equals(memberId); boolean isManager = memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), memberId, Role.party_MANAGER); + party.getId(), memberId, Role.PARTY_MANAGER); boolean isSubManager = memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), memberId, Role.party_SUBMANAGER); + party.getId(), memberId, Role.PARTY_SUBMANAGER); if (!isOwner && !isManager && !isSubManager) throw new ExerciseException(ExerciseErrorCode.INSUFFICIENT_PERMISSION); diff --git a/src/main/java/umc/cockple/demo/domain/member/domain/MemberParty.java b/src/main/java/umc/cockple/demo/domain/member/domain/MemberParty.java index 8d3cc6794..cf85ec7cd 100644 --- a/src/main/java/umc/cockple/demo/domain/member/domain/MemberParty.java +++ b/src/main/java/umc/cockple/demo/domain/member/domain/MemberParty.java @@ -47,7 +47,7 @@ public static MemberParty createOwner(Member member, Party party) { return MemberParty.builder() .member(member) .party(party) - .role(Role.party_MANAGER) + .role(Role.PARTY_MANAGER) .joinedAt(LocalDateTime.now()) .status(ACTIVE) .build(); @@ -57,20 +57,20 @@ public static MemberParty create(Party party, Member member) { return MemberParty.builder() .member(member) .party(party) - .role(Role.party_MEMBER) + .role(Role.PARTY_MEMBER) .joinedAt(LocalDateTime.now()) .status(ACTIVE) .build(); } public boolean isLeader() { - if (this.role == Role.party_MANAGER) return true; + if (this.role == Role.PARTY_MANAGER) return true; return false; } public boolean isViceLeader() { - if (this.role == Role.party_SUBMANAGER) return true; + if (this.role == Role.PARTY_SUBMANAGER) return true; return false; } diff --git a/src/main/java/umc/cockple/demo/domain/party/converter/PartyConverter.java b/src/main/java/umc/cockple/demo/domain/party/converter/PartyConverter.java index 4f87a3a86..e331e6c68 100644 --- a/src/main/java/umc/cockple/demo/domain/party/converter/PartyConverter.java +++ b/src/main/java/umc/cockple/demo/domain/party/converter/PartyConverter.java @@ -211,9 +211,9 @@ public PartyMemberSuggestionDTO.Response toPartyMemberSuggestionDTO(Member membe private int getRolePriority(String role) { return switch (role) { - case "party_MANAGER" -> 0; // 모임장 역할 - case "party_SUBMANAGER" -> 1; // 부모임장 - case "party_MEMBER" -> 2; // 일반 멤버 + case "PARTY_MANAGER" -> 0; // 모임장 역할 + case "PARTY_SUBMANAGER" -> 1; // 부모임장 + case "PARTY_MEMBER" -> 2; // 일반 멤버 default -> 99; }; } diff --git a/src/main/java/umc/cockple/demo/domain/party/exception/PartyErrorCode.java b/src/main/java/umc/cockple/demo/domain/party/exception/PartyErrorCode.java index e844ab093..abc159c4d 100644 --- a/src/main/java/umc/cockple/demo/domain/party/exception/PartyErrorCode.java +++ b/src/main/java/umc/cockple/demo/domain/party/exception/PartyErrorCode.java @@ -24,10 +24,10 @@ public enum PartyErrorCode implements BaseErrorCode { INVALID_ORDER_TYPE(HttpStatus.BAD_REQUEST, "PARTY106", "유효하지 않은 정렬 기준입니다. (최신순, 오래된 순, 운동 많은 순 중 하나여야 합니다.)"), INVALID_KEYWORD(HttpStatus.BAD_REQUEST, "PARTY107", "유효하지 않은 키워드입니다."), MALE_LEVEL_NOT_NEEDED(HttpStatus.BAD_REQUEST, "PARTY108", "여복 모임은 남자 급수를 설정할 수 없습니다."), - INVALID_ROLE_VALUE(HttpStatus.BAD_REQUEST, "PARTY411", "유효하지 않은 역할 값입니다. (party_SUBMANAGER 또는 party_MEMBER를 입력해주세요.)"), + INVALID_ROLE_VALUE(HttpStatus.BAD_REQUEST, "PARTY411", "유효하지 않은 역할 값입니다. (PARTY_SUBMANAGER 또는 PARTY_MEMBER를 입력해주세요.)"), PARTY_NOT_FOUND(HttpStatus.NOT_FOUND, "PARTY201", "존재하지 않는 모임입니다."), - JoinRequest_NOT_FOUND(HttpStatus.NOT_FOUND, "PARTY202", "존재하지 않는 가입신청입니다."), + JOIN_REQUEST_NOT_FOUND(HttpStatus.NOT_FOUND, "PARTY202", "존재하지 않는 가입신청입니다."), JOIN_REQUEST_PARTY_NOT_FOUND(HttpStatus.NOT_FOUND, "PARTY203", "해당 모임에서 존재하지 않는 가입신청입니다."), NOT_MEMBER(HttpStatus.BAD_REQUEST, "PARTY204", "해당 모임의 멤버가 아닙니다."), INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "PARTY205", "존재하지 않는 모임 초대입니다."), diff --git a/src/main/java/umc/cockple/demo/domain/party/service/PartyCommandServiceImpl.java b/src/main/java/umc/cockple/demo/domain/party/service/PartyCommandServiceImpl.java index f3624188a..9616b587a 100644 --- a/src/main/java/umc/cockple/demo/domain/party/service/PartyCommandServiceImpl.java +++ b/src/main/java/umc/cockple/demo/domain/party/service/PartyCommandServiceImpl.java @@ -190,7 +190,7 @@ public void updateMemberRole(Long partyId, Long targetMemberId, Long currentMemb // 모임장 권한 검증 validateOwnerPermission(party, currentMemberId); // 대상이 모임장인 경우 변경 불가 - if (targetMemberParty.getRole() == Role.party_MANAGER) { + if (targetMemberParty.getRole() == Role.PARTY_MANAGER) { throw new PartyException(PartyErrorCode.CANNOT_ASSIGN_TO_OWNER); } // 이미 같은 역할인 경우 @@ -199,10 +199,10 @@ public void updateMemberRole(Long partyId, Long targetMemberId, Long currentMemb } // SUBOWNER 지정 시, 기존 부모임장 자동 해제 - if (newRole == Role.party_SUBMANAGER) { - memberPartyRepository.findByPartyIdAndRole(partyId, Role.party_SUBMANAGER) + if (newRole == Role.PARTY_SUBMANAGER) { + memberPartyRepository.findByPartyIdAndRole(partyId, Role.PARTY_SUBMANAGER) .ifPresent(mp -> { - mp.changeRole(Role.party_MEMBER); + mp.changeRole(Role.PARTY_MEMBER); createRoleNotification(partyId, NotificationTarget.PARTY_SUBOWNER_RELEASED, mp.getMember().getNickname()); }); @@ -212,7 +212,7 @@ public void updateMemberRole(Long partyId, Long targetMemberId, Long currentMemb targetMemberParty.changeRole(newRole); // 알림 발송 (전체 멤버 대상) - NotificationTarget notifTarget = (newRole == Role.party_SUBMANAGER) + NotificationTarget notifTarget = (newRole == Role.PARTY_SUBMANAGER) ? NotificationTarget.PARTY_SUBOWNER_ASSIGNED : NotificationTarget.PARTY_SUBOWNER_RELEASED; createRoleNotification(partyId, notifTarget, targetMember.getNickname()); @@ -335,7 +335,7 @@ public void addKeyword(Long partyId, Long memberId, PartyKeywordDTO.Request requ //가입신청 조회 private PartyJoinRequest findJoinRequestOrThrow(Long requestId) { return partyJoinRequestRepository.findById(requestId) - .orElseThrow(() -> new PartyException(PartyErrorCode.JoinRequest_NOT_FOUND)); + .orElseThrow(() -> new PartyException(PartyErrorCode.JOIN_REQUEST_NOT_FOUND)); } private PartyInvitation findInvitationOrThrow(Long invitationId) { @@ -387,7 +387,7 @@ private void validateIsNotOwner(Party party, Long memberId) { // 부모임장은 권한이 없음을 검증 private void validateIsNotSubOwner(Party party, Long memberId) { - memberPartyRepository.findByPartyIdAndRole(party.getId(), Role.party_SUBMANAGER) + memberPartyRepository.findByPartyIdAndRole(party.getId(), Role.PARTY_SUBMANAGER) .ifPresent(mp -> { if (mp.getMember().getId().equals(memberId)) { throw new PartyException(PartyErrorCode.INVALID_ACTION_FOR_SUBOWNER); @@ -427,7 +427,7 @@ private void validateRemovalPermission(Party party, Member remover, MemberParty if (remover.getId().equals(memberPartyToRemove.getMember().getId())) { //부모임장인 경우에만 가능 MemberParty removerMemberParty = findMemberPartyOrThrow(party, remover); - if (removerMemberParty.getRole() == Role.party_SUBMANAGER) { + if (removerMemberParty.getRole() == Role.PARTY_SUBMANAGER) { return; } else { throw new PartyException(PartyErrorCode.CANNOT_REMOVE_SELF); @@ -439,11 +439,11 @@ private void validateRemovalPermission(Party party, Member remover, MemberParty Role removerRole = removerMemberParty.getRole(); Role targetRole = memberPartyToRemove.getRole(); //모임장은 모두 삭제 가능 - if (removerRole == Role.party_MANAGER) { + if (removerRole == Role.PARTY_MANAGER) { return; } //부모임장은 일반 멤버만 삭제 가능 (모임장을 삭제하려할 경우 권한 없음) - if (removerRole == Role.party_SUBMANAGER && targetRole == Role.party_MEMBER) { + if (removerRole == Role.PARTY_SUBMANAGER && targetRole == Role.PARTY_MEMBER) { return; } //일반 멤버는 권한 없음 diff --git a/src/main/java/umc/cockple/demo/global/enums/Role.java b/src/main/java/umc/cockple/demo/global/enums/Role.java index b347e011f..0daea6101 100644 --- a/src/main/java/umc/cockple/demo/global/enums/Role.java +++ b/src/main/java/umc/cockple/demo/global/enums/Role.java @@ -2,9 +2,9 @@ public enum Role { - party_MEMBER, //일반 멤버 + PARTY_MEMBER, //일반 멤버 - party_MANAGER, //모임장 + PARTY_MANAGER, //모임장 - party_SUBMANAGER //부모임장 + PARTY_SUBMANAGER //부모임장 } diff --git a/src/test/java/umc/cockple/demo/domain/bookmark/integration/BookmarkIntegrationTest.java b/src/test/java/umc/cockple/demo/domain/bookmark/integration/BookmarkIntegrationTest.java index 984dfb50f..8ffea1ca0 100644 --- a/src/test/java/umc/cockple/demo/domain/bookmark/integration/BookmarkIntegrationTest.java +++ b/src/test/java/umc/cockple/demo/domain/bookmark/integration/BookmarkIntegrationTest.java @@ -63,7 +63,7 @@ void setUp() { PartyAddr addr = partyAddrRepository.save(PartyFixture.createPartyAddr("경기도", "안산시")); bookmarkParty = partyRepository.save(PartyFixture.createParty("테스트 모임", member.getId(), addr)); - memberPartyRepository.save(MemberFixture.createMemberParty(bookmarkParty, member, Role.party_MANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(bookmarkParty, member, Role.PARTY_MANAGER)); bookmarkExercise = exerciseRepository.save(Exercise.builder() .party(bookmarkParty) diff --git a/src/test/java/umc/cockple/demo/domain/chat/integration/ChatIntegrationTest.java b/src/test/java/umc/cockple/demo/domain/chat/integration/ChatIntegrationTest.java index 0671cf4df..4d3643739 100644 --- a/src/test/java/umc/cockple/demo/domain/chat/integration/ChatIntegrationTest.java +++ b/src/test/java/umc/cockple/demo/domain/chat/integration/ChatIntegrationTest.java @@ -58,8 +58,8 @@ void setUp() { PartyAddr addr = partyAddrRepository.save(PartyFixture.createPartyAddr("서울특별시", "강남구")); party = partyRepository.save(PartyFixture.createParty("배드민턴 모임", member.getId(), addr)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, member, Role.party_MANAGER)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, otherMember, Role.party_MEMBER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, member, Role.PARTY_MANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, otherMember, Role.PARTY_MEMBER)); partyChatRoom = chatRoomRepository.save(ChatFixture.createPartyChatRoom(party)); directChatRoom = chatRoomRepository.save(ChatFixture.createDirectChatRoom()); diff --git a/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseCommandIntegrationTest.java b/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseCommandIntegrationTest.java index fa378c12c..bcaef277e 100644 --- a/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseCommandIntegrationTest.java +++ b/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseCommandIntegrationTest.java @@ -70,9 +70,9 @@ void setUp() { PartyAddr addr = partyAddrRepository.save(PartyFixture.createPartyAddr("서울특별시", "강남구")); party = partyRepository.save(PartyFixture.createParty("테스트 모임", manager.getId(), addr)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, manager, Role.party_MANAGER)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, subManager, Role.party_SUBMANAGER)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, normalMember, Role.party_MEMBER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, manager, Role.PARTY_MANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, subManager, Role.PARTY_SUBMANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, normalMember, Role.PARTY_MEMBER)); } @AfterEach @@ -606,7 +606,7 @@ void notPartyMember_outsideNotAccepted() throws Exception { void ageNotAllowed() throws Exception { Member youngMember = memberRepository.save( MemberFixture.createMember("어린회원", Gender.MALE, Level.B, 4001L, LocalDate.of(2010, 1, 1))); - memberPartyRepository.save(MemberFixture.createMemberParty(party, youngMember, Role.party_MEMBER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, youngMember, Role.PARTY_MEMBER)); SecurityContextHelper.setAuthentication(youngMember.getId(), youngMember.getNickname()); diff --git a/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseQueryIntegrationTest.java b/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseQueryIntegrationTest.java index 6e8b1b8e7..de9e789f5 100644 --- a/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseQueryIntegrationTest.java +++ b/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseQueryIntegrationTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import umc.cockple.demo.domain.bookmark.domain.ExerciseBookmark; @@ -39,7 +38,6 @@ import java.time.LocalTime; import java.util.ArrayList; import java.util.List; -import java.util.Map; import javax.sql.DataSource; @@ -79,9 +77,9 @@ void setUp() { PartyAddr addr = partyAddrRepository.save(PartyFixture.createPartyAddr("서울특별시", "강남구")); party = partyRepository.save(PartyFixture.createParty("테스트 모임", manager.getId(), addr)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, manager, Role.party_MANAGER)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, subManager, Role.party_SUBMANAGER)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, normalMember, Role.party_MEMBER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, manager, Role.PARTY_MANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, subManager, Role.PARTY_SUBMANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, normalMember, Role.PARTY_MEMBER)); } @AfterEach @@ -139,7 +137,7 @@ class Success { .andExpect(jsonPath("$.data.participants.list[0].gender").value("MALE")) .andExpect(jsonPath("$.data.participants.list[0].level").isString()) .andExpect(jsonPath("$.data.participants.list[0].participantType").value("PARTY_MEMBER")) - .andExpect(jsonPath("$.data.participants.list[0].partyPosition").value("party_MEMBER")) + .andExpect(jsonPath("$.data.participants.list[0].partyPosition").value("PARTY_MEMBER")) .andExpect(jsonPath("$.data.participants.list[0].isWithdrawn").value(false)) .andExpect(jsonPath("$.data.waiting.currentWaitingCount").value(1)) .andExpect(jsonPath("$.data.waiting.manCount").value(0)) @@ -147,7 +145,7 @@ class Success { .andExpect(jsonPath("$.data.waiting.list[0].name").value(subManager.getMemberName())) .andExpect(jsonPath("$.data.waiting.list[0].gender").value("FEMALE")) .andExpect(jsonPath("$.data.waiting.list[0].participantType").value("PARTY_MEMBER")) - .andExpect(jsonPath("$.data.waiting.list[0].partyPosition").value("party_SUBMANAGER")) + .andExpect(jsonPath("$.data.waiting.list[0].partyPosition").value("PARTY_SUBMANAGER")) .andExpect(jsonPath("$.data.waiting.list[0].isWithdrawn").value(false)); } @@ -277,13 +275,13 @@ class Success { .andExpect(status().isOk()) .andExpect(jsonPath("$.data.participants.list[0].name").value(manager.getMemberName())) .andExpect(jsonPath("$.data.participants.list[0].participantType").value("PARTY_MEMBER")) - .andExpect(jsonPath("$.data.participants.list[0].partyPosition").value("party_MANAGER")) + .andExpect(jsonPath("$.data.participants.list[0].partyPosition").value("PARTY_MANAGER")) .andExpect(jsonPath("$.data.participants.list[1].name").value(subManager.getMemberName())) .andExpect(jsonPath("$.data.participants.list[1].participantType").value("PARTY_MEMBER")) - .andExpect(jsonPath("$.data.participants.list[1].partyPosition").value("party_SUBMANAGER")) + .andExpect(jsonPath("$.data.participants.list[1].partyPosition").value("PARTY_SUBMANAGER")) .andExpect(jsonPath("$.data.participants.list[2].name").value(normalMember.getMemberName())) .andExpect(jsonPath("$.data.participants.list[2].participantType").value("PARTY_MEMBER")) - .andExpect(jsonPath("$.data.participants.list[2].partyPosition").value("party_MEMBER")) + .andExpect(jsonPath("$.data.participants.list[2].partyPosition").value("PARTY_MEMBER")) .andExpect(jsonPath("$.data.participants.list[3].name").value(outsider.getMemberName())) .andExpect(jsonPath("$.data.participants.list[3].participantType").value("EXTERNAL_PARTICIPANT")) .andExpect(jsonPath("$.data.participants.list[3].partyPosition").value(nullValue())) @@ -1501,7 +1499,7 @@ void setUp() { filteredParty = partyRepository.save(filteredParty); filteredParty.addLevel(Gender.MALE, Level.B); filteredParty = partyRepository.save(filteredParty); - memberPartyRepository.save(MemberFixture.createMemberParty(filteredParty, manager, Role.party_MANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(filteredParty, manager, Role.PARTY_MANAGER)); startDate = LocalDate.of(2026, 3, 23); endDate = LocalDate.of(2026, 4, 5); diff --git a/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseRecommendationIntegrationTest.java b/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseRecommendationIntegrationTest.java index cb51d226d..64e04dced 100644 --- a/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseRecommendationIntegrationTest.java +++ b/src/test/java/umc/cockple/demo/domain/exercise/integration/ExerciseRecommendationIntegrationTest.java @@ -72,7 +72,7 @@ void setUp() { partyRepository.save(party); // 모임장을 모임 멤버로 등록 - memberPartyRepository.save(MemberFixture.createMemberParty(party, manager, Role.party_MANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, manager, Role.PARTY_MANAGER)); } @AfterEach @@ -155,7 +155,7 @@ class Success { void 소속된_모임의_운동은_추천되지_않는다() throws Exception { // given - outsider를 모임에 가입시킴 memberPartyRepository.save( - MemberFixture.createMemberParty(party, outsider, Role.party_MEMBER)); + MemberFixture.createMemberParty(party, outsider, Role.PARTY_MEMBER)); exerciseRepository.save(ExerciseFixture.createRecommendableExercise(party, LocalDate.now().plusDays(3), 37.5, 127.0, "테스트 체육관")); diff --git a/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseLifecycleServiceTest.java b/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseLifecycleServiceTest.java index af8355184..c317929bd 100644 --- a/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseLifecycleServiceTest.java +++ b/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseLifecycleServiceTest.java @@ -128,9 +128,9 @@ void subManagerCreatesExercise_success() { Member subManager = MemberFixture.createMember("부모임장", Gender.FEMALE, Level.B, 1002L); ReflectionTestUtils.setField(subManager, "id", 2L); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.party_MANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.PARTY_MANAGER)) .willReturn(false); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.party_SUBMANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.PARTY_SUBMANAGER)) .willReturn(true); Exercise savedExercise = Exercise.builder() @@ -175,9 +175,9 @@ void normalMember_throwsException() { Member normalMember = MemberFixture.createMember("일반멤버", Gender.FEMALE, Level.B, 1002L); ReflectionTestUtils.setField(normalMember, "id", 2L); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.party_MANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.PARTY_MANAGER)) .willReturn(false); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.party_SUBMANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.PARTY_SUBMANAGER)) .willReturn(false); assertThatThrownBy(() -> @@ -278,9 +278,9 @@ void subManagerDeletesExercise_success() { Member subManager = MemberFixture.createMember("부모임장", Gender.FEMALE, Level.B, 1002L); ReflectionTestUtils.setField(subManager, "id", 2L); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.party_MANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.PARTY_MANAGER)) .willReturn(false); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.party_SUBMANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.PARTY_SUBMANAGER)) .willReturn(true); // when @@ -303,9 +303,9 @@ void normalMember_throwsException() { Member normalMember = MemberFixture.createMember("일반멤버", Gender.FEMALE, Level.B, 1002L); ReflectionTestUtils.setField(normalMember, "id", 2L); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.party_MANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.PARTY_MANAGER)) .willReturn(false); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.party_SUBMANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.PARTY_SUBMANAGER)) .willReturn(false); assertThatThrownBy(() -> @@ -384,9 +384,9 @@ void subManagerUpdatesExercise_success() { Member subManager = MemberFixture.createMember("부모임장", Gender.FEMALE, Level.B, 1002L); ReflectionTestUtils.setField(subManager, "id", 2L); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.party_MANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.PARTY_MANAGER)) .willReturn(false); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.party_SUBMANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.PARTY_SUBMANAGER)) .willReturn(true); Exercise savedExercise = Exercise.builder() @@ -418,9 +418,9 @@ void normalMember_throwsException() { Member normalMember = MemberFixture.createMember("일반멤버", Gender.FEMALE, Level.B, 1002L); ReflectionTestUtils.setField(normalMember, "id", 2L); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.party_MANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.PARTY_MANAGER)) .willReturn(false); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.party_SUBMANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.PARTY_SUBMANAGER)) .willReturn(false); assertThatThrownBy(() -> diff --git a/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseParticipationServiceTest.java b/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseParticipationServiceTest.java index 68ce83bee..62be82d77 100644 --- a/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseParticipationServiceTest.java +++ b/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseParticipationServiceTest.java @@ -341,9 +341,9 @@ void subManagerCancelsMemberParticipation_success() { ExerciseCancelDTO.ByManagerRequest request = new ExerciseCancelDTO.ByManagerRequest(false); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.party_MANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.PARTY_MANAGER)) .willReturn(false); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.party_SUBMANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), subManager.getId(), Role.PARTY_SUBMANAGER)) .willReturn(true); given(memberRepository.findById(participant.getId())).willReturn(Optional.of(participant)); given(memberExerciseRepository.findByExerciseAndMember(exercise, participant)) @@ -392,9 +392,9 @@ void normalMember_throwsException() { ExerciseCancelDTO.ByManagerRequest request = new ExerciseCancelDTO.ByManagerRequest(false); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.party_MANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.PARTY_MANAGER)) .willReturn(false); - given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.party_SUBMANAGER)) + given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole(party.getId(), normalMember.getId(), Role.PARTY_SUBMANAGER)) .willReturn(false); assertThatThrownBy(() -> diff --git a/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseQueryServiceTest.java b/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseQueryServiceTest.java index 117fd8d8f..1a8c53480 100644 --- a/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseQueryServiceTest.java +++ b/src/test/java/umc/cockple/demo/domain/exercise/service/ExerciseQueryServiceTest.java @@ -142,7 +142,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of()); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), manager.getId(), Role.party_MANAGER)) + party.getId(), manager.getId(), Role.PARTY_MANAGER)) .willReturn(true); // when @@ -169,7 +169,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of()); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), subManager.getId(), Role.party_MANAGER)) + party.getId(), subManager.getId(), Role.PARTY_MANAGER)) .willReturn(false); // when @@ -196,7 +196,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of()); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), normalMember.getId(), Role.party_MANAGER)) + party.getId(), normalMember.getId(), Role.PARTY_MANAGER)) .willReturn(false); // when @@ -223,7 +223,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of()); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), outsider.getId(), Role.party_MANAGER)) + party.getId(), outsider.getId(), Role.PARTY_MANAGER)) .willReturn(false); // when @@ -253,7 +253,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of()); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), manager.getId(), Role.party_MANAGER)) + party.getId(), manager.getId(), Role.PARTY_MANAGER)) .willReturn(true); given(memberPartyRepository.findMemberRolesByPartyAndMembers( party.getId(), List.of(withdrawnMember.getId()))) @@ -278,7 +278,7 @@ class Success { MemberExercise memberExercise = MemberFixture.createMemberExercise(activeMember, exercise); - MemberParty memberParty = MemberFixture.createMemberParty(party, activeMember, Role.party_MEMBER); + MemberParty memberParty = MemberFixture.createMemberParty(party, activeMember, Role.PARTY_MEMBER); given(exerciseRepository.findExerciseWithBasicInfo(exercise.getId())) .willReturn(Optional.of(exercise)); @@ -289,7 +289,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of()); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), manager.getId(), Role.party_MANAGER)) + party.getId(), manager.getId(), Role.PARTY_MANAGER)) .willReturn(true); given(memberPartyRepository.findMemberRolesByPartyAndMembers( party.getId(), List.of(activeMember.getId()))) @@ -321,7 +321,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of(guest)); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), manager.getId(), Role.party_MANAGER)) + party.getId(), manager.getId(), Role.PARTY_MANAGER)) .willReturn(true); given(memberRepository.findMemberNamesByIds(any())) .willReturn(Map.of(manager.getId(), "모임장")); @@ -366,9 +366,9 @@ class Success { ReflectionTestUtils.setField(guest, "id", 71L); ReflectionTestUtils.setField(guest, "createdAt", LocalDateTime.now().minusMinutes(1)); - MemberParty managerParty = MemberFixture.createMemberParty(party, manager, Role.party_MANAGER); - MemberParty subManagerParty = MemberFixture.createMemberParty(party, subManager, Role.party_SUBMANAGER); - MemberParty memberParty = MemberFixture.createMemberParty(party, normalMember, Role.party_MEMBER); + MemberParty managerParty = MemberFixture.createMemberParty(party, manager, Role.PARTY_MANAGER); + MemberParty subManagerParty = MemberFixture.createMemberParty(party, subManager, Role.PARTY_SUBMANAGER); + MemberParty memberParty = MemberFixture.createMemberParty(party, normalMember, Role.PARTY_MEMBER); given(exerciseRepository.findExerciseWithBasicInfo(exercise.getId())) .willReturn(Optional.of(exercise)); @@ -379,7 +379,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of(guest)); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), manager.getId(), Role.party_MANAGER)) + party.getId(), manager.getId(), Role.PARTY_MANAGER)) .willReturn(true); given(memberPartyRepository.findMemberRolesByPartyAndMembers( party.getId(), List.of(manager.getId(), subManager.getId(), normalMember.getId(), outsider.getId()))) @@ -398,9 +398,9 @@ class Success { ExerciseDetailDTO.ParticipantInfo::participantType, ExerciseDetailDTO.ParticipantInfo::partyPosition) .containsExactly( - tuple("모임장", "PARTY_MEMBER", "party_MANAGER"), - tuple("부모임장", "PARTY_MEMBER", "party_SUBMANAGER"), - tuple("일반멤버", "PARTY_MEMBER", "party_MEMBER"), + tuple("모임장", "PARTY_MEMBER", "PARTY_MANAGER"), + tuple("부모임장", "PARTY_MEMBER", "PARTY_SUBMANAGER"), + tuple("일반멤버", "PARTY_MEMBER", "PARTY_MEMBER"), tuple("외부회원", "EXTERNAL_PARTICIPANT", null), tuple("게스트", "GUEST", null) ); @@ -424,8 +424,8 @@ class Success { MemberExercise second = MemberFixture.createMemberExercise(secondMember, exercise); ReflectionTestUtils.setField(second, "createdAt", LocalDateTime.now()); - MemberParty firstParty = MemberFixture.createMemberParty(party, firstMember, Role.party_MEMBER); - MemberParty secondParty = MemberFixture.createMemberParty(party, secondMember, Role.party_MEMBER); + MemberParty firstParty = MemberFixture.createMemberParty(party, firstMember, Role.PARTY_MEMBER); + MemberParty secondParty = MemberFixture.createMemberParty(party, secondMember, Role.PARTY_MEMBER); given(exerciseRepository.findExerciseWithBasicInfo(exercise.getId())) .willReturn(Optional.of(exercise)); @@ -436,7 +436,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of()); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), manager.getId(), Role.party_MANAGER)) + party.getId(), manager.getId(), Role.PARTY_MANAGER)) .willReturn(true); given(memberPartyRepository.findMemberRolesByPartyAndMembers( party.getId(), List.of(firstMember.getId(), secondMember.getId()))) @@ -468,7 +468,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of(guest)); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), manager.getId(), Role.party_MANAGER)) + party.getId(), manager.getId(), Role.PARTY_MANAGER)) .willReturn(true); given(memberRepository.findMemberNamesByIds(any())) .willReturn(Map.of(manager.getId(), "모임장")); @@ -500,8 +500,8 @@ class Success { MemberExercise second = MemberFixture.createMemberExercise(secondMember, exercise); ReflectionTestUtils.setField(second, "createdAt", LocalDateTime.now()); - MemberParty firstParty = MemberFixture.createMemberParty(party, firstMember, Role.party_MEMBER); - MemberParty secondParty = MemberFixture.createMemberParty(party, secondMember, Role.party_MEMBER); + MemberParty firstParty = MemberFixture.createMemberParty(party, firstMember, Role.PARTY_MEMBER); + MemberParty secondParty = MemberFixture.createMemberParty(party, secondMember, Role.PARTY_MEMBER); given(exerciseRepository.findExerciseWithBasicInfo(exercise.getId())) .willReturn(Optional.of(exercise)); @@ -512,7 +512,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of()); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), manager.getId(), Role.party_MANAGER)) + party.getId(), manager.getId(), Role.PARTY_MANAGER)) .willReturn(true); given(memberPartyRepository.findMemberRolesByPartyAndMembers( party.getId(), List.of(firstMember.getId(), secondMember.getId()))) @@ -549,8 +549,8 @@ class Success { MemberExercise second = MemberFixture.createMemberExercise(femaleMember, exercise); ReflectionTestUtils.setField(second, "createdAt", LocalDateTime.now()); - MemberParty maleParty = MemberFixture.createMemberParty(party, maleMember, Role.party_MEMBER); - MemberParty femaleParty = MemberFixture.createMemberParty(party, femaleMember, Role.party_MEMBER); + MemberParty maleParty = MemberFixture.createMemberParty(party, maleMember, Role.PARTY_MEMBER); + MemberParty femaleParty = MemberFixture.createMemberParty(party, femaleMember, Role.PARTY_MEMBER); given(exerciseRepository.findExerciseWithBasicInfo(exercise.getId())) .willReturn(Optional.of(exercise)); @@ -561,7 +561,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of()); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), manager.getId(), Role.party_MANAGER)) + party.getId(), manager.getId(), Role.PARTY_MANAGER)) .willReturn(true); given(memberPartyRepository.findMemberRolesByPartyAndMembers( party.getId(), List.of(maleMember.getId(), femaleMember.getId()))) @@ -594,8 +594,8 @@ class Success { MemberExercise femaleExercise = MemberFixture.createMemberExercise(femaleMember, exercise); ReflectionTestUtils.setField(femaleExercise, "createdAt", LocalDateTime.now()); - MemberParty maleParty = MemberFixture.createMemberParty(party, maleMember, Role.party_MEMBER); - MemberParty femaleParty = MemberFixture.createMemberParty(party, femaleMember, Role.party_MEMBER); + MemberParty maleParty = MemberFixture.createMemberParty(party, maleMember, Role.PARTY_MEMBER); + MemberParty femaleParty = MemberFixture.createMemberParty(party, femaleMember, Role.PARTY_MEMBER); given(exerciseRepository.findExerciseWithBasicInfo(exercise.getId())) .willReturn(Optional.of(exercise)); @@ -606,7 +606,7 @@ class Success { given(guestRepository.findByExerciseId(exercise.getId())) .willReturn(List.of()); given(memberPartyRepository.existsByPartyIdAndMemberIdAndRole( - party.getId(), manager.getId(), Role.party_MANAGER)) + party.getId(), manager.getId(), Role.PARTY_MANAGER)) .willReturn(true); given(memberPartyRepository.findMemberRolesByPartyAndMembers( party.getId(), List.of(maleMember.getId(), femaleMember.getId()))) diff --git a/src/test/java/umc/cockple/demo/domain/member/integration/MemberIntegrationTest.java b/src/test/java/umc/cockple/demo/domain/member/integration/MemberIntegrationTest.java index bba271356..28d577edc 100644 --- a/src/test/java/umc/cockple/demo/domain/member/integration/MemberIntegrationTest.java +++ b/src/test/java/umc/cockple/demo/domain/member/integration/MemberIntegrationTest.java @@ -112,7 +112,7 @@ class Failure { void manager_cannotWithdraw() throws Exception { PartyAddr addr = partyAddrRepository.save(PartyFixture.createPartyAddr("서울특별시", "강남구")); Party party = partyRepository.save(PartyFixture.createParty("테스트 모임", member.getId(), addr)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, member, Role.party_MANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, member, Role.PARTY_MANAGER)); SecurityContextHelper.setAuthentication(member.getId(), member.getNickname()); @@ -127,7 +127,7 @@ void manager_cannotWithdraw() throws Exception { void subManager_cannotWithdraw() throws Exception { PartyAddr addr = partyAddrRepository.save(PartyFixture.createPartyAddr("서울특별시", "강남구")); Party party = partyRepository.save(PartyFixture.createParty("테스트 모임", member.getId(), addr)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, member, Role.party_SUBMANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, member, Role.PARTY_SUBMANAGER)); SecurityContextHelper.setAuthentication(member.getId(), member.getNickname()); @@ -204,8 +204,8 @@ class Success { .build()); // 모임 2개 - memberPartyRepository.save(MemberFixture.createMemberParty(null, freshMember, Role.party_MEMBER)); - memberPartyRepository.save(MemberFixture.createMemberParty(null, freshMember, Role.party_MEMBER)); + memberPartyRepository.save(MemberFixture.createMemberParty(null, freshMember, Role.PARTY_MEMBER)); + memberPartyRepository.save(MemberFixture.createMemberParty(null, freshMember, Role.PARTY_MEMBER)); SecurityContextHelper.setAuthentication(freshMember.getId(), freshMember.getNickname()); @@ -322,8 +322,8 @@ void getMyProfile_success() throws Exception { .build()); // 모임 2개, 운동 2개, 키워드 2개 - memberPartyRepository.save(MemberFixture.createMemberParty(null, freshMember, Role.party_MEMBER)); - memberPartyRepository.save(MemberFixture.createMemberParty(null, freshMember, Role.party_MEMBER)); + memberPartyRepository.save(MemberFixture.createMemberParty(null, freshMember, Role.PARTY_MEMBER)); + memberPartyRepository.save(MemberFixture.createMemberParty(null, freshMember, Role.PARTY_MEMBER)); memberExerciseRepository.save(MemberExercise.builder() .member(freshMember) diff --git a/src/test/java/umc/cockple/demo/domain/member/service/MemberCommandServiceTest.java b/src/test/java/umc/cockple/demo/domain/member/service/MemberCommandServiceTest.java index 290c391a4..f74620c03 100644 --- a/src/test/java/umc/cockple/demo/domain/member/service/MemberCommandServiceTest.java +++ b/src/test/java/umc/cockple/demo/domain/member/service/MemberCommandServiceTest.java @@ -703,7 +703,7 @@ class Failure { void 활성_모임의_모임장이면_MANAGER_CANNOT_LEAVE_예외를_던진다() { // given MemberParty leaderParty = MemberParty.builder() - .role(Role.party_MANAGER) + .role(Role.PARTY_MANAGER) .status(MemberPartyStatus.ACTIVE) .joinedAt(LocalDateTime.now()) .build(); @@ -723,7 +723,7 @@ class Failure { void 활성_모임의_부모임장이면_SUBMANAGER_CANNOT_LEAVE_예외를_던진다() { // given MemberParty subManagerParty = MemberParty.builder() - .role(Role.party_SUBMANAGER) + .role(Role.PARTY_SUBMANAGER) .status(MemberPartyStatus.ACTIVE) .joinedAt(LocalDateTime.now()) .build(); @@ -743,7 +743,7 @@ class Failure { void 비활성_모임의_모임장이면_탈퇴가_가능하다() { // given: BANNED 상태의 모임이라면 탈퇴 검증을 통과해야 한다 MemberParty bannedParty = MemberParty.builder() - .role(Role.party_MANAGER) + .role(Role.PARTY_MANAGER) .status(MemberPartyStatus.BANNED) .joinedAt(LocalDateTime.now()) .build(); diff --git a/src/test/java/umc/cockple/demo/domain/member/service/MemberQueryServiceTest.java b/src/test/java/umc/cockple/demo/domain/member/service/MemberQueryServiceTest.java index b7c521268..9c1eb0a16 100644 --- a/src/test/java/umc/cockple/demo/domain/member/service/MemberQueryServiceTest.java +++ b/src/test/java/umc/cockple/demo/domain/member/service/MemberQueryServiceTest.java @@ -125,8 +125,8 @@ class Success { @DisplayName("참여한_모임_수가_올바르게_반환된다") void 참여한_모임_수가_올바르게_반환된다() { // given - member.getMemberParties().add(MemberFixture.createMemberParty(null, member, umc.cockple.demo.global.enums.Role.party_MEMBER)); - member.getMemberParties().add(MemberFixture.createMemberParty(null, member, umc.cockple.demo.global.enums.Role.party_MEMBER)); + member.getMemberParties().add(MemberFixture.createMemberParty(null, member, umc.cockple.demo.global.enums.Role.PARTY_MEMBER)); + member.getMemberParties().add(MemberFixture.createMemberParty(null, member, umc.cockple.demo.global.enums.Role.PARTY_MEMBER)); given(memberRepository.findById(member.getId())).willReturn(Optional.of(member)); diff --git a/src/test/java/umc/cockple/demo/domain/party/integration/PartyIntegrationTest.java b/src/test/java/umc/cockple/demo/domain/party/integration/PartyIntegrationTest.java index 87f8f8245..25fe51839 100644 --- a/src/test/java/umc/cockple/demo/domain/party/integration/PartyIntegrationTest.java +++ b/src/test/java/umc/cockple/demo/domain/party/integration/PartyIntegrationTest.java @@ -1,18 +1,39 @@ package umc.cockple.demo.domain.party.integration; -import org.junit.jupiter.api.*; +import com.fasterxml.jackson.databind.ObjectMapper; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import umc.cockple.demo.domain.chat.domain.ChatRoom; +import umc.cockple.demo.domain.chat.domain.ChatRoomMember; +import umc.cockple.demo.domain.chat.repository.ChatRoomMemberRepository; +import umc.cockple.demo.domain.chat.repository.ChatRoomRepository; import umc.cockple.demo.domain.exercise.domain.Exercise; import umc.cockple.demo.domain.exercise.repository.ExerciseRepository; import umc.cockple.demo.domain.member.domain.Member; +import umc.cockple.demo.domain.member.domain.MemberAddr; +import umc.cockple.demo.domain.member.domain.MemberParty; +import umc.cockple.demo.domain.member.exception.MemberErrorCode; +import umc.cockple.demo.domain.member.repository.MemberAddrRepository; import umc.cockple.demo.domain.member.repository.MemberExerciseRepository; import umc.cockple.demo.domain.member.repository.MemberPartyRepository; import umc.cockple.demo.domain.member.repository.MemberRepository; import umc.cockple.demo.domain.party.domain.Party; import umc.cockple.demo.domain.party.domain.PartyAddr; +import umc.cockple.demo.domain.party.domain.PartyInvitation; +import umc.cockple.demo.domain.party.domain.PartyJoinRequest; +import umc.cockple.demo.domain.party.dto.*; +import umc.cockple.demo.domain.party.enums.*; import umc.cockple.demo.domain.party.exception.PartyErrorCode; import umc.cockple.demo.domain.party.repository.PartyAddrRepository; +import umc.cockple.demo.domain.party.repository.PartyInvitationRepository; +import umc.cockple.demo.domain.party.repository.PartyJoinRequestRepository; import umc.cockple.demo.domain.party.repository.PartyRepository; import umc.cockple.demo.global.enums.Gender; import umc.cockple.demo.global.enums.Level; @@ -24,20 +45,42 @@ import umc.cockple.demo.support.fixture.PartyFixture; import java.time.LocalDate; +import java.util.List; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@Transactional class PartyIntegrationTest extends IntegrationTestBase { - @Autowired MockMvc mockMvc; - @Autowired MemberRepository memberRepository; - @Autowired PartyRepository partyRepository; - @Autowired MemberPartyRepository memberPartyRepository; - @Autowired PartyAddrRepository partyAddrRepository; - @Autowired ExerciseRepository exerciseRepository; - @Autowired MemberExerciseRepository memberExerciseRepository; + @Autowired + MockMvc mockMvc; + @Autowired + MemberRepository memberRepository; + @Autowired + PartyRepository partyRepository; + @Autowired + MemberPartyRepository memberPartyRepository; + @Autowired + PartyAddrRepository partyAddrRepository; + @Autowired + ExerciseRepository exerciseRepository; + @Autowired + MemberExerciseRepository memberExerciseRepository; + @Autowired + MemberAddrRepository memberAddrRepository; + @Autowired + ChatRoomRepository chatRoomRepository; + @Autowired + ChatRoomMemberRepository chatRoomMemberRepository; + @Autowired + PartyJoinRequestRepository partyJoinRequestRepository; + @Autowired + PartyInvitationRepository partyInvitationRepository; + @Autowired + ObjectMapper objectMapper; private Member manager; private Member normalMember; @@ -45,86 +88,1564 @@ class PartyIntegrationTest extends IntegrationTestBase { @BeforeEach void setUp() { - manager = memberRepository.save(MemberFixture.createMember("매니저", Gender.MALE, Level.A, 1001L)); + // 매니저 및 주소 정보 생성 + manager = memberRepository.save(MemberFixture.createMember("매니저", Gender.MALE, Level.A, 1001L, LocalDate.of(1995, 1, 1))); + memberAddrRepository.save(MemberAddr.builder() + .member(manager) + .addr1("서울특별시") + .addr2("강남구") + .addr3("역삼동") + .streetAddr("테헤란로") + .latitude(37.5) + .longitude(127.0) + .isMain(true) + .build()); + + // 일반 멤버 생성 normalMember = memberRepository.save(MemberFixture.createMember("일반멤버", Gender.FEMALE, Level.B, 1002L)); + // 모임 및 주소 정보 생성 PartyAddr addr = partyAddrRepository.save(PartyFixture.createPartyAddr("서울특별시", "강남구")); party = partyRepository.save(PartyFixture.createParty("테스트 모임", manager.getId(), addr)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, manager, Role.party_MANAGER)); - memberPartyRepository.save(MemberFixture.createMemberParty(party, normalMember, Role.party_MEMBER)); + // 모임 멤버 생성 + memberPartyRepository.save(MemberFixture.createMemberParty(party, manager, Role.PARTY_MANAGER)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, normalMember, Role.PARTY_MEMBER)); + + // 채팅방 생성 + ChatRoom chatRoom = chatRoomRepository.save(ChatRoom.createPartyChatRoom(party)); + chatRoomMemberRepository.save(ChatRoomMember.create(chatRoom, manager)); + chatRoomMemberRepository.save(ChatRoomMember.create(chatRoom, normalMember)); + + // 추천 조회용 모임 (manager의 조건에 맞춤) + Party suggestedParty = PartyFixture.createParty("추천 모임", normalMember.getId(), addr); + suggestedParty.addLevel(Gender.MALE, Level.A); + partyRepository.save(suggestedParty); SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); } - @AfterEach - void tearDown() { - memberExerciseRepository.deleteAll(); - exerciseRepository.deleteAll(); - memberPartyRepository.deleteAll(); - partyRepository.deleteAll(); - partyAddrRepository.deleteAll(); - memberRepository.deleteAll(); - } @Nested @DisplayName("GET /api/parties/{partyId}/members - 모임 멤버 조회") class GetPartyMembers { @Test - @DisplayName("200 - 멤버 목록과 마지막 운동일을 정상 반환한다") - void success_withLastExerciseDate() throws Exception { - Exercise exercise = exerciseRepository.save( - ExerciseFixture.createExercise(party, LocalDate.of(2025, 1, 10))); + @DisplayName("200 - 멤버 목록을 역할, 성별 통계 및 마지막 운동일과 함께 조회한다") + void success_getPartyMembers() throws Exception { + // 부모임장 추가 + Member subManager = memberRepository.save(MemberFixture.createMember("부매니저", Gender.MALE, Level.A, 1003L)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, subManager, Role.PARTY_SUBMANAGER)); + + // 운동 기록 추가 + Exercise exercise = exerciseRepository.save(ExerciseFixture.createExercise(party, LocalDate.of(2025, 1, 10))); memberExerciseRepository.save(MemberFixture.createMemberExercise(normalMember, exercise)); mockMvc.perform(get("/api/parties/{partyId}/members", party.getId())) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.summary.totalCount").value(2)) - .andExpect(jsonPath("$.data.summary.maleCount").value(1)) + .andExpect(jsonPath("$.data.summary.totalCount").value(3)) + .andExpect(jsonPath("$.data.summary.maleCount").value(2)) .andExpect(jsonPath("$.data.summary.femaleCount").value(1)) - // 첫 번째 멤버(매니저) 전체 필드 검증 - .andExpect(jsonPath("$.data.members[0].memberId").value(manager.getId())) - .andExpect(jsonPath("$.data.members[0].nickname").value("매니저")) - .andExpect(jsonPath("$.data.members[0].profileImageUrl").doesNotExist()) - .andExpect(jsonPath("$.data.members[0].role").value("party_MANAGER")) - .andExpect(jsonPath("$.data.members[0].gender").value("MALE")) - .andExpect(jsonPath("$.data.members[0].level").value("A조")) + .andExpect(jsonPath("$.data.members[0].role").value("PARTY_MANAGER")) .andExpect(jsonPath("$.data.members[0].isMe").value(true)) - .andExpect(jsonPath("$.data.members[0].lastExerciseDate").doesNotExist()) - // 두 번째 멤버(일반멤버) 마지막 운동일 검증 - .andExpect(jsonPath("$.data.members[1].lastExerciseDate").value("2025-01-10")); + .andExpect(jsonPath("$.data.members[1].role").value("PARTY_SUBMANAGER")) + .andExpect(jsonPath("$.data.members[2].role").value("PARTY_MEMBER")) + .andExpect(jsonPath("$.data.members[2].lastExerciseDate").value("2025-01-10")); } @Test - @DisplayName("200 - 운동 기록이 없는 멤버의 lastExerciseDate는 null이다") - void success_noExerciseHistory() throws Exception { + @DisplayName("404 - 존재하지 않는 파티면 에러를 반환한다") + void fail_getPartyMembers_partyNotFound() throws Exception { + mockMvc.perform(get("/api/parties/{partyId}/members", 999L)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + + @Test + @DisplayName("400 - 비활성화된 파티면 에러를 반환한다") + void fail_getPartyMembers_partyInactive() throws Exception { + party.delete(); + partyRepository.save(party); + mockMvc.perform(get("/api/parties/{partyId}/members", party.getId())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_IS_DELETED.getCode())); + } + } + + @Nested + @DisplayName("DELETE /api/parties/{partyId}/members/my - 모임 탈퇴") + class LeaveParty { + + @Test + @DisplayName("200 - 일반 멤버가 모임을 성공적으로 탈퇴한다") + void success_leaveParty() throws Exception { + // DB에서 최신 정보 보장 + Member member = memberRepository.findById(normalMember.getId()).orElseThrow(); + Party targetParty = partyRepository.findById(party.getId()).orElseThrow(); + + // normalMember 세션으로 설정 + SecurityContextHelper.setAuthentication(member.getId(), member.getNickname()); + + mockMvc.perform(delete("/api/parties/{partyId}/members/my", targetParty.getId())) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.summary.totalCount").value(2)) - .andExpect(jsonPath("$.data.members[0].lastExerciseDate").isEmpty()) - .andExpect(jsonPath("$.data.members[1].lastExerciseDate").isEmpty()); + .andExpect(jsonPath("$.code").value("COMMON200")); + + // DB에서 제거되었는지 확인 + boolean exists = memberPartyRepository.existsByPartyAndMember(targetParty, member); + assertThat(exists).isFalse(); } @Test - @DisplayName("404 - 존재하지 않는 파티면 에러를 반환한다") - void fail_partyNotFound() throws Exception { - mockMvc.perform(get("/api/parties/{partyId}/members", 999L)) + @DisplayName("403 - 모임장은 탈퇴할 수 없다") + void fail_leaveParty_owner() throws Exception { + // manager(모임장) 세션으로 설정 + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + mockMvc.perform(delete("/api/parties/{partyId}/members/my", party.getId())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INVALID_ACTION_FOR_OWNER.getCode())); + } + + @Test + @DisplayName("403 - 부모임장은 탈퇴할 수 없다") + void fail_leaveParty_subOwner() throws Exception { + // 부모임장 생성 및 가입 + Member subManager = memberRepository.save(MemberFixture.createMember("부매니저", Gender.MALE, Level.A, 3001L)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, subManager, Role.PARTY_SUBMANAGER)); + + // 부모임장 세션으로 설정 + SecurityContextHelper.setAuthentication(subManager.getId(), subManager.getNickname()); + + mockMvc.perform(delete("/api/parties/{partyId}/members/my", party.getId())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INVALID_ACTION_FOR_SUBOWNER.getCode())); + } + + @Test + @DisplayName("400 - 해당 모임의 멤버가 아니면 탈퇴할 수 없다") + void fail_leaveParty_notMember() throws Exception { + // 가입하지 않은 새로운 멤버 생성 + Member nonMember = memberRepository.save(MemberFixture.createMember("외부인", Gender.MALE, Level.A, 4002L)); + SecurityContextHelper.setAuthentication(nonMember.getId(), nonMember.getNickname()); + + mockMvc.perform(delete("/api/parties/{partyId}/members/my", party.getId())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.NOT_MEMBER.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 파티에서 탈퇴 시도 시 PARTY_NOT_FOUND 에러를 반환한다") + void fail_leaveParty_partyNotFound() throws Exception { + mockMvc.perform(delete("/api/parties/{partyId}/members/my", 9999L)) .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())) - .andExpect(jsonPath("$.message").value(PartyErrorCode.PARTY_NOT_FOUND.getMessage())); + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("GET /api/my/parties - 내 모임 조회") + class GetMyParties { + + Party party2; + Party party3; + + @BeforeEach + void setUpMyParties() { + PartyAddr addr = partyAddrRepository.findAll().get(0); + + // party: 1번째 생성, 운동 횟수 10 + ReflectionTestUtils.setField(party, "exerciseCount", 10); + partyRepository.save(party); + + // party2: 2번째 생성, 운동 횟수 20 + party2 = partyRepository.save(PartyFixture.createParty("테스트 모임 2", manager.getId(), addr)); + memberPartyRepository.save(MemberFixture.createMemberParty(party2, manager, Role.PARTY_MANAGER)); + ReflectionTestUtils.setField(party2, "exerciseCount", 20); + partyRepository.save(party2); + + // party3: 3번째 생성, 운동 횟수 5 + party3 = partyRepository.save(PartyFixture.createParty("테스트 모임 3", manager.getId(), addr)); + memberPartyRepository.save(MemberFixture.createMemberParty(party3, manager, Role.PARTY_MANAGER)); + ReflectionTestUtils.setField(party3, "exerciseCount", 5); + partyRepository.save(party3); } @Test - @DisplayName("400 - 비활성화된 파티면 에러를 반환한다") - void fail_partyInactive() throws Exception { + @DisplayName("200 - 사용자가 가입한 모임 목록을 최신순(기본)으로 페이징하여 반환한다") + void success_getMyParties() throws Exception { + mockMvc.perform(get("/api/my/parties") + .param("created", "false") + .param("sort", "최신순") + .param("size", "10") + .param("page", "0")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(3)) + .andExpect(jsonPath("$.data.content[0].partyId").value(party3.getId())) + .andExpect(jsonPath("$.data.content[1].partyId").value(party2.getId())) + .andExpect(jsonPath("$.data.content[2].partyId").value(party.getId())); + } + + @Test + @DisplayName("200 - 사용자가 가입한 모임 목록을 오래된 순으로 페이징하여 반환한다") + void success_getMyParties_oldest() throws Exception { + mockMvc.perform(get("/api/my/parties") + .param("created", "false") + .param("sort", "오래된 순") + .param("size", "10") + .param("page", "0")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(3)) + .andExpect(jsonPath("$.data.content[0].partyId").value(party.getId())) + .andExpect(jsonPath("$.data.content[1].partyId").value(party2.getId())) + .andExpect(jsonPath("$.data.content[2].partyId").value(party3.getId())); + } + + @Test + @DisplayName("200 - 사용자가 가입한 모임 목록을 운동 많은 순으로 페이징하여 반환한다") + void success_getMyParties_exerciseCount() throws Exception { + mockMvc.perform(get("/api/my/parties") + .param("created", "false") + .param("sort", "운동 많은 순") + .param("size", "10") + .param("page", "0")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(3)) + .andExpect(jsonPath("$.data.content[0].partyId").value(party2.getId())) // 20회 + .andExpect(jsonPath("$.data.content[1].partyId").value(party.getId())) // 10회 + .andExpect(jsonPath("$.data.content[2].partyId").value(party3.getId())); // 5회 + } + + @Test + @DisplayName("200 - 가입한 모임이 없을 경우 빈 목록을 반환한다") + void success_emptyMyParties() throws Exception { + Member newMember = memberRepository.save(MemberFixture.createMember("뉴비", + Gender.MALE, Level.BEGINNER, 3003L)); + SecurityContextHelper.setAuthentication(newMember.getId(), newMember.getNickname()); + + mockMvc.perform(get("/api/my/parties") + .param("created", "false") + .param("sort", "최신순") + .param("size", "10") + .param("page", "0")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content").isEmpty()) + .andExpect(jsonPath("$.data.empty").value(true)); + } + } + + @Nested + @DisplayName("GET /api/my/parties/simple - 내 모임 간략화 조회") + class GetSimpleMyParties { + + Party party2; + Party party3; + + @BeforeEach + void setUpSimpleMyParties() { + PartyAddr addr = partyAddrRepository.findAll().get(0); + + // party2 + party2 = partyRepository.save(PartyFixture.createParty("간략 모임 2", manager.getId(), addr)); + memberPartyRepository.save(MemberFixture.createMemberParty(party2, manager, Role.PARTY_MANAGER)); + + // party3 + party3 = partyRepository.save(PartyFixture.createParty("간략 모임 3", manager.getId(), addr)); + memberPartyRepository.save(MemberFixture.createMemberParty(party3, manager, Role.PARTY_MANAGER)); + } + + @Test + @DisplayName("200 - 사용자가 가입한 모임의 간략화된 목록을 페이징하여 반환한다") + void success_getSimpleMyParties() throws Exception { + mockMvc.perform(get("/api/my/parties/simple") + .param("page", "0") + .param("size", "10") + .param("sort", "createdAt,DESC")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(3)) + .andExpect(jsonPath("$.data.content[0].partyId").value(party3.getId())) + .andExpect(jsonPath("$.data.content[1].partyId").value(party2.getId())) + .andExpect(jsonPath("$.data.content[2].partyId").value(party.getId())); + } + + @Test + @DisplayName("200 - 가입한 모임이 없을 경우 빈 목록을 반환한다") + void success_emptySimpleMyParties() throws Exception { + Member newMember = memberRepository.save(MemberFixture.createMember("뉴비", + Gender.MALE, Level.BEGINNER, 3003L)); + SecurityContextHelper.setAuthentication(newMember.getId(), newMember.getNickname()); + + mockMvc.perform(get("/api/my/parties/simple") + .param("page", "0") + .param("size", "10") + .param("sort", "createdAt,DESC")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.message").value("요청에 성공했습니다.")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content").isEmpty()) + .andExpect(jsonPath("$.data.empty").value(true)); + } + } + + @Nested + @DisplayName("GET /api/my/parties/suggestions - 모임 추천 조회") + class GetRecommendedParties { + + Party recParty1; + Party recParty2; + Party recParty3; + + @BeforeEach + void setUpRecommends() { + PartyAddr addr = partyAddrRepository.findAll().get(0); + + // recParty1: 1번째 생성, 운동 횟수 10 + recParty1 = partyRepository.findAll().stream() + .filter(p -> p.getPartyName().equals("추천 모임")) + .findFirst().orElseThrow(); + ReflectionTestUtils.setField(recParty1, "exerciseCount", 10); + partyRepository.save(recParty1); + + // recParty2: 1번째 생성, 운동 횟수 20 + recParty2 = partyRepository.save(PartyFixture.createParty("추천 모임 2", normalMember.getId(), addr)); + recParty2.addLevel(Gender.MALE, Level.A); // manager 조건에 맞도록 + ReflectionTestUtils.setField(recParty2, "exerciseCount", 20); + partyRepository.save(recParty2); + + // recParty3: 3번째 생성, 운동 횟수 5 + recParty3 = partyRepository.save(PartyFixture.createParty("추천 모임 3", normalMember.getId(), addr)); + recParty3.addLevel(Gender.MALE, Level.A); // manager 조건에 맞도록 + ReflectionTestUtils.setField(recParty3, "exerciseCount", 5); + partyRepository.save(recParty3); + } + + @Test + @DisplayName("200 - Cockple 추천 모드 시 추천된 모임 목록 3개를 반환한다") + void success_getRecommendedParties_cockpleRecommend() throws Exception { + mockMvc.perform(get("/api/my/parties/suggestions") + .param("isCockpleRecommend", "true") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(3)); + } + + @Test + @DisplayName("200 - 필터 모드 시 조건에 맞는 모임 목록을 운동 많은 순으로 반환한다") + void success_getRecommendedParties_exerciseCount() throws Exception { + mockMvc.perform(get("/api/my/parties/suggestions") + .param("isCockpleRecommend", "false") + .param("sort", "운동 많은 순") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(3)) + .andExpect(jsonPath("$.data.content[0].partyId").value(recParty2.getId())) // 20회 + .andExpect(jsonPath("$.data.content[1].partyId").value(recParty1.getId())) // 10회 + .andExpect(jsonPath("$.data.content[2].partyId").value(recParty3.getId())); // 5회 + } + + @Test + @DisplayName("200 - 필터 모드 시 조건에 맞는 모임 목록을 최신순으로 반환한다") + void success_getRecommendedParties_latest() throws Exception { + mockMvc.perform(get("/api/my/parties/suggestions") + .param("isCockpleRecommend", "false") + .param("addr1", "서울특별시") + .param("addr2", "강남구") + .param("sort", "최신순") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(3)) + .andExpect(jsonPath("$.data.content[0].partyId").value(recParty3.getId())) + .andExpect(jsonPath("$.data.content[1].partyId").value(recParty2.getId())) + .andExpect(jsonPath("$.data.content[2].partyId").value(recParty1.getId())); + } + + @Test + @DisplayName("200 - 필터 모드 시 조건에 맞는 모임 목록을 오래된 순으로 반환한다") + void success_getRecommendedParties_oldest() throws Exception { + mockMvc.perform(get("/api/my/parties/suggestions") + .param("isCockpleRecommend", "false") + .param("sort", "오래된 순") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(3)) + .andExpect(jsonPath("$.data.content[0].partyId").value(recParty1.getId())) + .andExpect(jsonPath("$.data.content[1].partyId").value(recParty2.getId())) + .andExpect(jsonPath("$.data.content[2].partyId").value(recParty3.getId())); + } + + @Test + @DisplayName("200 - 검색 모드 시 모임명으로 검색된 결과를 반환한다") + void success_getRecommendedParties_search() throws Exception { + mockMvc.perform(get("/api/my/parties/suggestions") + .param("search", "추천 모임 2") + .param("isCockpleRecommend", "false") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content.length()").value(1)) + .andExpect(jsonPath("$.data.content[0].partyName").value("추천 모임 2")); + } + + @Test + @DisplayName("400 - 유효하지 않은 정렬 기준 입력 시 INVALID_ORDER_TYPE 에러를 반환한다") + void fail_getRecommendedParties_invalidOrderType() throws Exception { + mockMvc.perform(get("/api/my/parties/suggestions") + .param("isCockpleRecommend", "false") + .param("sort", "잘못된순")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INVALID_ORDER_TYPE.getCode())); + } + + @Test + @DisplayName("400 - isCockpleRecommend에 부적절한 타입 입력 시 400 에러를 반환한다") + void fail_getRecommendedParties_invalidBooleanType() throws Exception { + mockMvc.perform(get("/api/my/parties/suggestions") + .param("isCockpleRecommend", "not-boolean")) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("GET /api/parties/{partyId} - 모임 상세 조회") + class GetPartyDetails { + + @Test + @DisplayName("200 - 모임 상세 정보를 정상적으로 조회한다 (비회원 상태)") + void success_getPartyDetails_nonMember() throws Exception { + // 모임에 가입하지 않은 새로운 유저 생성 및 인증 설정 + Member nonMember = memberRepository.save(MemberFixture.createMember("비회원", Gender.MALE, Level.C, 2001L)); + SecurityContextHelper.setAuthentication(nonMember.getId(), nonMember.getNickname()); + + mockMvc.perform(get("/api/parties/{partyId}", party.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.partyId").value(party.getId())) + .andExpect(jsonPath("$.data.memberStatus").value("NOT_MEMBER")) + .andExpect(jsonPath("$.data.hasPendingJoinRequest").value(false)); + } + + @Test + @DisplayName("200 - 모임원인 경우 memberStatus가 MEMBER로 반환된다") + void success_getPartyDetails_member() throws Exception { + // manager는 setUp에서 이미 party의 멤버로 설정됨 + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + mockMvc.perform(get("/api/parties/{partyId}", party.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.memberStatus").value("MEMBER")) + .andExpect(jsonPath("$.data.memberRole").value("PARTY_MANAGER")); + } + + @Test + @DisplayName("404 - 존재하지 않는 모임 조회 시 PARTY_NOT_FOUND 에러를 반환한다") + void fail_getPartyDetails_partyNotFound() throws Exception { + mockMvc.perform(get("/api/parties/{partyId}", 9999L)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + + @Test + @DisplayName("400 - 삭제된 모임 조회 시 PARTY_IS_DELETED 에러를 반환한다") + void fail_getPartyDetails_partyDeleted() throws Exception { + // 모임 삭제 (비활성화) party.delete(); partyRepository.save(party); - mockMvc.perform(get("/api/parties/{partyId}/members", party.getId())) + mockMvc.perform(get("/api/parties/{partyId}", party.getId())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_IS_DELETED.getCode())); + } + } + + @Nested + @DisplayName("POST /api/parties/{partyId}/join-requests - 모임 가입 신청") + class CreateJoinRequest { + + @Test + @DisplayName("200 - 가입하지 않은 회원이 모임 가입을 신청한다") + void success_createJoinRequest() throws Exception { + // 가입하지 않은 멤버 생성 + Member applicant = memberRepository.save(MemberFixture.createMember("신청자", Gender.MALE, Level.A, 5001L, LocalDate.of(1995, 1, 1))); + SecurityContextHelper.setAuthentication(applicant.getId(), applicant.getNickname()); + + mockMvc.perform(post("/api/parties/{partyId}/join-requests", party.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON201")); + + // 가입 신청 데이터 확인 + boolean exists = partyJoinRequestRepository.existsByPartyAndMemberAndStatus(party, applicant, RequestStatus.PENDING); + assertThat(exists).isTrue(); + } + + @Test + @DisplayName("409 - 이미 가입된 회원이 다시 가입 신청을 한다") + void fail_createJoinRequest_alreadyMember() throws Exception { + // 이미 가입된 normalMember 사용 + SecurityContextHelper.setAuthentication(normalMember.getId(), normalMember.getNickname()); + + mockMvc.perform(post("/api/parties/{partyId}/join-requests", party.getId())) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.ALREADY_MEMBER.getCode())); + } + + @Test + @DisplayName("400 - 성별 조건이 맞지 않는 모임에 신청한다") + void fail_createJoinRequest_genderMismatch() throws Exception { + // 여복 모임 생성 + PartyAddr addr = partyAddrRepository.save(PartyFixture.createPartyAddr("서울", "강남")); + Party womenParty = partyRepository.save(Party.builder() + .partyName("여복 전용 모임") + .partyType(ParticipationType.WOMEN_DOUBLES) + .status(PartyStatus.ACTIVE) + .ownerId(manager.getId()) + .partyAddr(addr) + .minBirthYear(1900) + .maxBirthYear(2099) + .activityTime(ActivityTime.MORNING) + .designatedCock("테스트콕") + .exerciseCount(0) + .price(0) + .joinPrice(0) + .build()); + + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + mockMvc.perform(post("/api/parties/{partyId}/join-requests", womenParty.getId())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.GENDER_NOT_MATCH.getCode())); + } + + @Test + @DisplayName("409 - 이미 대기중인 가입 신청이 있는 상태에서 다시 신청하면 JOIN_REQUEST_ALREADY_EXISTS 에러를 반환한다") + void fail_createJoinRequest_alreadyExists() throws Exception { + // 가입하지 않은 멤버 생성 + Member applicant = memberRepository.save(MemberFixture.createMember("신청자2", Gender.MALE, Level.A, 5002L, LocalDate.of(1995, 1, 1))); + + // 기존 가입 신청 추가 + PartyJoinRequest joinRequest = PartyJoinRequest.create(applicant, party); + partyJoinRequestRepository.save(joinRequest); + + SecurityContextHelper.setAuthentication(applicant.getId(), applicant.getNickname()); + + mockMvc.perform(post("/api/parties/{partyId}/join-requests", party.getId())) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.JOIN_REQUEST_ALREADY_EXISTS.getCode())); + } + + @Test + @DisplayName("400 - 삭제된(비활성화된) 모임에 가입 신청하면 PARTY_IS_DELETED 에러를 반환한다") + void fail_createJoinRequest_partyDeleted() throws Exception { + // 파티 생성 후 삭제 + PartyAddr addr = partyAddrRepository.save(PartyFixture.createPartyAddr("서울", "금천")); + Party deletedParty = partyRepository.save(PartyFixture.createParty("삭제된 모임", manager.getId(), addr)); + deletedParty.delete(); + partyRepository.save(deletedParty); + + Member applicant = memberRepository.save(MemberFixture.createMember("신청자3", Gender.MALE, Level.A, 5003L)); + SecurityContextHelper.setAuthentication(applicant.getId(), applicant.getNickname()); + + mockMvc.perform(post("/api/parties/{partyId}/join-requests", deletedParty.getId())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_IS_DELETED.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 파티에 가입 신청하면 PARTY_NOT_FOUND 에러를 반환한다") + void fail_createJoinRequest_partyNotFound() throws Exception { + Member applicant = memberRepository.save(MemberFixture.createMember("신청자4", Gender.MALE, Level.A, 5004L)); + SecurityContextHelper.setAuthentication(applicant.getId(), applicant.getNickname()); + + mockMvc.perform(post("/api/parties/{partyId}/join-requests", 9999L)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("PATCH /api/parties/{partyId} - 모임 정보 수정") + class UpdateParty { + + @Test + @DisplayName("200 - 모임장이 유효한 데이터로 모임 정보를 정상적으로 수정한다") + void success_updateParty() throws Exception { + // given + PartyUpdateDTO.Request request = PartyUpdateDTO.Request.builder() + .activityDay(List.of("월", "수")) + .activityTime("오전") + .designatedCock("수정된 콕") + .joinPrice(2000) + .price(15000) + .content("수정된 내용입니다.") + .build(); + + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}", party.getId()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")); + + // 검증 + Party updatedParty = partyRepository.findById(party.getId()).orElseThrow(); + assertThat(updatedParty.getDesignatedCock()).isEqualTo("수정된 콕"); + assertThat(updatedParty.getJoinPrice()).isEqualTo(2000); + assertThat(updatedParty.getPrice()).isEqualTo(15000); + assertThat(updatedParty.getContent()).isEqualTo("수정된 내용입니다."); + } + + @Test + @DisplayName("400 - 필수 필드(activityDay, activityTime) 누락 시 에러를 반환한다") + void fail_updateParty_missingRequiredFields() throws Exception { + // given + PartyUpdateDTO.Request request = PartyUpdateDTO.Request.builder() + .activityDay(null) + .activityTime("") + .build(); + + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}", party.getId()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("COMMON400_VALIDATION")); + } + + @Test + @DisplayName("403 - 모임장이 아닌 일반 멤버가 수정을 시도하면 INSUFFICIENT_PERMISSION 에러를 반환한다") + void fail_updateParty_notOwner() throws Exception { + // given + PartyUpdateDTO.Request request = PartyUpdateDTO.Request.builder() + .activityDay(List.of("토", "일")) + .activityTime("오후") + .build(); + + // 일반 멤버로 세션 설정 + SecurityContextHelper.setAuthentication(normalMember.getId(), normalMember.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}", party.getId()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INSUFFICIENT_PERMISSION.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 파티 수정 시 PARTY_NOT_FOUND 에러를 반환한다") + void fail_updateParty_partyNotFound() throws Exception { + // given + PartyUpdateDTO.Request request = PartyUpdateDTO.Request.builder() + .activityDay(List.of("월")) + .activityTime("오전") + .build(); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}", 9999L) + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("PATCH /api/parties/{partyId}/status - 모임 삭제") + class DeleteParty { + + @Test + @DisplayName("200 - 모임장이 모임을 성공적으로 삭제(비활성화)한다") + void success_deleteParty() throws Exception { + // given + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/status", party.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")); + + // 검증 + Party deletedParty = partyRepository.findById(party.getId()).orElseThrow(); + assertThat(deletedParty.getStatus()).isEqualTo(PartyStatus.INACTIVE); + } + + @Test + @DisplayName("403 - 모임장이 아닌 멤버가 삭제를 시도하면 INSUFFICIENT_PERMISSION 예외를 반환한다") + void fail_deleteParty_notOwner() throws Exception { + // given + SecurityContextHelper.setAuthentication(normalMember.getId(), normalMember.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/status", party.getId())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INSUFFICIENT_PERMISSION.getCode())); + } + + @Test + @DisplayName("400 - 이미 삭제된 모임을 다시 삭제 시도하면 PARTY_IS_DELETED 예외를 반환한다") + void fail_deleteParty_partyDeleted() throws Exception { + // given + party.delete(); // 상태 INACTIVE 변경 + partyRepository.save(party); + + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/status", party.getId())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_IS_DELETED.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 파티 삭제 시 PARTY_NOT_FOUND 에러를 반환한다") + void fail_deleteParty_partyNotFound() throws Exception { + mockMvc.perform(patch("/api/parties/{partyId}/status", 9999L)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("DELETE /api/parties/{partyId}/members/{memberId} - 모임 멤버 삭제") + class RemoveMember { + + @Test + @DisplayName("200 - 모임장이 일반 멤버를 성공적으로 강퇴한다") + void success_removeMember() throws Exception { + // given + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(delete("/api/parties/{partyId}/members/{memberId}", party.getId(), normalMember.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")); + + // 검증 + boolean exists = memberPartyRepository.existsByPartyAndMember(party, normalMember); + assertThat(exists).isFalse(); + } + + @Test + @DisplayName("403 - 모임장이 아닌 멤버가 삭제를 시도하면 INSUFFICIENT_PERMISSION 에러를 반환한다") + void fail_removeMember_notOwner() throws Exception { + // given + Member someoneElse = memberRepository.save(MemberFixture.createMember("다른멤버", Gender.MALE, Level.B, 1010L)); + memberPartyRepository.save(MemberFixture.createMemberParty(party, someoneElse, Role.PARTY_MEMBER)); + SecurityContextHelper.setAuthentication(normalMember.getId(), normalMember.getNickname()); + + // when & then + mockMvc.perform(delete("/api/parties/{partyId}/members/{memberId}", party.getId(), someoneElse.getId())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INSUFFICIENT_PERMISSION.getCode())); + } + + @Test + @DisplayName("400 - 모임장이 자기 자신을 강퇴하려 할 경우 CANNOT_REMOVE_SELF 에러를 반환한다") + void fail_removeMember_selfAsManager() throws Exception { + // given + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(delete("/api/parties/{partyId}/members/{memberId}", party.getId(), manager.getId())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.CANNOT_REMOVE_SELF.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 파티의 멤버 삭제 시 PARTY_NOT_FOUND 에러를 반환한다") + void fail_removeMember_partyNotFound() throws Exception { + mockMvc.perform(delete("/api/parties/{partyId}/members/{memberId}", 9999L, normalMember.getId())) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 멤버를 강퇴하려 할 때 MEMBER_NOT_FOUND 에러를 반환한다") + void fail_removeMember_memberNotFound() throws Exception { + mockMvc.perform(delete("/api/parties/{partyId}/members/{memberId}", party.getId(), 9999L)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(MemberErrorCode.MEMBER_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("GET /api/parties/{partyId}/join-requests - 모임 가입 신청 조회") + class GetJoinRequests { + + @Test + @DisplayName("200 - 모임장이 가입 신청 목록을 정상적으로 조회한다") + void success_getJoinRequests() throws Exception { + // given + Member applicant = memberRepository.save(MemberFixture.createMember("가입희망자", Gender.FEMALE, Level.B, 1010L)); + + PartyJoinRequest joinRequest = PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.PENDING) + .build(); + partyJoinRequestRepository.save(joinRequest); + + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(get("/api/parties/{partyId}/join-requests", party.getId()) + .param("status", "PENDING") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content[0].userId").value(applicant.getId())); + } + + @Test + @DisplayName("200 - 모임장이 가입 승인된 멤버 목록(APPROVED)을 정상적으로 조회한다") + void success_getJoinRequests_approved() throws Exception { + // given + Member applicant = memberRepository.save(MemberFixture.createMember("승인된멤버", Gender.MALE, Level.C, 1015L)); + + PartyJoinRequest joinRequest = PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.APPROVED) + .build(); + partyJoinRequestRepository.save(joinRequest); + + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(get("/api/parties/{partyId}/join-requests", party.getId()) + .param("status", "APPROVED") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content[0].userId").value(applicant.getId())); + } + + @Test + @DisplayName("403 - 모임장이 아닌 사용자가 조회하면 INSUFFICIENT_PERMISSION 예외가 반환된다") + void fail_getJoinRequests_notOwner() throws Exception { + // given + SecurityContextHelper.setAuthentication(normalMember.getId(), normalMember.getNickname()); + + // when & then + mockMvc.perform(get("/api/parties/{partyId}/join-requests", party.getId()) + .param("status", "PENDING")) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INSUFFICIENT_PERMISSION.getCode())); + } + + @Test + @DisplayName("400 - 잘못된 상태값을 전달하면 INVALID_REQUEST_STATUS 예외가 반환된다") + void fail_getJoinRequests_invalidStatus() throws Exception { + // given + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(get("/api/parties/{partyId}/join-requests", party.getId()) + .param("status", "UNKNOWN_STATUS")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INVALID_REQUEST_STATUS.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 파티의 가입 신청 목록 조회 시 PARTY_NOT_FOUND 에러를 반환한다") + void fail_getJoinRequests_partyNotFound() throws Exception { + mockMvc.perform(get("/api/parties/{partyId}/join-requests", 9999L) + .param("status", "PENDING")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("GET /api/parties/{partyId}/members/suggestions - 신규 멤버 추천받기") + class GetRecommendedMembers { + + @Test + @DisplayName("200 - 추천 조건(지역/나이/급수)에 맞는 멤버가 추천 목록에 포함된다") + void success_getRecommendedMembers() throws Exception { + // given + // party의 추천 조건: addr1=서울특별시, minBirthYear=1990, maxBirthYear=2005 + // party에 남성 A급 레벨 추가 + party.addLevel(Gender.MALE, Level.A); + partyRepository.save(party); + + // 추천 조건을 모두 만족하는 멤버: 남성, A급, 생년 1995, 서울특별시 주소(isMain=true) + Member suggestedMember = memberRepository.save( + MemberFixture.createMember("추천회원", Gender.MALE, Level.A, 1080L, LocalDate.of(1995, 6, 1)) + ); + memberAddrRepository.save(MemberAddr.builder() + .member(suggestedMember) + .addr1("서울특별시") + .addr2("강남구") + .addr3("역삼동") + .streetAddr("테헤란로") + .latitude(37.5) + .longitude(127.0) + .isMain(true) + .build()); + + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(get("/api/parties/{partyId}/members/suggestions", party.getId()) + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content[0].userId").value(suggestedMember.getId())); + } + + @Test + @DisplayName("404 - 존재하지 않는 모임의 추천 멤버를 조회하면 PARTY_NOT_FOUND 예외 발생") + void fail_getRecommendedMembers_partyNotFound() throws Exception { + // given + Long invalidPartyId = 9999L; + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(get("/api/parties/{partyId}/members/suggestions", invalidPartyId)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("POST /api/parties/{partyId}/invitations - 신규 멤버 초대 보내기") + class CreateInvitation { + + @Test + @DisplayName("200 - 모임장이 새로운 멤버를 초대하고 invitationId를 반환한다") + void success_createInvitation() throws Exception { + // given + Member newMember = memberRepository.save(MemberFixture.createMember("새멤버", Gender.FEMALE, Level.B, 1090L)); + PartyInviteCreateDTO.Request request = new PartyInviteCreateDTO.Request(newMember.getId()); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(post("/api/parties/{partyId}/invitations", party.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON201")) + .andExpect(jsonPath("$.data.invitationId").exists()); + } + + @Test + @DisplayName("403 - 모임장이 아닌 사용자가 초대하면 INSUFFICIENT_PERMISSION 발생") + void fail_createInvitation_notOwner() throws Exception { + // given + Member newMember = memberRepository.save(MemberFixture.createMember("새멤버", Gender.FEMALE, Level.B, 1091L)); + PartyInviteCreateDTO.Request request = new PartyInviteCreateDTO.Request(newMember.getId()); + SecurityContextHelper.setAuthentication(normalMember.getId(), normalMember.getNickname()); + + // when & then + mockMvc.perform(post("/api/parties/{partyId}/invitations", party.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INSUFFICIENT_PERMISSION.getCode())); + } + + @Test + @DisplayName("409 - 이미 모임 멤버인 사람을 초대하면 ALREADY_MEMBER 발생") + void fail_createInvitation_alreadyMember() throws Exception { + // given - normalMember는 setUp()에서 이미 모임 멤버로 추가된 상태 + PartyInviteCreateDTO.Request request = new PartyInviteCreateDTO.Request(normalMember.getId()); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(post("/api/parties/{partyId}/invitations", party.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.ALREADY_MEMBER.getCode())); + } + + @Test + @DisplayName("409 - 이미 대기 중인 초대가 있는 멤버를 중복 초대하면 INVITATION_ALREADY_EXISTS 발생") + void fail_createInvitation_duplicateInvitation() throws Exception { + // given + Member newMember = memberRepository.save(MemberFixture.createMember("새멤버", Gender.FEMALE, Level.B, 1092L)); + partyInvitationRepository.save(PartyInvitation.create(party, manager, newMember)); + + PartyInviteCreateDTO.Request request = new PartyInviteCreateDTO.Request(newMember.getId()); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(post("/api/parties/{partyId}/invitations", party.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INVITATION_ALREADY_EXISTS.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 파티에서 멤버 초대 시 PARTY_NOT_FOUND 에러를 반환한다") + void fail_createInvitation_partyNotFound() throws Exception { + PartyInviteCreateDTO.Request request = new PartyInviteCreateDTO.Request(normalMember.getId()); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + mockMvc.perform(post("/api/parties/{partyId}/invitations", 9999L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 회원을 모임에 초대하면 MEMBER_NOT_FOUND 에러를 반환한다") + void fail_createInvitation_memberNotFound() throws Exception { + PartyInviteCreateDTO.Request request = new PartyInviteCreateDTO.Request(9999L); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + mockMvc.perform(post("/api/parties/{partyId}/invitations", party.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(MemberErrorCode.MEMBER_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("PATCH /api/parties/invitations/{invitationId} - 모임 초대 처리") + class ActionInvitation { + + @Test + @DisplayName("200 - 초대받은 멤버가 승인하면 모임 멤버로 추가된다") + void success_actionInvitation_approve() throws Exception { + // given + Member invitee = memberRepository.save(MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, 1100L)); + + PartyInvitation invitation = partyInvitationRepository.save( + PartyInvitation.create(party, manager, invitee) + ); + + PartyInviteActionDTO.Request request = new PartyInviteActionDTO.Request(RequestAction.APPROVE); + SecurityContextHelper.setAuthentication(invitee.getId(), invitee.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/invitations/{invitationId}", invitation.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")); + + // 검증 + PartyInvitation updated = partyInvitationRepository.findById(invitation.getId()).orElseThrow(); + assertThat(updated.getStatus()).isEqualTo(RequestStatus.APPROVED); + assertThat(memberPartyRepository.existsByPartyAndMember(party, invitee)).isTrue(); + } + + @Test + @DisplayName("200 - 초대받은 멤버가 거절하면 상태가 REJECTED로 바뀌고 멤버로 추가되지 않는다") + void success_actionInvitation_reject() throws Exception { + // given + Member invitee = memberRepository.save(MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, 1101L)); + + PartyInvitation invitation = partyInvitationRepository.save( + PartyInvitation.create(party, manager, invitee) + ); + + PartyInviteActionDTO.Request request = new PartyInviteActionDTO.Request(RequestAction.REJECT); + SecurityContextHelper.setAuthentication(invitee.getId(), invitee.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/invitations/{invitationId}", invitation.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")); + + // 검증 + PartyInvitation updated = partyInvitationRepository.findById(invitation.getId()).orElseThrow(); + assertThat(updated.getStatus()).isEqualTo(RequestStatus.REJECTED); + assertThat(memberPartyRepository.existsByPartyAndMember(party, invitee)).isFalse(); + } + + @Test + @DisplayName("403 - 초대받은 사람이 아닌 제3자가 처리하면 NOT_YOUR_INVITATION 발생") + void fail_actionInvitation_notYourInvitation() throws Exception { + // given + Member invitee = memberRepository.save(MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, 1102L)); + + PartyInvitation invitation = partyInvitationRepository.save( + PartyInvitation.create(party, manager, invitee) + ); + + PartyInviteActionDTO.Request request = new PartyInviteActionDTO.Request(RequestAction.APPROVE); + // normalMember는 초대받은 사람이 아님 + SecurityContextHelper.setAuthentication(normalMember.getId(), normalMember.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/invitations/{invitationId}", invitation.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.NOT_YOUR_INVITATION.getCode())); + } + + @Test + @DisplayName("409 - 이미 처리된 초대를 다시 처리하면 INVITATION_ALREADY_ACTIONS 발생") + void fail_actionInvitation_alreadyActions() throws Exception { + // given + Member invitee = memberRepository.save(MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, 1103L)); + + // 이미 APPROVED 처리된 초대 + PartyInvitation invitation = partyInvitationRepository.save( + PartyInvitation.create(party, manager, invitee) + ); + invitation.updateStatus(RequestStatus.APPROVED); + partyInvitationRepository.save(invitation); + + PartyInviteActionDTO.Request request = new PartyInviteActionDTO.Request(RequestAction.REJECT); + SecurityContextHelper.setAuthentication(invitee.getId(), invitee.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/invitations/{invitationId}", invitation.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INVITATION_ALREADY_ACTIONS.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 회원이 초대를 처리하려 할 때 MEMBER_NOT_FOUND 에러를 반환한다") + void fail_actionInvitation_memberNotFound() throws Exception { + // given + Member invitee = memberRepository.save(MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, 1104L)); + PartyInvitation invitation = partyInvitationRepository.save( + PartyInvitation.create(party, manager, invitee) + ); + + PartyInviteActionDTO.Request request = new PartyInviteActionDTO.Request(RequestAction.APPROVE); + // 인증 정보에는 유효하지만 DB에는 없는 ID 설정 + SecurityContextHelper.setAuthentication(9999L, "ghost"); + + // when & then + mockMvc.perform(patch("/api/parties/invitations/{invitationId}", invitation.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(MemberErrorCode.MEMBER_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("PATCH /api/parties/{partyId}/join-requests/{requestId} - 모임 가입 신청 처리") + class ActionJoinRequest { + + @Test + @DisplayName("200 - 모임장이 가입 신청을 성공적으로 승인한다") + void success_actionJoinRequest_approve() throws Exception { + // given + Member applicant = memberRepository.save(MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 1020L)); + + PartyJoinRequest joinRequest = partyJoinRequestRepository.save(PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.PENDING) + .build()); + + PartyJoinActionDTO.Request actionRequest = new PartyJoinActionDTO.Request(RequestAction.APPROVE); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/join-requests/{requestId}", party.getId(), joinRequest.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(actionRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")); + + // 검증 + PartyJoinRequest updatedRequest = partyJoinRequestRepository.findById(joinRequest.getId()).orElseThrow(); + assertThat(updatedRequest.getStatus()).isEqualTo(RequestStatus.APPROVED); + boolean isMember = memberPartyRepository.existsByPartyAndMember(party, applicant); + assertThat(isMember).isTrue(); + } + + @Test + @DisplayName("200 - 모임장이 가입 신청을 성공적으로 거절한다") + void success_actionJoinRequest_reject() throws Exception { + // given + Member applicant = memberRepository.save(MemberFixture.createMember("탈락자", Gender.FEMALE, Level.B, 1030L)); + + PartyJoinRequest joinRequest = partyJoinRequestRepository.save(PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.PENDING) + .build()); + + PartyJoinActionDTO.Request actionRequest = new PartyJoinActionDTO.Request(RequestAction.REJECT); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/join-requests/{requestId}", party.getId(), joinRequest.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(actionRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")); + + // 검증 + PartyJoinRequest updatedRequest = partyJoinRequestRepository.findById(joinRequest.getId()).orElseThrow(); + assertThat(updatedRequest.getStatus()).isEqualTo(RequestStatus.REJECTED); + boolean isMember = memberPartyRepository.existsByPartyAndMember(party, applicant); + assertThat(isMember).isFalse(); + } + + @Test + @DisplayName("403 - 모임장이 아닌 사용자가 가입 신청을 처리하려 하면 INSUFFICIENT_PERMISSION 발생") + void fail_actionJoinRequest_notOwner() throws Exception { + // given + Member applicant = memberRepository.save(MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 1040L)); + + PartyJoinRequest joinRequest = partyJoinRequestRepository.save(PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.PENDING) + .build()); + + PartyJoinActionDTO.Request actionRequest = new PartyJoinActionDTO.Request(RequestAction.APPROVE); + SecurityContextHelper.setAuthentication(normalMember.getId(), normalMember.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/join-requests/{requestId}", party.getId(), joinRequest.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(actionRequest))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INSUFFICIENT_PERMISSION.getCode())); + } + + @Test + @DisplayName("409 - 이미 처리된 가입 신청을 다시 처리하려 할 때 JOIN_REQUEST_ALREADY_ACTIONS 상태 반환") + void fail_actionJoinRequest_alreadyHandled() throws Exception { + // given + Member applicant = memberRepository.save(MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 1050L)); + + PartyJoinRequest joinRequest = partyJoinRequestRepository.save(PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.APPROVED) + .build()); + + PartyJoinActionDTO.Request actionRequest = new PartyJoinActionDTO.Request(RequestAction.APPROVE); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/join-requests/{requestId}", party.getId(), joinRequest.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(actionRequest))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.JOIN_REQUEST_ALREADY_ACTIONS.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 파티의 가입 신청 요청 처리 시 PARTY_NOT_FOUND 에러를 반환한다") + void fail_actionJoinRequest_partyNotFound() throws Exception { + // given + Member applicant = memberRepository.save(MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 1060L)); + PartyJoinRequest joinRequest = partyJoinRequestRepository.save(PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.PENDING) + .build()); + + PartyJoinActionDTO.Request actionRequest = new PartyJoinActionDTO.Request(RequestAction.APPROVE); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/join-requests/{requestId}", 9999L, joinRequest.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(actionRequest))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("POST /api/parties - 모임 생성") + class CreateParty { + + @Test + @DisplayName("200 - 모임을 성공적으로 생성하고 DB 저장 상태를 확인한다") + void success_createParty() throws Exception { + // given + PartyCreateDTO.Request request = PartyCreateDTO.Request.builder() + .partyName("새로운 통합 모임") + .partyType("혼복") + .minBirthYear(1990) + .maxBirthYear(2000) + .activityTime("오전") + .addr1("서울특별시") + .addr2("강남구") + .activityDay(List.of("월", "수")) + .price(10000) + .joinPrice(5000) + .designatedCock("통합테스트콕") + .maleLevel(List.of("A조")) + .femaleLevel(List.of("B조")) + .build(); + + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(post("/api/parties") + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON201")) + .andExpect(jsonPath("$.data.partyId").exists()); + + // 검증 + List parties = partyRepository.findAll(); + Party createdParty = parties.stream() + .filter(p -> p.getPartyName().equals("새로운 통합 모임")) + .findFirst() + .orElseThrow(); + + assertThat(createdParty.getOwnerId()).isEqualTo(manager.getId()); + assertThat(createdParty.getDesignatedCock()).isEqualTo("통합테스트콕"); + } + + @Test + @DisplayName("400 - 본인의 나이가 모임 조건에 맞지 않을 때 에러를 반환한다") + void fail_createParty_invalidAgeRange() throws Exception { + // given + // manager는 1995년생. 모임 조건을 2000~2010으로 설정. + PartyCreateDTO.Request request = PartyCreateDTO.Request.builder() + .partyName("청년 모임") + .partyType("혼복") + .minBirthYear(2000) + .maxBirthYear(2010) + .activityTime("오후") + .activityDay(List.of("금")) + .addr1("서울특별시") + .addr2("강남구") + .price(10000) + .joinPrice(0) + .femaleLevel(List.of("A조")) + .maleLevel(List.of("A조")) + .designatedCock("청년콕") + .build(); + + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(post("/api/parties") + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_IS_DELETED.getCode())) - .andExpect(jsonPath("$.message").value(PartyErrorCode.PARTY_IS_DELETED.getMessage())); + .andExpect(jsonPath("$.code").value(PartyErrorCode.AGE_NOT_MATCH.getCode())); + } + + @Test + @DisplayName("400 - 혼복 모임에서 남자 급수 정보가 누락되었을 때 에러를 반환한다") + void fail_createParty_missingMaleLevelInMixDoubles() throws Exception { + // given + PartyCreateDTO.Request request = PartyCreateDTO.Request.builder() + .partyName("혼복 모임") + .partyType("혼복") + .minBirthYear(1990) + .maxBirthYear(2005) + .activityTime("오전") + .activityDay(List.of("토")) + .addr1("서울특별시") + .addr2("강남구") + .price(10000) + .joinPrice(0) + .designatedCock("혼복콕") + .maleLevel(null) + .femaleLevel(List.of("A조")) + .build(); + + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(post("/api/parties") + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.MALE_LEVEL_REQUIRED.getCode())); + } + } + + @Nested + @DisplayName("PATCH /api/parties/{partyId}/members/{memberId}/role - 멤버 역할(부모임장) 설정") + class UpdateMemberRole { + + @Test + @DisplayName("200 - 모임장이 일반 멤버를 부모임장으로 성공적으로 임명한다") + void success_updateMemberRole() throws Exception { + // given + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_SUBMANAGER); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/members/{memberId}/role", party.getId(), normalMember.getId()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")); + + // 검증 + MemberParty targetMemberParty = memberPartyRepository.findByPartyAndMember(party, normalMember).orElseThrow(); + assertThat(targetMemberParty.getRole()).isEqualTo(Role.PARTY_SUBMANAGER); + } + + @Test + @DisplayName("403 - 모임장이 아닌 멤버가 역할 수정을 시도하면 INSUFFICIENT_PERMISSION 예외를 반환한다") + void fail_updateMemberRole_notOwner() throws Exception { + // given + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_SUBMANAGER); + // 일반 멤버가 권한 변경 시도 + SecurityContextHelper.setAuthentication(normalMember.getId(), normalMember.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/members/{memberId}/role", party.getId(), normalMember.getId()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INSUFFICIENT_PERMISSION.getCode())); + } + + @Test + @DisplayName("403 - 대상자가 모임장인 경우 권한 변경은 실패하며 CANNOT_ASSIGN_TO_OWNER 예외를 반환한다") + void fail_updateMemberRole_targetIsOwner() throws Exception { + // given + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_MEMBER); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(patch("/api/parties/{partyId}/members/{memberId}/role", party.getId(), manager.getId()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.CANNOT_ASSIGN_TO_OWNER.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 파티의 멤버 역할 수정 시 PARTY_NOT_FOUND 에러를 반환한다") + void fail_updateMemberRole_partyNotFound() throws Exception { + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_SUBMANAGER); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + mockMvc.perform(patch("/api/parties/{partyId}/members/{memberId}/role", 9999L, normalMember.getId()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 멤버의 역할 수정 시 MEMBER_NOT_FOUND 에러를 반환한다") + void fail_updateMemberRole_memberNotFound() throws Exception { + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_SUBMANAGER); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + mockMvc.perform(patch("/api/parties/{partyId}/members/{memberId}/role", party.getId(), 9999L) + .contentType("application/json") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(MemberErrorCode.MEMBER_NOT_FOUND.getCode())); + } + } + + @Nested + @DisplayName("POST /api/parties/{partyId}/keywords - 키워드 추가") + class AddKeyword { + + @Test + @DisplayName("200 - 모임장이 유효한 키워드를 정상적으로 추가한다") + void success_addKeyword() throws Exception { + // given + PartyKeywordDTO.Request request = new PartyKeywordDTO.Request( + List.of("친목", "가입비 무료") + ); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(post("/api/parties/{partyId}/keywords", party.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("COMMON200")); + + // 검증 - DB에 키워드가 실제로 저장됐는지 확인 + Party updatedParty = partyRepository.findById(party.getId()).orElseThrow(); + assertThat(updatedParty.getKeywords()).hasSize(2); + } + + @Test + @DisplayName("403 - 모임장이 아닌 사용자가 키워드를 추가하면 INSUFFICIENT_PERMISSION 발생") + void fail_addKeyword_notOwner() throws Exception { + // given + PartyKeywordDTO.Request request = new PartyKeywordDTO.Request(List.of("친목")); + SecurityContextHelper.setAuthentication(normalMember.getId(), normalMember.getNickname()); + + // when & then + mockMvc.perform(post("/api/parties/{partyId}/keywords", party.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INSUFFICIENT_PERMISSION.getCode())); + } + + @Test + @DisplayName("400 - 유효하지 않은 키워드 문자열을 전달하면 INVALID_KEYWORD 발생") + void fail_addKeyword_invalidKeyword() throws Exception { + // given + PartyKeywordDTO.Request request = new PartyKeywordDTO.Request(List.of("존재하지않는키워드")); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + // when & then + mockMvc.perform(post("/api/parties/{partyId}/keywords", party.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.INVALID_KEYWORD.getCode())); + } + + @Test + @DisplayName("404 - 존재하지 않는 파티에 키워드 추가 시 PARTY_NOT_FOUND 에러를 반환한다") + void fail_addKeyword_partyNotFound() throws Exception { + PartyKeywordDTO.Request request = new PartyKeywordDTO.Request(List.of("새키워드")); + SecurityContextHelper.setAuthentication(manager.getId(), manager.getNickname()); + + mockMvc.perform(post("/api/parties/{partyId}/keywords", 9999L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(PartyErrorCode.PARTY_NOT_FOUND.getCode())); } } } diff --git a/src/test/java/umc/cockple/demo/domain/party/service/PartyCommandServiceTest.java b/src/test/java/umc/cockple/demo/domain/party/service/PartyCommandServiceTest.java new file mode 100644 index 000000000..e90511e4c --- /dev/null +++ b/src/test/java/umc/cockple/demo/domain/party/service/PartyCommandServiceTest.java @@ -0,0 +1,1852 @@ +package umc.cockple.demo.domain.party.service; + +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.context.ApplicationEventPublisher; +import org.springframework.test.util.ReflectionTestUtils; +import umc.cockple.demo.domain.chat.service.ChatRoomService; +import umc.cockple.demo.domain.file.service.FileService; +import umc.cockple.demo.domain.member.domain.Member; +import umc.cockple.demo.domain.member.domain.MemberParty; +import umc.cockple.demo.domain.member.exception.MemberErrorCode; +import umc.cockple.demo.domain.member.exception.MemberException; +import umc.cockple.demo.domain.member.repository.MemberPartyRepository; +import umc.cockple.demo.domain.member.repository.MemberRepository; +import umc.cockple.demo.domain.notification.service.NotificationCommandService; +import umc.cockple.demo.domain.party.converter.PartyConverter; +import umc.cockple.demo.domain.party.domain.Party; +import umc.cockple.demo.domain.party.domain.PartyAddr; +import umc.cockple.demo.domain.party.domain.PartyInvitation; +import umc.cockple.demo.domain.party.domain.PartyJoinRequest; +import umc.cockple.demo.domain.party.dto.*; +import umc.cockple.demo.domain.party.enums.ParticipationType; +import umc.cockple.demo.domain.party.enums.PartyStatus; +import umc.cockple.demo.domain.party.enums.RequestAction; +import umc.cockple.demo.domain.party.enums.RequestStatus; +import umc.cockple.demo.domain.party.events.PartyMemberJoinedEvent; +import umc.cockple.demo.domain.party.exception.PartyErrorCode; +import umc.cockple.demo.domain.party.exception.PartyException; +import umc.cockple.demo.domain.party.repository.PartyAddrRepository; +import umc.cockple.demo.domain.party.repository.PartyInvitationRepository; +import umc.cockple.demo.domain.party.repository.PartyJoinRequestRepository; +import umc.cockple.demo.domain.party.repository.PartyRepository; +import umc.cockple.demo.global.enums.Gender; +import umc.cockple.demo.global.enums.Level; +import umc.cockple.demo.global.enums.Role; +import umc.cockple.demo.support.fixture.MemberFixture; +import umc.cockple.demo.support.fixture.PartyFixture; + +import java.time.LocalDate; +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.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PartyCommandServiceTest { + + @InjectMocks + private PartyCommandServiceImpl partyCommandService; + + @Mock + private PartyRepository partyRepository; + @Mock + private MemberRepository memberRepository; + @Mock + private NotificationCommandService notificationCommandService; + @Mock + private PartyAddrRepository partyAddrRepository; + @Mock + private MemberPartyRepository memberPartyRepository; + @Mock + private ChatRoomService chatRoomService; + @Mock + private ApplicationEventPublisher applicationEventPublisher; + @Mock + private PartyJoinRequestRepository partyJoinRequestRepository; + @Mock + private PartyInvitationRepository partyInvitationRepository; + @Mock + private FileService fileService; + + private PartyConverter partyConverter; + + @BeforeEach + void setUp() { + partyConverter = new PartyConverter(fileService); + ReflectionTestUtils.setField(partyCommandService, "partyConverter", partyConverter); + } + + @Nested + @DisplayName("leaveParty") + class LeaveParty { + + @Test + @DisplayName("성공 - 일반 멤버가 모임을 탈퇴한다") + void success_leaveParty() { + // given + Long partyId = 1L; + Long memberId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, 1L); + ReflectionTestUtils.setField(owner, "id", 1L); + Party party = PartyFixture.createParty("탈퇴 테스트 모임", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member member = MemberFixture.createMember("일반멤버", Gender.MALE, Level.A, 10L); + ReflectionTestUtils.setField(member, "id", memberId); + + MemberParty memberParty = MemberFixture.createMemberParty(party, member, Role.PARTY_MEMBER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(memberPartyRepository.findByPartyAndMember(party, member)).willReturn(Optional.of(memberParty)); + + // when + partyCommandService.leaveParty(partyId, memberId); + + // then + verify(memberPartyRepository).delete(memberParty); + verify(chatRoomService).leavePartyChatRoom(partyId, memberId); + verify(applicationEventPublisher).publishEvent(any(PartyMemberJoinedEvent.class)); + } + + @Test + @DisplayName("실패 - 존재하지 않는 모임인 경우 PARTY_NOT_FOUND 예외가 발생한다") + void fail_leaveParty_partyNotFound() { + // given + given(partyRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.leaveParty(999L, 1L)) + .isInstanceOf(PartyException.class) + .satisfies( + e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); + } + + @Test + @DisplayName("실패 - 삭제된 모임인 경우 PARTY_IS_DELETED 예외가 발생한다") + void fail_leaveParty_partyDeleted() { + // given + Long partyId = 1L; + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("삭제된 모임", 1L, addr); + party.delete(); + + Member member = MemberFixture.createMember("일반멤버", Gender.MALE, Level.A, 1L); + ReflectionTestUtils.setField(member, "id", 1L); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(1L)).willReturn(Optional.of(member)); + + // when & then + assertThatThrownBy(() -> partyCommandService.leaveParty(partyId, 1L)) + .isInstanceOf(PartyException.class) + .satisfies( + e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.PARTY_IS_DELETED)); + } + + @Test + @DisplayName("실패 - 모임장이 탈퇴하려 할 경우 INVALID_ACTION_FOR_OWNER 예외가 발생한다") + void fail_leaveParty_isOwner() { + // given + Long partyId = 1L; + Long ownerId = 1L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, 1L); + ReflectionTestUtils.setField(owner, "id", ownerId); + Party party = PartyFixture.createParty("탈퇴 테스트 모임", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(ownerId)).willReturn(Optional.of(owner)); + + // when & then + assertThatThrownBy(() -> partyCommandService.leaveParty(partyId, ownerId)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()) + .isEqualTo(PartyErrorCode.INVALID_ACTION_FOR_OWNER)); + } + + @Test + @DisplayName("실패 - 부모임장이 탈퇴하려 할 경우 INVALID_ACTION_FOR_SUBOWNER 예외가 발생한다") + void fail_leaveParty_isSubOwner() { + // given + Long partyId = 1L; + Long subManagerId = 2L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, 1L); + ReflectionTestUtils.setField(owner, "id", 1L); + Party party = PartyFixture.createParty("탈퇴 테스트 모임", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member subManager = MemberFixture.createMember("부모임장", Gender.MALE, Level.A, 2L); + ReflectionTestUtils.setField(subManager, "id", subManagerId); + + MemberParty subManagerParty = MemberFixture.createMemberParty(party, subManager, Role.PARTY_SUBMANAGER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(subManagerId)).willReturn(Optional.of(subManager)); + given(memberPartyRepository.findByPartyIdAndRole(partyId, Role.PARTY_SUBMANAGER)) + .willReturn(Optional.of(subManagerParty)); + + // when & then + assertThatThrownBy(() -> partyCommandService.leaveParty(partyId, subManagerId)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()) + .isEqualTo(PartyErrorCode.INVALID_ACTION_FOR_SUBOWNER)); + } + + @Test + @DisplayName("실패 - 모임 멤버가 아닌 경우 NOT_MEMBER 예외가 발생한다") + void fail_leaveParty_notMember() { + // given + Long partyId = 1L; + Long memberId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("탈퇴 테스트 모임", 1L, addr); + ReflectionTestUtils.setField(party, "id", partyId); + Member member = MemberFixture.createMember("외부인", Gender.MALE, Level.A, 10L); + ReflectionTestUtils.setField(member, "id", memberId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(memberPartyRepository.findByPartyAndMember(party, member)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.leaveParty(partyId, memberId)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.NOT_MEMBER)); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원인 경우 MEMBER_NOT_FOUND 예외가 발생한다") + void fail_leaveParty_memberNotFound() { + // given + Long partyId = 1L; + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("모임명", 1L, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(10L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.leaveParty(partyId, 10L)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat(((MemberException) e).getCode()).isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("createJoinRequest") + class CreateJoinRequest { + + @Test + @DisplayName("성공 - 사용자가 특정 모임에 가입 신청을 성공적으로 완료한다") + void success_createJoinRequest() { + // given + Long partyId = 1L; + Long memberId = 1L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("가입 신청 모임", 10L, addr); + Member member = MemberFixture.createMember("지원자", Gender.MALE, Level.B, 1L, LocalDate.of(1995, 1, 1)); + ReflectionTestUtils.setField(member, "id", memberId); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberPartyRepository.existsByPartyAndMember(party, member)).willReturn(false); + given(partyJoinRequestRepository.existsByPartyAndMemberAndStatus(party, member, RequestStatus.PENDING)).willReturn(false); + given(partyJoinRequestRepository.save(any(PartyJoinRequest.class))).willAnswer(invocation -> invocation.getArgument(0)); + + // when + PartyJoinCreateDTO.Response response = partyCommandService.createJoinRequest(partyId, memberId); + + // then + assertThat(response).isNotNull(); + verify(partyJoinRequestRepository).save(any(PartyJoinRequest.class)); + } + + @Test + @DisplayName("실패 - 이미 해당 모임의 멤버인 경우 ALREADY_MEMBER 예외가 발생한다") + void fail_createJoinRequest_alreadyMember() { + // given + Long partyId = 1L; + Long memberId = 1L; + + Party party = PartyFixture.createParty("가입 신청 모임", 10L, null); + Member member = MemberFixture.createMember("지원자", Gender.MALE, Level.B, 1L); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberPartyRepository.existsByPartyAndMember(party, member)).willReturn(true); + + // when & then + assertThatThrownBy(() -> partyCommandService.createJoinRequest(partyId, memberId)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.ALREADY_MEMBER)); + } + + @Test + @DisplayName("실패 - 대기 중인 가입 신청이 이미 존재하는 경우 JOIN_REQUEST_ALREADY_EXISTS 예외가 발생한다") + void fail_createJoinRequest_alreadyRequested() { + // given + Long partyId = 1L; + Long memberId = 1L; + + Party party = PartyFixture.createParty("가입 신청 모임", 10L, null); + Member member = MemberFixture.createMember("지원자", Gender.MALE, Level.B, 1L); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberPartyRepository.existsByPartyAndMember(party, member)).willReturn(false); + given(partyJoinRequestRepository.existsByPartyAndMemberAndStatus(party, member, RequestStatus.PENDING)).willReturn(true); + + // when & then + assertThatThrownBy(() -> partyCommandService.createJoinRequest(partyId, memberId)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.JOIN_REQUEST_ALREADY_EXISTS)); + } + + @Test + @DisplayName("실패 - 모임 유형에 맞지 않는 성별인 경우 GENDER_NOT_MATCH 예외가 발생한다") + void fail_createJoinRequest_genderMismatch() { + // given + Long partyId = 1L; + Long memberId = 1L; + + // 여복 모임 생성 + Party party = Party.builder() + .partyName("여복 모임") + .partyType(ParticipationType.WOMEN_DOUBLES) + .status(PartyStatus.ACTIVE) + .ownerId(10L) + .build(); + Member member = MemberFixture.createMember("남자지원자", Gender.MALE, Level.B, 1L); // 남성 지원 + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberPartyRepository.existsByPartyAndMember(party, member)).willReturn(false); + given(partyJoinRequestRepository.existsByPartyAndMemberAndStatus(party, member, RequestStatus.PENDING)).willReturn(false); + + // when & then + assertThatThrownBy(() -> partyCommandService.createJoinRequest(partyId, memberId)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.GENDER_NOT_MATCH)); + } + + @Test + @DisplayName("실패 - 모임의 나이 조건에 맞지 않는 경우 AGE_NOT_MATCH 예외가 발생한다") + void fail_createJoinRequest_ageMismatch() { + // given + Long partyId = 1L; + Long memberId = 1L; + + // 1990~2000년생 모임 + Party party = Party.builder() + .partyName("나이 제한 모임") + .minBirthYear(1990) + .maxBirthYear(2000) + .status(PartyStatus.ACTIVE) + .ownerId(10L) + .build(); + // 1980년생 지원자 (범위 밖) + Member member = MemberFixture.createMember("나이많은지원자", Gender.MALE, Level.B, 1L, LocalDate.of(1980, 1, 1)); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberPartyRepository.existsByPartyAndMember(party, member)).willReturn(false); + given(partyJoinRequestRepository.existsByPartyAndMemberAndStatus(party, member, RequestStatus.PENDING)).willReturn(false); + + // when & then + assertThatThrownBy(() -> partyCommandService.createJoinRequest(partyId, memberId)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.AGE_NOT_MATCH)); + } + + @Test + @DisplayName("실패 - 존재하지 않는 파티인 경우 PARTY_NOT_FOUND 예외 발생") + void fail_createJoinRequest_partyNotFound() { + // given + Member member = MemberFixture.createMember("사용자", Gender.MALE, Level.B, 1L); + ReflectionTestUtils.setField(member, "id", 1L); + given(memberRepository.findById(1L)).willReturn(Optional.of(member)); + given(partyRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.createJoinRequest(999L, 1L)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원인 경우 MEMBER_NOT_FOUND 예외 발생") + void fail_createJoinRequest_memberNotFound() { + // given + given(memberRepository.findById(1L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.createJoinRequest(1L, 1L)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat(((MemberException) e).getCode()).isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("createParty - 모임 생성") + class CreateParty { + + @Test + @DisplayName("성공 - 올바른 데이터 입력 시 모임이 생성되고 채팅방이 개설된다") + void success_createParty() { + // given + Long memberId = 1L; + PartyCreateDTO.Request request = PartyCreateDTO.Request.builder() + .partyName("테스트 모임") + .partyType("혼복") + .minBirthYear(1990) + .maxBirthYear(2000) + .activityTime("오전") + .addr1("서울") + .addr2("강남") + .activityDay(List.of("월", "수")) + .price(10000) + .joinPrice(5000) + .designatedCock("테스트콕") + .maleLevel(List.of("A조")) + .femaleLevel(List.of("B조")) + .build(); + + Member owner = Member.builder() + .id(memberId) + .gender(Gender.MALE) + .level(Level.A) + .birth(LocalDate.of(1995, 1, 1)) + .build(); + + PartyAddr partyAddr = PartyAddr.builder().id(1L).build(); + Party savedParty = Party.builder().id(1L).partyName("테스트 모임").build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(owner)); + given(partyAddrRepository.findByAddr1AndAddr2(anyString(), anyString())).willReturn(Optional.of(partyAddr)); + given(partyRepository.save(any(Party.class))).willReturn(savedParty); + + // when + PartyCreateDTO.Response response = partyCommandService.createParty(memberId, request); + + // then + assertThat(response).isNotNull(); + assertThat(response.partyId()).isEqualTo(1L); + verify(partyRepository, times(1)).save(any(Party.class)); + verify(chatRoomService, times(1)).createPartyChatRoom(any(Party.class), eq(owner)); + } + + @Test + @DisplayName("실패 - 혼복 모임 생성 시 남자 급수 정보가 누락되면 MALE_LEVEL_REQUIRED 예외가 발생한다") + void fail_createParty_mixDoubles_maleLevelMissing() { + // given + Long memberId = 1L; + PartyCreateDTO.Request request = PartyCreateDTO.Request.builder() + .partyName("혼복 모임") + .partyType("혼복") + .minBirthYear(1990) + .maxBirthYear(2000) + .activityTime("오전") + .activityDay(List.of("월")) + .femaleLevel(List.of("A조")) + .maleLevel(null) // 누락 + .build(); + + Member owner = Member.builder() + .id(memberId) + .gender(Gender.MALE) + .birth(LocalDate.of(1995, 1, 1)) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(owner)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.createParty(memberId, request)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.MALE_LEVEL_REQUIRED); + } + + @Test + @DisplayName("실패 - 여복 모임 생성 시 남자 급수 정보가 포함되면 MALE_LEVEL_NOT_NEEDED 예외가 발생한다") + void fail_createParty_womenDoubles_maleLevelProvided() { + // given + Long memberId = 1L; + PartyCreateDTO.Request request = PartyCreateDTO.Request.builder() + .partyName("여복 모임") + .partyType("여복") + .minBirthYear(1990) + .maxBirthYear(2010) + .activityTime("오전") + .activityDay(List.of("토")) + .femaleLevel(List.of("A조")) + .maleLevel(List.of("A조")) // 포함됨 + .build(); + + Member owner = Member.builder() + .id(memberId) + .gender(Gender.FEMALE) + .birth(LocalDate.of(2000, 1, 1)) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(owner)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.createParty(memberId, request)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.MALE_LEVEL_NOT_NEEDED); + } + + @Test + @DisplayName("실패 - 모임 유형의 성별 조건과 생성자의 성별이 맞지 않으면 GENDER_NOT_MATCH 예외가 발생한다") + void fail_createParty_genderMismatch() { + // given + Long memberId = 1L; + PartyCreateDTO.Request request = PartyCreateDTO.Request.builder() + .partyName("여복 모임") + .partyType("여복") + .minBirthYear(1990) + .maxBirthYear(2010) + .activityTime("오전") + .activityDay(List.of("일")) + .femaleLevel(List.of("A조")) + .build(); + + Member maleOwner = Member.builder() + .id(memberId) + .gender(Gender.MALE) // 남성이 여복 모임 생성 시도 + .birth(LocalDate.of(2000, 1, 1)) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(maleOwner)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.createParty(memberId, request)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.GENDER_NOT_MATCH); + } + + @Test + @DisplayName("실패 - 생성자의 나이가 모임의 나이 제한 범위를 벗어나면 AGE_NOT_MATCH 예외가 발생한다") + void fail_createParty_ageMismatch() { + // given + Long memberId = 1L; + PartyCreateDTO.Request request = PartyCreateDTO.Request.builder() + .partyName("청년 모임") + .partyType("혼복") + .minBirthYear(2000) + .maxBirthYear(2010) + .maleLevel(List.of("A조")) + .femaleLevel(List.of("A조")) + .activityTime("오후") + .activityDay(List.of("금")) + .build(); + + Member oldOwner = Member.builder() + .id(memberId) + .gender(Gender.MALE) + .birth(LocalDate.of(1980, 1, 1)) // 80년생이 00~10년생 모임 생성 시도 + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(oldOwner)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.createParty(memberId, request)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.AGE_NOT_MATCH); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원인 경우 MEMBER_NOT_FOUND 예외 발생") + void fail_createParty_memberNotFound() { + // given + Long memberId = 1L; + PartyCreateDTO.Request request = PartyCreateDTO.Request.builder() + .partyName("테스트 모임") + .partyType("혼복") + .activityTime("오전") + .addr1("서울") + .addr2("강남") + .activityDay(List.of("월", "수")) + .price(10000) + .joinPrice(5000) + .designatedCock("테스트콕") + .maleLevel(List.of("A조")) + .femaleLevel(List.of("B조")) + .build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.createParty(memberId, request)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat(((MemberException) e).getCode()).isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("updateParty") + class UpdateParty { + + @Test + @DisplayName("성공 - 모임장이 모임 정보를 정상적으로 수정한다") + void success_updateParty() { + // given + Long partyId = 1L; + Long memberId = 1L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, 1L); + ReflectionTestUtils.setField(owner, "id", memberId); + + Party party = PartyFixture.createParty("기존 모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + PartyUpdateDTO.Request request = PartyUpdateDTO.Request.builder() + .activityDay(List.of("토", "일")) + .activityTime("오전") + .designatedCock("새 콕") + .joinPrice(0) + .price(10000) + .content("새로운 내용") + .build(); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(memberId)).willReturn(Optional.of(owner)); + + // when + partyCommandService.updateParty(partyId, memberId, request); + + // then + assertThat(party.getDesignatedCock()).isEqualTo("새 콕"); + assertThat(party.getActiveDays().size()).isEqualTo(2); // 토, 일 + assertThat(party.getJoinPrice()).isEqualTo(0); + assertThat(party.getPrice()).isEqualTo(10000); + assertThat(party.getContent()).isEqualTo("새로운 내용"); + + verify(notificationCommandService, times(1)).createNotification(any()); + } + + @Test + @DisplayName("실패 - 조회된 모임이 없는 경우 PARTY_NOT_FOUND 예외 발생") + void fail_updateParty_partyNotFound() { + // given + Long partyId = 99L; + Long memberId = 1L; + PartyUpdateDTO.Request request = PartyUpdateDTO.Request.builder().build(); + + given(partyRepository.findById(partyId)).willReturn(Optional.empty()); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.updateParty(partyId, memberId, request)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND); + } + + @Test + @DisplayName("실패 - 모임장이 아닌 사용자가 수정을 시도할 경우 INSUFFICIENT_PERMISSION 예외 발생") + void fail_updateParty_insufficientPermission() { + // given + Long partyId = 1L; + Long memberId = 10L; // 일반 멤버 (ownerId=1 과 다름) + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, 1L); + ReflectionTestUtils.setField(owner, "id", 1L); + + Member normalMember = MemberFixture.createMember("일반멤버", Gender.MALE, Level.A, 2L); + ReflectionTestUtils.setField(normalMember, "id", memberId); + + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + PartyUpdateDTO.Request request = PartyUpdateDTO.Request.builder() + .activityTime("오전").build(); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(memberId)).willReturn(Optional.of(normalMember)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.updateParty(partyId, memberId, request)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.INSUFFICIENT_PERMISSION); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원인 경우 MEMBER_NOT_FOUND 예외 발생") + void fail_updateParty_memberNotFound() { + // given + Long partyId = 1L; + Long memberId = 1L; + Party party = PartyFixture.createParty("모임명", 1L, null); + PartyUpdateDTO.Request request = PartyUpdateDTO.Request.builder() + .activityTime("오전") + .build(); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.updateParty(partyId, memberId, request)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat(((MemberException) e).getCode()).isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("deleteParty") + class DeleteParty { + + @Test + @DisplayName("성공 - 모임장이 모임을 정상적으로 삭제(비활성화)한다") + void success_deleteParty() { + // given + Long partyId = 1L; + Long ownerId = 1L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + + Party party = PartyFixture.createParty("삭제할 모임", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(ownerId)).willReturn(Optional.of(owner)); + + // when + partyCommandService.deleteParty(partyId, ownerId); + + // then + assertThat(party.getStatus()).isEqualTo(PartyStatus.INACTIVE); + } + + @Test + @DisplayName("실패 - 모임장이 아닌 멤버가 모임 삭제를 시도할 경우 INSUFFICIENT_PERMISSION 발생") + void fail_deleteParty_notOwner() { + // given + Long partyId = 1L; + Long ownerId = 1L; + Long notOwnerId = 2L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + + Member notOwner = MemberFixture.createMember("일반멤버", Gender.MALE, Level.A, notOwnerId); + ReflectionTestUtils.setField(notOwner, "id", notOwnerId); + + Party party = PartyFixture.createParty("삭제할 모임", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(notOwnerId)).willReturn(Optional.of(notOwner)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.deleteParty(partyId, notOwnerId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.INSUFFICIENT_PERMISSION); + } + + @Test + @DisplayName("실패 - 이미 삭제된 모임을 삭제하려고 시도할 경우 PARTY_IS_DELETED 발생") + void fail_deleteParty_partyDeleted() { + // given + Long partyId = 1L; + Long ownerId = 1L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + + Party party = PartyFixture.createParty("이미 삭제된 모임", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + party.delete(); // 상태를 INACTIVE로 변경 + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(ownerId)).willReturn(Optional.of(owner)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.deleteParty(partyId, ownerId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.PARTY_IS_DELETED); + } + + @Test + @DisplayName("실패 - 조회된 모임이 존재하지 않을 경우 PARTY_NOT_FOUND 발생") + void fail_deleteParty_partyNotFound() { + // given + Long invalidId = 999L; + given(partyRepository.findById(invalidId)).willReturn(Optional.empty()); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.deleteParty(invalidId, 1L)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원인 경우 MEMBER_NOT_FOUND 예외 발생") + void fail_deleteParty_memberNotFound() { + // given + Long partyId = 1L; + Long memberId = 1L; + Party party = PartyFixture.createParty("모임명", 1L, null); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.deleteParty(partyId, memberId)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat(((MemberException) e).getCode()).isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("updateMemberRole") + class UpdateMemberRole { + + @Test + @DisplayName("성공 - 모임장이 일반 멤버를 부모임장으로 지정하면 기존 부모임장은 일반 멤버로 강등되고 새 부모임장이 지정된다") + void success_updateMemberRole() { + // given + Long partyId = 1L; + Long currentOwnerId = 1L; + Long targetMemberId = 10L; + Long oldSubManagerId = 20L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, currentOwnerId); + ReflectionTestUtils.setField(owner, "id", currentOwnerId); + + Member targetMember = MemberFixture.createMember("일반멤버", Gender.MALE, Level.A, targetMemberId); + ReflectionTestUtils.setField(targetMember, "id", targetMemberId); + + Member oldSubManager = MemberFixture.createMember("기존부모임장", Gender.MALE, Level.A, oldSubManagerId); + ReflectionTestUtils.setField(oldSubManager, "id", oldSubManagerId); + + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + MemberParty targetMemberParty = MemberFixture.createMemberParty(party, targetMember, Role.PARTY_MEMBER); + MemberParty oldSubManagerParty = MemberFixture.createMemberParty(party, oldSubManager, Role.PARTY_SUBMANAGER); + + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_SUBMANAGER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(targetMemberId)).willReturn(Optional.of(targetMember)); + given(memberPartyRepository.findByPartyAndMember(party, targetMember)).willReturn(Optional.of(targetMemberParty)); + given(memberPartyRepository.findByPartyIdAndRole(partyId, Role.PARTY_SUBMANAGER)).willReturn(Optional.of(oldSubManagerParty)); + given(memberPartyRepository.findAllByPartyIdWithMember(partyId)).willReturn(List.of(targetMemberParty, oldSubManagerParty)); + + // when + partyCommandService.updateMemberRole(partyId, targetMemberId, currentOwnerId, request); + + // then + assertThat(targetMemberParty.getRole()).isEqualTo(Role.PARTY_SUBMANAGER); + assertThat(oldSubManagerParty.getRole()).isEqualTo(Role.PARTY_MEMBER); + verify(notificationCommandService, times(4)).createNotification(any()); + } + + @Test + @DisplayName("실패 - 이미 요청한 역할과 같은 역할인 경우 변경 없이 반환된다") + void fail_updateMemberRole_sameRole() { + // given + Long partyId = 1L; + Long ownerId = 1L; + Long targetId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + + Member targetMember = MemberFixture.createMember("타겟", Gender.MALE, Level.A, targetId); + ReflectionTestUtils.setField(targetMember, "id", targetId); + + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + MemberParty targetMemberParty = spy(MemberFixture.createMemberParty(party, targetMember, Role.PARTY_SUBMANAGER)); + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_SUBMANAGER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(targetId)).willReturn(Optional.of(targetMember)); + given(memberPartyRepository.findByPartyAndMember(party, targetMember)).willReturn(Optional.of(targetMemberParty)); + + // when + partyCommandService.updateMemberRole(partyId, targetId, ownerId, request); + + // then + verify(targetMemberParty, never()).changeRole(any()); + verify(notificationCommandService, never()).createNotification(any()); + } + + @Test + @DisplayName("실패 - 대상 멤버가 이미 모임장인 경우 권한을 변경하려 하면 CANNOT_ASSIGN_TO_OWNER 발생") + void fail_updateMemberRole_targetIsOwner() { + // given + Long partyId = 1L; + Long ownerId = 1L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + // 타겟이 이미 모임장 권한을 가짐 + MemberParty memberParty = MemberFixture.createMemberParty(party, owner, Role.PARTY_MANAGER); + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_SUBMANAGER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(ownerId)).willReturn(Optional.of(owner)); + given(memberPartyRepository.findByPartyAndMember(party, owner)).willReturn(Optional.of(memberParty)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.updateMemberRole(partyId, ownerId, ownerId, request)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.CANNOT_ASSIGN_TO_OWNER); + } + + @Test + @DisplayName("실패 - 현재 사용자가 모임장이 아닐 경우 INSUFFICIENT_PERMISSION 발생") + void fail_updateMemberRole_notOwner() { + // given + Long partyId = 1L; + Long ownerId = 1L; + Long notOwnerId = 2L; + Long targetId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + + Member notOwner = MemberFixture.createMember("일반멤버", Gender.MALE, Level.A, notOwnerId); + ReflectionTestUtils.setField(notOwner, "id", notOwnerId); + + Member targetMember = MemberFixture.createMember("타겟", Gender.MALE, Level.A, targetId); + ReflectionTestUtils.setField(targetMember, "id", targetId); + + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + MemberParty targetMemberParty = MemberFixture.createMemberParty(party, targetMember, Role.PARTY_MEMBER); + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_SUBMANAGER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(targetId)).willReturn(Optional.of(targetMember)); + given(memberPartyRepository.findByPartyAndMember(party, targetMember)).willReturn(Optional.of(targetMemberParty)); + + // when & then (notOwnerId를 currentMemberId로 전달하여 실행) + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.updateMemberRole(partyId, targetId, notOwnerId, request)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.INSUFFICIENT_PERMISSION); + } + + @Test + @DisplayName("실패 - 존재하지 않는 파티인 경우 PARTY_NOT_FOUND 예외 발생") + void fail_updateMemberRole_partyNotFound() { + // given + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_SUBMANAGER); + given(partyRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.updateMemberRole(999L, 1L, 1L, request)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원인 경우 MEMBER_NOT_FOUND 예외 발생") + void fail_updateMemberRole_memberNotFound() { + // given + Long partyId = 1L; + Party party = PartyFixture.createParty("모임명", 1L, null); + PartyMemberRoleDTO.Request request = new PartyMemberRoleDTO.Request(Role.PARTY_SUBMANAGER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(1L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.updateMemberRole(partyId, 1L, 1L, request)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat(((MemberException) e).getCode()).isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("removeMember") + class RemoveMember { + + @Test + @DisplayName("성공 - 모임장이 일반 멤버를 성공적으로 강퇴한다") + void success_removeMember() { + // given + Long partyId = 1L; + Long ownerId = 1L; + Long targetMemberId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Member targetMember = MemberFixture.createMember("타겟", Gender.MALE, Level.A, targetMemberId); + ReflectionTestUtils.setField(targetMember, "id", targetMemberId); + + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + MemberParty ownerParty = MemberFixture.createMemberParty(party, owner, Role.PARTY_MANAGER); + MemberParty targetMemberParty = MemberFixture.createMemberParty(party, targetMember, Role.PARTY_MEMBER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(ownerId)).willReturn(Optional.of(owner)); + given(memberRepository.findById(targetMemberId)).willReturn(Optional.of(targetMember)); + given(memberPartyRepository.findByPartyAndMember(party, owner)).willReturn(Optional.of(ownerParty)); + given(memberPartyRepository.findByPartyAndMember(party, targetMember)).willReturn(Optional.of(targetMemberParty)); + + // when + partyCommandService.removeMember(partyId, targetMemberId, ownerId); + + // then + verify(memberPartyRepository, times(1)).delete(targetMemberParty); + verify(chatRoomService, times(1)).leavePartyChatRoom(partyId, targetMemberId); + } + + @Test + @DisplayName("성공 - 부모임장이 일반 멤버를 성공적으로 강퇴한다") + void success_removeMember_bySubManager() { + // given + Long partyId = 1L; + Long subManagerId = 2L; + Long targetMemberId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member subManager = MemberFixture.createMember("부모임장", Gender.MALE, Level.A, subManagerId); + ReflectionTestUtils.setField(subManager, "id", subManagerId); + Member targetMember = MemberFixture.createMember("일반멤버", Gender.MALE, Level.A, targetMemberId); + ReflectionTestUtils.setField(targetMember, "id", targetMemberId); + + Party party = PartyFixture.createParty("모임명", 1L, addr); // ownerId = 1L + ReflectionTestUtils.setField(party, "id", partyId); + + MemberParty subManagerParty = MemberFixture.createMemberParty(party, subManager, Role.PARTY_SUBMANAGER); + MemberParty targetMemberParty = MemberFixture.createMemberParty(party, targetMember, Role.PARTY_MEMBER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(subManagerId)).willReturn(Optional.of(subManager)); + given(memberRepository.findById(targetMemberId)).willReturn(Optional.of(targetMember)); + given(memberPartyRepository.findByPartyAndMember(party, subManager)).willReturn(Optional.of(subManagerParty)); + given(memberPartyRepository.findByPartyAndMember(party, targetMember)).willReturn(Optional.of(targetMemberParty)); + + // when + partyCommandService.removeMember(partyId, targetMemberId, subManagerId); + + // then + verify(memberPartyRepository, times(1)).delete(targetMemberParty); + verify(chatRoomService, times(1)).leavePartyChatRoom(partyId, targetMemberId); + } + + @Test + @DisplayName("실패 - 권한이 없는 멤버가 타인을 강퇴하려 하면 INSUFFICIENT_PERMISSION 발생") + void fail_removeMember_insufficientPermission() { + // given + Long partyId = 1L; + Long subManagerId = 2L; + Long targetOwnerId = 1L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, targetOwnerId); + ReflectionTestUtils.setField(owner, "id", targetOwnerId); + Member subManager = MemberFixture.createMember("부모임장", Gender.MALE, Level.A, subManagerId); + ReflectionTestUtils.setField(subManager, "id", subManagerId); + + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + MemberParty ownerParty = MemberFixture.createMemberParty(party, owner, Role.PARTY_MANAGER); + MemberParty subManagerParty = MemberFixture.createMemberParty(party, subManager, Role.PARTY_SUBMANAGER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(subManagerId)).willReturn(Optional.of(subManager)); + given(memberRepository.findById(targetOwnerId)).willReturn(Optional.of(owner)); + given(memberPartyRepository.findByPartyAndMember(party, subManager)).willReturn(Optional.of(subManagerParty)); + given(memberPartyRepository.findByPartyAndMember(party, owner)).willReturn(Optional.of(ownerParty)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.removeMember(partyId, targetOwnerId, subManagerId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.INSUFFICIENT_PERMISSION); + } + + @Test + @DisplayName("실패 - 모임장이 자신을 강퇴하려 할 경우 CANNOT_REMOVE_SELF 발생") + void fail_removeMember_cannotRemoveSelf() { + // given + Long partyId = 1L; + Long ownerId = 1L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + MemberParty ownerParty = MemberFixture.createMemberParty(party, owner, Role.PARTY_MANAGER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(ownerId)).willReturn(Optional.of(owner)); + given(memberPartyRepository.findByPartyAndMember(party, owner)).willReturn(Optional.of(ownerParty)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.removeMember(partyId, ownerId, ownerId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.CANNOT_REMOVE_SELF); + } + + @Test + @DisplayName("실패 - 대상 멤버가 모임 소속이 아닐 경우 NOT_MEMBER 발생") + void fail_removeMember_notMember() { + // given + Long partyId = 1L; + Long ownerId = 1L; + Long targetMemberId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Member targetMember = MemberFixture.createMember("타겟", Gender.MALE, Level.A, targetMemberId); + ReflectionTestUtils.setField(targetMember, "id", targetMemberId); + + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(ownerId)).willReturn(Optional.of(owner)); + given(memberRepository.findById(targetMemberId)).willReturn(Optional.of(targetMember)); + + // 타겟 멤버가 모임 소속이 아님 -> findMemberPartyOrThrow 에서 NOT_MEMBER 발생 + given(memberPartyRepository.findByPartyAndMember(party, targetMember)).willReturn(Optional.empty()); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.removeMember(partyId, targetMemberId, ownerId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.NOT_MEMBER); + } + + @Test + @DisplayName("실패 - 존재하지 않는 파티인 경우 PARTY_NOT_FOUND 예외 발생") + void fail_removeMember_partyNotFound() { + // given + given(partyRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.removeMember(999L, 1L, 1L)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원인 경우 MEMBER_NOT_FOUND 예외 발생") + void fail_removeMember_memberNotFound() { + // given + Long partyId = 1L; + Party party = PartyFixture.createParty("모임명", 1L, null); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(1L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.removeMember(partyId, 10L, 1L)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat(((MemberException) e).getCode()).isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("actionJoinRequest") + class ActionJoinRequest { + + @Test + @DisplayName("성공 - 모임장이 가입 신청을 승인하고 알림과 채팅방 진입, 이벤트를 발생시킨다") + void success_actionJoinRequest_approve() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Long requestId = 100L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member applicant = MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 20L); + ReflectionTestUtils.setField(applicant, "id", 20L); + + PartyJoinRequest joinRequest = PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.PENDING) + .build(); + ReflectionTestUtils.setField(joinRequest, "id", requestId); + + PartyJoinActionDTO.Request requestDTO = new PartyJoinActionDTO.Request(RequestAction.APPROVE); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(partyJoinRequestRepository.findById(requestId)).willReturn(Optional.of(joinRequest)); + given(memberPartyRepository.existsByPartyAndMember(party, applicant)).willReturn(false); + + // when + partyCommandService.actionJoinRequest(partyId, ownerId, requestDTO, requestId); + + // then + assertThat(joinRequest.getStatus()).isEqualTo(RequestStatus.APPROVED); + verify(chatRoomService).joinPartyChatRoom(partyId, applicant); + verify(applicationEventPublisher).publishEvent(any(PartyMemberJoinedEvent.class)); + verify(notificationCommandService).createNotification(any()); + } + + @Test + @DisplayName("성공 - 모임장이 가입 신청을 거절하면 상태만 REJECTED로 바뀌고 다른 사이드이펙트가 발생하지 않는다") + void success_actionJoinRequest_reject() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Long requestId = 100L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member applicant = MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 20L); + ReflectionTestUtils.setField(applicant, "id", 20L); + + PartyJoinRequest joinRequest = PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.PENDING) + .build(); + ReflectionTestUtils.setField(joinRequest, "id", requestId); + + PartyJoinActionDTO.Request requestDTO = new PartyJoinActionDTO.Request(RequestAction.REJECT); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(partyJoinRequestRepository.findById(requestId)).willReturn(Optional.of(joinRequest)); + given(memberPartyRepository.existsByPartyAndMember(party, applicant)).willReturn(false); + + // when + partyCommandService.actionJoinRequest(partyId, ownerId, requestDTO, requestId); + + // then + assertThat(joinRequest.getStatus()).isEqualTo(RequestStatus.REJECTED); + verifyNoInteractions(chatRoomService); + verifyNoInteractions(applicationEventPublisher); + verifyNoInteractions(notificationCommandService); + } + + @Test + @DisplayName("실패 - 대상자가 이미 모임 멤버인 경우 ALREADY_MEMBER 검증 에러가 발생한다") + void fail_actionJoinRequest_alreadyMember() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Long requestId = 100L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member applicant = MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 20L); + ReflectionTestUtils.setField(applicant, "id", 20L); + + PartyJoinRequest joinRequest = PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.PENDING) + .build(); + ReflectionTestUtils.setField(joinRequest, "id", requestId); + + PartyJoinActionDTO.Request requestDTO = new PartyJoinActionDTO.Request(RequestAction.APPROVE); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(partyJoinRequestRepository.findById(requestId)).willReturn(Optional.of(joinRequest)); + given(memberPartyRepository.existsByPartyAndMember(party, applicant)).willReturn(true); // 이미 멤버 + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.actionJoinRequest(partyId, ownerId, requestDTO, requestId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.ALREADY_MEMBER); + } + + @Test + @DisplayName("실패 - 이미 처리된 가입 신청을 다시 처리하려 할 때 JOIN_REQUEST_ALREADY_ACTIONS 발생") + void fail_actionJoinRequest_alreadyActions() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Long requestId = 100L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member applicant = MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 20L); + ReflectionTestUtils.setField(applicant, "id", 20L); + + PartyJoinRequest joinRequest = PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.APPROVED) // 이미 승인됨 + .build(); + ReflectionTestUtils.setField(joinRequest, "id", requestId); + + PartyJoinActionDTO.Request requestDTO = new PartyJoinActionDTO.Request(RequestAction.REJECT); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(partyJoinRequestRepository.findById(requestId)).willReturn(Optional.of(joinRequest)); + given(memberPartyRepository.existsByPartyAndMember(party, applicant)).willReturn(false); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.actionJoinRequest(partyId, ownerId, requestDTO, requestId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.JOIN_REQUEST_ALREADY_ACTIONS); + } + + @Test + @DisplayName("실패 - 해당 가입 요청을 찾을 수 없는 경우 JOIN_REQUEST_NOT_FOUND 발생") + void fail_actionJoinRequest_notFound() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Long requestId = 999L; // 존재하지 않는 ID + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + PartyJoinActionDTO.Request requestDTO = new PartyJoinActionDTO.Request(RequestAction.APPROVE); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(partyJoinRequestRepository.findById(requestId)).willReturn(Optional.empty()); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.actionJoinRequest(partyId, ownerId, requestDTO, requestId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.JOIN_REQUEST_NOT_FOUND); + } + + @Test + @DisplayName("실패 - 모임장이 아닌 사용자가 가입 신청을 처리하려 할 때 INSUFFICIENT_PERMISSION 발생") + void fail_actionJoinRequest_insufficientPermission() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Long notOwnerId = 99L; + Long requestId = 100L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); // 실제 모임장 + ReflectionTestUtils.setField(owner, "id", ownerId); + Party party = PartyFixture.createParty("모임명", owner.getId(), addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member applicant = MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 20L); + ReflectionTestUtils.setField(applicant, "id", 20L); + + PartyJoinRequest joinRequest = PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.PENDING) + .build(); + ReflectionTestUtils.setField(joinRequest, "id", requestId); + + PartyJoinActionDTO.Request requestDTO = new PartyJoinActionDTO.Request(RequestAction.APPROVE); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(partyJoinRequestRepository.findById(requestId)).willReturn(Optional.of(joinRequest)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.actionJoinRequest(partyId, notOwnerId, requestDTO, requestId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.INSUFFICIENT_PERMISSION); + } + + @Test + @DisplayName("실패 - 처리하려는 가입 신청이 해당 모임의 것이 아닌 경우 JOIN_REQUEST_PARTY_NOT_FOUND 발생") + void fail_actionJoinRequest_joinRequestPartyNotFound() { + // given + Long partyId = 1L; + Long wrongPartyId = 2L; + Long ownerId = 10L; + Long requestId = 100L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + + Party targetParty = PartyFixture.createParty("대상 모임", owner.getId(), addr); + ReflectionTestUtils.setField(targetParty, "id", partyId); + + Party wrongParty = PartyFixture.createParty("다른 모임", owner.getId(), addr); + ReflectionTestUtils.setField(wrongParty, "id", wrongPartyId); + + Member applicant = MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 20L); + ReflectionTestUtils.setField(applicant, "id", 20L); + + // 다른 모임으로 가입신청 + PartyJoinRequest joinRequest = PartyJoinRequest.builder() + .party(wrongParty) + .member(applicant) + .status(RequestStatus.PENDING) + .build(); + ReflectionTestUtils.setField(joinRequest, "id", requestId); + + PartyJoinActionDTO.Request requestDTO = new PartyJoinActionDTO.Request(RequestAction.APPROVE); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(targetParty)); + given(partyJoinRequestRepository.findById(requestId)).willReturn(Optional.of(joinRequest)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.actionJoinRequest(partyId, ownerId, requestDTO, requestId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.JOIN_REQUEST_PARTY_NOT_FOUND); + } + + @Test + @DisplayName("실패 - 존재하지 않는 파티인 경우 PARTY_NOT_FOUND 예외 발생") + void fail_actionJoinRequest_partyNotFound() { + // given + given(partyRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.actionJoinRequest(999L, 1L, new PartyJoinActionDTO.Request(RequestAction.APPROVE), 1L)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); + } + } + + @Nested + @DisplayName("createInvitation") + class CreateInvitation { + + @Test + @DisplayName("성공 - 모임장이 새로운 멤버를 초대하고 invitationId를 반환한다") + void success_createInvitation() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Long inviteeId = 20L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Member invitee = MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, inviteeId); + ReflectionTestUtils.setField(invitee, "id", inviteeId); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(ownerId)).willReturn(Optional.of(owner)); + given(memberRepository.findById(inviteeId)).willReturn(Optional.of(invitee)); + given(memberPartyRepository.existsByPartyAndMember(party, invitee)).willReturn(false); + given(partyInvitationRepository.existsByPartyAndInviteeAndStatus(party, invitee, RequestStatus.PENDING)).willReturn(false); + + PartyInvitation savedInvitation = PartyInvitation.create(party, owner, invitee); + ReflectionTestUtils.setField(savedInvitation, "id", 100L); + given(partyInvitationRepository.save(any())).willReturn(savedInvitation); + + // when + PartyInviteCreateDTO.Response response = partyCommandService.createInvitation(partyId, inviteeId, ownerId); + + // then + assertThat(response.invitationId()).isEqualTo(100L); + verify(notificationCommandService).createNotification(any()); + } + + @Test + @DisplayName("실패 - 모임장이 아닌 사용자가 초대하려 하면 INSUFFICIENT_PERMISSION 발생") + void fail_createInvitation_notOwner() { + // given + Long partyId = 1L; + Long nonOwnerId = 99L; + Long inviteeId = 20L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member nonOwner = MemberFixture.createMember("일반멤버", Gender.MALE, Level.B, nonOwnerId); + ReflectionTestUtils.setField(nonOwner, "id", nonOwnerId); + Member invitee = MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, inviteeId); + ReflectionTestUtils.setField(invitee, "id", inviteeId); + Party party = PartyFixture.createParty("모임명", 10L, addr); // ownerId = 10L + ReflectionTestUtils.setField(party, "id", partyId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(nonOwnerId)).willReturn(Optional.of(nonOwner)); + given(memberRepository.findById(inviteeId)).willReturn(Optional.of(invitee)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.createInvitation(partyId, inviteeId, nonOwnerId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.INSUFFICIENT_PERMISSION); + } + + @Test + @DisplayName("실패 - 이미 모임에 가입한 멤버를 초대하면 ALREADY_MEMBER 발생") + void fail_createInvitation_alreadyMember() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Long inviteeId = 20L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Member invitee = MemberFixture.createMember("대상멤버", Gender.FEMALE, Level.B, inviteeId); + ReflectionTestUtils.setField(invitee, "id", inviteeId); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(ownerId)).willReturn(Optional.of(owner)); + given(memberRepository.findById(inviteeId)).willReturn(Optional.of(invitee)); + given(memberPartyRepository.existsByPartyAndMember(party, invitee)).willReturn(true); // 이미 멤버 + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.createInvitation(partyId, inviteeId, ownerId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.ALREADY_MEMBER); + } + + @Test + @DisplayName("실패 - 이미 대기 중인 초대가 있는 멤버를 중복 초대하면 INVITATION_ALREADY_EXISTS 발생") + void fail_createInvitation_duplicateInvitation() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Long inviteeId = 20L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Member invitee = MemberFixture.createMember("대상멤버", Gender.FEMALE, Level.B, inviteeId); + ReflectionTestUtils.setField(invitee, "id", inviteeId); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(ownerId)).willReturn(Optional.of(owner)); + given(memberRepository.findById(inviteeId)).willReturn(Optional.of(invitee)); + given(memberPartyRepository.existsByPartyAndMember(party, invitee)).willReturn(false); + given(partyInvitationRepository.existsByPartyAndInviteeAndStatus(party, invitee, RequestStatus.PENDING)).willReturn(true); // 이미 대기중 초대 + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.createInvitation(partyId, inviteeId, ownerId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.INVITATION_ALREADY_EXISTS); + } + + @Test + @DisplayName("실패 - 존재하지 않는 파티인 경우 PARTY_NOT_FOUND 예외 발생") + void fail_createInvitation_partyNotFound() { + // given + given(partyRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.createInvitation(999L, 1L, 1L)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원인 경우 MEMBER_NOT_FOUND 예외 발생") + void fail_createInvitation_memberNotFound() { + // given + Long partyId = 1L; + Party party = PartyFixture.createParty("모임명", 1L, null); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(1L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.createInvitation(partyId, 1L, 1L)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat(((MemberException) e).getCode()).isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("actionInvitation") + class ActionInvitation { + + @Test + @DisplayName("성공 - 초대받은 멤버가 승인하면 모임 멤버로 추가되고 알림이 발생한다") + void success_actionInvitation_approve() { + // given + Long invitationId = 100L; + Long ownerId = 10L; + Long inviteeId = 20L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Member invitee = MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, inviteeId); + ReflectionTestUtils.setField(invitee, "id", inviteeId); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", 1L); + + PartyInvitation invitation = PartyInvitation.create(party, owner, invitee); + ReflectionTestUtils.setField(invitation, "id", invitationId); + + PartyInviteActionDTO.Request request = new PartyInviteActionDTO.Request(RequestAction.APPROVE); + + given(partyInvitationRepository.findById(invitationId)).willReturn(Optional.of(invitation)); + given(memberRepository.findById(inviteeId)).willReturn(Optional.of(invitee)); + given(memberPartyRepository.existsByPartyAndMember(party, invitee)).willReturn(false); + + // when + partyCommandService.actionInvitation(inviteeId, request, invitationId); + + // then + assertThat(invitation.getStatus()).isEqualTo(RequestStatus.APPROVED); + verify(chatRoomService).joinPartyChatRoom(party.getId(), invitee); + verify(applicationEventPublisher).publishEvent(any(PartyMemberJoinedEvent.class)); + verify(notificationCommandService).createNotification(any()); + } + + @Test + @DisplayName("성공 - 초대받은 멤버가 거절하면 상태만 REJECTED로 바뀌고 사이드이펙트가 없다") + void success_actionInvitation_reject() { + // given + Long invitationId = 100L; + Long ownerId = 10L; + Long inviteeId = 20L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Member invitee = MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, inviteeId); + ReflectionTestUtils.setField(invitee, "id", inviteeId); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", 1L); + + PartyInvitation invitation = PartyInvitation.create(party, owner, invitee); + ReflectionTestUtils.setField(invitation, "id", invitationId); + + PartyInviteActionDTO.Request request = new PartyInviteActionDTO.Request(RequestAction.REJECT); + + given(partyInvitationRepository.findById(invitationId)).willReturn(Optional.of(invitation)); + given(memberRepository.findById(inviteeId)).willReturn(Optional.of(invitee)); + given(memberPartyRepository.existsByPartyAndMember(party, invitee)).willReturn(false); + + // when + partyCommandService.actionInvitation(inviteeId, request, invitationId); + + // then + assertThat(invitation.getStatus()).isEqualTo(RequestStatus.REJECTED); + verifyNoInteractions(chatRoomService); + verifyNoInteractions(applicationEventPublisher); + verifyNoInteractions(notificationCommandService); + } + + @Test + @DisplayName("실패 - 초대받은 사람이 아닌 제3자가 처리하려 하면 NOT_YOUR_INVITATION 발생") + void fail_actionInvitation_notYourInvitation() { + // given + Long invitationId = 100L; + Long ownerId = 10L; + Long inviteeId = 20L; + Long otherId = 99L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Member invitee = MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, inviteeId); + ReflectionTestUtils.setField(invitee, "id", inviteeId); + Member other = MemberFixture.createMember("제3자", Gender.MALE, Level.C, otherId); + ReflectionTestUtils.setField(other, "id", otherId); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", 1L); + + PartyInvitation invitation = PartyInvitation.create(party, owner, invitee); + ReflectionTestUtils.setField(invitation, "id", invitationId); + + PartyInviteActionDTO.Request request = new PartyInviteActionDTO.Request(RequestAction.APPROVE); + + given(partyInvitationRepository.findById(invitationId)).willReturn(Optional.of(invitation)); + given(memberRepository.findById(otherId)).willReturn(Optional.of(other)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.actionInvitation(otherId, request, invitationId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.NOT_YOUR_INVITATION); + } + + @Test + @DisplayName("실패 - 이미 처리된 초대를 다시 처리하려 할 때 INVITATION_ALREADY_ACTIONS 발생") + void fail_actionInvitation_alreadyActions() { + // given + Long invitationId = 100L; + Long ownerId = 10L; + Long inviteeId = 20L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Member invitee = MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, inviteeId); + ReflectionTestUtils.setField(invitee, "id", inviteeId); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", 1L); + + // 이미 승인된 초대 + PartyInvitation invitation = PartyInvitation.create(party, owner, invitee); + ReflectionTestUtils.setField(invitation, "id", invitationId); + invitation.updateStatus(RequestStatus.APPROVED); + + PartyInviteActionDTO.Request request = new PartyInviteActionDTO.Request(RequestAction.REJECT); + + given(partyInvitationRepository.findById(invitationId)).willReturn(Optional.of(invitation)); + given(memberRepository.findById(inviteeId)).willReturn(Optional.of(invitee)); + given(memberPartyRepository.existsByPartyAndMember(party, invitee)).willReturn(false); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.actionInvitation(inviteeId, request, invitationId)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.INVITATION_ALREADY_ACTIONS); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원인 경우 MEMBER_NOT_FOUND 예외 발생") + void fail_actionInvitation_memberNotFound() { + // given + Long invitationId = 100L; + Party party = PartyFixture.createParty("모임명", 1L, null); + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, 1L); + Member invitee = MemberFixture.createMember("초대대상", Gender.FEMALE, Level.B, 20L); + PartyInvitation invitation = PartyInvitation.create(party, owner, invitee); + + PartyInviteActionDTO.Request request = new PartyInviteActionDTO.Request(RequestAction.APPROVE); + given(partyInvitationRepository.findById(invitationId)).willReturn(Optional.of(invitation)); + given(memberRepository.findById(20L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.actionInvitation(20L, request, invitationId)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat(((MemberException) e).getCode()).isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("addKeyword") + class AddKeyword { + + @Test + @DisplayName("성공 - 모임장이 유효한 키워드 목록을 모임에 추가한다") + void success_addKeyword() { + // given + Long partyId = 1L; + Long ownerId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + PartyKeywordDTO.Request request = new PartyKeywordDTO.Request( + List.of("친목", "가입비 무료") + ); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + + // when + partyCommandService.addKeyword(partyId, ownerId, request); + + // then + assertThat(party.getKeywords()).hasSize(2); + } + + @Test + @DisplayName("실패 - 모임장이 아닌 사용자가 키워드를 추가하면 INSUFFICIENT_PERMISSION 발생") + void fail_addKeyword_notOwner() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Long nonOwnerId = 99L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + PartyKeywordDTO.Request request = new PartyKeywordDTO.Request(List.of("친목")); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.addKeyword(partyId, nonOwnerId, request)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.INSUFFICIENT_PERMISSION); + } + + @Test + @DisplayName("실패 - 유효하지 않은 키워드 문자열을 전달하면 INVALID_KEYWORD 발생") + void fail_addKeyword_invalidKeyword() { + // given + Long partyId = 1L; + Long ownerId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + PartyKeywordDTO.Request request = new PartyKeywordDTO.Request(List.of("존재하지않는키워드")); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyCommandService.addKeyword(partyId, ownerId, request)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.INVALID_KEYWORD); + } + + @Test + @DisplayName("실패 - 존재하지 않는 파티인 경우 PARTY_NOT_FOUND 예외 발생") + void fail_addKeyword_partyNotFound() { + // given + PartyKeywordDTO.Request request = new PartyKeywordDTO.Request(List.of("새키워드")); + given(partyRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyCommandService.addKeyword(999L, 10L, request)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); + } + } +} diff --git a/src/test/java/umc/cockple/demo/domain/party/service/PartyQueryServiceTest.java b/src/test/java/umc/cockple/demo/domain/party/service/PartyQueryServiceTest.java index 5f6a9005e..e9427a595 100644 --- a/src/test/java/umc/cockple/demo/domain/party/service/PartyQueryServiceTest.java +++ b/src/test/java/umc/cockple/demo/domain/party/service/PartyQueryServiceTest.java @@ -1,5 +1,6 @@ package umc.cockple.demo.domain.party.service; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -7,17 +8,32 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; import org.springframework.test.util.ReflectionTestUtils; +import umc.cockple.demo.domain.bookmark.repository.PartyBookmarkRepository; +import umc.cockple.demo.domain.exercise.repository.ExerciseRepository; +import umc.cockple.demo.domain.file.service.FileService; import umc.cockple.demo.domain.member.domain.Member; +import umc.cockple.demo.domain.member.domain.MemberAddr; import umc.cockple.demo.domain.member.domain.MemberParty; +import umc.cockple.demo.domain.member.exception.MemberErrorCode; +import umc.cockple.demo.domain.member.exception.MemberException; +import umc.cockple.demo.domain.member.repository.MemberAddrRepository; import umc.cockple.demo.domain.member.repository.MemberExerciseRepository; import umc.cockple.demo.domain.member.repository.MemberPartyRepository; +import umc.cockple.demo.domain.member.repository.MemberRepository; import umc.cockple.demo.domain.party.converter.PartyConverter; import umc.cockple.demo.domain.party.domain.Party; import umc.cockple.demo.domain.party.domain.PartyAddr; -import umc.cockple.demo.domain.party.dto.PartyMemberDTO; +import umc.cockple.demo.domain.party.domain.PartyJoinRequest; +import umc.cockple.demo.domain.party.dto.*; +import umc.cockple.demo.domain.party.enums.RequestStatus; import umc.cockple.demo.domain.party.exception.PartyErrorCode; import umc.cockple.demo.domain.party.exception.PartyException; +import umc.cockple.demo.domain.party.repository.PartyJoinRequestRepository; import umc.cockple.demo.domain.party.repository.PartyRepository; import umc.cockple.demo.global.enums.Gender; import umc.cockple.demo.global.enums.Level; @@ -27,13 +43,13 @@ import java.time.LocalDate; import java.util.List; -import java.util.Map; import java.util.Optional; +import java.util.Set; 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.ArgumentMatchers.eq; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; @@ -46,19 +62,78 @@ class PartyQueryServiceTest { @Mock private PartyRepository partyRepository; @Mock + private MemberRepository memberRepository; + private PartyConverter partyConverter; + @Mock private MemberPartyRepository memberPartyRepository; @Mock private MemberExerciseRepository memberExerciseRepository; + @Mock + private ExerciseRepository exerciseRepository; + @Mock + private PartyBookmarkRepository partyBookmarkRepository; + @Mock + private MemberAddrRepository memberAddrRepository; + @Mock + private FileService fileService; + @Mock + private PartyJoinRequestRepository partyJoinRequestRepository; + + @BeforeEach + void setUp() { + partyConverter = new PartyConverter(fileService); + ReflectionTestUtils.setField(partyQueryService, "partyConverter", partyConverter); + } @Nested @DisplayName("getPartyMembers") class GetPartyMembers { @Test - @DisplayName("멤버 목록과 마지막 운동일을 함께 반환한다") - void success() { + @DisplayName("성공 - 모임의 멤버들을 역할별로 성공적으로 조회한다.") + void success_getPartyMembers() { + // given + Long partyId = 1L; + Long currentMemberId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울특별시", "강남구"); + Party party = PartyFixture.createParty("테스트 모임", 10L, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member manager = MemberFixture.createMember("모임장", Gender.MALE, Level.A, 1001L); + Member subManager = MemberFixture.createMember("부모임장", Gender.FEMALE, Level.B, 1002L); + Member normalMember = MemberFixture.createMember("일반멤버", Gender.MALE, Level.C, 1003L); + + ReflectionTestUtils.setField(manager, "id", 10L); + ReflectionTestUtils.setField(subManager, "id", 20L); + ReflectionTestUtils.setField(normalMember, "id", 30L); + + MemberParty mp1 = MemberFixture.createMemberParty(party, manager, Role.PARTY_MANAGER); + MemberParty mp2 = MemberFixture.createMemberParty(party, subManager, Role.PARTY_SUBMANAGER); + MemberParty mp3 = MemberFixture.createMemberParty(party, normalMember, Role.PARTY_MEMBER); + List memberParties = List.of(mp1, mp2, mp3); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberPartyRepository.findAllByPartyIdWithMember(partyId)).willReturn(memberParties); + given(memberExerciseRepository.findLastExerciseDateByMemberIdsAndPartyId(anyList(), + eq(partyId))) + .willReturn(List.of()); + + // when + PartyMemberDTO.Response result = partyQueryService.getPartyMembers(partyId, currentMemberId); + + // then + assertThat(result.members()).hasSize(3); + assertThat(result.summary().totalCount()).isEqualTo(3); + assertThat(result.summary().maleCount()).isEqualTo(2); + assertThat(result.summary().femaleCount()).isEqualTo(1); + } + + @Test + @DisplayName("성공 - 멤버 목록과 마지막 운동일을 함께 반환한다") + void success_getPartyMembers_withExerciseHistory() { // given Long partyId = 1L; Long currentMemberId = 10L; @@ -71,8 +146,8 @@ void success() { ReflectionTestUtils.setField(manager, "id", 10L); ReflectionTestUtils.setField(member1, "id", 20L); - MemberParty mp1 = MemberFixture.createMemberParty(party, manager, Role.party_MANAGER); - MemberParty mp2 = MemberFixture.createMemberParty(party, member1, Role.party_MEMBER); + MemberParty mp1 = MemberFixture.createMemberParty(party, manager, Role.PARTY_MANAGER); + MemberParty mp2 = MemberFixture.createMemberParty(party, member1, Role.PARTY_MEMBER); List memberParties = List.of(mp1, mp2); LocalDate lastDate = LocalDate.of(2025, 1, 10); @@ -88,26 +163,23 @@ void success() { given(memberPartyRepository.findAllByPartyIdWithMember(partyId)).willReturn(memberParties); given(memberExerciseRepository.findLastExerciseDateByMemberIdsAndPartyId( List.of(10L, 20L), partyId)).willReturn(rawResult); - given(partyConverter.toPartyMemberDTO(eq(memberParties), eq(currentMemberId), any())) - .willReturn(expected); // when PartyMemberDTO.Response result = partyQueryService.getPartyMembers(partyId, currentMemberId); // then - assertThat(result).isEqualTo(expected); - verify(memberExerciseRepository).findLastExerciseDateByMemberIdsAndPartyId( - List.of(10L, 20L), partyId); - verify(partyConverter).toPartyMemberDTO( - eq(memberParties), - eq(currentMemberId), - eq(Map.of(20L, lastDate)) - ); + assertThat(result.summary().totalCount()).isEqualTo(2); + assertThat(result.members()).hasSize(2); + // 마지막 운동일 확인 (멤버1 id: 20L) + assertThat(result.members().stream() + .filter(m -> m.memberId().equals(20L)) + .findFirst() + .get().lastExerciseDate()).isEqualTo(lastDate); } @Test - @DisplayName("운동 기록이 없는 멤버는 빈 Map이 converter에 전달된다") - void noExerciseHistory() { + @DisplayName("성공 - 운동 기록이 없는 멤버는 빈 Map이 converter에 전달된다") + void success_getPartyMembers_noExerciseHistory() { // given Long partyId = 1L; Long currentMemberId = 10L; @@ -117,41 +189,38 @@ void noExerciseHistory() { ReflectionTestUtils.setField(party, "id", partyId); Member manager = MemberFixture.createMember("매니저", Gender.MALE, Level.A, 1001L); ReflectionTestUtils.setField(manager, "id", 10L); - MemberParty mp = MemberFixture.createMemberParty(party, manager, Role.party_MANAGER); + MemberParty mp = MemberFixture.createMemberParty(party, manager, Role.PARTY_MANAGER); List memberParties = List.of(mp); given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); given(memberPartyRepository.findAllByPartyIdWithMember(partyId)).willReturn(memberParties); given(memberExerciseRepository.findLastExerciseDateByMemberIdsAndPartyId( List.of(10L), partyId)).willReturn(List.of()); - given(partyConverter.toPartyMemberDTO(any(), any(), any())).willReturn(null); // when - partyQueryService.getPartyMembers(partyId, currentMemberId); + PartyMemberDTO.Response result = partyQueryService.getPartyMembers(partyId, currentMemberId); // then - verify(partyConverter).toPartyMemberDTO( - eq(memberParties), - eq(currentMemberId), - eq(Map.of()) - ); + assertThat(result.members()).hasSize(1); + assertThat(result.members().get(0).lastExerciseDate()).isNull(); } @Test - @DisplayName("존재하지 않는 파티면 PartyException을 던진다") - void partyNotFound() { + @DisplayName("실패 - 존재하지 않는 파티면 PartyException을 던진다") + void fail_getPartyMembers_partyNotFound() { // given given(partyRepository.findById(99L)).willReturn(Optional.empty()); // when & then assertThatThrownBy(() -> partyQueryService.getPartyMembers(99L, 1L)) .isInstanceOf(PartyException.class) - .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); + .satisfies(e -> assertThat(((PartyException) e).getCode()) + .isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); } @Test - @DisplayName("비활성화된 파티면 PartyException을 던진다") - void partyInactive() { + @DisplayName("실패 - 비활성화된 파티면 PartyException을 던진다") + void fail_getPartyMembers_partyInactive() { // given PartyAddr addr = PartyFixture.createPartyAddr("서울특별시", "강남구"); Party inactiveParty = PartyFixture.createParty("테스트 모임", 10L, addr); @@ -163,8 +232,763 @@ void partyInactive() { // when & then assertThatThrownBy(() -> partyQueryService.getPartyMembers(1L, 1L)) .isInstanceOf(PartyException.class) - .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.PARTY_IS_DELETED)); + .satisfies(e -> assertThat(((PartyException) e).getCode()) + .isEqualTo(PartyErrorCode.PARTY_IS_DELETED)); + } + } + + @Nested + @DisplayName("getMyParties") + class GetMyParties { + + @Test + @DisplayName("성공 - 내 모임 목록을 최신순(기본값)으로 페이징하여 반환한다") + void success_getMyParties() { + // given + Long memberId = 10L; + Pageable pageable = PageRequest.of(0, 10); + + PartyAddr addr = PartyFixture.createPartyAddr("서울특별시", "강남구"); + Party party1 = PartyFixture.createParty("모임1", 10L, addr); + ReflectionTestUtils.setField(party1, "id", 1L); + Party party2 = PartyFixture.createParty("모임2", 10L, addr); + ReflectionTestUtils.setField(party2, "id", 2L); + Party party3 = PartyFixture.createParty("모임3", 10L, addr); + ReflectionTestUtils.setField(party3, "id", 3L); + + Slice partySlice = new SliceImpl<>(List.of(party3, party2, party1), pageable, false); + + given(partyRepository.findMyParty(eq(memberId), eq(false), any(Pageable.class))) + .willReturn(partySlice); + given(exerciseRepository.findTotalExerciseCountsByPartyIds(anyList())) + .willReturn(List.of()); + given(exerciseRepository.findUpcomingExercisesByPartyIds(anyList())) + .willReturn(List.of()); + given(partyBookmarkRepository.findAllPartyIdsByMemberId(memberId)) + .willReturn(Set.of(1L, 2L, 3L)); + + // when + Slice result = partyQueryService.getMyParties(memberId, false, "최신순", pageable); + + // then + assertThat(result.getContent()).hasSize(3); + assertThat(result.getContent().get(0).partyId()).isEqualTo(3L); + assertThat(result.getContent().get(1).partyId()).isEqualTo(2L); + assertThat(result.getContent().get(2).partyId()).isEqualTo(1L); + + org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(Pageable.class); + verify(partyRepository).findMyParty(eq(memberId), eq(false), captor.capture()); + assertThat(captor.getValue().getSort().getOrderFor("createdAt").getDirection()) + .isEqualTo(org.springframework.data.domain.Sort.Direction.DESC); + } + + @Test + @DisplayName("성공 - 내 모임 목록을 오래된 순으로 페이징하여 반환한다") + void success_getMyParties_oldest() { + // given + Long memberId = 10L; + Pageable pageable = PageRequest.of(0, 10); + + PartyAddr addr = PartyFixture.createPartyAddr("서울특별시", "강남구"); + + Party party1 = PartyFixture.createParty("모임1", 10L, addr); + ReflectionTestUtils.setField(party1, "id", 1L); + Party party2 = PartyFixture.createParty("모임2", 10L, addr); + ReflectionTestUtils.setField(party2, "id", 2L); + Party party3 = PartyFixture.createParty("모임3", 10L, addr); + ReflectionTestUtils.setField(party3, "id", 3L); + + // 오래된 순 응답 가정 + Slice partySlice = new SliceImpl<>(List.of(party1, party2, party3), pageable, false); + + given(partyRepository.findMyParty(eq(memberId), eq(false), any(Pageable.class))) + .willReturn(partySlice); + given(exerciseRepository.findTotalExerciseCountsByPartyIds(anyList())) + .willReturn(List.of()); + given(exerciseRepository.findUpcomingExercisesByPartyIds(anyList())) + .willReturn(List.of()); + given(partyBookmarkRepository.findAllPartyIdsByMemberId(memberId)) + .willReturn(Set.of()); + + // when + Slice result = partyQueryService.getMyParties(memberId, false, "오래된 순", pageable); + + // then + assertThat(result.getContent()).hasSize(3); + assertThat(result.getContent().get(0).partyId()).isEqualTo(1L); + + org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(Pageable.class); + verify(partyRepository).findMyParty(eq(memberId), eq(false), captor.capture()); + assertThat(captor.getValue().getSort().getOrderFor("createdAt").getDirection()) + .isEqualTo(org.springframework.data.domain.Sort.Direction.ASC); + } + + @Test + @DisplayName("성공 - 내 모임 목록을 운동 많은 순으로 페이징하여 반환한다") + void success_getMyParties_exerciseCount() { + // given + Long memberId = 10L; + Pageable pageable = PageRequest.of(0, 10); + + PartyAddr addr = PartyFixture.createPartyAddr("서울특별시", "강남구"); + + Party party1 = PartyFixture.createParty("모임1", 10L, addr); + ReflectionTestUtils.setField(party1, "id", 1L); + Party party2 = PartyFixture.createParty("모임2", 10L, addr); + ReflectionTestUtils.setField(party2, "id", 2L); + Party party3 = PartyFixture.createParty("모임3", 10L, addr); + ReflectionTestUtils.setField(party3, "id", 3L); + + // 운동 많은 순 응답 가정 (20회, 10회, 5회) + Slice partySlice = new SliceImpl<>(List.of(party2, party1, party3), pageable, false); + + given(partyRepository.findMyParty(eq(memberId), eq(false), any(Pageable.class))) + .willReturn(partySlice); + given(exerciseRepository.findTotalExerciseCountsByPartyIds(anyList())) + .willReturn(List.of()); + given(exerciseRepository.findUpcomingExercisesByPartyIds(anyList())) + .willReturn(List.of()); + given(partyBookmarkRepository.findAllPartyIdsByMemberId(memberId)) + .willReturn(Set.of()); + + // when + Slice result = partyQueryService.getMyParties(memberId, false, "운동 많은 순", pageable); + + // then + assertThat(result.getContent()).hasSize(3); + assertThat(result.getContent().get(0).partyId()).isEqualTo(2L); + + org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(Pageable.class); + verify(partyRepository).findMyParty(eq(memberId), eq(false), captor.capture()); + assertThat(captor.getValue().getSort().getOrderFor("exerciseCount").getDirection()) + .isEqualTo(org.springframework.data.domain.Sort.Direction.DESC); + } + + @Test + @DisplayName("실패 - 유효하지 않은 정렬 기준을 전달하면 INVALID_ORDER_TYPE 발생") + void fail_getMyParties_invalidSort() { + // given + Long memberId = 10L; + Pageable pageable = PageRequest.of(0, 10); + + // when & then + assertThatThrownBy(() -> partyQueryService.getMyParties(memberId, false, "존재하지않는정렬", pageable)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()) + .isEqualTo(PartyErrorCode.INVALID_ORDER_TYPE)); + } + } + + @Nested + @DisplayName("getSimpleMyParties") + class GetSimpleMyParties { + + @Test + @DisplayName("성공 - 유효한 회원 ID가 주어지면 가입한 모임 3개의 간략화된 목록을 반환한다") + void success_getSimpleMyParties() { + // given + Long memberId = 1L; + Pageable pageable = PageRequest.of(0, 10); + + Member member = MemberFixture.createMember("사용자", Gender.MALE, Level.A, 1001L); + ReflectionTestUtils.setField(member, "id", memberId); + PartyAddr addr = PartyFixture.createPartyAddr("서울특별시", "강남구"); + + Party party1 = PartyFixture.createParty("모임1", 10L, addr); + ReflectionTestUtils.setField(party1, "id", 1L); + Party party2 = PartyFixture.createParty("모임2", 10L, addr); + ReflectionTestUtils.setField(party2, "id", 2L); + Party party3 = PartyFixture.createParty("모임3", 10L, addr); + ReflectionTestUtils.setField(party3, "id", 3L); + + MemberParty mp1 = MemberFixture.createMemberParty(party1, member, Role.PARTY_MEMBER); + MemberParty mp2 = MemberFixture.createMemberParty(party2, member, Role.PARTY_MEMBER); + MemberParty mp3 = MemberFixture.createMemberParty(party3, member, Role.PARTY_MEMBER); + + Slice memberPartySlice = new SliceImpl<>(List.of(mp1, mp2, mp3), pageable, false); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(memberPartyRepository.findByMember(member, pageable)).willReturn(memberPartySlice); + + // when + Slice result = partyQueryService.getSimpleMyParties(memberId, pageable); + + // then + assertThat(result.getContent()).hasSize(3); + assertThat(result.getContent().get(0).partyId()).isEqualTo(1L); + assertThat(result.getContent().get(1).partyId()).isEqualTo(2L); + assertThat(result.getContent().get(2).partyId()).isEqualTo(3L); + + verify(memberRepository).findById(memberId); + verify(memberPartyRepository).findByMember(member, pageable); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원일 경우 MemberException을 던진다") + void fail_getSimpleMyParties_memberNotFound() { + // given + Long invalidMemberId = 999L; + Pageable pageable = PageRequest.of(0, 10); + + given(memberRepository.findById(invalidMemberId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyQueryService.getSimpleMyParties(invalidMemberId, pageable)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat( + ((MemberException) e) + .getCode()) + .isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("getRecommendedParties") + class GetRecommendedParties { + + @Test + @DisplayName("성공 - Cockple 추천 모드 시 유저 정보(주소, 생년월일, 키워드)를 기반으로 추천 목록을 반환한다") + void success_getRecommendedParties_cockpleRecommend() { + // given + Long memberId = 1L; + Pageable pageable = PageRequest.of(0, 10); + PartyFilterDTO.Request filter = PartyFilterDTO.Request.builder().build(); + + Member member = MemberFixture.createMember("매니저", Gender.MALE, Level.A, 1001L, + LocalDate.of(1995, 1, 1)); + ReflectionTestUtils.setField(member, "id", memberId); + + MemberAddr addr = MemberAddr.builder() + .member(member) + .addr1("서울특별시") + .isMain(true) + .build(); + + Party suggestedParty = PartyFixture.createParty("추천 모임", 2L, + PartyFixture.createPartyAddr("서울특별시", "강남구")); + ReflectionTestUtils.setField(suggestedParty, "id", 100L); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(memberAddrRepository.findByMemberAndIsMain(member, true)).willReturn(Optional.of(addr)); + given(partyRepository.findRecommendedParties(anyString(), anyInt(), any(), any(), anyLong())) + .willReturn(List.of(suggestedParty)); + given(partyBookmarkRepository.findAllPartyIdsByMemberId(memberId)).willReturn(Set.of()); + + // when + Slice result = partyQueryService.getRecommendedParties(memberId, true, + filter, "최신순", pageable); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).partyName()).isEqualTo("추천 모임"); + verify(partyRepository).findRecommendedParties(eq("서울특별시"), eq(1995), eq(Gender.MALE), + eq(Level.A), eq(memberId)); + } + + @Test + @DisplayName("성공 - 필터 모드 시 조건에 맞는 모임 목록을 최신순으로 반환한다") + void success_getRecommendedParties_latest() { + // given + Long memberId = 1L; + Pageable pageable = PageRequest.of(0, 10); + PartyFilterDTO.Request filter = PartyFilterDTO.Request.builder().addr1("서울특별시").build(); + + PartyAddr addr = PartyFixture.createPartyAddr("서울특별시", "강남구"); + Party p1 = PartyFixture.createParty("모임1", 2L, addr); + ReflectionTestUtils.setField(p1, "id", 1L); + Party p2 = PartyFixture.createParty("모임2", 2L, addr); + ReflectionTestUtils.setField(p2, "id", 2L); + Party p3 = PartyFixture.createParty("모임3", 2L, addr); + ReflectionTestUtils.setField(p3, "id", 3L); + + Slice partySlice = new SliceImpl<>(List.of(p3, p2, p1), pageable, false); + + given(partyRepository.searchParties(eq(memberId), eq(filter), any(Pageable.class))) + .willReturn(partySlice); + given(partyBookmarkRepository.findAllPartyIdsByMemberId(memberId)).willReturn(Set.of()); + + // when + Slice result = partyQueryService.getRecommendedParties(memberId, false, filter, "최신순", pageable); + + // then + assertThat(result.getContent()).hasSize(3); + assertThat(result.getContent().get(0).partyId()).isEqualTo(3L); + + org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(Pageable.class); + verify(partyRepository).searchParties(eq(memberId), eq(filter), captor.capture()); + assertThat(captor.getValue().getSort().getOrderFor("createdAt").getDirection()) + .isEqualTo(org.springframework.data.domain.Sort.Direction.DESC); + } + + @Test + @DisplayName("성공 - 필터 모드 시 조건에 맞는 모임 목록을 오래된 순으로 반환한다") + void success_getRecommendedParties_oldest() { + // given + Long memberId = 1L; + Pageable pageable = PageRequest.of(0, 10); + PartyFilterDTO.Request filter = PartyFilterDTO.Request.builder().build(); + + PartyAddr addr = PartyFixture.createPartyAddr("서울특별시", "강남구"); + Party p1 = PartyFixture.createParty("모임1", 2L, addr); + ReflectionTestUtils.setField(p1, "id", 1L); + Party p2 = PartyFixture.createParty("모임2", 2L, addr); + ReflectionTestUtils.setField(p2, "id", 2L); + Party p3 = PartyFixture.createParty("모임3", 2L, addr); + ReflectionTestUtils.setField(p3, "id", 3L); + + Slice partySlice = new SliceImpl<>(List.of(p1, p2, p3), pageable, false); + + given(partyRepository.searchParties(eq(memberId), eq(filter), any(Pageable.class))) + .willReturn(partySlice); + given(partyBookmarkRepository.findAllPartyIdsByMemberId(memberId)).willReturn(Set.of()); + + // when + Slice result = partyQueryService.getRecommendedParties(memberId, false, filter, "오래된 순", pageable); + + // then + assertThat(result.getContent()).hasSize(3); + assertThat(result.getContent().get(0).partyId()).isEqualTo(1L); + + org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(Pageable.class); + verify(partyRepository).searchParties(eq(memberId), eq(filter), captor.capture()); + assertThat(captor.getValue().getSort().getOrderFor("createdAt").getDirection()) + .isEqualTo(org.springframework.data.domain.Sort.Direction.ASC); + } + + @Test + @DisplayName("성공 - 필터 모드 시 조건에 맞는 모임 목록을 운동 많은 순으로 반환한다") + void success_getRecommendedParties_exerciseCount() { + // given + Long memberId = 1L; + Pageable pageable = PageRequest.of(0, 10); + PartyFilterDTO.Request filter = PartyFilterDTO.Request.builder().build(); + + PartyAddr addr = PartyFixture.createPartyAddr("서울특별시", "강남구"); + Party p1 = PartyFixture.createParty("모임1", 2L, addr); + ReflectionTestUtils.setField(p1, "id", 1L); + Party p2 = PartyFixture.createParty("모임2", 2L, addr); + ReflectionTestUtils.setField(p2, "id", 2L); + Party p3 = PartyFixture.createParty("모임3", 2L, addr); + ReflectionTestUtils.setField(p3, "id", 3L); + + Slice partySlice = new SliceImpl<>(List.of(p2, p1, p3), pageable, false); // 20회, 10회, 5회 가정 + + given(partyRepository.searchParties(eq(memberId), eq(filter), any(Pageable.class))) + .willReturn(partySlice); + given(partyBookmarkRepository.findAllPartyIdsByMemberId(memberId)).willReturn(Set.of()); + + // when + Slice result = partyQueryService.getRecommendedParties(memberId, false, filter, "운동 많은 순", pageable); + + // then + assertThat(result.getContent()).hasSize(3); + assertThat(result.getContent().get(0).partyId()).isEqualTo(2L); + + org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(Pageable.class); + verify(partyRepository).searchParties(eq(memberId), eq(filter), captor.capture()); + assertThat(captor.getValue().getSort().getOrderFor("exerciseCount").getDirection()) + .isEqualTo(org.springframework.data.domain.Sort.Direction.DESC); + } + + @Test + @DisplayName("성공 - 검색 모드 시 검색 키워드에 맞는 모임 목록을 반환한다") + void success_getRecommendedParties_search() { + // given + Long memberId = 1L; + Pageable pageable = PageRequest.of(0, 10); + PartyFilterDTO.Request filter = PartyFilterDTO.Request.builder().search("검색값").build(); + + PartyAddr addr = PartyFixture.createPartyAddr("서울특별시", "강남구"); + Party party = PartyFixture.createParty("검색결과모임", 2L, addr); + ReflectionTestUtils.setField(party, "id", 100L); + Slice partySlice = new SliceImpl<>(List.of(party), pageable, false); + + given(partyRepository.searchParties(eq(memberId), eq(filter), any(Pageable.class))) + .willReturn(partySlice); + given(partyBookmarkRepository.findAllPartyIdsByMemberId(memberId)).willReturn(Set.of()); + + // when + Slice result = partyQueryService.getRecommendedParties(memberId, false, filter, "최신순", pageable); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).partyName()).isEqualTo("검색결과모임"); + verify(partyRepository).searchParties(eq(memberId), eq(filter), any(Pageable.class)); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원 ID로 추천 요청 시 MEMBER_NOT_FOUND이 발생한다") + void fail_getRecommendedParties_memberNotFound() { + // given + Long memberId = 999L; + Pageable pageable = PageRequest.of(0, 10); + PartyFilterDTO.Request filter = PartyFilterDTO.Request.builder().build(); + + given(memberRepository.findById(memberId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyQueryService.getRecommendedParties(memberId, true, filter, "최신순", + pageable)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat( + ((MemberException) e) + .getCode()) + .isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + + @Test + @DisplayName("실패 - 대표 주소가 설정되지 않은 회원이 추천 요청 시 MAIN_ADDRESS_NULL이 발생한다") + void fail_getRecommendedParties_mainAddressNotFound() { + // given + Long memberId = 1L; + Pageable pageable = PageRequest.of(0, 10); + PartyFilterDTO.Request filter = PartyFilterDTO.Request.builder().build(); + + Member member = MemberFixture.createMember("매니저", Gender.MALE, Level.A, 1001L); + ReflectionTestUtils.setField(member, "id", memberId); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(memberAddrRepository.findByMemberAndIsMain(member, true)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyQueryService.getRecommendedParties(memberId, true, filter, "최신순", + pageable)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat( + ((MemberException) e) + .getCode()) + .isEqualTo(MemberErrorCode.MAIN_ADDRESS_NULL)); + } + } + + @Nested + @DisplayName("getPartyDetails") + class GetPartyDetails { + + @Test + @DisplayName("성공 - 모임 상세 정보를 정상적으로 조회한다 (비회원, 신청 전)") + void success_getPartyDetails_nonMember() { + // given + Long partyId = 1L; + Long memberId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("상세 모임", 11L, addr); + ReflectionTestUtils.setField(party, "id", partyId); + Member member = MemberFixture.createMember("사용자", Gender.MALE, Level.A, 1000L); + ReflectionTestUtils.setField(member, "id", memberId); + + PartyDetailDTO.Response expected = PartyDetailDTO.Response.builder() + .partyId(partyId) + .partyName("상세 모임") + .memberStatus("NOT_MEMBER") + .hasPendingJoinRequest(false) + .build(); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(memberPartyRepository.findByPartyAndMember(party, member)).willReturn(Optional.empty()); + given(partyBookmarkRepository.existsByMemberAndParty(member, party)).willReturn(false); + given(partyJoinRequestRepository.existsByPartyAndMemberAndStatus(party, member, + RequestStatus.PENDING)).willReturn(false); + + // when + PartyDetailDTO.Response result = partyQueryService.getPartyDetails(partyId, memberId); + + // then + assertThat(result.partyId()).isEqualTo(partyId); + assertThat(result.partyName()).isEqualTo("상세 모임"); + assertThat(result.memberStatus()).isEqualTo("NOT_MEMBER"); + assertThat(result.hasPendingJoinRequest()).isFalse(); + assertThat(result.isBookmarked()).isFalse(); + verify(partyRepository).findById(partyId); + } + + @Test + @DisplayName("성공 - 모임원인 경우 memberStatus가 MEMBER로 반환된다") + void success_getPartyDetails_member() { + // given + Long partyId = 1L; + Long memberId = 10L; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("상세 모임", 11L, addr); + ReflectionTestUtils.setField(party, "id", partyId); + Member member = MemberFixture.createMember("사용자", Gender.MALE, Level.A, 1000L); + ReflectionTestUtils.setField(member, "id", memberId); + + MemberParty memberParty = MemberFixture.createMemberParty(party, member, Role.PARTY_MEMBER); + PartyDetailDTO.Response expected = PartyDetailDTO.Response.builder() + .partyId(partyId) + .memberStatus("MEMBER") + .memberRole("PARTY_MEMBER") + .build(); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(memberPartyRepository.findByPartyAndMember(party, member)) + .willReturn(Optional.of(memberParty)); + given(partyBookmarkRepository.existsByMemberAndParty(member, party)).willReturn(true); + + // when + PartyDetailDTO.Response result = partyQueryService.getPartyDetails(partyId, memberId); + + // then + assertThat(result.memberStatus()).isEqualTo("MEMBER"); + assertThat(result.memberRole()).isEqualTo("PARTY_MEMBER"); + assertThat(result.hasPendingJoinRequest()).isNull(); // 멤버이므로 null 반환 + assertThat(result.isBookmarked()).isTrue(); + } + + @Test + @DisplayName("실패 - 존재하지 않는 모임 조회 시 PARTY_NOT_FOUND이 발생한다") + void fail_getPartyDetails_partyNotFound() { + // given + given(partyRepository.findById(999L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyQueryService.getPartyDetails(999L, 1L)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()) + .isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); + } + + @Test + @DisplayName("실패 - 삭제된 모임 조회 시 PARTY_IS_DELETED이 발생한다") + void fail_getPartyDetails_partyDeleted() { + // given + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("삭제된 모임", 11L, addr); + party.delete(); + given(partyRepository.findById(1L)).willReturn(Optional.of(party)); + given(memberRepository.findById(1L)).willReturn( + Optional.of(MemberFixture.createMember("테스터", Gender.MALE, Level.A, 1L))); + + // when & then + assertThatThrownBy(() -> partyQueryService.getPartyDetails(1L, 1L)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()) + .isEqualTo(PartyErrorCode.PARTY_IS_DELETED)); + } + + @Test + @DisplayName("실패 - 존재하지 않는 회원인 경우 MEMBER_NOT_FOUND 예외 발생") + void fail_getPartyDetails_memberNotFound() { + // given + Long partyId = 1L; + Party party = PartyFixture.createParty("상세 모임", 11L, null); + ReflectionTestUtils.setField(party, "id", partyId); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findById(1L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyQueryService.getPartyDetails(partyId, 1L)) + .isInstanceOf(MemberException.class) + .satisfies(e -> assertThat(((MemberException) e).getCode()) + .isEqualTo(MemberErrorCode.MEMBER_NOT_FOUND)); + } + } + + @Nested + @DisplayName("getJoinRequests") + class GetJoinRequests { + + @Test + @DisplayName("성공 - 모임장이 가입 신청 목록을 정상적으로 조회한다") + void success_getJoinRequests() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Pageable pageable = PageRequest.of(0, 10); + String status = "PENDING"; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + Member applicant = MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 20L); + ReflectionTestUtils.setField(applicant, "id", 20L); + + PartyJoinRequest joinRequest = PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.PENDING) + .build(); + ReflectionTestUtils.setField(joinRequest, "id", 100L); + + Slice requestSlice = new SliceImpl<>(List.of(joinRequest), pageable, false); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + + given(partyJoinRequestRepository.findByPartyAndStatus(party, RequestStatus.PENDING, pageable)) + .willReturn(requestSlice); + + // when + Slice result = partyQueryService.getJoinRequests(partyId, ownerId, status, pageable); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).joinRequestId()).isEqualTo(100L); + verify(partyJoinRequestRepository).findByPartyAndStatus(party, RequestStatus.PENDING, pageable); + } + + @Test + @DisplayName("성공 - 모임장이 가입 승인된 멤버 목록(APPROVED)을 정상적으로 조회한다") + void success_getJoinRequests_approved() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Pageable pageable = PageRequest.of(0, 10); + String status = "APPROVED"; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member applicant = MemberFixture.createMember("지원자", Gender.FEMALE, Level.B, 20L); + ReflectionTestUtils.setField(applicant, "id", 20L); + + PartyJoinRequest joinRequest = PartyJoinRequest.builder() + .party(party) + .member(applicant) + .status(RequestStatus.APPROVED) + .build(); + ReflectionTestUtils.setField(joinRequest, "id", 101L); + + Slice requestSlice = new SliceImpl<>(List.of(joinRequest), pageable, false); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + + given(partyJoinRequestRepository.findByPartyAndStatus(party, RequestStatus.APPROVED, pageable)) + .willReturn(requestSlice); + + // when + Slice result = partyQueryService.getJoinRequests(partyId, ownerId, status, pageable); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).joinRequestId()).isEqualTo(101L); + verify(partyJoinRequestRepository).findByPartyAndStatus(party, RequestStatus.APPROVED, pageable); + } + + @Test + @DisplayName("실패 - 모임장이 아닌 사용자가 조회하면 INSUFFICIENT_PERMISSION 발생") + void fail_getJoinRequests_notOwner() { + // given + Long partyId = 1L; + Long nonOwnerId = 20L; + Pageable pageable = PageRequest.of(0, 10); + String status = "PENDING"; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("모임명", 10L, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member nonOwner = MemberFixture.createMember("일반멤버", Gender.FEMALE, Level.B, nonOwnerId); + ReflectionTestUtils.setField(nonOwner, "id", nonOwnerId); + MemberParty nonOwnerParty = MemberFixture.createMemberParty(party, nonOwner, Role.PARTY_MEMBER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + + + // when & then + assertThatThrownBy(() -> partyQueryService.getJoinRequests(partyId, nonOwnerId, status, pageable)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.INSUFFICIENT_PERMISSION)); + } + + @Test + @DisplayName("실패 - 잘못된 상태값을 입력하면 INVALID_REQUEST_STATUS 발생") + void fail_getJoinRequests_invalidStatus() { + // given + Long partyId = 1L; + Long ownerId = 10L; + Pageable pageable = PageRequest.of(0, 10); + String invalidStatus = "UNKNOWN"; + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("모임명", ownerId, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member owner = MemberFixture.createMember("모임장", Gender.MALE, Level.A, ownerId); + ReflectionTestUtils.setField(owner, "id", ownerId); + MemberParty ownerParty = MemberFixture.createMemberParty(party, owner, Role.PARTY_MANAGER); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + + + // when & then + assertThatThrownBy(() -> partyQueryService.getJoinRequests(partyId, ownerId, invalidStatus, pageable)) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()).isEqualTo(PartyErrorCode.INVALID_REQUEST_STATUS)); + } + + @Test + @DisplayName("실패 - 존재하지 않는 파티인 경우 PARTY_NOT_FOUND 예외 발생") + void fail_getJoinRequests_partyNotFound() { + // given + Long invalidId = 999L; + given(partyRepository.findById(invalidId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> partyQueryService.getJoinRequests(invalidId, 1L, "PENDING", PageRequest.of(0, 10))) + .isInstanceOf(PartyException.class) + .satisfies(e -> assertThat(((PartyException) e).getCode()) + .isEqualTo(PartyErrorCode.PARTY_NOT_FOUND)); } } + @Nested + @DisplayName("getRecommendedMembers") + class GetRecommendedMembers { + + @Test + @DisplayName("성공 - 조건에 맞는 추천 멤버 목록을 정상적으로 조회한다") + void success_getRecommendedMembers() { + // given + Long partyId = 1L; + String levelSearch = "B"; + Pageable pageable = PageRequest.of(0, 10); + + PartyAddr addr = PartyFixture.createPartyAddr("서울", "강남"); + Party party = PartyFixture.createParty("모임명", 1L, addr); + ReflectionTestUtils.setField(party, "id", partyId); + + Member suggestedMember = MemberFixture.createMember("추천회원", Gender.MALE, Level.B, 20L); + ReflectionTestUtils.setField(suggestedMember, "id", 20L); + + Slice membersSlice = new SliceImpl<>(List.of(suggestedMember), pageable, false); + + given(partyRepository.findById(partyId)).willReturn(Optional.of(party)); + given(memberRepository.findRecommendedMembers(party, levelSearch, pageable)) + .willReturn(membersSlice); + + // when + Slice result = partyQueryService.getRecommendedMembers(partyId, levelSearch, pageable); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).userId()).isEqualTo(20L); + verify(memberRepository).findRecommendedMembers(party, levelSearch, pageable); + } + + @Test + @DisplayName("실패 - 존재하지 않는 모임의 추천 멤버를 조회하면 PARTY_NOT_FOUND 발생") + void fail_getRecommendedMembers_partyNotFound() { + // given + Long partyId = 999L; + String levelSearch = null; + Pageable pageable = PageRequest.of(0, 10); + + given(partyRepository.findById(partyId)).willReturn(Optional.empty()); + + // when & then + PartyException exception = assertThrows(PartyException.class, + () -> partyQueryService.getRecommendedMembers(partyId, levelSearch, pageable)); + assertThat(exception.getCode()).isEqualTo(PartyErrorCode.PARTY_NOT_FOUND); + } + } }