+
+
+ HERE:WE
+
+
+
+
+ 🚀 크루 초대 링크 안내
+
+
+
+ 안녕하세요, 닉네임님!
+ 아래 링크를 눌러 초대된 크루에 참여해주세요🥳
+
+
+
+
+ 🌟 크루 이름
+
+
+ 크루 설명
+
+
+ 현재 참여인원: 0명
+
+
+
+
+
+
+ 이 초대 링크는 7일 후에 만료됩니다.
+
+
+
+
+ 본 메일은 HERE:WE 시스템에 의해 자동 발송되었습니다.
+ © 2025 HERE:WE. All Rights Reserved.
+
+
+
+
+
diff --git a/src/test/java/com/genius/herewe/business/crew/facade/CrewFacadeTest.java b/src/test/java/com/genius/herewe/business/crew/facade/CrewFacadeTest.java
new file mode 100644
index 0000000..f7352c3
--- /dev/null
+++ b/src/test/java/com/genius/herewe/business/crew/facade/CrewFacadeTest.java
@@ -0,0 +1,457 @@
+package com.genius.herewe.business.crew.facade;
+
+import static com.genius.herewe.core.global.exception.ErrorCode.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.*;
+
+import java.util.Optional;
+
+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.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.genius.herewe.business.crew.domain.Crew;
+import com.genius.herewe.business.crew.domain.CrewMember;
+import com.genius.herewe.business.crew.domain.CrewRole;
+import com.genius.herewe.business.crew.dto.CrewCreateRequest;
+import com.genius.herewe.business.crew.dto.CrewExpelRequest;
+import com.genius.herewe.business.crew.dto.CrewModifyRequest;
+import com.genius.herewe.business.crew.dto.CrewPreviewResponse;
+import com.genius.herewe.business.crew.dto.CrewProfileResponse;
+import com.genius.herewe.business.crew.dto.CrewResponse;
+import com.genius.herewe.business.crew.fixture.CrewFixture;
+import com.genius.herewe.business.crew.service.CrewMemberService;
+import com.genius.herewe.business.crew.service.CrewService;
+import com.genius.herewe.core.global.exception.BusinessException;
+import com.genius.herewe.core.user.domain.User;
+import com.genius.herewe.core.user.fixture.UserFixture;
+import com.genius.herewe.core.user.service.UserService;
+import com.genius.herewe.infra.file.service.FilesStorage;
+
+@ExtendWith(MockitoExtension.class)
+class CrewFacadeTest {
+ private CrewFacade crewFacade;
+ @Mock
+ private UserService userService;
+ @Mock
+ private CrewService crewService;
+ @Mock
+ private CrewMemberService crewMemberService;
+ @Mock
+ private FilesStorage filesStorage;
+
+ @BeforeEach
+ void init() {
+ crewFacade = new DefaultCrewFacade(userService, crewService, crewMemberService, filesStorage);
+ }
+
+ @Nested
+ @DisplayName("크루에 대한 나의 정보 조회 시")
+ class Context_inquiry_my_crew_info {
+ Long userId = 1L;
+ Long crewId = 1L;
+ User user = UserFixture.createById(userId);
+ CrewMember crewMember = CrewMember.createByRole(CrewRole.MEMBER);
+
+ @Nested
+ @DisplayName("사용자/크루 정보를 조회하기 위해 정보를 전달했을 때")
+ class Describe_pass_info_to_inquiry_crew_info {
+ @Test
+ @DisplayName("사용자 식별자를 통해 사용자를 조회할 수 없으면 MEMBER_NOT_FOUND 예외가 발생한다.")
+ public void it_throws_MEMBER_NOT_FOUND_exception() {
+ //given
+ Long fakeUserId = 999L;
+ given(userService.findById(fakeUserId)).willThrow(new BusinessException(MEMBER_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.inquiryCrewProfile(fakeUserId, crewId))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(MEMBER_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("사용자/크루 식별자를 통해 크루 참여 정보를 조회할 수 없으면 CREW_JOIN_INFO_NOT_FOUND 예외가 발생한다.")
+ public void it_throws_CREW_JOIN_INFO_NOT_FOUND_exception() {
+ //given
+ Long fakeUserId = 999L;
+ given(crewMemberService.find(fakeUserId, crewId)).willThrow(
+ new BusinessException(CREW_JOIN_INFO_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.inquiryCrewProfile(fakeUserId, crewId))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(CREW_JOIN_INFO_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("엔티티 조회에 문제가 없다면 사용자의 정보와 크루 권한 정보를 받아온다.")
+ public void it_return_my_crew_info() {
+ //given
+ given(userService.findById(userId)).willReturn(user);
+ given(crewMemberService.find(userId, crewId)).willReturn(crewMember);
+
+ //when
+ CrewProfileResponse crewProfileResponse = crewFacade.inquiryCrewProfile(userId, crewId);
+
+ //then
+ assertThat(crewProfileResponse.nickname()).isEqualTo(user.getNickname());
+ assertThat(crewProfileResponse.crewRole()).isEqualTo(crewMember.getRole());
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("Crew 생성 시")
+ class Context_create_crew {
+ CrewCreateRequest crewCreateRequest = new CrewCreateRequest("name", "introduce");
+
+ @Nested
+ @DisplayName("생성하려는 사용자의 PK(userId)를 전달했을 때")
+ class Describe_pass_userId {
+ @Test
+ @DisplayName("PK에 해당하는 사용자가 없으면 MEMBER_NOT_FOUND 예외가 발생한다")
+ public void it_throws_MEMBER_NOT_FOUND_exception() {
+ //given
+ Long userId = 999L;
+ given(userService.findById(userId)).willThrow(new BusinessException(MEMBER_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.createCrew(userId, crewCreateRequest))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(MEMBER_NOT_FOUND.getMessage());
+ }
+ }
+
+ @Nested
+ @DisplayName("크루 생성에 필요한 DTO를 전달했을 때")
+ class Describe_pass_CrewCreateRequest {
+ @Test
+ @DisplayName("크루 이름과 소개를 전달하면 크루 생성이 정상적으로 완료된다.")
+ public void it_return_crew() {
+ //given
+ User user = UserFixture.createDefault();
+ Crew crew = CrewFixture.createByName(crewCreateRequest.name());
+ CrewMember crewMember = CrewMember.createByRole(CrewRole.LEADER);
+ crewMember.joinCrew(user, crew);
+
+ given(userService.findById(user.getId())).willReturn(user);
+ given(crewService.save(any(Crew.class))).willReturn(crew);
+ given(crewMemberService.save(any(CrewMember.class))).willReturn(crewMember);
+
+ //when
+ CrewPreviewResponse crewPreviewResponse = crewFacade.createCrew(user.getId(), crewCreateRequest);
+
+ //then
+ assertThat(crewPreviewResponse).isNotNull();
+ assertThat(crewPreviewResponse.name()).isEqualTo(crewCreateRequest.name());
+ assertThat(crewPreviewResponse.participantCount()).isEqualTo(1);
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("Crew 수정 시")
+ class Context_modify_crew {
+ CrewModifyRequest modifyRequest = new CrewModifyRequest("new name", "new introduce");
+
+ @Nested
+ @DisplayName("crew 조회를 위한 데이터 전달 시")
+ class Describe_pass_crew_info {
+ @Test
+ @DisplayName("crewId를 전달했을 때 크루를 찾을 수 없다면 CREW_NOT_FOUND 예외를 발생한다.")
+ public void it_throws_CREW_NOT_FOUND_exception() {
+ //given
+ Long userId = 1L;
+ Long crewId = 999L;
+ given(crewService.findById(crewId)).willThrow(new BusinessException(CREW_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.modifyCrew(userId, crewId, modifyRequest))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(CREW_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("crew 참여 정보를 조회하지 못한다면 CREW_JOIN_INFO_NOT_FOUND 예외를 발생한다.")
+ public void it_throws_CREW_JOIN_INFO_NOT_FOUND_exception() {
+ //given
+ Long userId = 1L;
+ Long crewId = 1L;
+ Crew crew = CrewFixture.createDefault();
+ given(crewService.findById(crewId)).willReturn(crew);
+ given(crewMemberService.find(userId, crewId)).willThrow(
+ new BusinessException(CREW_JOIN_INFO_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.modifyCrew(userId, crewId, modifyRequest))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(CREW_JOIN_INFO_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("크루 리더가 아니라면 LEADER_PERMISSION_DENIED 예외가 발생한다.")
+ public void it_throws_LEADER_PERMISSION_DENIED_exception() {
+ //given
+ Long userId = 1L;
+ Long crewId = 1L;
+ Crew crew = CrewFixture.createDefault();
+ CrewMember crewMember = CrewMember.createByRole(CrewRole.MEMBER);
+
+ given(crewService.findById(crewId)).willReturn(crew);
+ given(crewMemberService.find(userId, crewId)).willReturn(crewMember);
+
+ //when&then
+ assertThatThrownBy(() -> crewFacade.modifyCrew(userId, crewId, modifyRequest))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(LEADER_PERMISSION_DENIED.getMessage());
+ }
+ }
+
+ @Nested
+ @DisplayName("크루 수정에 필요한 정보 전달 시")
+ class Describe_pass_modify_info {
+ @Test
+ @DisplayName("이름과 소개글을 수정할 수 있다.")
+ public void it_modify_crew() {
+ //given
+ Long userId = 1L;
+ Long crewId = 1L;
+ Crew crew = CrewFixture.createDefault();
+ CrewMember crewMember = CrewMember.createByRole(CrewRole.LEADER);
+
+ given(crewService.findById(crewId)).willReturn(crew);
+ given(crewMemberService.find(userId, crewId)).willReturn(crewMember);
+
+ //when
+ CrewPreviewResponse modified = crewFacade.modifyCrew(userId, crewId, modifyRequest);
+
+ //then
+ assertThat(modified).isNotNull();
+ assertThat(modified.crewId()).isEqualTo(crewId);
+ assertThat(modified.name()).isEqualTo(modifyRequest.name());
+ assertThat(modified.participantCount()).isEqualTo(crew.getParticipantCount());
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("크루의 정보 조회 시")
+ class Context_inquiry_crew_info {
+ @Nested
+ @DisplayName("crew 조회를 위한 데이터 전달 시")
+ class Describe_pass_crew_info {
+ @Test
+ @DisplayName("crewId를 전달했을 때 크루를 찾을 수 없다면 CREW_NOT_FOUND 예외를 발생한다.")
+ public void it_throws_CREW_NOT_FOUND_exception() {
+ //given
+ Long userId = 1L;
+ Long crewId = 999L;
+ given(crewService.findById(crewId)).willThrow(new BusinessException(CREW_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.inquiryCrew(userId, crewId))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(CREW_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("crew 참여 정보를 조회하지 못한다면 CREW_JOIN_INFO_NOT_FOUND 예외를 발생한다.")
+ public void it_throws_CREW_JOIN_INFO_NOT_FOUND_exception() {
+ //given
+ Long userId = 1L;
+ Long crewId = 1L;
+ Crew crew = CrewFixture.createDefault();
+ given(crewService.findById(crewId)).willReturn(crew);
+ given(crewMemberService.find(userId, crewId)).willThrow(
+ new BusinessException(CREW_JOIN_INFO_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.inquiryCrew(userId, crewId))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(CREW_JOIN_INFO_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("crew 데이터 조회에 성공한다면, 크루의 정보를 전달한다.")
+ public void it_throws_crew_info() {
+ //given
+ Long userId = 1L;
+ Long crewId = 1L;
+ Crew crew = CrewFixture.createDefault();
+ CrewMember crewMember = CrewMember.createByRole(CrewRole.MEMBER);
+ given(crewService.findById(crewId)).willReturn(crew);
+ given(crewMemberService.find(userId, crewId)).willReturn(crewMember);
+
+ //when
+ CrewResponse crewResponse = crewFacade.inquiryCrew(userId, crewId);
+
+ //then
+ assertThat(crewResponse).isNotNull();
+ assertThat(crewResponse.crewId()).isEqualTo(crewId);
+ assertThat(crewResponse.name()).isEqualTo(crew.getName());
+ assertThat(crewResponse.introduce()).isEqualTo(crew.getIntroduce());
+ assertThat(crewResponse.leaderName()).isEqualTo(crew.getLeaderName());
+ assertThat(crewResponse.role()).isEqualTo(crewMember.getRole());
+ assertThat(crewResponse.participantCount()).isEqualTo(crew.getParticipantCount());
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("크루 삭제 시")
+ class Context_delete_crew {
+ @Nested
+ @DisplayName("크루의 식별자(PK)를 전달했을 때")
+ class Describe_pass_crew_pk {
+ @Test
+ @DisplayName("크루의 식별자를 통해 크루를 찾지 못하면 CREW_NOT_FOUND 예외가 발생한다.")
+ public void it_throws_CREW_NOT_FOUND_exception() {
+ //given
+ Long fakeCrewId = 999L;
+ given(crewService.findById(fakeCrewId)).willThrow(new BusinessException(CREW_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.deleteCrew(fakeCrewId))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(CREW_NOT_FOUND.getMessage());
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("특정 사용자를 크루에서 내보낼 때")
+ class Context_expel_from_crew {
+ User user = UserFixture.createDefault();
+ User targetUser = UserFixture.builder().id(2L).nickname("expel target").build();
+ Crew crew = CrewFixture.createDefault();
+ Long userId = user.getId();
+ Long crewId = crew.getId();
+ CrewExpelRequest expelRequest = new CrewExpelRequest(crewId, targetUser.getNickname());
+
+ @Nested
+ @DisplayName("요청한 사용자의 정보를 전달받았을 때")
+ class Describe_pass_request_user_info {
+ @Test
+ @DisplayName("사용자 식별자를 통해 조회했을 때 사용자가 존재하지 않는다면 MEMBER_NOT_FOUND 예외가 발생한다.")
+ public void it_throws_MEMBER_NOT_FOUND_exception() {
+ //given
+ Long fakeUserId = 999L;
+ given(userService.findById(argThat(id -> !id.equals(userId))))
+ .willThrow(new BusinessException(MEMBER_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.expelCrew(fakeUserId, expelRequest))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(MEMBER_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("크루 식별자를 통해 크루를 조회할 때, 존재하지 않으면 CREW_NOT_FOUND 예외가 발생한다.")
+ public void it_throws_CREW_NOT_FOUND_exception() {
+ //given
+ Long fakeCrewId = 999L;
+ given(crewService.findById(argThat(id -> !id.equals(expelRequest.crewId()))))
+ .willThrow(new BusinessException(CREW_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(
+ () -> crewFacade.expelCrew(userId, new CrewExpelRequest(fakeCrewId, user.getNickname())))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(CREW_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("사용자가 크루에 대해 크루 가입 정보가 없을 때 CREW_JOIN_INFO_NOT_FOUND 예외가 발생한다.")
+ public void it_throws_CREW_JOIN_INFO_NOT_FOUND_exception() {
+ //given
+ given(userService.findById(userId)).willReturn(user);
+ given(crewService.findById(crewId)).willReturn(crew);
+ given(crewMemberService.find(userId, crewId))
+ .willReturn(CrewMember.createByRole(CrewRole.LEADER));
+ given(crewMemberService.find(userId, crewId))
+ .willThrow(new BusinessException(CREW_JOIN_INFO_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.expelCrew(userId, expelRequest))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(CREW_JOIN_INFO_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("사용자의 ROLE을 확인했을 때 LEADER가 아니라면 LEADER_PERMISSION_DENIED 예외가 발생한다.")
+ public void it_throws_LEADER_PERMISSION_DENIED_exception() {
+ //given
+ given(userService.findById(userId)).willReturn(user);
+ given(crewService.findById(crewId)).willReturn(crew);
+ given(crewMemberService.find(userId, crewId))
+ .willReturn(CrewMember.createByRole(CrewRole.MEMBER));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.expelCrew(userId, expelRequest))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(LEADER_PERMISSION_DENIED.getMessage());
+ }
+ }
+
+ @Nested
+ @DisplayName("크루에서 내보낼 사용자의 정보를 받아서 조회했을 때")
+ class Describe_pass_target_nickname {
+ @BeforeEach
+ void init() {
+ given(userService.findById(userId)).willReturn(user);
+ given(crewService.findById(crewId)).willReturn(crew);
+ }
+
+ @Test
+ @DisplayName("닉네임을 통해 사용자를 조회할 수 없을 때 MEMBER_NOT_FOUND 예외가 발생한다.")
+ public void it_throws_MEMBER_NOT_FOUND_exception() {
+ //given
+ String fakeNickname = "fake nickname";
+ given(crewMemberService.find(userId, crewId)).willReturn(CrewMember.createByRole(CrewRole.LEADER));
+ given(userService.findByNickname(argThat(nickname -> !nickname.equals(user.getNickname()))))
+ .willThrow(new BusinessException(MEMBER_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.expelCrew(userId, new CrewExpelRequest(crewId, fakeNickname)))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(MEMBER_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("사용자가 해당 크루의 멤버가 아닌 경우 CREW_JOIN_INFO_NOT_FOUND 예외가 발생한다.")
+ public void it_throws_CREW_JOIN_INFO_NOT_FOUND_exception() {
+ //given
+ given(crewMemberService.find(userId, crewId))
+ .willReturn(CrewMember.createByRole(CrewRole.LEADER));
+ given(userService.findByNickname(targetUser.getNickname())).willReturn(Optional.of(targetUser));
+ given(crewMemberService.find(targetUser.getId(), crewId)).willThrow(
+ new BusinessException(CREW_JOIN_INFO_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.expelCrew(userId, expelRequest))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(CREW_JOIN_INFO_NOT_FOUND.getMessage());
+ }
+
+ @Test
+ @DisplayName("사용자가 해당 크루의 리더인 경우 LEADER_CANNOT_EXPEL 예외가 발생한다.")
+ public void it_throws_LEADER_CANNOT_EXPEL_exception() {
+ //given
+ given(userService.findByNickname(targetUser.getNickname())).willReturn(Optional.of(targetUser));
+ given(crewMemberService.find(userId, crewId))
+ .willReturn(CrewMember.createByRole(CrewRole.LEADER));
+ given(crewMemberService.find(targetUser.getId(), crewId))
+ .willReturn(CrewMember.createByRole(CrewRole.LEADER));
+
+ //when & then
+ assertThatThrownBy(() -> crewFacade.expelCrew(userId, expelRequest))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(LEADER_CANNOT_EXPEL.getMessage());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/genius/herewe/business/crew/fixture/CrewFixture.java b/src/test/java/com/genius/herewe/business/crew/fixture/CrewFixture.java
new file mode 100644
index 0000000..113c27b
--- /dev/null
+++ b/src/test/java/com/genius/herewe/business/crew/fixture/CrewFixture.java
@@ -0,0 +1,63 @@
+package com.genius.herewe.business.crew.fixture;
+
+import com.genius.herewe.business.crew.domain.Crew;
+
+public class CrewFixture {
+ public static Crew createDefault() {
+ return builder()
+ .build();
+ }
+
+ public static Crew createByName(String name) {
+ return builder()
+ .name(name)
+ .build();
+ }
+
+ public static CrewFixture.CrewBuilder builder() {
+ return new CrewFixture.CrewBuilder();
+ }
+
+ public static class CrewBuilder {
+ private Long id = 1L;
+ private String leaderName = "";
+ private String name = "crew name";
+ private String introduce = "crew introduce";
+ private int participantCount = 1;
+
+ public CrewBuilder id(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ public CrewBuilder leaderName(String leaderName) {
+ this.leaderName = leaderName;
+ return this;
+ }
+
+ public CrewBuilder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public CrewBuilder introduce(String introduce) {
+ this.introduce = introduce;
+ return this;
+ }
+
+ public CrewBuilder participantCount(int participantCount) {
+ this.participantCount = participantCount;
+ return this;
+ }
+
+ public Crew build() {
+ return Crew.builder()
+ .id(id)
+ .leaderName(leaderName)
+ .name(name)
+ .introduce(introduce)
+ .participantCount(participantCount)
+ .build();
+ }
+ }
+}
diff --git a/src/test/java/com/genius/herewe/business/invitation/Fixture/InvitationFixture.java b/src/test/java/com/genius/herewe/business/invitation/Fixture/InvitationFixture.java
new file mode 100644
index 0000000..b53c0eb
--- /dev/null
+++ b/src/test/java/com/genius/herewe/business/invitation/Fixture/InvitationFixture.java
@@ -0,0 +1,53 @@
+package com.genius.herewe.business.invitation.Fixture;
+
+import java.time.LocalDateTime;
+
+import com.genius.herewe.business.invitation.domain.Invitation;
+
+public class InvitationFixture {
+ public static Invitation createDefault() {
+ return builder()
+ .build();
+ }
+
+ public static Invitation createExpired() {
+ LocalDateTime now = LocalDateTime.now();
+ return builder()
+ .invitedAt(now.minusDays(4))
+ .expiredAt(now.minusDays(2))
+ .build();
+ }
+
+ public static InvitationBuilder builder() {
+ return new InvitationBuilder();
+ }
+
+ public static class InvitationBuilder {
+ private String token = "invitation UUID token";
+ private LocalDateTime invitedAt = LocalDateTime.now();
+ private LocalDateTime expiredAt = LocalDateTime.now().plusDays(2);
+
+ public InvitationBuilder token(String token) {
+ this.token = token;
+ return this;
+ }
+
+ public InvitationBuilder invitedAt(LocalDateTime invitedAt) {
+ this.invitedAt = invitedAt;
+ return this;
+ }
+
+ public InvitationBuilder expiredAt(LocalDateTime expiredAt) {
+ this.expiredAt = expiredAt;
+ return this;
+ }
+
+ public Invitation build() {
+ return Invitation.builder()
+ .token(token)
+ .invitedAt(invitedAt)
+ .expiredAt(expiredAt)
+ .build();
+ }
+ }
+}
diff --git a/src/test/java/com/genius/herewe/business/invitation/facade/InvitationFacadeTest.java b/src/test/java/com/genius/herewe/business/invitation/facade/InvitationFacadeTest.java
new file mode 100644
index 0000000..afbf2e5
--- /dev/null
+++ b/src/test/java/com/genius/herewe/business/invitation/facade/InvitationFacadeTest.java
@@ -0,0 +1,321 @@
+package com.genius.herewe.business.invitation.facade;
+
+import static com.genius.herewe.core.global.exception.ErrorCode.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.*;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+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.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.genius.herewe.business.crew.domain.Crew;
+import com.genius.herewe.business.crew.domain.CrewMember;
+import com.genius.herewe.business.crew.domain.CrewRole;
+import com.genius.herewe.business.crew.fixture.CrewFixture;
+import com.genius.herewe.business.crew.service.CrewMemberService;
+import com.genius.herewe.business.crew.service.CrewService;
+import com.genius.herewe.business.invitation.Fixture.InvitationFixture;
+import com.genius.herewe.business.invitation.domain.Invitation;
+import com.genius.herewe.business.invitation.dto.InvitationRequest;
+import com.genius.herewe.business.invitation.service.InvitationService;
+import com.genius.herewe.core.global.exception.BusinessException;
+import com.genius.herewe.core.user.domain.User;
+import com.genius.herewe.core.user.fixture.UserFixture;
+import com.genius.herewe.core.user.service.UserService;
+import com.genius.herewe.infra.mail.dto.MailRequest;
+import com.genius.herewe.infra.mail.service.MailManager;
+
+@ExtendWith(MockitoExtension.class)
+class InvitationFacadeTest {
+ private InvitationFacade invitationFacade;
+ @Mock
+ private UserService userService;
+ @Mock
+ private CrewService crewService;
+ @Mock
+ private CrewMemberService crewMemberService;
+ @Mock
+ private InvitationService invitationService;
+ @Mock
+ private MailManager mailManager;
+ private User user;
+ private Crew crew;
+ private InvitationRequest invitationRequest;
+
+ @BeforeEach
+ void init() {
+ invitationFacade = new DefaultInvitationFacade("http://BASE_URL", "/invite/",
+ userService, crewService, crewMemberService, invitationService, mailManager);
+ user = UserFixture.createDefault();
+ crew = CrewFixture.createDefault();
+ invitationRequest = new InvitationRequest(crew.getId(), user.getNickname());
+ }
+
+ @Nested
+ @DisplayName("특정 사용자에게 크루 초대 요청 시")
+ class Context_when_invite_to_crew {
+
+ @Nested
+ @DisplayName("초대 대상의 닉네임을 전달했을 때")
+ class Describe_pass_nickname {
+ @Test
+ @DisplayName("존재하지 않는 닉네임이라면 MEMBER_NOT_FOUND 예외가 발생한다.")
+ public void it_throws_MEMBER_NOT_FOUND_exception() {
+ //given
+ String fakeNickname = "fake Nickname";
+ given(userService.findByNickname(fakeNickname)).willThrow(new BusinessException(MEMBER_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> invitationFacade.inviteCrew(
+ new InvitationRequest(crew.getId(), fakeNickname)
+ ))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(MEMBER_NOT_FOUND.getMessage());
+ }
+ }
+
+ @Nested
+ @DisplayName("초대할 크루의 식별자(PK)를 전달했을 때")
+ class Describe_pass_crew_PK {
+ @Test
+ @DisplayName("존재하지 않는다면 CREW_NOT_FOUND 예외가 발생한다.")
+ public void it_throws_CREW_NOT_FOUND_exception() {
+ //given
+ Long fakeCrewId = 999L;
+ given(userService.findByNickname(user.getNickname())).willReturn(Optional.of(user));
+ given(crewService.findById(fakeCrewId)).willThrow(new BusinessException(CREW_NOT_FOUND));
+
+ //when & then
+ assertThatThrownBy(() -> invitationFacade.inviteCrew(
+ new InvitationRequest(fakeCrewId, user.getNickname())
+ ))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(CREW_NOT_FOUND.getMessage());
+ }
+ }
+
+ @Nested
+ @DisplayName("크루 참여 정보를 조회했을 때")
+ class Describe_inquiry_crewMember {
+ @Test
+ @DisplayName("크루 참여 정보가 존재한다면 ALREADY_JOINED_CREW 예외가 발생한다.")
+ public void it_throws_ALREADY_JOINED_CREW_exception() {
+ //given
+ Long userId = user.getId();
+ Long crewId = crew.getId();
+ given(userService.findByNickname(user.getNickname())).willReturn(Optional.of(user));
+ given(crewService.findById(crew.getId())).willReturn(crew);
+ given(crewMemberService.findOptional(userId, crewId)).willReturn(
+ Optional.of(CrewMember.createByRole(CrewRole.MEMBER)));
+
+ //when & then
+ assertThatThrownBy(() -> invitationFacade.inviteCrew(invitationRequest))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining(ALREADY_JOINED_CREW.getMessage());
+ }
+ }
+
+ @Nested
+ @DisplayName("초대 엔티티를 조회했을 때")
+ class Describe_inquiry_invitation {
+ Long userId;
+ Long crewId;
+ @Captor
+ private ArgumentCaptor