diff --git a/walkguide-backend/demo/src/test/java/com/walkguide/service/SosServiceTest.java b/walkguide-backend/demo/src/test/java/com/walkguide/service/SosServiceTest.java new file mode 100644 index 0000000..1763963 --- /dev/null +++ b/walkguide-backend/demo/src/test/java/com/walkguide/service/SosServiceTest.java @@ -0,0 +1,253 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.SosRequest; +import com.walkguide.dto.response.SosEventResponse; +import com.walkguide.entity.PairingRelation; +import com.walkguide.entity.SosEvent; +import com.walkguide.entity.User; +import com.walkguide.enums.PairingStatus; +import com.walkguide.enums.SosStatus; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.*; +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.ArgumentCaptor; +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 org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +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("SosService Unit Tests") +class SosServiceTest { + + @Mock SosEventRepository sosEventRepository; + @Mock PairingRelationRepository pairingRelationRepository; + @Mock UserRepository userRepository; + @Mock ActivityLogService activityLogService; + @Mock FcmService fcmService; + @Mock LocationBroadcaster locationBroadcaster; + + @InjectMocks SosService sosService; + + private User guardian; + private User user; + private PairingRelation activePairing; + private SosEvent savedSos; + + @BeforeEach + void setUp() { + guardian = User.builder() + .id(1L).email("guardian@test.com").displayName("Guardian Test") + .fcmToken("guardian-fcm-token").build(); + + user = User.builder() + .id(2L).email("user@test.com").displayName("User Test") + .fcmToken("user-fcm-token").build(); + + activePairing = PairingRelation.builder() + .id(1L).guardian(guardian).user(user).status(PairingStatus.ACTIVE).build(); + + savedSos = SosEvent.builder() + .id(50L).userId(2L).triggerType("MANUAL") + .lat(-7.257).lng(112.752) + .status(SosStatus.TRIGGERED) + .createdAt(LocalDateTime.now()).build(); + } + + // ===== triggerSos TESTS ===== + + @Test + @DisplayName("triggerSos - MANUAL dengan koordinat: harus simpan SOS dan return response") + void triggerSos_manualWithCoords_shouldSaveAndReturnResponse() { + SosRequest req = new SosRequest(); + req.setTriggerType("MANUAL"); + req.setLat(-7.257); + req.setLng(112.752); + + when(sosEventRepository.save(any(SosEvent.class))).thenReturn(savedSos); + when(userRepository.findById(2L)).thenReturn(Optional.of(user)); + when(pairingRelationRepository.findByUser_IdAndStatus(2L, PairingStatus.ACTIVE)) + .thenReturn(Optional.empty()); // tidak ada guardian → skip FCM + doNothing().when(activityLogService).createLog(any(), any(), any(), any()); + + SosEventResponse result = sosService.triggerSos(2L, req); + + assertThat(result).isNotNull(); + assertThat(result.getId()).isEqualTo(50L); + assertThat(result.getUserId()).isEqualTo(2L); + assertThat(result.getTriggerType()).isEqualTo("MANUAL"); + assertThat(result.getStatus()).isEqualTo("TRIGGERED"); + verify(sosEventRepository).save(any(SosEvent.class)); + } + + @Test + @DisplayName("triggerSos - triggerType null: harus default ke MANUAL") + void triggerSos_nullTriggerType_shouldDefaultToManual() { + SosRequest req = new SosRequest(); + req.setTriggerType(null); + req.setLat(-7.257); + req.setLng(112.752); + + when(sosEventRepository.save(any(SosEvent.class))).thenReturn(savedSos); + when(userRepository.findById(2L)).thenReturn(Optional.of(user)); + when(pairingRelationRepository.findByUser_IdAndStatus(2L, PairingStatus.ACTIVE)) + .thenReturn(Optional.empty()); + doNothing().when(activityLogService).createLog(any(), any(), any(), any()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SosEvent.class); + sosService.triggerSos(2L, req); + + verify(sosEventRepository).save(captor.capture()); + assertThat(captor.getValue().getTriggerType()).isEqualTo("MANUAL"); + } + + @Test + @DisplayName("triggerSos - ada pairing aktif: harus kirim FCM dan WebSocket ke guardian") + void triggerSos_activePairing_shouldNotifyGuardianViaFcmAndWebSocket() { + SosRequest req = new SosRequest(); + req.setTriggerType("BUTTON"); + req.setLat(-7.257); + req.setLng(112.752); + + when(sosEventRepository.save(any(SosEvent.class))).thenReturn(savedSos); + when(userRepository.findById(2L)).thenReturn(Optional.of(user)); + when(pairingRelationRepository.findByUser_IdAndStatus(2L, PairingStatus.ACTIVE)) + .thenReturn(Optional.of(activePairing)); + doNothing().when(activityLogService).createLog(any(), any(), any(), any()); + + sosService.triggerSos(2L, req); + + verify(fcmService).sendHighPriority( + eq("guardian-fcm-token"), + contains("SOS ALERT"), + anyString(), + anyMap() + ); + verify(locationBroadcaster).broadcastSos(eq(1L), any(SosEventResponse.class)); + } + + @Test + @DisplayName("triggerSos - user tidak ditemukan: harus throw ResourceNotFoundException") + void triggerSos_userNotFound_shouldThrowException() { + SosRequest req = new SosRequest(); + req.setTriggerType("MANUAL"); + + when(sosEventRepository.save(any(SosEvent.class))).thenReturn(savedSos); + when(userRepository.findById(99L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> sosService.triggerSos(99L, req)) + .isInstanceOf(ResourceNotFoundException.class); + } + + // ===== acknowledgeSos TESTS ===== + + @Test + @DisplayName("acknowledgeSos - SOS ditemukan: harus ubah status ke ACKNOWLEDGED") + void acknowledgeSos_sosFound_shouldChangeStatusToAcknowledged() { + when(sosEventRepository.findById(50L)).thenReturn(Optional.of(savedSos)); + when(userRepository.findById(2L)).thenReturn(Optional.of(user)); + when(sosEventRepository.save(any(SosEvent.class))).thenAnswer(inv -> inv.getArgument(0)); + when(pairingRelationRepository.findByUser_IdAndStatus(2L, PairingStatus.ACTIVE)) + .thenReturn(Optional.empty()); + doNothing().when(activityLogService).createLog(any(), any(), any(), any()); + + SosEventResponse result = sosService.acknowledgeSos(1L, 50L); + + assertThat(result).isNotNull(); + assertThat(result.getStatus()).isEqualTo("ACKNOWLEDGED"); + ArgumentCaptor captor = ArgumentCaptor.forClass(SosEvent.class); + verify(sosEventRepository).save(captor.capture()); + assertThat(captor.getValue().getStatus()).isEqualTo(SosStatus.ACKNOWLEDGED); + assertThat(captor.getValue().getAcknowledgedAt()).isNotNull(); + } + + @Test + @DisplayName("acknowledgeSos - ada pairing user: harus kirim FCM ke user bahwa guardian sudah respon") + void acknowledgeSos_activePairingForUser_shouldNotifyUser() { + when(sosEventRepository.findById(50L)).thenReturn(Optional.of(savedSos)); + when(userRepository.findById(2L)).thenReturn(Optional.of(user)); + when(sosEventRepository.save(any(SosEvent.class))).thenAnswer(inv -> inv.getArgument(0)); + when(pairingRelationRepository.findByUser_IdAndStatus(2L, PairingStatus.ACTIVE)) + .thenReturn(Optional.of(activePairing)); + doNothing().when(activityLogService).createLog(any(), any(), any(), any()); + + sosService.acknowledgeSos(1L, 50L); + + verify(fcmService).sendToToken( + eq("user-fcm-token"), + contains("Guardian Merespons SOS"), + anyString(), + anyMap() + ); + } + + @Test + @DisplayName("acknowledgeSos - SOS tidak ditemukan: harus throw ResourceNotFoundException") + void acknowledgeSos_sosNotFound_shouldThrowException() { + when(sosEventRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> sosService.acknowledgeSos(1L, 999L)) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("SOS event tidak ditemukan"); + } + + // ===== getSosEvents TESTS ===== + + @Test + @DisplayName("getSosEvents - harus return halaman SOS milik user") + void getSosEvents_shouldReturnPagedSosEvents() { + Pageable pageable = PageRequest.of(0, 10); + Page page = new PageImpl<>(List.of(savedSos), pageable, 1); + + when(sosEventRepository.findByUserIdOrderByCreatedAtDesc(2L, pageable)).thenReturn(page); + + Page result = sosService.getSosEvents(2L, pageable); + + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getId()).isEqualTo(50L); + } + + // ===== getSosEventsForGuardian TESTS ===== + + @Test + @DisplayName("getSosEventsForGuardian - ada pairing aktif: harus return SOS milik user yang dipair") + void getSosEventsForGuardian_activePairing_shouldReturnUserSosEvents() { + Pageable pageable = PageRequest.of(0, 10); + Page page = new PageImpl<>(List.of(savedSos), pageable, 1); + + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.of(activePairing)); + when(sosEventRepository.findByUserIdOrderByCreatedAtDesc(2L, pageable)).thenReturn(page); + + Page result = sosService.getSosEventsForGuardian(1L, pageable); + + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getUserId()).isEqualTo(2L); + } + + @Test + @DisplayName("getSosEventsForGuardian - tidak ada pairing: harus throw ResourceNotFoundException") + void getSosEventsForGuardian_noPairing_shouldThrowException() { + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.empty()); + + assertThatThrownBy(() -> sosService.getSosEventsForGuardian(1L, PageRequest.of(0, 10))) + .isInstanceOf(ResourceNotFoundException.class); + } +} \ No newline at end of file