Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fcm 푸시 오류 수정 #111

Merged
merged 5 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
Loading