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(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) .thenReturn(Optional.of(activePairing)); 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(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) .thenReturn(Optional.of(activePairing)); 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"); } @Test @DisplayName("resolveSos - SOS ditemukan: harus ubah status ke RESOLVED") void resolveSos_sosFound_shouldChangeStatusToResolved() { when(sosEventRepository.findById(50L)).thenReturn(Optional.of(savedSos)); when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) .thenReturn(Optional.of(activePairing)); 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.resolveSos(1L, 50L); assertThat(result).isNotNull(); assertThat(result.getStatus()).isEqualTo("RESOLVED"); ArgumentCaptor captor = ArgumentCaptor.forClass(SosEvent.class); verify(sosEventRepository).save(captor.capture()); assertThat(captor.getValue().getStatus()).isEqualTo(SosStatus.RESOLVED); assertThat(captor.getValue().getAcknowledgedAt()).isNotNull(); } // ===== 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); } }