From 67cf0b4a438dd832721d727d3b5106271839a960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=AA=A8?= Date: Mon, 12 Feb 2024 14:54:27 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B9=9C=EA=B5=AC=EA=B0=80=20=EB=8B=B9?= =?UTF-8?q?=EC=9D=BC=20=EB=AF=B8=EC=85=98=EC=9D=84=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=9E=AC=EC=B4=89=ED=95=98=EA=B8=B0=20(#309)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 미션 캘린더 조회 시 오늘 미션 완료했는지 여부 UrgingStatus 필드 추가 * move: 패키지 이동 * move: PushNotificationConstants 패키지 이동 * feat: 친구가 당일 미션을 완료하지 않은 경우 재촉하기 기능 구현 * test: 미션 재촉하기 테스트 코드 작성 * style: spotless * refactor: memberId 비교 equals로 변경 * style: 출력문 제거 * fix: Long 비교 equals로 변경 * style: spotless --- .../follow/application/FollowService.java | 4 +- .../member/application/MemberService.java | 4 +- .../domain/mission/domain/Mission.java | 11 + .../application/MissionRecordService.java | 17 +- .../MissionRecordCalendarResponse.java | 8 +- .../dto/response/UrgingStatus.java | 12 + .../notification/api/PushController.java | 23 ++ .../notification/application}/FcmService.java | 2 +- .../notification/application/PushService.java | 64 +++++ .../notification/domain/NotificationType.java | 1 + .../dto/request/PushUrgingSendRequest.java | 6 + .../constants/PushNotificationConstants.java | 4 +- .../global/error/exception/ErrorCode.java | 5 + .../config/fcm/FcmConfig.java | 2 +- .../application/PushServiceTest.java | 219 ++++++++++++++++++ 15 files changed, 371 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/depromeet/domain/missionRecord/dto/response/UrgingStatus.java create mode 100644 src/main/java/com/depromeet/domain/notification/api/PushController.java rename src/main/java/com/depromeet/{global/config/fcm => domain/notification/application}/FcmService.java (96%) create mode 100644 src/main/java/com/depromeet/domain/notification/application/PushService.java create mode 100644 src/main/java/com/depromeet/domain/notification/dto/request/PushUrgingSendRequest.java rename src/main/java/com/depromeet/{domain => global}/common/constants/PushNotificationConstants.java (67%) rename src/main/java/com/depromeet/{global => infra}/config/fcm/FcmConfig.java (97%) create mode 100644 src/test/java/com/depromeet/domain/notification/application/PushServiceTest.java diff --git a/src/main/java/com/depromeet/domain/follow/application/FollowService.java b/src/main/java/com/depromeet/domain/follow/application/FollowService.java index 85c22a86e..791383809 100644 --- a/src/main/java/com/depromeet/domain/follow/application/FollowService.java +++ b/src/main/java/com/depromeet/domain/follow/application/FollowService.java @@ -1,6 +1,6 @@ package com.depromeet.domain.follow.application; -import static com.depromeet.domain.common.constants.PushNotificationConstants.*; +import static com.depromeet.global.common.constants.PushNotificationConstants.*; import com.depromeet.domain.follow.dao.MemberRelationRepository; import com.depromeet.domain.follow.domain.MemberRelation; @@ -13,10 +13,10 @@ import com.depromeet.domain.mission.domain.Mission; import com.depromeet.domain.missionRecord.domain.ImageUploadStatus; import com.depromeet.domain.missionRecord.domain.MissionRecord; +import com.depromeet.domain.notification.application.FcmService; import com.depromeet.domain.notification.dao.NotificationRepository; import com.depromeet.domain.notification.domain.Notification; import com.depromeet.domain.notification.domain.NotificationType; -import com.depromeet.global.config.fcm.FcmService; import com.depromeet.global.error.exception.CustomException; import com.depromeet.global.error.exception.ErrorCode; import com.depromeet.global.util.MemberUtil; diff --git a/src/main/java/com/depromeet/domain/member/application/MemberService.java b/src/main/java/com/depromeet/domain/member/application/MemberService.java index 98b6b9018..8793b8cff 100644 --- a/src/main/java/com/depromeet/domain/member/application/MemberService.java +++ b/src/main/java/com/depromeet/domain/member/application/MemberService.java @@ -1,6 +1,6 @@ package com.depromeet.domain.member.application; -import static com.depromeet.domain.common.constants.PushNotificationConstants.*; +import static com.depromeet.global.common.constants.PushNotificationConstants.*; import com.depromeet.domain.auth.dao.RefreshTokenRepository; import com.depromeet.domain.auth.dto.request.UsernameCheckRequest; @@ -16,7 +16,7 @@ import com.depromeet.domain.member.dto.response.MemberFindOneResponse; import com.depromeet.domain.member.dto.response.MemberSearchResponse; import com.depromeet.domain.member.dto.response.MemberSocialInfoResponse; -import com.depromeet.global.config.fcm.FcmService; +import com.depromeet.domain.notification.application.FcmService; import com.depromeet.global.error.exception.CustomException; import com.depromeet.global.error.exception.ErrorCode; import com.depromeet.global.util.MemberUtil; diff --git a/src/main/java/com/depromeet/domain/mission/domain/Mission.java b/src/main/java/com/depromeet/domain/mission/domain/Mission.java index e27430c37..157384410 100644 --- a/src/main/java/com/depromeet/domain/mission/domain/Mission.java +++ b/src/main/java/com/depromeet/domain/mission/domain/Mission.java @@ -121,4 +121,15 @@ public void updateMission(String name, String content, MissionVisibility visibil this.content = content; this.visibility = visibility; } + + public boolean isCompletedMissionToday() { + return this.getMissionRecords().stream() + .filter( + record -> + record.getStartedAt() + .toLocalDate() + .equals(LocalDateTime.now().toLocalDate())) + .findFirst() + .isPresent(); + } } diff --git a/src/main/java/com/depromeet/domain/missionRecord/application/MissionRecordService.java b/src/main/java/com/depromeet/domain/missionRecord/application/MissionRecordService.java index 745a2c417..b75e62211 100644 --- a/src/main/java/com/depromeet/domain/missionRecord/application/MissionRecordService.java +++ b/src/main/java/com/depromeet/domain/missionRecord/application/MissionRecordService.java @@ -91,13 +91,28 @@ public MissionRecordFindOneResponse findOneMissionRecord(Long recordId) { @Transactional(readOnly = true) public MissionRecordCalendarResponse findAllMissionRecord(Long missionId, YearMonth yearMonth) { + final Member member = memberUtil.getCurrentMember(); List missionRecords = missionRecordRepository.findAllByMissionIdAndYearMonth(missionId, yearMonth); List missionRecordFindResponses = missionRecords.stream().map(MissionRecordFindResponse::from).toList(); Mission mission = findMissionById(missionId); + + UrgingStatus urgingStatus = getUrgingStatus(mission, member); + return MissionRecordCalendarResponse.of( - mission.getStartedAt(), mission.getFinishedAt(), missionRecordFindResponses); + mission.getStartedAt(), + mission.getFinishedAt(), + missionRecordFindResponses, + urgingStatus); + } + + private UrgingStatus getUrgingStatus(Mission mission, Member member) { + if (member.getId().equals(mission.getMember().getId()) + || mission.isCompletedMissionToday()) { + return UrgingStatus.NONE; + } + return UrgingStatus.URGING; } public MissionRecordUpdateResponse updateMissionRecord( diff --git a/src/main/java/com/depromeet/domain/missionRecord/dto/response/MissionRecordCalendarResponse.java b/src/main/java/com/depromeet/domain/missionRecord/dto/response/MissionRecordCalendarResponse.java index 76ebba389..206abdcf4 100644 --- a/src/main/java/com/depromeet/domain/missionRecord/dto/response/MissionRecordCalendarResponse.java +++ b/src/main/java/com/depromeet/domain/missionRecord/dto/response/MissionRecordCalendarResponse.java @@ -24,12 +24,14 @@ public record MissionRecordCalendarResponse( defaultValue = "2024-01-15 00:34:00", type = "string") LocalDateTime missionFinishedAt, - @Schema(description = "미션 기록들") List missionRecords) { + @Schema(description = "미션 기록들") List missionRecords, + @Schema(description = "재촉하기 여부") UrgingStatus urgingStatus) { public static MissionRecordCalendarResponse of( LocalDateTime missionStartedAt, LocalDateTime missionFinishedAt, - List missionRecords) { + List missionRecords, + UrgingStatus urgingStatus) { return new MissionRecordCalendarResponse( - missionStartedAt, missionFinishedAt, missionRecords); + missionStartedAt, missionFinishedAt, missionRecords, urgingStatus); } } diff --git a/src/main/java/com/depromeet/domain/missionRecord/dto/response/UrgingStatus.java b/src/main/java/com/depromeet/domain/missionRecord/dto/response/UrgingStatus.java new file mode 100644 index 000000000..a128a7eb0 --- /dev/null +++ b/src/main/java/com/depromeet/domain/missionRecord/dto/response/UrgingStatus.java @@ -0,0 +1,12 @@ +package com.depromeet.domain.missionRecord.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum UrgingStatus { + URGING, + NONE, + ; +} diff --git a/src/main/java/com/depromeet/domain/notification/api/PushController.java b/src/main/java/com/depromeet/domain/notification/api/PushController.java new file mode 100644 index 000000000..c6b5fd381 --- /dev/null +++ b/src/main/java/com/depromeet/domain/notification/api/PushController.java @@ -0,0 +1,23 @@ +package com.depromeet.domain.notification.api; + +import com.depromeet.domain.notification.application.PushService; +import com.depromeet.domain.notification.dto.request.PushUrgingSendRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "7. [알림]", description = "알림 관련 API") +@RestController +@RequestMapping("/notifications") +@RequiredArgsConstructor +public class PushController { + private final PushService pushService; + + @Operation(summary = "재촉하기", description = "당일 미션을 완료하지 않은 친구에게 재촉하기 Push Message를 발송합니다.") + @PostMapping("/urging") + public void urgingSend(@Valid @RequestBody PushUrgingSendRequest request) { + pushService.sendUrgingPush(request); + } +} diff --git a/src/main/java/com/depromeet/global/config/fcm/FcmService.java b/src/main/java/com/depromeet/domain/notification/application/FcmService.java similarity index 96% rename from src/main/java/com/depromeet/global/config/fcm/FcmService.java rename to src/main/java/com/depromeet/domain/notification/application/FcmService.java index 27d5e81ad..12a405aa4 100644 --- a/src/main/java/com/depromeet/global/config/fcm/FcmService.java +++ b/src/main/java/com/depromeet/domain/notification/application/FcmService.java @@ -1,4 +1,4 @@ -package com.depromeet.global.config.fcm; +package com.depromeet.domain.notification.application; import com.google.api.core.ApiFuture; import com.google.firebase.messaging.*; diff --git a/src/main/java/com/depromeet/domain/notification/application/PushService.java b/src/main/java/com/depromeet/domain/notification/application/PushService.java new file mode 100644 index 000000000..3a4d5eaf6 --- /dev/null +++ b/src/main/java/com/depromeet/domain/notification/application/PushService.java @@ -0,0 +1,64 @@ +package com.depromeet.domain.notification.application; + +import static com.depromeet.global.common.constants.PushNotificationConstants.PUSH_URGING_TITLE; + +import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.mission.dao.MissionRepository; +import com.depromeet.domain.mission.domain.Mission; +import com.depromeet.domain.notification.dao.NotificationRepository; +import com.depromeet.domain.notification.domain.Notification; +import com.depromeet.domain.notification.domain.NotificationType; +import com.depromeet.domain.notification.dto.request.PushUrgingSendRequest; +import com.depromeet.global.common.constants.PushNotificationConstants; +import com.depromeet.global.error.exception.CustomException; +import com.depromeet.global.error.exception.ErrorCode; +import com.depromeet.global.util.MemberUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class PushService { + private final MemberUtil memberUtil; + private final FcmService fcmService; + private final MissionRepository missionRepository; + private final NotificationRepository notificationRepository; + + public void sendUrgingPush(PushUrgingSendRequest request) { + final Member currentMember = memberUtil.getCurrentMember(); + final Mission mission = + missionRepository + .findById(request.missionId()) + .orElseThrow(() -> new CustomException(ErrorCode.MISSION_NOT_FOUND)); + final Member targetMember = mission.getMember(); + + validateSelfSending(currentMember.getId(), targetMember.getId()); + validateMissionNotCompletedToday(mission); + + fcmService.sendMessageSync( + targetMember.getFcmInfo().getFcmToken(), + PUSH_URGING_TITLE, + String.format( + PushNotificationConstants.PUSH_URGING_CONTENT, + currentMember.getProfile().getNickname(), + mission.getName())); + Notification notification = + Notification.createNotification( + NotificationType.MISSION_URGING, currentMember, targetMember); + notificationRepository.save(notification); + } + + private void validateMissionNotCompletedToday(Mission mission) { + if (mission.isCompletedMissionToday()) { + throw new CustomException(ErrorCode.TODAY_COMPLETED_MISSION_SENDING_NOT_ALLOWED); + } + } + + private void validateSelfSending(Long currentMemberId, Long targetMemberId) { + if (currentMemberId.equals(targetMemberId)) { + throw new CustomException(ErrorCode.SELF_SENDING_NOT_ALLOWED); + } + } +} diff --git a/src/main/java/com/depromeet/domain/notification/domain/NotificationType.java b/src/main/java/com/depromeet/domain/notification/domain/NotificationType.java index 04a53dc20..41980b239 100644 --- a/src/main/java/com/depromeet/domain/notification/domain/NotificationType.java +++ b/src/main/java/com/depromeet/domain/notification/domain/NotificationType.java @@ -7,6 +7,7 @@ @AllArgsConstructor public enum NotificationType { FOLLOW("팔로우"), + MISSION_URGING("재촉하기"), ; private final String value; diff --git a/src/main/java/com/depromeet/domain/notification/dto/request/PushUrgingSendRequest.java b/src/main/java/com/depromeet/domain/notification/dto/request/PushUrgingSendRequest.java new file mode 100644 index 000000000..70bfd1caa --- /dev/null +++ b/src/main/java/com/depromeet/domain/notification/dto/request/PushUrgingSendRequest.java @@ -0,0 +1,6 @@ +package com.depromeet.domain.notification.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record PushUrgingSendRequest( + @NotNull(message = "missionId는 null일 수 없습니다.") Long missionId) {} diff --git a/src/main/java/com/depromeet/domain/common/constants/PushNotificationConstants.java b/src/main/java/com/depromeet/global/common/constants/PushNotificationConstants.java similarity index 67% rename from src/main/java/com/depromeet/domain/common/constants/PushNotificationConstants.java rename to src/main/java/com/depromeet/global/common/constants/PushNotificationConstants.java index 0f74ce743..b90d712a8 100644 --- a/src/main/java/com/depromeet/domain/common/constants/PushNotificationConstants.java +++ b/src/main/java/com/depromeet/global/common/constants/PushNotificationConstants.java @@ -1,4 +1,4 @@ -package com.depromeet.domain.common.constants; +package com.depromeet.global.common.constants; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -9,4 +9,6 @@ public class PushNotificationConstants { public static final String PUSH_SERVICE_CONTENT = "%s님이 회원님을 팔로우하기 시작했습니다🥳"; public static final String PUSH_NON_COMPLETE_MISSION_SERVICE_CONTENT = "아직 오늘 미션을 완료하지 않았어요! 10분 동안 빠르게 완료해볼까요?"; + public static final String PUSH_URGING_TITLE = "누가 내 미션을 기다린대요"; + public static final String PUSH_URGING_CONTENT = "%s님이 %s 미션을 기다리고 있어요 🥺"; } diff --git a/src/main/java/com/depromeet/global/error/exception/ErrorCode.java b/src/main/java/com/depromeet/global/error/exception/ErrorCode.java index caa8f2b9f..f4a49109f 100644 --- a/src/main/java/com/depromeet/global/error/exception/ErrorCode.java +++ b/src/main/java/com/depromeet/global/error/exception/ErrorCode.java @@ -54,6 +54,11 @@ public enum ErrorCode { IMAGE_KEY_NOT_FOUND(HttpStatus.NOT_FOUND, "이미지 키를 찾을 수 없습니다."), IMAGE_FILE_EXTENSION_NOT_FOUND(HttpStatus.NOT_FOUND, "이미지 파일 형식을 찾을 수 없습니다."), + // Notification + SELF_SENDING_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "본인에게 메세지를 전송할 수 없습니다."), + TODAY_COMPLETED_MISSION_SENDING_NOT_ALLOWED( + HttpStatus.BAD_REQUEST, "오늘 미션을 완료한 미션에는 메세지를 전송할 수 없습니다."), + // Reaction REACTION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 리액션을 찾을 수 없습니다."), REACTION_ALREADY_EXISTS(HttpStatus.CONFLICT, "리액션은 미션기록 당 한번만 가능합니다."), diff --git a/src/main/java/com/depromeet/global/config/fcm/FcmConfig.java b/src/main/java/com/depromeet/infra/config/fcm/FcmConfig.java similarity index 97% rename from src/main/java/com/depromeet/global/config/fcm/FcmConfig.java rename to src/main/java/com/depromeet/infra/config/fcm/FcmConfig.java index d9010fd86..da2f66d75 100644 --- a/src/main/java/com/depromeet/global/config/fcm/FcmConfig.java +++ b/src/main/java/com/depromeet/infra/config/fcm/FcmConfig.java @@ -1,4 +1,4 @@ -package com.depromeet.global.config.fcm; +package com.depromeet.infra.config.fcm; import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; diff --git a/src/test/java/com/depromeet/domain/notification/application/PushServiceTest.java b/src/test/java/com/depromeet/domain/notification/application/PushServiceTest.java new file mode 100644 index 000000000..385f79c7c --- /dev/null +++ b/src/test/java/com/depromeet/domain/notification/application/PushServiceTest.java @@ -0,0 +1,219 @@ +package com.depromeet.domain.notification.application; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.depromeet.DatabaseCleaner; +import com.depromeet.domain.member.dao.MemberRepository; +import com.depromeet.domain.member.domain.FcmInfo; +import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.member.domain.Profile; +import com.depromeet.domain.mission.dao.MissionRepository; +import com.depromeet.domain.mission.domain.Mission; +import com.depromeet.domain.mission.domain.MissionCategory; +import com.depromeet.domain.mission.domain.MissionVisibility; +import com.depromeet.domain.missionRecord.dao.MissionRecordRepository; +import com.depromeet.domain.missionRecord.domain.MissionRecord; +import com.depromeet.domain.notification.dao.NotificationRepository; +import com.depromeet.domain.notification.domain.Notification; +import com.depromeet.domain.notification.domain.NotificationType; +import com.depromeet.domain.notification.dto.request.PushUrgingSendRequest; +import com.depromeet.global.error.exception.CustomException; +import com.depromeet.global.error.exception.ErrorCode; +import com.depromeet.global.security.PrincipalDetails; +import com.depromeet.global.util.MemberUtil; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class PushServiceTest { + @Autowired private DatabaseCleaner databaseCleaner; + + @Autowired private MemberUtil memberUtil; + + @MockBean private FcmService fcmService; + + @Autowired private MissionRepository missionRepository; + + @Autowired private NotificationRepository notificationRepository; + + @Autowired private MemberRepository memberRepository; + + @Autowired private MissionRecordRepository missionRecordRepository; + + @Autowired private PushService pushService; + + @BeforeEach + void setUp() { + databaseCleaner.execute(); + PrincipalDetails principal = new PrincipalDetails(1L, "USER"); + Authentication authentication = + new UsernamePasswordAuthenticationToken( + principal, "password", principal.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + @Nested + class 친구에게_미션을_재촉할_때 { + @Test + void 로그인된_회원이_존재하지_않는다면_예외를_발생시킨다() { + // given + PushUrgingSendRequest request = new PushUrgingSendRequest(1L); + + // when, then + assertThatThrownBy(() -> pushService.sendUrgingPush(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.MEMBER_NOT_FOUND.getMessage()); + } + + @Test + void 미션이_존재하지_않는다면_예외를_발생시킨다() { + // given + PushUrgingSendRequest request = new PushUrgingSendRequest(1L); + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("testNickname1", "testImageUrl1"))); + + // when, then + assertThatThrownBy(() -> pushService.sendUrgingPush(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.MISSION_NOT_FOUND.getMessage()); + } + + @Test + void 본인에게_재촉할_경우_예외를_발생시킨다() { + // given + PushUrgingSendRequest request = new PushUrgingSendRequest(1L); + + // when, then + Member currentMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("testNickname1", "testImageUrl1"))); + LocalDateTime today = LocalDateTime.now(); + LocalDateTime missionStartedAt = today; + LocalDateTime missionFinishedAt = today.plusWeeks(2); + missionRepository.save( + Mission.createMission( + "testMissionName", + "testMissionContent", + 1, + MissionCategory.ETC, + MissionVisibility.ALL, + missionStartedAt, + missionFinishedAt, + currentMember)); + + // when, then + assertThatThrownBy(() -> pushService.sendUrgingPush(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.SELF_SENDING_NOT_ALLOWED.getMessage()); + } + + @Test + void 미션이_당일_완료된_경우_예외를_발생시킨다() { + // given + PushUrgingSendRequest request = new PushUrgingSendRequest(1L); + Member currentMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("testNickname1", "testImageUrl1"))); + Member targetMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("testNickname2", "testImageUrl2"))); + + LocalDateTime today = LocalDateTime.now(); + LocalDateTime missionStartedAt = today; + LocalDateTime missionFinishedAt = today.plusWeeks(2); + Mission mission = + missionRepository.save( + Mission.createMission( + "testMissionName", + "testMissionContent", + 1, + MissionCategory.ETC, + MissionVisibility.ALL, + missionStartedAt, + missionFinishedAt, + targetMember)); + + LocalDateTime missionRecordStartedAt = today; + LocalDateTime missionRecordFinishedAt = + missionRecordStartedAt.plusMinutes(32).plusSeconds(14); + Duration duration = Duration.ofMinutes(32).plusSeconds(14); + MissionRecord missionRecord = + missionRecordRepository.save( + MissionRecord.createMissionRecord( + duration, + missionRecordStartedAt, + missionRecordFinishedAt, + mission)); + missionRecord.updateUploadStatusPending(); + missionRecord.updateUploadStatusComplete("remark", "imageUrl"); + missionRecordRepository.save(missionRecord); + + // when, then + assertThatThrownBy(() -> pushService.sendUrgingPush(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.TODAY_COMPLETED_MISSION_SENDING_NOT_ALLOWED.getMessage()); + } + + @Test + void 정상적이라면_재촉하기_푸시메세지가_발송되고_히스토리가_저장된다() { + // given + when(fcmService.sendMessageSync(any(), any(), any())).thenReturn(null); + + PushUrgingSendRequest request = new PushUrgingSendRequest(1L); + Member currentMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("testNickname1", "testImageUrl1"))); + Member targetMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("testNickname2", "testImageUrl2"))); + targetMember.updateFcmToken(FcmInfo.createFcmInfo(), "testFcmToken"); + memberRepository.save(targetMember); + + LocalDateTime today = LocalDateTime.now(); + LocalDateTime missionStartedAt = today.minusDays(1); + LocalDateTime missionFinishedAt = today.plusWeeks(2); + missionRepository.save( + Mission.createMission( + "testMissionName", + "testMissionContent", + 1, + MissionCategory.ETC, + MissionVisibility.ALL, + missionStartedAt, + missionFinishedAt, + targetMember)); + + // when + pushService.sendUrgingPush(request); + + // then + Optional optionalNotification = notificationRepository.findById(1L); + assertTrue(optionalNotification.isPresent()); + assertEquals(1, notificationRepository.findAll().size()); + assertEquals( + NotificationType.MISSION_URGING, + optionalNotification.get().getNotificationType()); + } + } +}