diff --git a/build.gradle b/build.gradle index 2c9b5f25..fbc5531b 100644 --- a/build.gradle +++ b/build.gradle @@ -85,6 +85,16 @@ dependencies { // Component that test code convert to Swagger file and include restDocs Api testImplementation('com.epages:restdocs-api-spec-mockmvc:0.18.2') //2.2 + + +// Json Serializer + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0' + implementation 'com.fasterxml.jackson.core:jackson-core:2.15.0' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.15.0' + + // expo push notification + implementation('io.github.jav:expo-server-sdk:1.1.0') } // Querydsl diff --git a/src/main/java/com/e2i/wemeet/config/common/SchedulerConfig.java b/src/main/java/com/e2i/wemeet/config/common/SchedulerConfig.java new file mode 100644 index 00000000..d66eae20 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/config/common/SchedulerConfig.java @@ -0,0 +1,10 @@ +package com.e2i.wemeet.config.common; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class SchedulerConfig { + +} \ No newline at end of file diff --git a/src/main/java/com/e2i/wemeet/config/notification/NotificationConfig.java b/src/main/java/com/e2i/wemeet/config/notification/NotificationConfig.java new file mode 100644 index 00000000..70233744 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/config/notification/NotificationConfig.java @@ -0,0 +1,15 @@ +package com.e2i.wemeet.config.notification; + +import io.github.jav.exposerversdk.PushClient; +import io.github.jav.exposerversdk.PushClientException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class NotificationConfig { + + @Bean + public PushClient pushClient() throws PushClientException { + return new PushClient(); + } +} \ No newline at end of file diff --git a/src/main/java/com/e2i/wemeet/domain/meeting/MeetingRepository.java b/src/main/java/com/e2i/wemeet/domain/meeting/MeetingRepository.java index eec8d47c..9fcd0c75 100644 --- a/src/main/java/com/e2i/wemeet/domain/meeting/MeetingRepository.java +++ b/src/main/java/com/e2i/wemeet/domain/meeting/MeetingRepository.java @@ -42,6 +42,18 @@ public interface MeetingRepository extends JpaRepository, Meeting join MeetingRequest mr on mr.partnerTeam = pt and mr.team = t where mr.meetingRequestId = :meetingRequestId """) - List findCreatedAtByMeetingRequestId(@Param("meetingRequestId") final Long meetingRequestId); + List findCreatedAtByMeetingRequestId( + @Param("meetingRequestId") final Long meetingRequestId); + + /* + * 팀장의 pushToken 조회 + * */ + @Query(""" + select p.token + from PushToken p + join Team t on t.teamLeader.memberId = p.member.memberId and t.deletedAt is null + where t.teamId = :teamId + """) + Optional findLeaderPushTokenById(@Param("teamId") final Long teamId); } diff --git a/src/main/java/com/e2i/wemeet/domain/notification/PushTokenRepository.java b/src/main/java/com/e2i/wemeet/domain/notification/PushTokenRepository.java index 3b817452..ecb59571 100644 --- a/src/main/java/com/e2i/wemeet/domain/notification/PushTokenRepository.java +++ b/src/main/java/com/e2i/wemeet/domain/notification/PushTokenRepository.java @@ -1,5 +1,6 @@ package com.e2i.wemeet.domain.notification; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -10,4 +11,15 @@ public interface PushTokenRepository extends JpaRepository { @Query("select p from PushToken p where p.token = :token") Optional findByToken(@Param("token") String token); -} \ No newline at end of file + @Query("select p.token from PushToken p where p.member.memberId is not null") + List findAllMemberTokens(); + + @Query(""" + select p.token + from PushToken p + join Member m on m.memberId = p.member.memberId + where p.member.memberId is not null + and m.role = 'USER' + """) + List findTokensOfMemberWithoutTeam(); +} diff --git a/src/main/java/com/e2i/wemeet/service/meeting/MeetingEvent.java b/src/main/java/com/e2i/wemeet/service/meeting/MeetingEvent.java index 275f2c9e..1f477953 100644 --- a/src/main/java/com/e2i/wemeet/service/meeting/MeetingEvent.java +++ b/src/main/java/com/e2i/wemeet/service/meeting/MeetingEvent.java @@ -2,17 +2,22 @@ import com.e2i.wemeet.domain.cost.Spent; import com.e2i.wemeet.service.cost.SpendEvent; +import com.e2i.wemeet.service.notification.event.NotificationEvent; import com.e2i.wemeet.service.sns.SnsEvent; public record MeetingEvent( SnsEvent snsEvent, - SpendEvent spendEvent + SpendEvent spendEvent, + NotificationEvent notificationEvent + ) { - public static MeetingEvent of(String receivePhoneNumber, String message, Spent type, Long memberId) { + public static MeetingEvent of(String receivePhoneNumber, String token, String message, + Spent type, Long memberId) { SnsEvent snsEvent = new SnsEvent(receivePhoneNumber, message); SpendEvent spendEvent = new SpendEvent(type, memberId); - return new MeetingEvent(snsEvent, spendEvent); + NotificationEvent notificationEvent = new NotificationEvent(token, message); + return new MeetingEvent(snsEvent, spendEvent, notificationEvent); } } diff --git a/src/main/java/com/e2i/wemeet/service/meeting/MeetingHandleServiceImpl.java b/src/main/java/com/e2i/wemeet/service/meeting/MeetingHandleServiceImpl.java index 6bdb6c3c..263df09f 100644 --- a/src/main/java/com/e2i/wemeet/service/meeting/MeetingHandleServiceImpl.java +++ b/src/main/java/com/e2i/wemeet/service/meeting/MeetingHandleServiceImpl.java @@ -66,7 +66,8 @@ public Long sendRequest(final SendMeetingRequestDto requestDto, final Long membe MeetingRequest request = meetingRequestRepository.save(meetingRequest); // 이벤트 발행 - publishMeetingEvent(getMeetingRequestMessage(), memberLeaderId, partnerTeam, MEETING_REQUEST); + publishMeetingEvent(getMeetingRequestMessage(), memberLeaderId, partnerTeam, + MEETING_REQUEST); return request.getMeetingRequestId(); } @@ -110,7 +111,8 @@ public Long acceptRequest(final Long memberLeaderId, final Long meetingRequestId // 미팅 성사 이벤트 발행 Team myTeam = meetingRequest.getTeam(); String leaderNickname = meetingRequest.getPartnerTeam().getTeamLeader().getNickname(); - publishMeetingEvent(getMeetingAcceptMessage(leaderNickname), memberLeaderId, myTeam, MEETING_ACCEPT); + publishMeetingEvent(getMeetingAcceptMessage(leaderNickname), memberLeaderId, myTeam, + MEETING_ACCEPT); return saveMeeting(meetingRequest).getMeetingId(); } @@ -181,8 +183,11 @@ private void publishMeetingEvent(final String message, final Long memberLeaderId final Team targetTeam, final Spent spent) { String leaderPhoneNumber = meetingRepository.findLeaderPhoneNumberById( targetTeam.getTeamId()); + String leaderPushToken = meetingRepository.findLeaderPushTokenById( + targetTeam.getTeamId()).orElse(null); + eventPublisher.publishEvent( - MeetingEvent.of(leaderPhoneNumber, message, spent, memberLeaderId) + MeetingEvent.of(leaderPhoneNumber, leaderPushToken, message, spent, memberLeaderId) ); } diff --git a/src/main/java/com/e2i/wemeet/service/notification/NotificationHandleService.java b/src/main/java/com/e2i/wemeet/service/notification/NotificationHandleService.java new file mode 100644 index 00000000..602e8478 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/notification/NotificationHandleService.java @@ -0,0 +1,18 @@ +package com.e2i.wemeet.service.notification; + + +import java.util.List; + +public interface NotificationHandleService { + + /* + * Body 미포함 푸시 알림 전송 + */ + void sendPushNotification(List tokens, String title); + + /* + * Body 포함 푸시 알림 전송 + */ + void sendPushNotification(List tokens, String title, String body); + +} diff --git a/src/main/java/com/e2i/wemeet/service/notification/NotificationHandleServiceImpl.java b/src/main/java/com/e2i/wemeet/service/notification/NotificationHandleServiceImpl.java new file mode 100644 index 00000000..45b5da40 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/notification/NotificationHandleServiceImpl.java @@ -0,0 +1,38 @@ +package com.e2i.wemeet.service.notification; + +import io.github.jav.exposerversdk.ExpoPushMessage; +import io.github.jav.exposerversdk.PushClient; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + + +@RequiredArgsConstructor +@Service +public class NotificationHandleServiceImpl implements NotificationHandleService { + + private final PushClient client; + + @Override + public void sendPushNotification(List tokens, String title) { + sendPushNotification(tokens, title, null); + } + + @Override + public void sendPushNotification(List tokens, String title, String body) { + ExpoPushMessage expoPushMessage = createExpoPushMessage(tokens, title, body); + client.sendPushNotificationsAsync(List.of(expoPushMessage)); + } + + private ExpoPushMessage createExpoPushMessage(List tokens, String title, String body) { + ExpoPushMessage expoPushMessage = new ExpoPushMessage(); + expoPushMessage.setTo(tokens); + expoPushMessage.setTitle(title); + + if (body != null && !body.isBlank()) { + expoPushMessage.setBody(body); + } + + return expoPushMessage; + } +} diff --git a/src/main/java/com/e2i/wemeet/service/notification/NotificationScheduler.java b/src/main/java/com/e2i/wemeet/service/notification/NotificationScheduler.java new file mode 100644 index 00000000..e92a1658 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/notification/NotificationScheduler.java @@ -0,0 +1,22 @@ +package com.e2i.wemeet.service.notification; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class NotificationScheduler { + + private final NotificationService notificationService; + private static final String TITLE = "기다리고 기다리던 11시 11분이야!"; + private static final String BODY = "오늘의 추천 친구들을 확인해 봐! 🤩"; + + // 매일 23시 11분에 실행 + @Scheduled(cron = "0 11 23 * * ?") + public void sendPushNotificationForSuggestion() { + notificationService.sendToAllMembers(TITLE, BODY); + } +} + + diff --git a/src/main/java/com/e2i/wemeet/service/notification/NotificationService.java b/src/main/java/com/e2i/wemeet/service/notification/NotificationService.java new file mode 100644 index 00000000..5ddf8cae --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/notification/NotificationService.java @@ -0,0 +1,14 @@ +package com.e2i.wemeet.service.notification; + +public interface NotificationService { + + /* + * 전체 사용자 푸시 알림 전송 + */ + void sendToAllMembers(String title, String body); + + /* + * 팀이 없는 사용자 푸시 알림 전송 + */ + void sendToMembersWithoutTeam(String title, String body); +} diff --git a/src/main/java/com/e2i/wemeet/service/notification/NotificationServiceImpl.java b/src/main/java/com/e2i/wemeet/service/notification/NotificationServiceImpl.java new file mode 100644 index 00000000..6dfa8a01 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/notification/NotificationServiceImpl.java @@ -0,0 +1,26 @@ +package com.e2i.wemeet.service.notification; + +import com.e2i.wemeet.domain.notification.PushTokenRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class NotificationServiceImpl implements NotificationService { + + private final NotificationHandleService notificationHandleService; + private final PushTokenRepository pushTokenRepository; + + @Override + public void sendToAllMembers(String title, String body) { + List tokens = pushTokenRepository.findAllMemberTokens(); + notificationHandleService.sendPushNotification(tokens, title, body); + } + + @Override + public void sendToMembersWithoutTeam(String title, String body) { + List tokens = pushTokenRepository.findTokensOfMemberWithoutTeam(); + notificationHandleService.sendPushNotification(tokens, title, body); + } +} diff --git a/src/main/java/com/e2i/wemeet/service/notification/event/NotificatioEventService.java b/src/main/java/com/e2i/wemeet/service/notification/event/NotificatioEventService.java new file mode 100644 index 00000000..070cd327 --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/notification/event/NotificatioEventService.java @@ -0,0 +1,9 @@ +package com.e2i.wemeet.service.notification.event; + +public interface NotificatioEventService { + + /* + * 이벤트 전용 푸시 알림 전송 + */ + void sendForNotificationEvent(NotificationEvent notificationEvent); +} diff --git a/src/main/java/com/e2i/wemeet/service/notification/event/NotificationEvent.java b/src/main/java/com/e2i/wemeet/service/notification/event/NotificationEvent.java new file mode 100644 index 00000000..013ad24d --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/notification/event/NotificationEvent.java @@ -0,0 +1,8 @@ +package com.e2i.wemeet.service.notification.event; + +public record NotificationEvent(String token, String title) { + + public static NotificationEvent of(String token, String title) { + return new NotificationEvent(token, title); + } +} diff --git a/src/main/java/com/e2i/wemeet/service/notification/event/NotificationEventServiceImpl.java b/src/main/java/com/e2i/wemeet/service/notification/event/NotificationEventServiceImpl.java new file mode 100644 index 00000000..5e91001f --- /dev/null +++ b/src/main/java/com/e2i/wemeet/service/notification/event/NotificationEventServiceImpl.java @@ -0,0 +1,31 @@ +package com.e2i.wemeet.service.notification.event; + +import com.e2i.wemeet.service.meeting.MeetingEvent; +import com.e2i.wemeet.service.notification.NotificationHandleService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class NotificationEventServiceImpl implements NotificatioEventService { + + private final NotificationHandleService notificationHandleService; + + @EventListener(classes = MeetingEvent.class) + public void sendForNotificationEvent(final MeetingEvent event) { + sendForNotificationEvent(event.notificationEvent()); + } + + @EventListener(classes = NotificationEvent.class) + @Override + public void sendForNotificationEvent(final NotificationEvent notificationEvent) { + if (notificationEvent.token() == null) { + return; + } + + notificationHandleService.sendPushNotification(List.of(notificationEvent.token()), + notificationEvent.title()); + } +} diff --git a/src/main/resources/static/swagger-ui/openapi3.yaml b/src/main/resources/static/swagger-ui/openapi3.yaml index 912adf17..956d251f 100644 --- a/src/main/resources/static/swagger-ui/openapi3.yaml +++ b/src/main/resources/static/swagger-ui/openapi3.yaml @@ -4,18 +4,18 @@ info: description: We:meet API Documentation version: 0.1.0 servers: - - url: http://localhost:8080 - description: LOCAL server - - url: http://ec2-52-78-215-171.ap-northeast-2.compute.amazonaws.com:8080 - description: Develop Server - - url: http://wemeet-elb-1696815651.ap-northeast-2.elb.amazonaws.com - description: Production Server -tags: [ ] +- url: http://localhost:8080 + description: LOCAL server +- url: http://ec2-52-78-215-171.ap-northeast-2.compute.amazonaws.com:8080 + description: Develop Server +- url: http://wemeet-elb-1696815651.ap-northeast-2.elb.amazonaws.com + description: Production Server +tags: [] paths: /v1/credit: get: tags: - - 크레딧 관련 API + - 크레딧 관련 API summary: 로그인한 사용자의 크레딧을 조회합니다 description: |2 로그인한 사용자의 크레딧을 조회합니다. @@ -34,7 +34,7 @@ paths: /v1/meeting: post: tags: - - 미팅 관련 API + - 미팅 관련 API summary: 상대 이성 팀에게 미팅을 신청합니다. description: |2 상대 이성 팀에게 미팅을 신청합니다. @@ -62,7 +62,7 @@ paths: /v1/member: get: tags: - - 회원 관련 API + - 회원 관련 API summary: 로그인된 사용자의 정보를 조회합니다 description: |2 AccessToken 을 통해 로그인된 사용자의 상세 정보를 조회합니다. @@ -83,7 +83,7 @@ paths: ,\"lowUrl\":\"/v1/kai\"},\"emailAuthenticated\":true}}" post: tags: - - 회원 관련 API + - 회원 관련 API summary: 회원가입 API 입니다. description: |2 회원 정보를 통해 회원가입을 진행합니다. @@ -112,7 +112,7 @@ paths: ,\"data\":null}" delete: tags: - - 회원 관련 API + - 회원 관련 API summary: 회원 탈퇴 API 입니다. description: |2 회원 탈퇴를 수행합니다. (deleteAt 컬럼에 현재 시간을 기록합니다.) @@ -130,7 +130,7 @@ paths: ,\"data\":null}" patch: tags: - - 회원 관련 API + - 회원 관련 API summary: 회원 상세 정보를 수정합니다 description: |2 회원의 상세 정보를 수정합니다. @@ -158,7 +158,7 @@ paths: /v1/push: post: tags: - - 토큰 관련 API + - 토큰 관련 API summary: PUSH TOKEN을 저장합니다. description: |2 PUSH 토큰을 저장합니다. AccessToken 헤더를 전달하지 않으면 memberId가 null 로 저장됩니다. @@ -166,13 +166,13 @@ paths: body에 token, AccessToken 헤더에 해당 User에 AccessToken을 전달하면 됩니다. operationId: PUSH TOKEN 저장 요청 parameters: - - name: AccessToken - in: header - description: Access Token - required: true - schema: - type: string - example: Bearer token + - name: AccessToken + in: header + description: Access Token + required: true + schema: + type: string + example: Bearer token requestBody: content: application/json;charset=UTF-8: @@ -195,7 +195,7 @@ paths: /v1/recommend: post: tags: - - 회원 관련 API + - 회원 관련 API summary: 추천인을 등록합니다. description: |2 위밋을 추천해준 추천인의 전화번호를 입력하여 추천인으로 등록합니다. @@ -223,7 +223,7 @@ paths: /v1/suggestion: get: tags: - - 추천 관련 API + - 추천 관련 API summary: 오늘의 추천 받기 API 입니다. description: |2 오늘의 추천 정보를 반환합니다. @@ -247,7 +247,7 @@ paths: /v1/team: get: tags: - - 팀 관련 API + - 팀 관련 API summary: 내 팀 정보를 조회합니다. description: |2 내 팀 정보를 조회합니다. @@ -273,7 +273,7 @@ paths: :\"23\",\"emailAuthenticated\":true}}}}" put: tags: - - 팀 관련 API + - 팀 관련 API summary: 팀 수정 API 입니다. description: " 나의 팀 정보를 수정합니다.\n multipart/form-data 데이터로 보내주어야함 (data는\ \ json!)\n part - 'images': 사진 파일\n part - 'data': 팀 수정 요청 데이터\n\n \ @@ -310,7 +310,7 @@ paths: ,\"data\":null}" post: tags: - - 팀 관련 API + - 팀 관련 API summary: 팀 생성 API 입니다. description: " 팀을 생성합니다.\n multipart/form-data 데이터로 보내주어야함 (data는 json!)\n\ \ part - 'images': 사진 파일\n part - 'data': 팀 수정 요청 데이터\n\n 성공 시 AccessToken\ @@ -348,7 +348,7 @@ paths: ,\"data\":null}" delete: tags: - - 팀 관련 API + - 팀 관련 API summary: 팀 삭제 API 입니다. description: |2 현재 속한 팀을 삭제합니다. @@ -370,19 +370,19 @@ paths: /v1/auth/persist: get: tags: - - 토큰 관련 API + - 토큰 관련 API summary: AccessToken 을 통해 유저의 상태 정보를 반환합니다. description: |2 AccessToken 을 통해 유저의 상태 정보를 반환합니다. operationId: PERSIST LOGIN 요청 parameters: - - name: AccessToken - in: header - description: Access Token - required: true - schema: - type: string - example: Bearer token + - name: AccessToken + in: header + description: Access Token + required: true + schema: + type: string + example: Bearer token responses: "200": description: "200" @@ -401,27 +401,27 @@ paths: /v1/auth/refresh: post: tags: - - 토큰 관련 API + - 토큰 관련 API summary: "RefreshToken 을 사용하여 Access, Refresh Token을 갱신합니다." description: " Access Token & RefreshToken 을 사용하여 Access, Refresh Token을\ \ 갱신합니다. \n\n Access Token과 Refresh Token 을 Header 에 넘겨주어야합니다.\n Access\ \ Token 은 유저의 값을 받아오는 용도이기 때문에 만료된 상태여도 상관 없습니다.\n" operationId: Refresh Token parameters: - - name: AccessToken - in: header - description: Access Token - required: true - schema: - type: string - example: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJBQ0NFU1MiLCJpc3MiOiJXRTpNRUVUIiwiZXhwIjoxNjk5NTE0NDg0LCJpZCI6MTAwLCJyb2xlIjoiVVNFUiJ9.2Sg6hTYlQAcx3hmCmjmSbebPPPHukAa3iDIvO117ETZZZ0i1kS_6bgmyzbAmYI0Md2cOfjCeXZmrJ72nbVThPg - - name: RefreshToken - in: header - description: Refresh Token - required: true - schema: - type: string - example: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJSRUZSRVNIIiwiaXNzIjoiV0U6TUVFVCIsImV4cCI6MTcwMjEwNDY4NH0.UdEkkGDm7cY62H49o_Zjf5Hf5GQ_5JuvvOZILUr_HeEAaWTCHmPXm9D7e_OD1kYbtyMwo8qrGEc6Cksd7RAwgA + - name: AccessToken + in: header + description: Access Token + required: true + schema: + type: string + example: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJBQ0NFU1MiLCJpc3MiOiJXRTpNRUVUIiwiZXhwIjoxNjk5OTUyNzQ4LCJpZCI6MTAwLCJyb2xlIjoiVVNFUiJ9.4aTyxZI8wlHxYaoEocyZNDFQrhTIZP7FswsJkzdaQYOTtsEch22Y31sZVwQTSOfMv4V-46xCPzdMK8zEzrV8uQ + - name: RefreshToken + in: header + description: Refresh Token + required: true + schema: + type: string + example: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJSRUZSRVNIIiwiaXNzIjoiV0U6TUVFVCIsImV4cCI6MTcwMjU0Mjk0OH0.YIvB5vy0CsJVwI67wU6vkVunYMRg-cpKLSx7TyNRu0ewDPrhPpxNWruaBZIjmZN46EjFRSaw6E5BRVBokTAZLg responses: "200": description: "200" @@ -445,7 +445,7 @@ paths: /v1/heart/received: get: tags: - - 좋아요 관련 API + - 좋아요 관련 API summary: 받은 좋아요 내역을 조회합니다. description: |2 나의 팀이 받은 좋아요 내역을 조회합니다. @@ -464,13 +464,13 @@ paths: \ Detail Success\",\"data\":[{\"teamId\":1,\"memberNum\":3,\"\ region\":\"신촌\",\"profileImageURL\":\"https://test.image.com\"\ ,\"mainImageURL\":\"https://test.image.com\",\"receivedTime\"\ - :\"2023-11-09T15:51:30.318742\",\"leader\":{\"nickname\":\"팀장님\ + :\"2023-11-14T17:36:07.654838\",\"leader\":{\"nickname\":\"팀장님\ \",\"mbti\":\"ENFP\",\"college\":\"서울대\",\"admissionYear\":\"\ 19\",\"emailAuthenticated\":true}}]}" /v1/heart/sent: get: tags: - - 좋아요 관련 API + - 좋아요 관련 API summary: 보낸 좋아요 내역을 조회합니다. description: |2 내가 보낸 좋아요 내역을 조회합니다. @@ -488,13 +488,13 @@ paths: value: "{\"status\":\"SUCCESS\",\"message\":\"Get Sent Heart Detail\ \ Success\",\"data\":[{\"teamId\":1,\"memberNum\":3,\"region\"\ :\"신촌\",\"profileImageURL\":\"https://test.image.com\",\"mainImageURL\"\ - :\"https://test.image.com\",\"sentTime\":\"2023-11-09T15:51:30.283166\"\ + :\"https://test.image.com\",\"sentTime\":\"2023-11-14T17:36:07.445422\"\ ,\"leader\":{\"nickname\":\"팀장님\",\"mbti\":\"ENFP\",\"college\"\ :\"서울대\",\"admissionYear\":\"19\",\"emailAuthenticated\":true}}]}" /v1/heart/{partnerTeamId}: post: tags: - - 좋아요 관련 API + - 좋아요 관련 API summary: 상대 팀에게 좋아요를 보냅니다. description: |2+ 상대 팀에게 좋아요를 보냅니다. @@ -502,12 +502,12 @@ paths: operationId: 좋아요 보내기 parameters: - - name: partnerTeamId - in: path - description: "" - required: true - schema: - type: string + - name: partnerTeamId + in: path + description: "" + required: true + schema: + type: string responses: "200": description: "200" @@ -522,7 +522,7 @@ paths: /v1/meeting/accepted: get: tags: - - 미팅 관련 API + - 미팅 관련 API summary: 성사된 미팅 리스트를 조회합니다. description: |2 성사된 미팅 리스트를 조회합니다. @@ -540,14 +540,14 @@ paths: 성사된 미팅 리스트 조회: value: "{\"status\":\"SUCCESS\",\"message\":\"Get accepted meeting\ \ list success\",\"data\":[{\"meetingId\":1,\"teamId\":1,\"memberCount\"\ - :2,\"region\":\"HONGDAE\",\"meetingAcceptTime\":\"2023-11-09T15:51:30.435221\"\ + :2,\"region\":\"HONGDAE\",\"meetingAcceptTime\":\"2023-11-14T17:36:08.178798\"\ ,\"teamProfileImageUrl\":[\"https://profile1.com\"],\"leader\"\ :{\"leaderId\":1,\"nickname\":\"채원\",\"mbti\":\"INFP\",\"collegeName\"\ :\"고려대학교\",\"collegeType\":\"ENGINEERING\",\"admissionYear\":\"\ 2022\",\"leaderLowProfileImageUrl\":\"https://profile.com\",\"\ imageAuth\":true,\"emailAuthenticated\":false},\"deleted\":false,\"\ expired\":false},{\"meetingId\":2,\"teamId\":2,\"memberCount\"\ - :2,\"region\":\"HONGDAE\",\"meetingAcceptTime\":\"2023-11-09T15:51:30.435273\"\ + :2,\"region\":\"HONGDAE\",\"meetingAcceptTime\":\"2023-11-14T17:36:08.178959\"\ ,\"teamProfileImageUrl\":[\"https://profile2.com\"],\"leader\"\ :{\"leaderId\":2,\"nickname\":\"째림\",\"mbti\":\"INFJ\",\"collegeName\"\ :\"서울대학교\",\"collegeType\":\"ARTS\",\"admissionYear\":\"2019\"\ @@ -557,7 +557,7 @@ paths: /v1/meeting/message: post: tags: - - 미팅 관련 API + - 미팅 관련 API summary: 상대 이성 팀에게 쪽지와 함께 미팅을 신청합니다. description: |2 상대 이성 팀에게 쪽지와 함께 미팅을 신청합니다. @@ -586,7 +586,7 @@ paths: /v1/meeting/received: get: tags: - - 미팅 관련 API + - 미팅 관련 API summary: 받은 미팅 신청 리스트를 조회합니다. description: |2 우리 팀이 받은 미팅 신청 리스트를 조회합니다. @@ -604,7 +604,7 @@ paths: 받은 미팅 신청 리스트 조회: value: "{\"status\":\"SUCCESS\",\"message\":\"Get receive meeting\ \ request list success\",\"data\":[{\"meetingRequestId\":1,\"\ - acceptStatus\":\"PENDING\",\"requestTime\":\"2023-11-09T15:51:30.46193\"\ + acceptStatus\":\"PENDING\",\"requestTime\":\"2023-11-14T17:36:08.298026\"\ ,\"partnerTeamDeleted\":false,\"teamId\":1,\"memberCount\":4,\"\ region\":\"HONGDAE\",\"message\":\"재미있게 놀아요!\",\"teamProfileImageUrl\"\ :[\"https://profile1.com\"],\"leader\":{\"leaderId\":1,\"nickname\"\ @@ -612,7 +612,7 @@ paths: :\"ENGINEERING\",\"admissionYear\":\"2022\",\"leaderLowProfileImageUrl\"\ :\"https://profile.com\",\"imageAuth\":true,\"emailAuthenticated\"\ :true},\"deleted\":false,\"pending\":true},{\"meetingRequestId\"\ - :2,\"acceptStatus\":\"PENDING\",\"requestTime\":\"2023-11-09T15:51:30.461934\"\ + :2,\"acceptStatus\":\"PENDING\",\"requestTime\":\"2023-11-14T17:36:08.298032\"\ ,\"partnerTeamDeleted\":false,\"teamId\":2,\"memberCount\":4,\"\ region\":\"HONGDAE\",\"message\":\"재미있게 놀아요!\",\"teamProfileImageUrl\"\ :[\"https://profile2.com\"],\"leader\":{\"leaderId\":2,\"nickname\"\ @@ -623,7 +623,7 @@ paths: /v1/meeting/sent: get: tags: - - 미팅 관련 API + - 미팅 관련 API summary: 보낸 미팅 신청 리스트를 조회합니다. description: |2 보낸 미팅 신청 리스트를 조회합니다. @@ -641,7 +641,7 @@ paths: 보낸 신청 리스트 조회: value: "{\"status\":\"SUCCESS\",\"message\":\"Get sent meeting request\ \ list success\",\"data\":[{\"meetingRequestId\":1,\"acceptStatus\"\ - :\"PENDING\",\"requestTime\":\"2023-11-09T15:51:30.384274\",\"\ + :\"PENDING\",\"requestTime\":\"2023-11-14T17:36:07.933014\",\"\ partnerTeamDeleted\":false,\"teamId\":1,\"memberCount\":4,\"region\"\ :\"HONGDAE\",\"message\":\"재미있게 놀아요!\",\"teamProfileImageUrl\"\ :[\"https://profile1.com\"],\"leader\":{\"leaderId\":1,\"nickname\"\ @@ -649,7 +649,7 @@ paths: :\"ENGINEERING\",\"admissionYear\":\"2022\",\"leaderLowProfileImageUrl\"\ :\"https://profile.com\",\"imageAuth\":true,\"emailAuthenticated\"\ :true},\"deleted\":false,\"pending\":true},{\"meetingRequestId\"\ - :2,\"acceptStatus\":\"PENDING\",\"requestTime\":\"2023-11-09T15:51:30.384322\"\ + :2,\"acceptStatus\":\"PENDING\",\"requestTime\":\"2023-11-14T17:36:07.933116\"\ ,\"partnerTeamDeleted\":false,\"teamId\":2,\"memberCount\":4,\"\ region\":\"HONGDAE\",\"message\":\"재미있게 놀아요!\",\"teamProfileImageUrl\"\ :[\"https://profile2.com\"],\"leader\":{\"leaderId\":2,\"nickname\"\ @@ -660,7 +660,7 @@ paths: /v1/member/block: get: tags: - - 차단 관련 API + - 차단 관련 API summary: 차단한 사용자들의 ID를 조회합니다. description: |2 차단한 사용자들의 ID를 조회합니다. @@ -679,7 +679,7 @@ paths: /v1/member/profile-image: post: tags: - - 회원 관련 API + - 회원 관련 API summary: 회원 프로필 등록 API 입니다. description: |2 회원의 프로필 사진을 등록합니다. @@ -703,7 +703,7 @@ paths: /v1/member/role: get: tags: - - 회원 관련 API + - 회원 관련 API summary: 회원 Role 정보 조회 API 입니다. description: |2 사용자의 팀장 여부와 팀 소속 여부를 조회할 수 있습니다. @@ -722,7 +722,7 @@ paths: /v1/suggestion/check: get: tags: - - 추천 관련 API + - 추천 관련 API summary: 오늘의 추천 받기 여부 확인 API 입니다. description: |2 오늘의 추천을 받았는지에 대한 정보를 확인할 수 있습니다. @@ -746,7 +746,7 @@ paths: /v1/team/image: put: tags: - - 팀 관련 API + - 팀 관련 API summary: 팀 이미지를 업데이트합니다. description: | 팀 이미지를 업데이트합니다. @@ -771,7 +771,7 @@ paths: ]}" post: tags: - - 팀 관련 API + - 팀 관련 API summary: 팀 이미지를 업로드합니다. description: | 팀 이미지를 업로드 합니다. @@ -797,7 +797,7 @@ paths: ]}" delete: tags: - - 팀 관련 API + - 팀 관련 API summary: 팀 이미지를 삭제합니다. description: | 팀 이미지를 삭제합니다. @@ -827,18 +827,18 @@ paths: /v1/team/{teamId}: get: tags: - - 팀 관련 API + - 팀 관련 API summary: 팀 상세 정보 조회 API 입니다. description: |2 팀 ID로 상세정보를 조회합니다. operationId: 팀 상세 정보 조회 parameters: - - name: teamId - in: path - description: "" - required: true - schema: - type: string + - name: teamId + in: path + description: "" + required: true + schema: + type: string responses: "200": description: "200" @@ -866,7 +866,7 @@ paths: /v1/auth/mail/request: post: tags: - - 인증 관련 API + - 인증 관련 API summary: 이메일 인증번호를 발급하는 API 입니다. description: |2 mail 값으로 넘어온 이메일에 인증 번호를 발송합니다. @@ -894,7 +894,7 @@ paths: /v1/auth/mail/validate: post: tags: - - 인증 관련 API + - 인증 관련 API summary: 이메일 인증번호가 일치하는지 검증합니다 description: |2 이메일로 전송했던 인증 번호가 일치하는지 검증합니다. @@ -906,7 +906,7 @@ paths: $ref: '#/components/schemas/v1-auth-mail-validate521266742' examples: 이메일 인증번호 검증: - value: "{\"mail\":\"ghkdalsgus0809@swu.ac.kr\",\"authCode\":\"452113\"\ + value: "{\"mail\":\"ghkdalsgus0809@swu.ac.kr\",\"authCode\":\"628410\"\ }" responses: "200": @@ -922,7 +922,7 @@ paths: /v1/auth/phone/issue: post: tags: - - 인증 관련 API + - 인증 관련 API summary: 휴대폰 인증번호를 발급하는 API 입니다. description: |2 target 값으로 넘어온 휴대폰 번호에 SMS 인증 번호를 발송합니다. @@ -949,7 +949,7 @@ paths: /v1/auth/phone/validate: post: tags: - - 인증 관련 API + - 인증 관련 API summary: 휴대폰 인증번호가 일치하는지 검증하는 API 입니다. description: " SMS 인증 번호를 확인하는 API 입니다 \n\n 인증 번호가 일치하면 AccessToken 과\ \ RefreshToken을 반환합니다 \n\n 인증 번호가 일치하지만 회원가입 되어있지 않은 사용자라면 Token을 반환하지\ @@ -962,7 +962,7 @@ paths: $ref: '#/components/schemas/v1-auth-phone-validate1564587931' examples: 휴대폰 인증번호 검증: - value: "{\"phone\":\"+821088990011\",\"credential\":\"135167\"}" + value: "{\"phone\":\"+821088990011\",\"credential\":\"429954\"}" responses: "200": description: "200" @@ -978,7 +978,7 @@ paths: /v1/meeting/accept/{meetingRequestId}: post: tags: - - 미팅 관련 API + - 미팅 관련 API summary: 미팅 신청을 수락합니다. description: |2 상대 이성 팀으로부터 받은 미팅 신청을 수락합니다. @@ -986,12 +986,12 @@ paths: data 에는 미팅 ID가 반환됩니다. operationId: 미팅 신청 수락 parameters: - - name: meetingRequestId - in: path - description: "" - required: true - schema: - type: string + - name: meetingRequestId + in: path + description: "" + required: true + schema: + type: string responses: "200": description: "200" @@ -1006,18 +1006,18 @@ paths: /v1/meeting/reject/{meetingRequestId}: post: tags: - - 미팅 관련 API + - 미팅 관련 API summary: 미팅 신청을 거절합니다. description: |2 상대 이성 팀으로부터 받은 미팅 신청을 거절합니다. operationId: 미팅 신청 거절 parameters: - - name: meetingRequestId - in: path - description: "" - required: true - schema: - type: string + - name: meetingRequestId + in: path + description: "" + required: true + schema: + type: string responses: "200": description: "200" @@ -1032,7 +1032,7 @@ paths: /v1/member/block/{blockMemberId}: post: tags: - - 차단 관련 API + - 차단 관련 API summary: 지정된 사용자를 차단합니다. description: |2 url에서 ID로 넘겨받은 사용자를 차단합니다. @@ -1046,12 +1046,12 @@ paths: - 팀 상세 조회 불가 operationId: 차단하기 parameters: - - name: blockMemberId - in: path - description: "" - required: true - schema: - type: string + - name: blockMemberId + in: path + description: "" + required: true + schema: + type: string responses: "200": description: "200" @@ -1082,10 +1082,10 @@ components: description: 사용자의 푸시 토큰들을 반환합니다. items: oneOf: - - type: object - - type: boolean - - type: string - - type: number + - type: object + - type: boolean + - type: string + - type: number nickname: type: string description: 사용자의 닉네임을 반환합니다. @@ -1319,10 +1319,10 @@ components: description: 차단된 사용자의 ID items: oneOf: - - type: object - - type: boolean - - type: string - - type: number + - type: object + - type: boolean + - type: string + - type: number message: type: string description: 응답 메시지 @@ -1441,10 +1441,10 @@ components: description: 팀 사진 URL items: oneOf: - - type: object - - type: boolean - - type: string - - type: number + - type: object + - type: boolean + - type: string + - type: number message: type: string description: 응답 메시지 @@ -1573,10 +1573,10 @@ components: description: 팀 프로필 이미지 URL items: oneOf: - - type: object - - type: boolean - - type: string - - type: number + - type: object + - type: boolean + - type: string + - type: number message: type: string description: 응답 메시지 @@ -1914,10 +1914,10 @@ components: description: 팀 프로필 이미지 URL items: oneOf: - - type: object - - type: boolean - - type: string - - type: number + - type: object + - type: boolean + - type: string + - type: number message: type: string description: 응답 메시지 @@ -1932,10 +1932,10 @@ components: description: 업로드된 이미지 URL 목록을 반환합니다. items: oneOf: - - type: object - - type: boolean - - type: string - - type: number + - type: object + - type: boolean + - type: string + - type: number message: type: string description: 응답 메시지 diff --git a/src/test/java/com/e2i/wemeet/domain/notification/PushTokenRepositoryTest.java b/src/test/java/com/e2i/wemeet/domain/notification/PushTokenRepositoryTest.java index dff2b519..866db4d2 100644 --- a/src/test/java/com/e2i/wemeet/domain/notification/PushTokenRepositoryTest.java +++ b/src/test/java/com/e2i/wemeet/domain/notification/PushTokenRepositoryTest.java @@ -1,19 +1,33 @@ package com.e2i.wemeet.domain.notification; +import static com.e2i.wemeet.support.fixture.MemberFixture.KAI; +import static com.e2i.wemeet.support.fixture.MemberFixture.RIM; +import static com.e2i.wemeet.support.fixture.MemberFixture.SEYUN; import static org.assertj.core.api.Assertions.assertThat; +import com.e2i.wemeet.domain.member.Member; +import com.e2i.wemeet.domain.member.MemberRepository; +import com.e2i.wemeet.domain.team.TeamRepository; +import com.e2i.wemeet.support.fixture.TeamFixture; +import com.e2i.wemeet.support.fixture.TeamMemberFixture; import com.e2i.wemeet.support.module.AbstractRepositoryUnitTest; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; - class PushTokenRepositoryTest extends AbstractRepositoryUnitTest { @Autowired private PushTokenRepository pushTokenRepository; + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TeamRepository teamRepository; + @DisplayName("토큰 이름으로 PushToken을 조회할 수 있다.") @Test void findByToken() { @@ -24,6 +38,9 @@ void findByToken() { .build(); pushTokenRepository.save(token); + entityManager.flush(); + entityManager.clear(); + // when Optional findToken = pushTokenRepository.findByToken(tokenName); @@ -31,4 +48,62 @@ void findByToken() { assertThat(findToken).isPresent(); } -} \ No newline at end of file + @DisplayName("모든 사용자의 push token을 조회할 수 있다.") + @Test + void findAllMemberTokens() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Member rim = memberRepository.save(RIM.create(WOMANS_CODE)); + + pushTokenRepository.save(PushToken.builder().token("token1").member(kai).build()); + pushTokenRepository.save(PushToken.builder().token("token2").member(rim).build()); + + // when + List pushTokenList = pushTokenRepository.findAllMemberTokens(); + + // then + assertThat(pushTokenList).hasSize(2); + } + + @DisplayName("모든 사용자의 push token을 조회할 때 회원가입 전의 사용자는 조회되지 않는다.") + @Test + void findAllMemberTokens_PreRegisteredMembersShouldBeExcluded() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Member rim = memberRepository.save(RIM.create(WOMANS_CODE)); + + pushTokenRepository.save(PushToken.builder().token("token1").member(kai).build()); + pushTokenRepository.save(PushToken.builder().token("token2").member(rim).build()); + + pushTokenRepository.save(PushToken.builder().token("token3").build()); + + // when + List pushTokenList = pushTokenRepository.findAllMemberTokens(); + + // then + assertThat(pushTokenList).hasSize(2); + } + + @DisplayName("팀이 없는 사용자들의 push token을 조회할 수 있다.") + @Test + void findTokensOfMemberWithoutTeam() { + // given + Member kai = memberRepository.save(KAI.create(ANYANG_CODE)); + Member rim = memberRepository.save(RIM.create(WOMANS_CODE)); + Member seyun = memberRepository.save(SEYUN.create(KOREA_CODE)); + + teamRepository.save( + TeamFixture.HONGDAE_TEAM_1.create(kai, TeamMemberFixture.create_3_man())); + teamRepository.save(TeamFixture.WOMAN_TEAM.create(rim, TeamMemberFixture.create_1_woman())); + + pushTokenRepository.save(PushToken.builder().token("token1").member(kai).build()); + pushTokenRepository.save(PushToken.builder().token("token2").member(rim).build()); + pushTokenRepository.save(PushToken.builder().token("token3").member(seyun).build()); + + // when + List pushTokenList = pushTokenRepository.findTokensOfMemberWithoutTeam(); + + // then + assertThat(pushTokenList).hasSize(1); + } +} diff --git a/src/test/java/com/e2i/wemeet/service/notification/NotificationHandleServiceTest.java b/src/test/java/com/e2i/wemeet/service/notification/NotificationHandleServiceTest.java new file mode 100644 index 00000000..e50b54c3 --- /dev/null +++ b/src/test/java/com/e2i/wemeet/service/notification/NotificationHandleServiceTest.java @@ -0,0 +1,75 @@ +package com.e2i.wemeet.service.notification; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +import io.github.jav.exposerversdk.ExpoPushMessage; +import io.github.jav.exposerversdk.PushClient; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class NotificationHandleServiceTest { + + @Mock + private PushClient mockPushClient; + + @InjectMocks + private NotificationHandleServiceImpl notificationHandleService; + + @Captor + ArgumentCaptor> messageCaptor; + + + @DisplayName("푸시 알림을 전송할 수 있다.") + @Test + void sendPushNotificationWithBody() { + // given + List tokens = List.of("token1", "token2"); + String title = "Test Title"; + String body = "Test Body"; + + // when + notificationHandleService.sendPushNotification(tokens, title, body); + + // then + verify(mockPushClient).sendPushNotificationsAsync(messageCaptor.capture()); + + List capturedMessages = messageCaptor.getValue(); + assertThat(capturedMessages).hasSize(1); + ExpoPushMessage capturedMessage = capturedMessages.get(0); + + assertThat(capturedMessage.getTo()).isEqualTo(tokens); + assertThat(capturedMessage.getTitle()).isEqualTo(title); + assertThat(capturedMessage.getBody()).isEqualTo(body); + } + + @DisplayName("Body가 없는 푸시 알림을 전송할 수 있다.") + @Test + void sendPushNotificationWithoutBody() { + // given + List tokens = List.of("token1", "token2"); + String title = "Test Title"; + + // when + notificationHandleService.sendPushNotification(tokens, title); + + // then + verify(mockPushClient).sendPushNotificationsAsync(messageCaptor.capture()); + + List capturedMessages = messageCaptor.getValue(); + assertThat(capturedMessages).hasSize(1); + ExpoPushMessage capturedMessage = capturedMessages.get(0); + + assertThat(capturedMessage.getTo()).isEqualTo(tokens); + assertThat(capturedMessage.getTitle()).isEqualTo(title); + assertThat(capturedMessage.getBody()).isNull(); + } +} diff --git a/src/test/java/com/e2i/wemeet/service/notification/NotificationSchedulerTest.java b/src/test/java/com/e2i/wemeet/service/notification/NotificationSchedulerTest.java new file mode 100644 index 00000000..b7289bf1 --- /dev/null +++ b/src/test/java/com/e2i/wemeet/service/notification/NotificationSchedulerTest.java @@ -0,0 +1,33 @@ +package com.e2i.wemeet.service.notification; + +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.DisplayName; +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 NotificationSchedulerTest { + + @Mock + private NotificationService notificationService; + + @InjectMocks + private NotificationScheduler notificationScheduler; + + private static final String TITLE = "기다리고 기다리던 11시 11분이야!"; + private static final String BODY = "오늘의 추천 친구들을 확인해 봐! 🤩"; + + @DisplayName("sendPushNotificationForSuggestion 메소드가 실행되면 sendToAllMembers 메소드가 실행된다.") + @Test + void sendPushNotificationForSuggestionTest() { + // when + notificationScheduler.sendPushNotificationForSuggestion(); + + // then + verify(notificationService).sendToAllMembers(TITLE, BODY); + } +} diff --git a/src/test/java/com/e2i/wemeet/service/notification/NotificationServiceTest.java b/src/test/java/com/e2i/wemeet/service/notification/NotificationServiceTest.java new file mode 100644 index 00000000..bd1a1a86 --- /dev/null +++ b/src/test/java/com/e2i/wemeet/service/notification/NotificationServiceTest.java @@ -0,0 +1,57 @@ +package com.e2i.wemeet.service.notification; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import com.e2i.wemeet.domain.notification.PushTokenRepository; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +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 NotificationServiceTest { + + @Mock + private NotificationHandleService notificationHandleService; + + @Mock + private PushTokenRepository pushTokenRepository; + + @InjectMocks + private NotificationServiceImpl notificationService; + + private final List tokens = List.of("token1", "token2", "token3"); + private final String title = "Test Title"; + private final String body = "Test Body"; + + + @DisplayName("모든 사용자에게 푸시 알림을 보낼 수 있다.") + @Test + void sendToAllMembers() { + // given + given(pushTokenRepository.findAllMemberTokens()).willReturn(tokens); + + // when + notificationService.sendToAllMembers(title, body); + + // then + verify(notificationHandleService).sendPushNotification(tokens, title, body); + } + + @DisplayName("팀이 없는 사용자들에게 푸시 알림을 보낼 수 있다.") + @Test + void sendToMembersWithoutTeam() { + // given + given(pushTokenRepository.findTokensOfMemberWithoutTeam()).willReturn(tokens); + + // when + notificationService.sendToMembersWithoutTeam(title, body); + + // then + verify(notificationHandleService).sendPushNotification(tokens, title, body); + } +} \ No newline at end of file