diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java b/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java index 2e48de71..aacc7a41 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java @@ -62,9 +62,9 @@ public void joinMission(final Long memberId, final InvitationCode invitationCode missionValidator.validateMaxPersonnel(mission); missionMemberRepository.save(MissionMember.join(member, mission)); - if (member.isPushActivated()) { - eventPublisher.publishEvent(new JoinMissionEvent(mission.getId(), member.getDeviceToken(), member.getNickname())); - } else { + sendJoinPushMessage(member, mission); + + if (!member.isPushActivated()) { cancelRetryPushMessage(member.getId()); } } @@ -81,6 +81,16 @@ private void validateAlreadyJoin(final Member member, final Mission mission) { }); } + private void sendJoinPushMessage(final Member member, final Mission mission) { + Member hostMember = memberRepository.getMember(mission.getHostMemberId()); + + if (hostMember.isPushActivated() && !mission.isHostMember(member.getId())) { + eventPublisher.publishEvent( + new JoinMissionEvent(mission.getId(), hostMember.getDeviceToken(), member.getNickname()) + ); + } + } + public MissionsResponse findAllByMemberId(final Long memberId, final List filter) { Member member = memberRepository.getMember(memberId); List missionMembers = missionMemberRepository.findAllWithMissionByMemberId(memberId); diff --git a/src/main/java/com/nexters/goalpanzi/common/aop/RedissonLockAspect.java b/src/main/java/com/nexters/goalpanzi/common/aop/RedissonLockAspect.java index ecfcd3cd..dd0a75a4 100644 --- a/src/main/java/com/nexters/goalpanzi/common/aop/RedissonLockAspect.java +++ b/src/main/java/com/nexters/goalpanzi/common/aop/RedissonLockAspect.java @@ -12,8 +12,11 @@ import org.aspectj.lang.reflect.MethodSignature; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; +@Order(Ordered.HIGHEST_PRECEDENCE) @RequiredArgsConstructor @Slf4j @Aspect diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java index 3f7269e6..af30533d 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java @@ -217,6 +217,16 @@ public boolean isEndDate(final LocalDate today) { return today.isEqual(missionEndDate.toLocalDate()); } + /** + * 미션 호스트인지 검증 + * + * @param memberId + * @return 미션 호스트(생성한 사람) 여부 + */ + public boolean isHostMember(final Long memberId) { + return hostMemberId == memberId; + } + @Override public boolean equals(final Object o) { if (this == o) return true; diff --git a/src/test/java/com/nexters/goalpanzi/application/mission/MissionMemberServiceTest.java b/src/test/java/com/nexters/goalpanzi/application/mission/MissionMemberServiceTest.java index 9138b119..e6037957 100644 --- a/src/test/java/com/nexters/goalpanzi/application/mission/MissionMemberServiceTest.java +++ b/src/test/java/com/nexters/goalpanzi/application/mission/MissionMemberServiceTest.java @@ -1,14 +1,18 @@ package com.nexters.goalpanzi.application.mission; import com.nexters.goalpanzi.application.firebase.TopicGenerator; +import com.nexters.goalpanzi.application.mission.event.JoinMissionEvent; import com.nexters.goalpanzi.config.redis.RedisInitializer; +import com.nexters.goalpanzi.domain.member.Member; import com.nexters.goalpanzi.domain.member.repository.MemberRepository; +import com.nexters.goalpanzi.domain.mission.InvitationCode; import com.nexters.goalpanzi.domain.mission.Mission; import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository; import com.nexters.goalpanzi.domain.mission.repository.MissionRepository; import com.nexters.goalpanzi.domain.mission.repository.MissionRetryMessageRepository; import com.nexters.goalpanzi.infrastructure.firebase.PushMessageSender; import com.nexters.goalpanzi.infrastructure.firebase.TopicSubscriber; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -16,12 +20,15 @@ import org.springframework.boot.test.mock.mockito.MockBeans; import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.util.ReflectionTestUtils; import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import static com.nexters.goalpanzi.domain.firebase.PushMessage.MISSION_CANCELLATION_WARNING; import static com.nexters.goalpanzi.domain.firebase.PushMessage.MISSION_READY; +import static com.nexters.goalpanzi.fixture.MemberFixture.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -30,9 +37,7 @@ initializers = {RedisInitializer.class} ) @MockBeans({ - @MockBean(MemberRepository.class), - @MockBean(MissionRetryMessageRepository.class), - @MockBean(ApplicationEventPublisher.class) + @MockBean(MissionRetryMessageRepository.class) }) class MissionMemberServiceTest { @@ -48,6 +53,12 @@ class MissionMemberServiceTest { @MockBean private MissionRepository missionRepository; + @MockBean + private MemberRepository memberRepository; + + @MockBean + private ApplicationEventPublisher eventPublisher; + @MockBean private PushMessageSender pushMessageSender; @@ -56,6 +67,70 @@ class MissionMemberServiceTest { private static Long MISSION_ID = 1L; + @BeforeEach + void setUp() { + ReflectionTestUtils.setField( + missionMemberService, "eventPublisher", eventPublisher + ); + } + + @Test + void 호스트가_아닌_멤버가_미션에_참여했을_때_JoinMissionEvent를_발행한다() { + InvitationCode INVITATION_CODE = InvitationCode.generate(); + Long HOST_ID = MEMBER_ID + 1; + + Member mockMember = mock(Member.class); + when(mockMember.getId()).thenReturn(MEMBER_ID); + when(mockMember.getNickname()).thenReturn(NICKNAME_MEMBER_A); + + Member mockHostMember = mock(Member.class); + when(mockHostMember.getId()).thenReturn(HOST_ID); + when(mockHostMember.getDeviceToken()).thenReturn(DEVICE_TOKEN); + when(mockHostMember.isPushActivated()).thenReturn(true); + + Mission mockMission = mock(Mission.class); + when(mockMission.getId()).thenReturn(MISSION_ID); + when(mockMission.getHostMemberId()).thenReturn(HOST_ID); + when(mockMission.isHostMember(MEMBER_ID)).thenReturn(false); + + when(memberRepository.getMember(mockMember.getId())).thenReturn(mockMember); + when(memberRepository.getMember(mockHostMember.getId())).thenReturn(mockHostMember); + + when(missionRepository.findByInvitationCode(INVITATION_CODE)).thenReturn(Optional.of(mockMission)); + when(missionMemberRepository.findByMemberIdAndMissionId(MEMBER_ID, MISSION_ID)).thenReturn(Optional.empty()); + + missionMemberService.joinMission(MEMBER_ID, INVITATION_CODE); + + verify(eventPublisher).publishEvent( + eq(new JoinMissionEvent(MISSION_ID, DEVICE_TOKEN, NICKNAME_MEMBER_A)) + ); + } + + @Test + void 호스트가_미션에_참여했을_때_JoinMissionEvent를_발행하지_않는다() { + InvitationCode INVITATION_CODE = InvitationCode.generate(); + + Member mockMember = mock(Member.class); + when(mockMember.getId()).thenReturn(MEMBER_ID); + when(mockMember.getNickname()).thenReturn(NICKNAME_HOST); + when(mockMember.getDeviceToken()).thenReturn(DEVICE_TOKEN); + when(mockMember.isPushActivated()).thenReturn(true); + + Mission mockMission = mock(Mission.class); + when(mockMission.getId()).thenReturn(MISSION_ID); + when(mockMission.getHostMemberId()).thenReturn(MEMBER_ID); + when(mockMission.isHostMember(MEMBER_ID)).thenReturn(true); + + when(memberRepository.getMember(mockMember.getId())).thenReturn(mockMember); + + when(missionRepository.findByInvitationCode(INVITATION_CODE)).thenReturn(Optional.of(mockMission)); + when(missionMemberRepository.findByMemberIdAndMissionId(MEMBER_ID, MISSION_ID)).thenReturn(Optional.empty()); + + missionMemberService.joinMission(MEMBER_ID, INVITATION_CODE); + + verifyNoInteractions(eventPublisher); + } + @Test void 최소_인원을_채워_미션이_곧_시작될_경우_MISSION_READY_푸시_알림을_보낸다() { Mission mockMission = mock(Mission.class); diff --git a/src/test/java/com/nexters/goalpanzi/application/mission/event/handler/MissionMemberEventHandlerTest.java b/src/test/java/com/nexters/goalpanzi/application/mission/event/handler/MissionMemberEventHandlerTest.java index da275767..7ffd51e0 100644 --- a/src/test/java/com/nexters/goalpanzi/application/mission/event/handler/MissionMemberEventHandlerTest.java +++ b/src/test/java/com/nexters/goalpanzi/application/mission/event/handler/MissionMemberEventHandlerTest.java @@ -31,7 +31,7 @@ class MissionMemberEventHandlerTest { @Autowired - private ApplicationEventPublisher applicationEventPublisher; + private ApplicationEventPublisher eventPublisher; @Autowired private TransactionTemplate transactionTemplate; @@ -50,7 +50,7 @@ class MissionMemberEventHandlerTest { UpdateDeviceTokenEvent event = new UpdateDeviceTokenEvent(1L, null, "deviceToken"); transactionTemplate.execute(status -> { - applicationEventPublisher.publishEvent(event); + eventPublisher.publishEvent(event); return null; }); @@ -63,7 +63,7 @@ class MissionMemberEventHandlerTest { UpdateDeviceTokenEvent event = new UpdateDeviceTokenEvent(1L, "deprecatedDeviceToken", "deviceToken"); transactionTemplate.execute(status -> { - applicationEventPublisher.publishEvent(event); + eventPublisher.publishEvent(event); return null; }); @@ -78,7 +78,7 @@ class MissionMemberEventHandlerTest { UpdatePushActivationStatusEvent event = new UpdatePushActivationStatusEvent(1L, "deviceToken", true); transactionTemplate.execute(status -> { - applicationEventPublisher.publishEvent(event); + eventPublisher.publishEvent(event); return null; }); @@ -91,7 +91,7 @@ class MissionMemberEventHandlerTest { UpdatePushActivationStatusEvent event = new UpdatePushActivationStatusEvent(1L, "deviceToken", false); transactionTemplate.execute(status -> { - applicationEventPublisher.publishEvent(event); + eventPublisher.publishEvent(event); return null; }); diff --git a/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonLockTestBean.java b/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonLockTestBean.java index ed664b79..7765ca9d 100644 --- a/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonLockTestBean.java +++ b/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonLockTestBean.java @@ -1,7 +1,9 @@ package com.nexters.goalpanzi.config.redisson; import com.nexters.goalpanzi.common.annotation.RedissonLock; +import org.springframework.transaction.annotation.Transactional; +@Transactional(readOnly = true) public class RedissonLockTestBean { @RedissonLock(waitTime = 1L) diff --git a/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java b/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java index 93b9c093..ff9a1b06 100644 --- a/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java +++ b/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java @@ -250,4 +250,24 @@ class MissionTest { () -> assertThat(mission.isEndDate(endDate.minusDays(1).toLocalDate())).isFalse() ); } + + @Test + void 미션_호스트_여부를_검증한다() { + LocalDateTime now = LocalDateTime.now(); + Mission mission = Mission.create( + MEMBER_ID, + DESCRIPTION, + now, + now.plusDays(30), + TimeOfDay.EVERYDAY, + List.of(DayOfWeek.FRIDAY), + BOARD_COUNT, + InvitationCode.generate() + ); + + assertAll( + () -> assertThat(mission.isHostMember(MEMBER_ID)).isTrue(), + () -> assertThat(mission.isHostMember(MEMBER_ID + 1)).isFalse() + ); + } } \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 8cc01230..45e367d2 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -44,4 +44,11 @@ cloud: endpoint: https://kr.object.ncloudstorage.com firebase: - enabled: false \ No newline at end of file + enabled: false + +logging: + level: + org: + springframework: + transaction: + interceptor: TRACE \ No newline at end of file