From 97fe0098f2fea225e03bb41fd4e9fd7d6f9da50f Mon Sep 17 00:00:00 2001 From: Kim EunSu <88280787+rladmstn@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:57:27 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20=EB=8B=A8?= =?UTF-8?q?=EA=B1=B4=20=EC=9D=BD=EC=9D=8C=20=ED=91=9C=EC=8B=9C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : 알림 단건 읽음 표시 API 추가 * test : 알림 읽음 관련 테스트 작성 --- .../exception/CustomExceptionHandler.java | 13 ++ .../controller/NotificationController.java | 15 ++- .../CannotFoundNotificationException.java | 12 ++ .../NotificationValidationException.java | 14 +++ .../service/NotificationService.java | 23 +++- .../service/NotificationServiceTest.java | 112 ++++++++++++++++++ 6 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/gamzabat/algohub/feature/notification/exception/CannotFoundNotificationException.java create mode 100644 src/main/java/com/gamzabat/algohub/feature/notification/exception/NotificationValidationException.java create mode 100644 src/test/java/com/gamzabat/algohub/feature/notification/service/NotificationServiceTest.java diff --git a/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java b/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java index 8a7a76cb..38f6f9b2 100644 --- a/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java +++ b/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java @@ -12,7 +12,9 @@ import com.gamzabat.algohub.feature.group.studygroup.exception.GroupMemberValidationException; import com.gamzabat.algohub.feature.group.studygroup.exception.InvalidRoleException; import com.gamzabat.algohub.feature.notice.exception.NoticeValidationException; +import com.gamzabat.algohub.feature.notification.exception.CannotFoundNotificationException; import com.gamzabat.algohub.feature.notification.exception.CannotFoundNotificationSettingException; +import com.gamzabat.algohub.feature.notification.exception.NotificationValidationException; import com.gamzabat.algohub.feature.problem.exception.NotBojLinkException; import com.gamzabat.algohub.feature.problem.exception.SolvedAcApiErrorException; import com.gamzabat.algohub.feature.solution.exception.CannotFoundSolutionException; @@ -128,4 +130,15 @@ protected ResponseEntity handler(CannotFoundNotificationSettingEx return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ErrorResponse(HttpStatus.NOT_FOUND.value(), e.getError(), null)); } + + @ExceptionHandler(CannotFoundNotificationException.class) + protected ResponseEntity handler(CannotFoundNotificationException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse(HttpStatus.NOT_FOUND.value(), e.getError(), null)); + } + + @ExceptionHandler(NotificationValidationException.class) + protected ResponseEntity handler(NotificationValidationException e) { + return ResponseEntity.status(e.getCode()).body(new ErrorResponse(e.getCode(), e.getError(), null)); + } } diff --git a/src/main/java/com/gamzabat/algohub/feature/notification/controller/NotificationController.java b/src/main/java/com/gamzabat/algohub/feature/notification/controller/NotificationController.java index 91088938..cbb329c0 100644 --- a/src/main/java/com/gamzabat/algohub/feature/notification/controller/NotificationController.java +++ b/src/main/java/com/gamzabat/algohub/feature/notification/controller/NotificationController.java @@ -8,6 +8,7 @@ import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; @@ -50,9 +51,17 @@ public ResponseEntity> getNotifications(@AuthedUse } @PatchMapping - @Operation(summary = "알림 읽음을 표시하는 API", description = "알림 탭을 연 후 닫을 때 호출하면 봤던 알림들은 읽음 처리 되는 API") - public void updateIsRead(@AuthedUser User user) { - notificationService.updateIsRead(user); + @Operation(summary = "전체 알림 읽음 표시 API", description = "알림 탭을 연 후 닫을 때 호출하면 봤던 알림들은 읽음 처리 되는 API") + public ResponseEntity readAllNotifications(@AuthedUser User user) { + notificationService.readAllNotifications(user); + return ResponseEntity.ok().build(); + } + + @PatchMapping("/{notificationId}") + @Operation(summary = "알림 단건 읽음 표시 API", description = "알림 하나를 클릭했을 시 해당 알림은 읽음 처리 되는 API") + public ResponseEntity readNotification(@AuthedUser User user, @PathVariable Long notificationId) { + notificationService.readNotification(user, notificationId); + return ResponseEntity.ok().build(); } @GetMapping(value = "/settings") diff --git a/src/main/java/com/gamzabat/algohub/feature/notification/exception/CannotFoundNotificationException.java b/src/main/java/com/gamzabat/algohub/feature/notification/exception/CannotFoundNotificationException.java new file mode 100644 index 00000000..30be69d3 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/notification/exception/CannotFoundNotificationException.java @@ -0,0 +1,12 @@ +package com.gamzabat.algohub.feature.notification.exception; + +import lombok.Getter; + +@Getter +public class CannotFoundNotificationException extends RuntimeException { + private final String error; + + public CannotFoundNotificationException(String error) { + this.error = error; + } +} diff --git a/src/main/java/com/gamzabat/algohub/feature/notification/exception/NotificationValidationException.java b/src/main/java/com/gamzabat/algohub/feature/notification/exception/NotificationValidationException.java new file mode 100644 index 00000000..483bb4c4 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/notification/exception/NotificationValidationException.java @@ -0,0 +1,14 @@ +package com.gamzabat.algohub.feature.notification.exception; + +import lombok.Getter; + +@Getter +public class NotificationValidationException extends RuntimeException { + private final int code; + private final String error; + + public NotificationValidationException(int code, String error) { + this.code = code; + this.error = error; + } +} diff --git a/src/main/java/com/gamzabat/algohub/feature/notification/service/NotificationService.java b/src/main/java/com/gamzabat/algohub/feature/notification/service/NotificationService.java index e7bad4a8..100437fe 100644 --- a/src/main/java/com/gamzabat/algohub/feature/notification/service/NotificationService.java +++ b/src/main/java/com/gamzabat/algohub/feature/notification/service/NotificationService.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,7 +22,9 @@ import com.gamzabat.algohub.feature.notification.domain.NotificationSetting; import com.gamzabat.algohub.feature.notification.dto.GetNotificationResponse; import com.gamzabat.algohub.feature.notification.enums.NotificationCategory; +import com.gamzabat.algohub.feature.notification.exception.CannotFoundNotificationException; import com.gamzabat.algohub.feature.notification.exception.CannotFoundNotificationSettingException; +import com.gamzabat.algohub.feature.notification.exception.NotificationValidationException; import com.gamzabat.algohub.feature.notification.repository.EmitterRepositoryImpl; import com.gamzabat.algohub.feature.notification.repository.NotificationRepository; import com.gamzabat.algohub.feature.notification.repository.NotificationSettingRepository; @@ -158,13 +161,21 @@ public List getNotifications(User user) { return notifications.stream().map(GetNotificationResponse::toDTO).toList(); } - public void updateIsRead(User user) { + @Transactional + public void readAllNotifications(User user) { List notifications = notificationRepository.findAllByUserAndIsRead(user, false); - notifications.forEach(notification -> { - notification.updateIsRead(); - notificationRepository.save(notification); - }); - log.info("success to read status"); + notifications.forEach(Notification::updateIsRead); + log.info("success to read all notifications."); + } + + @Transactional + public void readNotification(User user, Long notificationId) { + Notification notification = notificationRepository.findById(notificationId) + .orElseThrow(() -> new CannotFoundNotificationException("존재하지 않는 알림입니다.")); + if (!notification.getUser().getId().equals(user.getId())) + throw new NotificationValidationException(HttpStatus.FORBIDDEN.value(), "알림의 주인이 일치하지 않습니다."); + notification.updateIsRead(); + log.info("success to read notification. notificationId : {}", notificationId); } @Transactional diff --git a/src/test/java/com/gamzabat/algohub/feature/notification/service/NotificationServiceTest.java b/src/test/java/com/gamzabat/algohub/feature/notification/service/NotificationServiceTest.java new file mode 100644 index 00000000..143e7343 --- /dev/null +++ b/src/test/java/com/gamzabat/algohub/feature/notification/service/NotificationServiceTest.java @@ -0,0 +1,112 @@ +package com.gamzabat.algohub.feature.notification.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Field; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +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; +import org.springframework.http.HttpStatus; + +import com.gamzabat.algohub.enums.Role; +import com.gamzabat.algohub.feature.group.studygroup.domain.StudyGroup; +import com.gamzabat.algohub.feature.notification.domain.Notification; +import com.gamzabat.algohub.feature.notification.exception.CannotFoundNotificationException; +import com.gamzabat.algohub.feature.notification.exception.NotificationValidationException; +import com.gamzabat.algohub.feature.notification.repository.NotificationRepository; +import com.gamzabat.algohub.feature.user.domain.User; + +@ExtendWith(MockitoExtension.class) +class NotificationServiceTest { + @InjectMocks + private NotificationService notificationService; + @Mock + private NotificationRepository notificationRepository; + private User user, user2; + private StudyGroup group; + private Notification notification1, notification2; + private final Long notificationId = 10L; + private final Long userId = 1L; + + @BeforeEach + void setUp() throws NoSuchFieldException, IllegalAccessException { + user = User.builder().email("email1").password("password").nickname("nickname1") + .role(Role.USER).profileImage("image1").build(); + user2 = User.builder().email("email2").password("password").nickname("nickname2") + .role(Role.USER).profileImage("image2").build(); + group = StudyGroup.builder() + .name("name") + .startDate(LocalDate.now()) + .endDate(LocalDate.now().plusDays(1)) + .groupImage("imageUrl") + .groupCode("code") + .build(); + notification1 = Notification.builder().isRead(false).studyGroup(group).user(user).message("message1").build(); + notification2 = Notification.builder().isRead(false).studyGroup(group).user(user).message("message2").build(); + + Field userId = User.class.getDeclaredField("id"); + userId.setAccessible(true); + userId.set(user, 1L); + + Field notificationId = Notification.class.getDeclaredField("id"); + notificationId.setAccessible(true); + notificationId.set(notification1, 10L); + notificationId.set(notification2, 20L); + } + + @Test + @DisplayName("전체 알림 읽음 표시 성공") + void readAllNotifications() { + // given + when(notificationRepository.findAllByUserAndIsRead(user, false)).thenReturn( + List.of(notification1, notification2)); + // when + notificationService.readAllNotifications(user); + // then + assertThat(notification1.isRead()).isTrue(); + assertThat(notification2.isRead()).isTrue(); + } + + @Test + @DisplayName("알림 단건 읽음 표시 성공") + void readNotification() { + // given + when(notificationRepository.findById(notificationId)).thenReturn(Optional.ofNullable(notification1)); + // when + notificationService.readNotification(user, notificationId); + // then + assertThat(notification1.isRead()).isTrue(); + } + + @Test + @DisplayName("알림 단건 읽음 표시 실패 : 존재하지 않는 알림") + void readNotificationFailed_1() { + // given + when(notificationRepository.findById(notificationId)).thenReturn(Optional.empty()); + // when, then + assertThatThrownBy(() -> notificationService.readNotification(user, notificationId)) + .isInstanceOf(CannotFoundNotificationException.class) + .hasFieldOrPropertyWithValue("error", "존재하지 않는 알림입니다."); + } + + @Test + @DisplayName("알림 단건 읽음 표시 실패 : 알림 주인 불일치") + void readNotificationFailed_2() { + // given + when(notificationRepository.findById(notificationId)).thenReturn(Optional.of(notification1)); + // when, then + assertThatThrownBy(() -> notificationService.readNotification(user2, notificationId)) + .isInstanceOf(NotificationValidationException.class) + .hasFieldOrPropertyWithValue("code", HttpStatus.FORBIDDEN.value()) + .hasFieldOrPropertyWithValue("error", "알림의 주인이 일치하지 않습니다."); + } +} \ No newline at end of file