diff --git a/walkguide-backend/demo/src/test/java/com/walkguide/service/AiConfigServiceTest.java b/walkguide-backend/demo/src/test/java/com/walkguide/service/AiConfigServiceTest.java new file mode 100644 index 0000000..e077f46 --- /dev/null +++ b/walkguide-backend/demo/src/test/java/com/walkguide/service/AiConfigServiceTest.java @@ -0,0 +1,212 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.AiConfigUpdateRequest; +import com.walkguide.dto.response.AiConfigResponse; +import com.walkguide.entity.AiConfig; +import com.walkguide.entity.PairingRelation; +import com.walkguide.entity.User; +import com.walkguide.enums.PairingStatus; +import com.walkguide.exception.PairingException; +import com.walkguide.repository.AiConfigRepository; +import com.walkguide.repository.PairingRelationRepository; +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 java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("AiConfigService Unit Tests") +class AiConfigServiceTest { + + @Mock AiConfigRepository aiConfigRepository; + @Mock PairingRelationRepository pairingRelationRepository; + @Mock FcmService fcmService; + + @InjectMocks AiConfigService aiConfigService; + + private User guardian; + private User user; + private PairingRelation activePairing; + private AiConfig existingConfig; + + @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(5L).guardian(guardian).user(user).status(PairingStatus.ACTIVE).build(); + + existingConfig = AiConfig.builder() + .id(10L).userId(2L).guardianId(1L) + .confidenceThreshold(0.5) + .alertDistanceClose(1.5) + .alertDistanceMedium(3.0) + .maxInferenceFps(5) + .build(); + } + + // ===== GET CONFIG TESTS ===== + + @Test + @DisplayName("getConfig - konfigurasi ada harus return data dari DB") + void getConfig_configExists_shouldReturnExistingConfig() { + when(aiConfigRepository.findByUserId(2L)).thenReturn(Optional.of(existingConfig)); + + AiConfigResponse result = aiConfigService.getConfig(2L); + + assertThat(result.getId()).isEqualTo(10L); + assertThat(result.getConfidenceThreshold()).isEqualTo(0.5); + assertThat(result.getAlertDistanceClose()).isEqualTo(1.5); + verify(aiConfigRepository, never()).save(any()); + } + + @Test + @DisplayName("getConfig - konfigurasi belum ada harus buat default dan simpan") + void getConfig_configNotExists_shouldCreateDefaultAndSave() { + AiConfig newConfig = AiConfig.builder().id(99L).userId(2L).build(); + + when(aiConfigRepository.findByUserId(2L)).thenReturn(Optional.empty()); + when(aiConfigRepository.save(any(AiConfig.class))).thenReturn(newConfig); + + AiConfigResponse result = aiConfigService.getConfig(2L); + + assertThat(result).isNotNull(); + verify(aiConfigRepository).save(any(AiConfig.class)); + } + + // ===== UPDATE CONFIG BY GUARDIAN TESTS ===== + + @Test + @DisplayName("updateConfigByGuardian - update confidenceThreshold berhasil") + void updateConfigByGuardian_updateThreshold_shouldSaveAndNotify() { + AiConfigUpdateRequest req = new AiConfigUpdateRequest(); + req.setConfidenceThreshold(0.75); + + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.of(activePairing)); + when(aiConfigRepository.findByUserId(2L)).thenReturn(Optional.of(existingConfig)); + when(aiConfigRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + AiConfigResponse result = aiConfigService.updateConfigByGuardian(1L, req); + + assertThat(result.getConfidenceThreshold()).isEqualTo(0.75); + verify(aiConfigRepository).save(any(AiConfig.class)); + verify(fcmService).sendToToken(eq("user-fcm"), anyString(), anyString(), anyMap()); + } + + @Test + @DisplayName("updateConfigByGuardian - update semua field sekaligus") + void updateConfigByGuardian_updateAllFields_shouldUpdateAll() { + AiConfigUpdateRequest req = new AiConfigUpdateRequest(); + req.setConfidenceThreshold(0.8); + req.setAlertDistanceClose(1.0); + req.setAlertDistanceMedium(2.5); + req.setMaxInferenceFps(10); + req.setEnabledLabels("person,car,motorcycle"); + + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.of(activePairing)); + when(aiConfigRepository.findByUserId(2L)).thenReturn(Optional.of(existingConfig)); + when(aiConfigRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + AiConfigResponse result = aiConfigService.updateConfigByGuardian(1L, req); + + assertThat(result.getConfidenceThreshold()).isEqualTo(0.8); + assertThat(result.getAlertDistanceClose()).isEqualTo(1.0); + assertThat(result.getAlertDistanceMedium()).isEqualTo(2.5); + assertThat(result.getMaxInferenceFps()).isEqualTo(10); + assertThat(result.getEnabledLabels()).isEqualTo("person,car,motorcycle"); + } + + @Test + @DisplayName("updateConfigByGuardian - field null tidak boleh mengubah nilai existing") + void updateConfigByGuardian_nullFields_shouldNotOverrideExisting() { + // Request dengan semua field null — tidak ada yang berubah + AiConfigUpdateRequest req = new AiConfigUpdateRequest(); + + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.of(activePairing)); + when(aiConfigRepository.findByUserId(2L)).thenReturn(Optional.of(existingConfig)); + when(aiConfigRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + AiConfigResponse result = aiConfigService.updateConfigByGuardian(1L, req); + + // Nilai lama tetap + assertThat(result.getConfidenceThreshold()).isEqualTo(0.5); + assertThat(result.getAlertDistanceClose()).isEqualTo(1.5); + } + + @Test + @DisplayName("updateConfigByGuardian - Guardian tanpa pairing aktif harus throw PairingException") + void updateConfigByGuardian_noPairing_shouldThrow() { + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.empty()); + + assertThatThrownBy(() -> aiConfigService.updateConfigByGuardian(1L, new AiConfigUpdateRequest())) + .isInstanceOf(PairingException.class) + .hasMessageContaining("Tidak ada user yang dipair"); + + verify(aiConfigRepository, never()).save(any()); + verify(fcmService, never()).sendToToken(any(), any(), any(), any()); + } + + @Test + @DisplayName("updateConfigByGuardian - konfigurasi belum ada harus buat baru lalu update") + void updateConfigByGuardian_configNotExists_shouldCreateAndUpdate() { + AiConfigUpdateRequest req = new AiConfigUpdateRequest(); + req.setConfidenceThreshold(0.6); + + AiConfig freshConfig = AiConfig.builder().userId(2L).guardianId(1L) + .confidenceThreshold(0.5).build(); + + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.of(activePairing)); + // Pertama kali findByUserId kosong → save default → kembalikan freshConfig + when(aiConfigRepository.findByUserId(2L)).thenReturn(Optional.empty()); + when(aiConfigRepository.save(any())).thenAnswer(inv -> { + AiConfig c = inv.getArgument(0); + c.setId(50L); + return c; + }); + + AiConfigResponse result = aiConfigService.updateConfigByGuardian(1L, req); + + assertThat(result).isNotNull(); + // save dipanggil 2x: sekali buat default, sekali update + verify(aiConfigRepository, times(2)).save(any(AiConfig.class)); + } + + @Test + @DisplayName("updateConfigByGuardian - harus kirim FCM ke user setelah update") + void updateConfigByGuardian_shouldSendFcmToUser() { + AiConfigUpdateRequest req = new AiConfigUpdateRequest(); + req.setMaxInferenceFps(8); + + when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)) + .thenReturn(Optional.of(activePairing)); + when(aiConfigRepository.findByUserId(2L)).thenReturn(Optional.of(existingConfig)); + when(aiConfigRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + aiConfigService.updateConfigByGuardian(1L, req); + + ArgumentCaptor tokenCaptor = ArgumentCaptor.forClass(String.class); + verify(fcmService).sendToToken(tokenCaptor.capture(), anyString(), anyString(), anyMap()); + assertThat(tokenCaptor.getValue()).isEqualTo("user-fcm"); + } +} \ No newline at end of file