diff --git a/backend/build.gradle b/backend/build.gradle index d36481f4e..54a71cbfe 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -47,6 +47,9 @@ dependencies { // Flyway implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' + + // Firebase + implementation 'com.google.firebase:firebase-admin:8.1.0' } tasks.named('test') { diff --git a/backend/src/main/java/com/festago/auth/application/AuthFacadeService.java b/backend/src/main/java/com/festago/auth/application/AuthFacadeService.java index b87ffea85..3e8a22347 100644 --- a/backend/src/main/java/com/festago/auth/application/AuthFacadeService.java +++ b/backend/src/main/java/com/festago/auth/application/AuthFacadeService.java @@ -2,9 +2,9 @@ import com.festago.auth.domain.AuthPayload; import com.festago.auth.domain.Role; +import com.festago.auth.domain.SocialType; import com.festago.auth.domain.UserInfo; import com.festago.auth.dto.LoginMemberDto; -import com.festago.auth.dto.LoginRequest; import com.festago.auth.dto.LoginResponse; import org.springframework.stereotype.Service; @@ -22,8 +22,9 @@ public AuthFacadeService(AuthService authService, OAuth2Clients oAuth2Clients, this.authProvider = authProvider; } - public LoginResponse login(LoginRequest request) { - LoginMemberDto loginMember = authService.login(getUserInfo(request)); + public LoginResponse login(SocialType socialType, String oAuthToken) { + UserInfo userInfo = getUserInfo(socialType, oAuthToken); + LoginMemberDto loginMember = authService.login(userInfo); String accessToken = getAccessToken(loginMember.memberId()); return LoginResponse.of(accessToken, loginMember); } @@ -32,9 +33,9 @@ private String getAccessToken(Long memberId) { return authProvider.provide(new AuthPayload(memberId, Role.MEMBER)); } - private UserInfo getUserInfo(LoginRequest request) { - OAuth2Client oAuth2Client = oAuth2Clients.getClient(request.socialType()); - return oAuth2Client.getUserInfo(request.accessToken()); + private UserInfo getUserInfo(SocialType socialType, String oAuthToken) { + OAuth2Client oAuth2Client = oAuth2Clients.getClient(socialType); + return oAuth2Client.getUserInfo(oAuthToken); } public void deleteMember(Long memberId) { diff --git a/backend/src/main/java/com/festago/auth/application/AuthService.java b/backend/src/main/java/com/festago/auth/application/AuthService.java index f559d219b..63101e914 100644 --- a/backend/src/main/java/com/festago/auth/application/AuthService.java +++ b/backend/src/main/java/com/festago/auth/application/AuthService.java @@ -6,6 +6,7 @@ import com.festago.common.exception.NotFoundException; import com.festago.member.domain.Member; import com.festago.member.repository.MemberRepository; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -24,12 +25,18 @@ public AuthService(MemberRepository memberRepository) { } public LoginMemberDto login(UserInfo userInfo) { - return memberRepository.findBySocialIdAndSocialType(userInfo.socialId(), userInfo.socialType()) - .map(LoginMemberDto::isExists) - .orElseGet(() -> { - Member member = signUp(userInfo); - return LoginMemberDto.isNew(member); - }); + LoginMemberDto loginMemberDto = handleLoginRequest(userInfo); + return loginMemberDto; + } + + public LoginMemberDto handleLoginRequest(UserInfo userInfo) { + Optional originMember = memberRepository.findBySocialIdAndSocialType(userInfo.socialId(), userInfo.socialType()); + if (originMember.isPresent()) { + Member member = originMember.get(); + return LoginMemberDto.isExists(member); + } + Member newMember = signUp(userInfo); + return LoginMemberDto.isNew(newMember); } private Member signUp(UserInfo userInfo) { diff --git a/backend/src/main/java/com/festago/auth/dto/LoginRequest.java b/backend/src/main/java/com/festago/auth/dto/LoginRequest.java index 8f82722aa..d05066b51 100644 --- a/backend/src/main/java/com/festago/auth/dto/LoginRequest.java +++ b/backend/src/main/java/com/festago/auth/dto/LoginRequest.java @@ -5,6 +5,7 @@ public record LoginRequest( @NotNull(message = "socialType 은 null 일 수 없습니다.") SocialType socialType, - @NotNull(message = "acessToken 은 null 일 수 없습니다.") String accessToken) { + @NotNull(message = "acessToken 은 null 일 수 없습니다.") String accessToken, + @NotNull(message = "fcmToken 은 null 일 수 없습니다.") String fcmToken) { } diff --git a/backend/src/main/java/com/festago/common/exception/ErrorCode.java b/backend/src/main/java/com/festago/common/exception/ErrorCode.java index e143257d8..bd3708054 100644 --- a/backend/src/main/java/com/festago/common/exception/ErrorCode.java +++ b/backend/src/main/java/com/festago/common/exception/ErrorCode.java @@ -58,7 +58,8 @@ public enum ErrorCode { INVALID_ENTRY_CODE_OFFSET("올바르지 않은 입장코드 오프셋입니다."), INVALID_ROLE_NAME("해당하는 Role이 없습니다."), FOR_TEST_ERROR("테스트용 에러입니다."), - ; + FAIL_SEND_FCM_MESSAGE("FCM Message 전송에 실패했습니다."), + FCM_NOT_FOUND("유효하지 않은 MemberFCM 이 감지 되었습니다."); private final String message; diff --git a/backend/src/main/java/com/festago/entry/application/EntryService.java b/backend/src/main/java/com/festago/entry/application/EntryService.java index 1e2f23ec3..68602f67e 100644 --- a/backend/src/main/java/com/festago/entry/application/EntryService.java +++ b/backend/src/main/java/com/festago/entry/application/EntryService.java @@ -8,10 +8,12 @@ import com.festago.entry.dto.EntryCodeResponse; import com.festago.entry.dto.TicketValidationRequest; import com.festago.entry.dto.TicketValidationResponse; +import com.festago.entry.dto.event.EntryProcessEvent; import com.festago.ticketing.domain.MemberTicket; import com.festago.ticketing.repository.MemberTicketRepository; import java.time.Clock; import java.time.LocalDateTime; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,11 +23,14 @@ public class EntryService { private final EntryCodeManager entryCodeManager; private final MemberTicketRepository memberTicketRepository; + private final ApplicationEventPublisher publisher; private final Clock clock; - public EntryService(EntryCodeManager entryCodeManager, MemberTicketRepository memberTicketRepository, Clock clock) { + public EntryService(EntryCodeManager entryCodeManager, MemberTicketRepository memberTicketRepository, + ApplicationEventPublisher publisher, Clock clock) { this.entryCodeManager = entryCodeManager; this.memberTicketRepository = memberTicketRepository; + this.publisher = publisher; this.clock = clock; } @@ -50,6 +55,7 @@ public TicketValidationResponse validate(TicketValidationRequest request) { EntryCodePayload entryCodePayload = entryCodeManager.extract(request.code()); MemberTicket memberTicket = findMemberTicket(entryCodePayload.getMemberTicketId()); memberTicket.changeState(entryCodePayload.getEntryState()); + publisher.publishEvent(new EntryProcessEvent(memberTicket.getOwner().getId())); return TicketValidationResponse.from(memberTicket); } } diff --git a/backend/src/main/java/com/festago/entry/dto/event/EntryProcessEvent.java b/backend/src/main/java/com/festago/entry/dto/event/EntryProcessEvent.java new file mode 100644 index 000000000..d94ac71d9 --- /dev/null +++ b/backend/src/main/java/com/festago/entry/dto/event/EntryProcessEvent.java @@ -0,0 +1,6 @@ +package com.festago.entry.dto.event; + +public record EntryProcessEvent( + Long memberId) { + +} diff --git a/backend/src/main/java/com/festago/fcm/application/FCMNotificationEventListener.java b/backend/src/main/java/com/festago/fcm/application/FCMNotificationEventListener.java new file mode 100644 index 000000000..1de6c5418 --- /dev/null +++ b/backend/src/main/java/com/festago/fcm/application/FCMNotificationEventListener.java @@ -0,0 +1,90 @@ +package com.festago.fcm.application; + +import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.InternalServerException; +import com.festago.entry.dto.event.EntryProcessEvent; +import com.festago.fcm.domain.FCMChannel; +import com.festago.fcm.dto.MemberFCMResponse; +import com.google.firebase.messaging.AndroidConfig; +import com.google.firebase.messaging.AndroidNotification; +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.SendResponse; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@Profile({"dev", "prod"}) +public class FCMNotificationEventListener { + + private static final Logger log = LoggerFactory.getLogger(FCMNotificationEventListener.class); + + private final FirebaseMessaging firebaseMessaging; + private final MemberFCMService memberFCMService; + + public FCMNotificationEventListener(FirebaseMessaging firebaseMessaging, MemberFCMService memberFCMService) { + this.firebaseMessaging = firebaseMessaging; + this.memberFCMService = memberFCMService; + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Async + public void sendFcmNotification(EntryProcessEvent event) { + List messages = createMessages(getMemberFCMToken(event.memberId()), FCMChannel.NOT_DEFINED.name()); + try { + BatchResponse batchResponse = firebaseMessaging.sendAll(messages); + checkAllSuccess(batchResponse, event.memberId()); + } catch (FirebaseMessagingException e) { + log.error("fail send FCM message", e); + throw new InternalServerException(ErrorCode.FAIL_SEND_FCM_MESSAGE); + } + } + + private List getMemberFCMToken(Long memberId) { + return memberFCMService.findMemberFCM(memberId).memberFCMs().stream() + .map(MemberFCMResponse::fcmToken) + .toList(); + } + + private List createMessages(List tokens, String channelId) { + return tokens.stream() + .map(token -> createMessage(token, channelId)) + .toList(); + } + + private Message createMessage(String token, String channelId) { + return Message.builder() + .setAndroidConfig(createAndroidConfig(channelId)) + .setToken(token) + .build(); + } + + private AndroidConfig createAndroidConfig(String channelId) { + return AndroidConfig.builder() + .setNotification(createAndroidNotification(channelId)) + .build(); + } + + private AndroidNotification createAndroidNotification(String channelId) { + return AndroidNotification.builder() + .setChannelId(channelId) + .build(); + } + + private void checkAllSuccess(BatchResponse batchResponse, Long memberId) { + List failSend = batchResponse.getResponses().stream() + .filter(sendResponse -> !sendResponse.isSuccessful()) + .toList(); + + log.warn("member {} 에 대한 다음 요청들이 실패했습니다. {}", memberId, failSend); + throw new InternalServerException(ErrorCode.FAIL_SEND_FCM_MESSAGE); + } +} diff --git a/backend/src/main/java/com/festago/fcm/application/MemberFCMService.java b/backend/src/main/java/com/festago/fcm/application/MemberFCMService.java new file mode 100644 index 000000000..e044feb7a --- /dev/null +++ b/backend/src/main/java/com/festago/fcm/application/MemberFCMService.java @@ -0,0 +1,48 @@ +package com.festago.fcm.application; + +import com.festago.auth.application.AuthExtractor; +import com.festago.auth.domain.AuthPayload; +import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.InternalServerException; +import com.festago.fcm.domain.MemberFCM; +import com.festago.fcm.dto.MemberFCMsResponse; +import com.festago.fcm.repository.MemberFCMRepository; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class MemberFCMService { + + private static final Logger log = LoggerFactory.getLogger(MemberFCMService.class); + private static final int LEAST_MEMBER_FCM = 1; + + private final MemberFCMRepository memberFCMRepository; + private final AuthExtractor authExtractor; + + public MemberFCMService(MemberFCMRepository memberFCMRepository, AuthExtractor authExtractor) { + this.memberFCMRepository = memberFCMRepository; + this.authExtractor = authExtractor; + } + + @Transactional(readOnly = true) + public MemberFCMsResponse findMemberFCM(Long memberId) { + List memberFCM = memberFCMRepository.findByMemberId(memberId); + if (memberFCM.size() < LEAST_MEMBER_FCM) { + log.error("member {} 의 FCM 토큰이 발급되지 않았습니다.", memberId); + throw new InternalServerException(ErrorCode.FCM_NOT_FOUND); + } + return MemberFCMsResponse.from(memberFCM); + } + + @Async + public void saveMemberFCM(String accessToken, String fcmToken) { + AuthPayload authPayload = authExtractor.extract(accessToken); + Long memberId = authPayload.getMemberId(); + memberFCMRepository.save(new MemberFCM(memberId, fcmToken)); + } +} diff --git a/backend/src/main/java/com/festago/fcm/config/FCMConfig.java b/backend/src/main/java/com/festago/fcm/config/FCMConfig.java new file mode 100644 index 000000000..93b3dfab2 --- /dev/null +++ b/backend/src/main/java/com/festago/fcm/config/FCMConfig.java @@ -0,0 +1,58 @@ +package com.festago.fcm.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.ClassPathResource; + +@Configuration +@Profile({"prod", "dev"}) +public class FCMConfig { + + @Value("${fcm.key.path}") + private String fcmPrivateKeyPath; + + @Value("${fcm.key.scope}") + private String fireBaseScope; + + @Bean + public FirebaseMessaging firebaseMessaging() throws IOException { + Optional defaultFirebaseApp = defaultFirebaseApp(); + if (defaultFirebaseApp.isPresent()) { + return FirebaseMessaging.getInstance(defaultFirebaseApp.get()); + } + return FirebaseMessaging.getInstance( + FirebaseApp.initializeApp(createFirebaseOption()) + ); + } + + private Optional defaultFirebaseApp() { + List firebaseAppList = FirebaseApp.getApps(); + if (firebaseAppList == null || firebaseAppList.isEmpty()) { + return Optional.empty(); + } + return firebaseAppList.stream() + .filter(firebaseApp -> firebaseApp.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) + .findAny(); + } + + private FirebaseOptions createFirebaseOption() throws IOException { + return FirebaseOptions.builder() + .setCredentials(createGoogleCredentials()) + .build(); + } + + private GoogleCredentials createGoogleCredentials() throws IOException { + return GoogleCredentials + .fromStream(new ClassPathResource(fcmPrivateKeyPath).getInputStream()) + .createScoped(fireBaseScope); + } +} diff --git a/backend/src/main/java/com/festago/fcm/domain/FCMChannel.java b/backend/src/main/java/com/festago/fcm/domain/FCMChannel.java new file mode 100644 index 000000000..836622edf --- /dev/null +++ b/backend/src/main/java/com/festago/fcm/domain/FCMChannel.java @@ -0,0 +1,5 @@ +package com.festago.fcm.domain; + +public enum FCMChannel { + NOT_DEFINED; +} diff --git a/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java b/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java new file mode 100644 index 000000000..8a97f7f07 --- /dev/null +++ b/backend/src/main/java/com/festago/fcm/domain/MemberFCM.java @@ -0,0 +1,56 @@ +package com.festago.fcm.domain; + +import com.festago.common.domain.BaseTimeEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +@Entity +@Table(name = "member_fcm") +public class MemberFCM extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private Long memberId; + + @NotNull + private String fcmToken; + + protected MemberFCM() { + } + + public MemberFCM(Long memberId, String fcmToken) { + this(null, memberId, fcmToken); + } + + public MemberFCM(Long id, Long memberId, String fcmToken) { + validate(memberId, fcmToken); + this.id = id; + this.memberId = memberId; + this.fcmToken = fcmToken; + } + + private void validate(Long memberId, String fcmToken) { + if (memberId == null || fcmToken == null) { + throw new IllegalArgumentException("MemberFCM 은 허용되지 않은 null 값으로 생성할 수 없습니다."); + } + } + + public Long getId() { + return id; + } + + public Long getMemberId() { + return memberId; + } + + public String getFcmToken() { + return fcmToken; + } +} diff --git a/backend/src/main/java/com/festago/fcm/dto/MemberFCMResponse.java b/backend/src/main/java/com/festago/fcm/dto/MemberFCMResponse.java new file mode 100644 index 000000000..61f252a9b --- /dev/null +++ b/backend/src/main/java/com/festago/fcm/dto/MemberFCMResponse.java @@ -0,0 +1,18 @@ +package com.festago.fcm.dto; + +import com.festago.fcm.domain.MemberFCM; + +public record MemberFCMResponse( + Long id, + Long memberId, + String fcmToken +) { + + public static MemberFCMResponse from(MemberFCM memberFCM) { + return new MemberFCMResponse( + memberFCM.getId(), + memberFCM.getMemberId(), + memberFCM.getFcmToken() + ); + } +} diff --git a/backend/src/main/java/com/festago/fcm/dto/MemberFCMsResponse.java b/backend/src/main/java/com/festago/fcm/dto/MemberFCMsResponse.java new file mode 100644 index 000000000..80df8a602 --- /dev/null +++ b/backend/src/main/java/com/festago/fcm/dto/MemberFCMsResponse.java @@ -0,0 +1,17 @@ +package com.festago.fcm.dto; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; + +import com.festago.fcm.domain.MemberFCM; +import java.util.List; + +public record MemberFCMsResponse(List memberFCMs +) { + + public static MemberFCMsResponse from(List memberFCMs) { + return memberFCMs.stream() + .map(MemberFCMResponse::from) + .collect(collectingAndThen(toList(), MemberFCMsResponse::new)); + } +} diff --git a/backend/src/main/java/com/festago/fcm/repository/MemberFCMRepository.java b/backend/src/main/java/com/festago/fcm/repository/MemberFCMRepository.java new file mode 100644 index 000000000..e5bee3719 --- /dev/null +++ b/backend/src/main/java/com/festago/fcm/repository/MemberFCMRepository.java @@ -0,0 +1,10 @@ +package com.festago.fcm.repository; + +import com.festago.fcm.domain.MemberFCM; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberFCMRepository extends JpaRepository { + + List findByMemberId(Long memberId); +} diff --git a/backend/src/main/java/com/festago/presentation/AuthController.java b/backend/src/main/java/com/festago/presentation/AuthController.java index 9d0e83f6f..36038cdbf 100644 --- a/backend/src/main/java/com/festago/presentation/AuthController.java +++ b/backend/src/main/java/com/festago/presentation/AuthController.java @@ -4,6 +4,7 @@ import com.festago.auth.application.AuthFacadeService; import com.festago.auth.dto.LoginRequest; import com.festago.auth.dto.LoginResponse; +import com.festago.fcm.application.MemberFCMService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -20,15 +21,18 @@ public class AuthController { private final AuthFacadeService authFacadeService; + private final MemberFCMService memberFCMService; - public AuthController(AuthFacadeService authFacadeService) { + public AuthController(AuthFacadeService authFacadeService, MemberFCMService memberFCMService) { this.authFacadeService = authFacadeService; + this.memberFCMService = memberFCMService; } @PostMapping("/oauth2") @Operation(description = "소셜 엑세스 토큰을 기반으로 로그인 요청을 보낸다.", summary = "OAuth2 로그인") public ResponseEntity login(@RequestBody LoginRequest request) { - LoginResponse response = authFacadeService.login(request); + LoginResponse response = authFacadeService.login(request.socialType(), request.accessToken()); + memberFCMService.saveMemberFCM(response.accessToken(), request.fcmToken()); return ResponseEntity.ok() .body(response); } diff --git a/backend/src/main/resources/db/migration/V5__MembeFCM_Added.sql b/backend/src/main/resources/db/migration/V5__MembeFCM_Added.sql new file mode 100644 index 000000000..f71564c9d --- /dev/null +++ b/backend/src/main/resources/db/migration/V5__MembeFCM_Added.sql @@ -0,0 +1,16 @@ +create table if not exists member_fcm +( + id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + member_id bigint, + fcm_token varchar(255), + primary key (id) +) engine innodb + default charset = utf8mb4 + collate = utf8mb4_0900_ai_ci; + +alter table member_fcm + add constraint fk_member_fcm__member + foreign key (member_id) + references member (id); diff --git a/backend/src/test/java/com/festago/application/EntryServiceTest.java b/backend/src/test/java/com/festago/application/EntryServiceTest.java index 962c1a3af..4da22514a 100644 --- a/backend/src/test/java/com/festago/application/EntryServiceTest.java +++ b/backend/src/test/java/com/festago/application/EntryServiceTest.java @@ -6,6 +6,8 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import com.festago.common.exception.BadRequestException; import com.festago.common.exception.NotFoundException; @@ -16,6 +18,7 @@ import com.festago.entry.dto.EntryCodeResponse; import com.festago.entry.dto.TicketValidationRequest; import com.festago.entry.dto.TicketValidationResponse; +import com.festago.entry.dto.event.EntryProcessEvent; import com.festago.festival.domain.Festival; import com.festago.member.domain.Member; import com.festago.stage.domain.Stage; @@ -42,6 +45,7 @@ import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; @ExtendWith(MockitoExtension.class) @DisplayNameGeneration(ReplaceUnderscores.class) @@ -54,6 +58,9 @@ class EntryServiceTest { @Mock MemberTicketRepository memberTicketRepository; + @Mock + ApplicationEventPublisher publisher; + @Spy Clock clock = Clock.systemDefaultZone(); @@ -246,6 +253,7 @@ class 티켓_검사 { softly.assertThat(memberTicket.getEntryState()).isEqualTo(EntryState.BEFORE_ENTRY); softly.assertThat(expect.updatedState()).isEqualTo(EntryState.BEFORE_ENTRY); }); + verify(publisher, times(1)).publishEvent(any(EntryProcessEvent.class)); } } } diff --git a/backend/src/test/java/com/festago/auth/application/AuthFacadeServiceTest.java b/backend/src/test/java/com/festago/auth/application/AuthFacadeServiceTest.java index e953d8e06..f2f51ef1a 100644 --- a/backend/src/test/java/com/festago/auth/application/AuthFacadeServiceTest.java +++ b/backend/src/test/java/com/festago/auth/application/AuthFacadeServiceTest.java @@ -9,7 +9,6 @@ import com.festago.auth.domain.SocialType; import com.festago.auth.domain.UserInfo; import com.festago.auth.dto.LoginMemberDto; -import com.festago.auth.dto.LoginRequest; import com.festago.auth.dto.LoginResponse; import com.festago.auth.infrastructure.FestagoOAuth2Client; import com.festago.member.domain.Member; @@ -45,8 +44,6 @@ void setUp() { @Test void 로그인() { - LoginRequest request = new LoginRequest(SocialType.FESTAGO, "1"); - Member member = MemberFixture.member() .id(1L) .build(); @@ -58,7 +55,7 @@ void setUp() { .willReturn(new LoginMemberDto(false, member.getId(), member.getNickname())); // when - LoginResponse response = authFacadeService.login(request); + LoginResponse response = authFacadeService.login(SocialType.FESTAGO, "1"); // then assertThat(response) diff --git a/backend/src/test/java/com/festago/auth/application/integration/AuthFacadeServiceIntegrationTest.java b/backend/src/test/java/com/festago/auth/application/integration/AuthFacadeServiceIntegrationTest.java index 0724545a0..4024fe487 100644 --- a/backend/src/test/java/com/festago/auth/application/integration/AuthFacadeServiceIntegrationTest.java +++ b/backend/src/test/java/com/festago/auth/application/integration/AuthFacadeServiceIntegrationTest.java @@ -6,7 +6,6 @@ import com.festago.application.integration.ApplicationIntegrationTest; import com.festago.auth.application.AuthFacadeService; import com.festago.auth.domain.SocialType; -import com.festago.auth.dto.LoginRequest; import com.festago.member.domain.Member; import com.festago.member.repository.MemberRepository; import com.festago.support.MemberFixture; @@ -33,13 +32,12 @@ class AuthFacadeServiceIntegrationTest extends ApplicationIntegrationTest { @Test void 회원이_탈퇴하고_재가입하면_새로운_계정으로_가입() { // given - LoginRequest request = new LoginRequest(SocialType.FESTAGO, "1"); - authFacadeService.login(request); + authFacadeService.login(SocialType.FESTAGO, "1"); Member member = memberRepository.findBySocialIdAndSocialType("1", SocialType.FESTAGO).get(); // when memberRepository.delete(member); - authFacadeService.login(request); + authFacadeService.login(SocialType.FESTAGO, "1"); // then assertThat(memberRepository.count()).isEqualTo(1); diff --git a/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java b/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java index 281261a03..19c22eb68 100644 --- a/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java +++ b/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java @@ -14,6 +14,7 @@ import com.festago.auth.domain.SocialType; import com.festago.auth.dto.LoginRequest; import com.festago.auth.dto.LoginResponse; +import com.festago.fcm.application.MemberFCMService; import com.festago.presentation.AuthController; import com.festago.support.CustomWebMvcTest; import com.festago.support.WithMockAuth; @@ -39,13 +40,16 @@ class AuthControllerTest { @MockBean AuthFacadeService authFacadeService; + @MockBean + MemberFCMService memberFCMService; + @Test void OAuth2_로그인을_한다() throws Exception { // given LoginResponse expected = new LoginResponse("accesstoken", "nickname", true); - given(authFacadeService.login(any(LoginRequest.class))) + given(authFacadeService.login(any(), any())) .willReturn(expected); - LoginRequest request = new LoginRequest(SocialType.FESTAGO, "code"); + LoginRequest request = new LoginRequest(SocialType.FESTAGO, "code", "fcmToken"); // when & then String response = mockMvc.perform(post("/auth/oauth2") diff --git a/backend/src/test/java/com/festago/fcm/application/FCMNotificationEventListenerTest.java b/backend/src/test/java/com/festago/fcm/application/FCMNotificationEventListenerTest.java new file mode 100644 index 000000000..795aa245c --- /dev/null +++ b/backend/src/test/java/com/festago/fcm/application/FCMNotificationEventListenerTest.java @@ -0,0 +1,75 @@ +package com.festago.fcm.application; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.festago.common.exception.InternalServerException; +import com.festago.entry.dto.event.EntryProcessEvent; +import com.festago.fcm.dto.MemberFCMResponse; +import com.festago.fcm.dto.MemberFCMsResponse; +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.SendResponse; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +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; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@ExtendWith(MockitoExtension.class) +class FCMNotificationEventListenerTest { + + @Mock + FirebaseMessaging firebaseMessaging; + + @Mock + MemberFCMService memberFCMService; + + @InjectMocks + FCMNotificationEventListener FCMNotificationEventListener; + + @Test + void 유저의_FCM_요청_중_하나라도_실패하면_예외() throws FirebaseMessagingException { + // given + given(memberFCMService.findMemberFCM(anyLong())).willReturn( + new MemberFCMsResponse(List.of(new MemberFCMResponse(1L, 1L, "token1"), new MemberFCMResponse(2L, 1L, "token2")))); + + given(firebaseMessaging.sendAll(any())).willReturn(new MockBatchResponse()); + + EntryProcessEvent event = new EntryProcessEvent(1L); + + // when & then + Assertions.assertThatThrownBy(() -> FCMNotificationEventListener.sendFcmNotification(event)) + .isInstanceOf(InternalServerException.class); + } + + private static class MockBatchResponse implements BatchResponse { + + @Override + public List getResponses() { + SendResponse mockResponse = mock(SendResponse.class); + when(mockResponse.isSuccessful()).thenReturn(false); + return List.of(mockResponse, mockResponse); + } + + @Override + public int getSuccessCount() { + return 0; + } + + @Override + public int getFailureCount() { + return 0; + } + } +} diff --git a/backend/src/test/java/com/festago/fcm/application/MemberFCMServiceTest.java b/backend/src/test/java/com/festago/fcm/application/MemberFCMServiceTest.java new file mode 100644 index 000000000..dd8eb7d46 --- /dev/null +++ b/backend/src/test/java/com/festago/fcm/application/MemberFCMServiceTest.java @@ -0,0 +1,72 @@ +package com.festago.fcm.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.festago.auth.application.AuthExtractor; +import com.festago.auth.domain.AuthPayload; +import com.festago.auth.domain.Role; +import com.festago.fcm.domain.MemberFCM; +import com.festago.fcm.dto.MemberFCMResponse; +import com.festago.fcm.repository.MemberFCMRepository; +import java.util.List; +import java.util.stream.Collectors; +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; + +@ExtendWith(MockitoExtension.class) +class MemberFCMServiceTest { + + @Mock + MemberFCMRepository memberFCMRepository; + + @Mock + AuthExtractor authExtractor; + + @InjectMocks + MemberFCMService memberFCMService; + + @Test + void 유저의_FCM_정보를_가져온다() { + // given + List memberFCMS = List.of( + new MemberFCM(1L, 1L, "token"), + new MemberFCM(2L, 1L, "token2") + ); + given(memberFCMRepository.findByMemberId(anyLong())) + .willReturn(memberFCMS); + + List expect = memberFCMS.stream() + .map(MemberFCMResponse::from) + .collect(Collectors.toList()); + + // when + List actual = memberFCMService.findMemberFCM(1L).memberFCMs(); + + // then + assertThat(actual).isEqualTo(expect); + } + + @Test + void 유저의_FCM_저장() { + // given + String accessToken = "accessToken"; + String fcmToken = "fcmToken"; + given(authExtractor.extract(any())) + .willReturn(new AuthPayload(1L, Role.MEMBER)); + + // when + memberFCMService.saveMemberFCM(accessToken, fcmToken); + + // then + verify(memberFCMRepository, times(1)) + .save(any(MemberFCM.class)); + } +} diff --git a/backend/src/test/java/com/festago/fcm/repository/MemberFCMRepositoryTest.java b/backend/src/test/java/com/festago/fcm/repository/MemberFCMRepositoryTest.java new file mode 100644 index 000000000..3ea135724 --- /dev/null +++ b/backend/src/test/java/com/festago/fcm/repository/MemberFCMRepositoryTest.java @@ -0,0 +1,36 @@ +package com.festago.fcm.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.festago.auth.domain.SocialType; +import com.festago.fcm.domain.MemberFCM; +import com.festago.member.domain.Member; +import com.festago.member.repository.MemberRepository; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +@DataJpaTest +class MemberFCMRepositoryTest { + + @Autowired + MemberFCMRepository memberFCMRepository; + + @Autowired + MemberRepository memberRepository; + + @Test + void member_의_MemberFCM_을_찾을_수_있다() { + // given + Member member = memberRepository.save(new Member("socialId", SocialType.FESTAGO, "nickname", "image.jpg")); + Long memberId = member.getId(); + MemberFCM expect = memberFCMRepository.save(new MemberFCM(memberId, "fcmToken")); + + // when + List actual = memberFCMRepository.findByMemberId(memberId); + + // then + assertThat(actual).contains(expect); + } +}