From 42543c27df059f94d194e7d4390f360b0daef76f Mon Sep 17 00:00:00 2001 From: 5803024019 Date: Fri, 15 May 2026 21:37:55 +0700 Subject: [PATCH] test: add NotificationServiceTest unit test --- .../service/NotificationServiceTest.java | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 walkguide-backend/demo/src/test/java/com/walkguide/service/NotificationServiceTest.java diff --git a/walkguide-backend/demo/src/test/java/com/walkguide/service/NotificationServiceTest.java b/walkguide-backend/demo/src/test/java/com/walkguide/service/NotificationServiceTest.java new file mode 100644 index 0000000..629d560 --- /dev/null +++ b/walkguide-backend/demo/src/test/java/com/walkguide/service/NotificationServiceTest.java @@ -0,0 +1,197 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.SendNotificationRequest; +import com.walkguide.dto.response.NotificationResponse; +import com.walkguide.entity.GuardianNotification; +import com.walkguide.entity.PairingRelation; +import com.walkguide.entity.User; +import com.walkguide.enums.NotificationType; +import com.walkguide.enums.PairingStatus; +import com.walkguide.exception.PairingException; +import com.walkguide.repository.GuardianNotificationRepository; +import com.walkguide.repository.PairingRelationRepository; +import com.walkguide.websocket.LocationBroadcaster; +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.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("NotificationService Unit Tests") +class NotificationServiceTest { + + @Mock GuardianNotificationRepository notifRepository; + @Mock PairingRelationRepository pairingRelationRepository; + @Mock FcmService fcmService; + @Mock LocationBroadcaster locationBroadcaster; + + @InjectMocks NotificationService notificationService; + + private User guardian; + private User user; + private PairingRelation activePairing; + + @BeforeEach + void setUp() { + guardian = User.builder() + .id(1L).email("guardian@test.com").role("ROLE_GUARDIAN") + .displayName("Guardian").fcmToken("guardian-fcm").build(); + + user = User.builder() + .id(2L).email("user@test.com").role("ROLE_USER") + .displayName("User").fcmToken("user-fcm").build(); + + activePairing = PairingRelation.builder() + .id(10L).guardian(guardian).user(user).status(PairingStatus.ACTIVE).build(); + } + + // ===== SEND NOTIFICATION TESTS ===== + + @Test + @DisplayName("sendNotification - TEXT type: harus simpan dan kirim FCM ke user") + void sendNotification_textType_shouldSaveAndSendFcm() { + SendNotificationRequest req = new SendNotificationRequest(); + req.setNotifType("TEXT"); + req.setContent("Hati-hati di pertigaan!"); + + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.of(activePairing)); + when(notifRepository.save(any(GuardianNotification.class))).thenAnswer(inv -> { + GuardianNotification n = inv.getArgument(0); + n.setId(100L); + return n; + }); + + NotificationResponse result = notificationService.sendNotification(1L, req); + + assertThat(result).isNotNull(); + assertThat(result.getContent()).isEqualTo("Hati-hati di pertigaan!"); + verify(notifRepository).save(any(GuardianNotification.class)); + verify(fcmService).sendToToken(eq("user-fcm"), anyString(), anyString(), anyMap()); + } + + @Test + @DisplayName("sendNotification - VOICE_NOTE type: harus simpan dengan tipe VOICE_NOTE") + void sendNotification_voiceNoteType_shouldSaveAsVoiceNote() { + SendNotificationRequest req = new SendNotificationRequest(); + req.setNotifType("VOICE_NOTE"); + req.setContent(null); + req.setVoiceNoteUrl("https://storage/voice/001.m4a"); + req.setVoiceNoteDuration(12); + + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.of(activePairing)); + when(notifRepository.save(any(GuardianNotification.class))).thenAnswer(inv -> { + GuardianNotification n = inv.getArgument(0); + n.setId(101L); + n.setNotifType(NotificationType.VOICE_NOTE); + n.setVoiceNoteUrl("https://storage/voice/001.m4a"); + return n; + }); + + NotificationResponse result = notificationService.sendNotification(1L, req); + + assertThat(result.getVoiceNoteUrl()).isEqualTo("https://storage/voice/001.m4a"); + verify(fcmService).sendToToken(eq("user-fcm"), anyString(), anyString(), anyMap()); + } + + @Test + @DisplayName("sendNotification - Guardian tidak punya active pairing harus throw PairingException") + void sendNotification_noPairing_shouldThrow() { + SendNotificationRequest req = new SendNotificationRequest(); + req.setNotifType("TEXT"); + req.setContent("test"); + + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.empty()); + + assertThatThrownBy(() -> notificationService.sendNotification(1L, req)) + .isInstanceOf(PairingException.class) + .hasMessageContaining("Tidak ada user yang dipair"); + + verify(notifRepository, never()).save(any()); + verify(fcmService, never()).sendToToken(any(), any(), any(), any()); + } + + // ===== MARK AS READ TESTS ===== + + @Test + @DisplayName("markAsRead - harus set isRead=true dan simpan") + void markAsRead_shouldSetReadTrue() { + GuardianNotification notif = GuardianNotification.builder() + .id(100L).userId(2L).guardianId(1L) + .notifType(NotificationType.TEXT).content("Test").isRead(false).build(); + + when(notifRepository.findByIdAndUserId(100L, 2L)).thenReturn(Optional.of(notif)); + when(notifRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + notificationService.markAsRead(2L, 100L); + + assertThat(notif.isRead()).isTrue(); + verify(notifRepository).save(notif); + } + + @Test + @DisplayName("markAsRead - notifikasi tidak ditemukan harus throw RuntimeException") + void markAsRead_notFound_shouldThrow() { + when(notifRepository.findByIdAndUserId(999L, 2L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> notificationService.markAsRead(2L, 999L)) + .isInstanceOf(RuntimeException.class); + } + + // ===== UNREAD COUNT TESTS ===== + + @Test + @DisplayName("getUnreadCount - harus return jumlah notif yang belum dibaca") + void getUnreadCount_shouldReturnCorrectCount() { + when(notifRepository.countByUserIdAndIsReadFalse(2L)).thenReturn(5L); + + long count = notificationService.getUnreadCount(2L); + + assertThat(count).isEqualTo(5L); + } + + @Test + @DisplayName("getUnreadCount - tidak ada notif belum dibaca harus return 0") + void getUnreadCount_noUnread_shouldReturnZero() { + when(notifRepository.countByUserIdAndIsReadFalse(2L)).thenReturn(0L); + + long count = notificationService.getUnreadCount(2L); + + assertThat(count).isZero(); + } + + // ===== GET NOTIFICATIONS TESTS ===== + + @Test + @DisplayName("getNotifications - harus return page notif untuk user") + void getNotifications_shouldReturnPagedResults() { + GuardianNotification notif = GuardianNotification.builder() + .id(1L).userId(2L).guardianId(1L) + .notifType(NotificationType.TEXT).content("Hello").isRead(false).build(); + + Page page = new PageImpl<>(List.of(notif)); + PageRequest pageable = PageRequest.of(0, 10); + + when(notifRepository.findByUserIdOrderByCreatedAtDesc(2L, pageable)).thenReturn(page); + + Page result = notificationService.getNotifications(2L, pageable); + + assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getContent().get(0).getContent()).isEqualTo("Hello"); + } +} \ No newline at end of file