Skip to content

Commit

Permalink
�fix: fcm 푸시 오류 수정 (#111)
Browse files Browse the repository at this point in the history
* fix: 빈 토큰 리스트 호출 오류 수정

* fix: 로그인 시 현재 계정으로 미션 구독

* refactor: fetch join 변경

* rename: 함수명 변경

* fix: fetch join 적용
  • Loading branch information
kimyu0218 authored Dec 20, 2024
1 parent 79abfac commit 0196c5e
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,31 @@ private List<String> findTopicSubscribers(final Long missionId) {
.toList();
}

/**
* <b>로그인 시 내 미션 구독 시작</b>
*
* @param memberId 멤버 아이디
* @param deviceIdentifier 디바이스 식별자
*/
@Transactional
public void subscribeToMyMissions(final Long memberId, final String deviceIdentifier) {
Device device = deviceRepository.getDevice(memberId, deviceIdentifier);
List<String> topics = findMySubscribedTopics(device.getId());
List<Mission> missions = missionRepository.findAllById(
findMySubscribableMission(memberId, topics)
);

topics.forEach(topic ->
topicSubscriber.subscribeToTopic(List.of(device.getDeviceToken()), topic)
);
missions.forEach(mission -> {
deviceSubscriptionRepository.save(new DeviceSubscription(device, mission));

String topic = TopicGenerator.getTopic(mission.getId());
topicSubscriber.subscribeToTopic(List.of(device.getDeviceToken()), topic);
});
}

/**
* <b>디바이스 토큰을 갱신하거나 푸시 알림 활성화 시 새로운 디바이스 토큰으로 내 미션 구독 시작</b><br>
* + UpdateMissionRetryPushMessageEvent를 통해 예약된 메시지의 디바이스 토큰 갱신
Expand Down Expand Up @@ -120,13 +145,13 @@ public void subscribeToMyMissions(final Long memberId, final Long deviceId) {
* <b>로그인 시 기존 디바이스가 구독한 미션 구독 취소</b>
* + CancelMissionRetryPushMessageEvent를 통해 예약된 메시지 취소
*
* @param memberId 멤버 아이디
* @param memberId (현재 로그인한) 멤버 아이디
* @param deviceIdentifier 디바이스 식별자
*/
@Transactional
public void unsubscribeFromMyMissions(final Long memberId, final String deviceIdentifier) {
Devices devices = new Devices(
deviceRepository.findAllByDeviceIdentifier(deviceIdentifier)
deviceRepository.findAllWithMemberByDeviceIdentifier(deviceIdentifier)
);
List<String> topics = devices.getActivatedDevices().stream()
.flatMap(device -> findMySubscribedTopics(device.getId()).stream())
Expand All @@ -135,9 +160,13 @@ public void unsubscribeFromMyMissions(final Long memberId, final String deviceId
topics.forEach(topic ->
topicSubscriber.unsubscribeFromTopic(devices.getActivatedDeviceTokens(), topic)
);
eventPublisher.publishEvent(
new CancelMissionRetryPushMessageEvent(memberId)
);

devices.getFilteredMemberIds(memberId)
.forEach(it ->
eventPublisher.publishEvent(
new CancelMissionRetryPushMessageEvent(it)
)
);
}

/**
Expand Down Expand Up @@ -172,7 +201,7 @@ private List<Long> findMySubscribableMission(final Long memberId, List<String> t
List<MissionMember> missionMembers = missionMemberRepository.findAllWithMissionByMemberId(memberId);
List<MissionMember> filteredMissionMembers = missionMembers.stream()
.filter(this::isSubscribableMission)
.filter(it -> isAlreadySubscribedMission(topicFilter, it.getMission().getId()))
.filter(it -> !isAlreadySubscribedMission(topicFilter, it.getMission().getId()))
.toList();

return filteredMissionMembers.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ void handleUpdatePushActivationStatusEvent(final UpdatePushActivationStatusEvent
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void handleLoginEvent(final LoginEvent event) {
deviceSubscriptionService.unsubscribeFromMyMissions(event.memberId(), event.deviceIdentifier());
deviceSubscriptionService.subscribeToMyMissions(event.memberId(), event.deviceIdentifier());
log.info("Handled LoginEvent for memberId: {}", event.memberId());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
public class TopicSubscriberImpl implements TopicSubscriber {

public void subscribeToTopic(final List<String> registrationTokens, final String topic) {
if (registrationTokens.isEmpty()) {
return;
}

try {
FirebaseMessaging.getInstance().subscribeToTopic(registrationTokens, topic);
} catch (FirebaseMessagingException e) {
Expand All @@ -21,6 +25,10 @@ public void subscribeToTopic(final List<String> registrationTokens, final String
}

public void unsubscribeFromTopic(final List<String> registrationTokens, final String topic) {
if (registrationTokens.isEmpty()) {
return;
}

try {
FirebaseMessaging.getInstance().unsubscribeFromTopic(registrationTokens, topic);
} catch (FirebaseMessagingException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,18 @@ public List<String> getActivatedDeviceTokens() {
.toList();
}

// TODO: 추후 불필요하면 삭제
public List<String> getDeactivatedDeviceTokens() {
return devices.stream()
.filter(it -> !it.getPushActivationStatus())
.map(Device::getDeviceToken)
.toList();
}

public List<Long> getFilteredMemberIds(final Long excludedMemberId) {
return devices.stream()
.filter(it -> it.getMember().getId() != excludedMemberId)
.map(it -> it.getMember().getId())
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.nexters.goalpanzi.exception.ErrorCode;
import com.nexters.goalpanzi.exception.NotFoundException;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;
import java.util.Optional;
Expand All @@ -14,7 +15,8 @@ public interface DeviceRepository extends JpaRepository<Device, Long> {

List<Device> findAllByMemberId(final Long memberId);

List<Device> findAllByDeviceIdentifier(final String deviceIdentifier);
@Query("SELECT d FROM Device d JOIN FETCH d.member WHERE d.deviceIdentifier = :deviceIdentifier")
List<Device> findAllWithMemberByDeviceIdentifier(final String deviceIdentifier);

boolean existsByDeviceIdentifier(final String deviceIdentifier);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ public interface MissionVerificationRepository extends JpaRepository<MissionVeri

Optional<MissionVerification> findByMemberIdAndMissionIdAndBoardNumber(final Long memberId, final Long missionId, final Integer boardNumber);

@Query("SELECT mv FROM MissionVerification mv WHERE mv.mission.id = :missionId AND DATE(mv.createdAt) = :date")
@Query("SELECT mv FROM MissionVerification mv"
+ " JOIN FETCH mv.mission ms"
+ " WHERE ms.id = :missionId AND DATE(mv.createdAt) = :date")
List<MissionVerification> findAllByMissionIdAndDate(final Long missionId, final LocalDate date);

@Query("SELECT mv FROM MissionVerification mv WHERE mv.member.id = :memberId AND mv.mission.id = :missionId AND DATE(mv.createdAt) = :date")
@Query("SELECT mv FROM MissionVerification mv"
+ " JOIN FETCH mv.member mb JOIN FETCH mv.mission ms"
+ " WHERE mb.id = :memberId AND ms.id = :missionId AND DATE(mv.createdAt) = :date")
Optional<MissionVerification> findByMemberIdAndMissionIdAndDate(Long memberId, Long missionId, LocalDate date);

default MissionVerification getMyVerification(final Long memberId, final Long missionId, final Integer boardNumber) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
import com.nexters.goalpanzi.domain.device.DeviceSubscription;
import com.nexters.goalpanzi.domain.device.repository.DeviceRepository;
import com.nexters.goalpanzi.domain.device.repository.DeviceSubscriptionRepository;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.mission.Mission;
import com.nexters.goalpanzi.domain.mission.MissionMember;
import com.nexters.goalpanzi.domain.mission.repository.MissionMemberRepository;
import com.nexters.goalpanzi.domain.mission.repository.MissionRepository;
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;
Expand All @@ -16,6 +21,8 @@

import java.util.List;

import static com.nexters.goalpanzi.domain.mission.MissionStatus.CREATED;
import static com.nexters.goalpanzi.fixture.DeviceFixture.DEVICE_IDENTIFIER;
import static com.nexters.goalpanzi.fixture.DeviceFixture.DEVICE_TOKEN;
import static com.nexters.goalpanzi.fixture.MemberFixture.MEMBER_ID;
import static org.mockito.Mockito.*;
Expand All @@ -35,39 +42,52 @@ class DeviceSubscriptionServiceTest {
@MockBean
private DeviceSubscriptionRepository deviceSubscriptionRepository;

@MockBean
private MissionRepository missionRepository;

@MockBean
private MissionMemberRepository missionMemberRepository;

@MockBean
private TopicSubscriber topicSubscriber;

private static Long MISSION_ID = 1L;
private static final Long MISSION_ID = 1L;
private static final Long DEVICE_ID = 1L;

private Mission MOCK_MISSION;
private Member MOCK_MEMBER;

@BeforeEach
void setUp() {
MOCK_MISSION = mock(Mission.class);
when(MOCK_MISSION.getId()).thenReturn(MISSION_ID);

MOCK_MEMBER = mock(Member.class);
when(MOCK_MEMBER.getId()).thenReturn(MEMBER_ID);
}

@Test
void 멤버의_디바이스_중_알림이_활성화된_디바이스_토큰은_미션을_구독한다() {
Mission mockMission = mock(Mission.class);
when(mockMission.getId()).thenReturn(MISSION_ID);

Device mockDevice = mock(Device.class);
when(mockDevice.getDeviceToken()).thenReturn(DEVICE_TOKEN);
when(mockDevice.getPushActivationStatus()).thenReturn(true);

when(deviceRepository.findAllByMemberId(MEMBER_ID)).thenReturn(List.of(mockDevice));

deviceSubscriptionService.subscribeToMission(MEMBER_ID, mockMission);
deviceSubscriptionService.subscribeToMission(MEMBER_ID, MOCK_MISSION);

verify(topicSubscriber)
.subscribeToTopic(List.of(DEVICE_TOKEN), TopicGenerator.getTopic(MISSION_ID));
}

@Test
void 멤버의_디바이스_중_알림이_비활성화된_디바이스_토큰은_미션을_구독하지_않는다() {
Mission mockMission = mock(Mission.class);
when(mockMission.getId()).thenReturn(MISSION_ID);

Device mockDevice = mock(Device.class);
when(mockDevice.getPushActivationStatus()).thenReturn(false);

when(deviceRepository.findAllByMemberId(MEMBER_ID)).thenReturn(List.of(mockDevice));

deviceSubscriptionService.subscribeToMission(MEMBER_ID, mockMission);
deviceSubscriptionService.subscribeToMission(MEMBER_ID, MOCK_MISSION);

verify(topicSubscriber)
.subscribeToTopic(List.of(), TopicGenerator.getTopic(MISSION_ID));
Expand All @@ -89,4 +109,65 @@ class DeviceSubscriptionServiceTest {
verify(topicSubscriber)
.unsubscribeFromTopic(List.of(DEVICE_TOKEN), TopicGenerator.getTopic(MISSION_ID));
}

@Test
void 구독했거나_구독_가능한_미션을_찾아_구독을_시작한다() {
Long SUBSCRIBED_MISSION_ID = 1L;
Long UNSUBSCRIBED_MISSION_ID = 2L;

Mission mockSubscribedMission = mock(Mission.class);
when(mockSubscribedMission.getId()).thenReturn(SUBSCRIBED_MISSION_ID);

Mission mockUnsubscribedMission = mock(Mission.class);
when(mockUnsubscribedMission.getId()).thenReturn(UNSUBSCRIBED_MISSION_ID);

MissionMember mockMissionMember = mock(MissionMember.class);
when(mockMissionMember.getMissionStatus()).thenReturn(CREATED);
when(mockMissionMember.getMission()).thenReturn(mockUnsubscribedMission);

Device mockDevice = mock(Device.class);
when(mockDevice.getId()).thenReturn(DEVICE_ID);
when(mockDevice.getDeviceToken()).thenReturn(DEVICE_TOKEN);

DeviceSubscription mockDeviceSubscription = mock(DeviceSubscription.class);
when(mockDeviceSubscription.getMission()).thenReturn(mockSubscribedMission);

when(deviceRepository.getDevice(MEMBER_ID, DEVICE_IDENTIFIER)).thenReturn(mockDevice);
when(deviceSubscriptionRepository.findAllWithMissionAndDeviceByDeviceId(DEVICE_ID))
.thenReturn(List.of(mockDeviceSubscription));

when(missionMemberRepository.findAllWithMissionByMemberId(MEMBER_ID))
.thenReturn(List.of(mockMissionMember));
when(missionRepository.findAllById(List.of(UNSUBSCRIBED_MISSION_ID)))
.thenReturn(List.of(mockUnsubscribedMission));

deviceSubscriptionService.subscribeToMyMissions(MEMBER_ID, DEVICE_IDENTIFIER);

verify(topicSubscriber)
.subscribeToTopic(List.of(DEVICE_TOKEN), TopicGenerator.getTopic(SUBSCRIBED_MISSION_ID));
verify(topicSubscriber)
.subscribeToTopic(List.of(DEVICE_TOKEN), TopicGenerator.getTopic(UNSUBSCRIBED_MISSION_ID));
}

@Test
void 구독한_미션을_모두_구독_해제한다() {
Device mockDevice = mock(Device.class);
when(mockDevice.getId()).thenReturn(DEVICE_ID);
when(mockDevice.getMember()).thenReturn(MOCK_MEMBER);
when(mockDevice.getDeviceToken()).thenReturn(DEVICE_TOKEN);
when(mockDevice.getPushActivationStatus()).thenReturn(true);

DeviceSubscription mockDeviceSubscription = mock(DeviceSubscription.class);
when(mockDeviceSubscription.getMission()).thenReturn(MOCK_MISSION);

when(deviceRepository.findAllWithMemberByDeviceIdentifier(DEVICE_IDENTIFIER))
.thenReturn(List.of(mockDevice));
when(deviceSubscriptionRepository.findAllWithMissionAndDeviceByDeviceId(DEVICE_ID))
.thenReturn(List.of(mockDeviceSubscription));

deviceSubscriptionService.unsubscribeFromMyMissions(MEMBER_ID, DEVICE_IDENTIFIER);

verify(topicSubscriber)
.unsubscribeFromTopic(List.of(DEVICE_TOKEN), TopicGenerator.getTopic(MISSION_ID));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.nexters.goalpanzi.application.firebase;

import com.google.firebase.messaging.FirebaseMessaging;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.List;

import static org.mockito.Mockito.*;

class TopicSubscriberImplTest {

private TopicSubscriberImpl topicSubscriber;

@Mock
private FirebaseMessaging firebaseMessaging;

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);

topicSubscriber = new TopicSubscriberImpl();

mockStatic(FirebaseMessaging.class);
when(FirebaseMessaging.getInstance()).thenReturn(firebaseMessaging);
}

@Test
void 비어있는_토큰_리스트를_전달하는_경우_FirebaseMessaging을_호출하지_않는다() {
topicSubscriber.subscribeToTopic(List.of(), "topic");
topicSubscriber.unsubscribeFromTopic(List.of(), "topic");

verifyNoInteractions(firebaseMessaging);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.nexters.goalpanzi.domain.device.repository;

import com.nexters.goalpanzi.domain.device.Device;
import com.nexters.goalpanzi.domain.device.OsType;
import com.nexters.goalpanzi.domain.member.Member;
import com.nexters.goalpanzi.domain.member.SocialType;
import com.nexters.goalpanzi.domain.member.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.util.List;

import static com.nexters.goalpanzi.fixture.DeviceFixture.DEVICE_IDENTIFIER;
import static com.nexters.goalpanzi.fixture.DeviceFixture.DEVICE_TOKEN;
import static com.nexters.goalpanzi.fixture.MemberFixture.EMAIL_HOST;
import static com.nexters.goalpanzi.fixture.MemberFixture.SOCIAL_ID;
import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
class DeviceRepositoryTest {

@Autowired
private DeviceRepository deviceRepository;

@Autowired
private MemberRepository memberRepository;

@Test
void 특정_디바이스를_멤버와_함께_조회한다() {
Member member = memberRepository.save(
Member.socialLogin(SOCIAL_ID, EMAIL_HOST, SocialType.GOOGLE)
);
Device device = deviceRepository.save(new Device(member, DEVICE_IDENTIFIER, DEVICE_TOKEN, OsType.AOS));

List<Device> devices = deviceRepository.findAllWithMemberByDeviceIdentifier(device.getDeviceIdentifier());
assertThat(devices.getFirst().getMember()).isEqualTo(member);
}
}

0 comments on commit 0196c5e

Please sign in to comment.