test: add VoiceCommandServiceTest unit test

This commit is contained in:
5803024019 2026-05-15 22:33:42 +07:00
parent 8f265c8efa
commit b7a9079930

View File

@ -0,0 +1,394 @@
package com.walkguide.service;
import com.walkguide.dto.request.VoiceCommandUpdateRequest;
import com.walkguide.dto.response.VoiceCommandResponse;
import com.walkguide.entity.PairingRelation;
import com.walkguide.entity.User;
import com.walkguide.entity.VoiceCommandConfig;
import com.walkguide.enums.PairingStatus;
import com.walkguide.enums.VoiceCommandKey;
import com.walkguide.exception.PairingException;
import com.walkguide.exception.ResourceNotFoundException;
import com.walkguide.repository.PairingRelationRepository;
import com.walkguide.repository.VoiceCommandConfigRepository;
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.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("VoiceCommandService Unit Tests")
class VoiceCommandServiceTest {
@Mock
VoiceCommandConfigRepository voiceCommandConfigRepository;
@Mock
PairingRelationRepository pairingRelationRepository;
@Mock
FcmService fcmService;
@InjectMocks
VoiceCommandService voiceCommandService;
private User guardianUser;
private User blindUser;
private PairingRelation activePairing;
private VoiceCommandConfig vcOpenWalkguide;
private VoiceCommandConfig vcCallGuardian;
private VoiceCommandConfig vcSendSos;
@BeforeEach
void setUp() {
guardianUser = User.builder()
.id(1L)
.email("guardian@test.com")
.role("ROLE_GUARDIAN")
.displayName("Guardian Test")
.fcmToken("guardian-fcm-token")
.build();
blindUser = User.builder()
.id(2L)
.email("user@test.com")
.role("ROLE_USER")
.displayName("User Test")
.fcmToken("user-fcm-token-xyz")
.build();
activePairing = PairingRelation.builder()
.id(10L)
.guardian(guardianUser)
.user(blindUser)
.status(PairingStatus.ACTIVE)
.build();
vcOpenWalkguide = VoiceCommandConfig.builder()
.id(100L)
.userId(2L)
.commandKey(VoiceCommandKey.OPEN_WALKGUIDE)
.triggerPhrase("buka walkguide")
.enabled(true)
.build();
vcCallGuardian = VoiceCommandConfig.builder()
.id(101L)
.userId(2L)
.commandKey(VoiceCommandKey.CALL_GUARDIAN)
.triggerPhrase("telepon guardian")
.enabled(true)
.build();
vcSendSos = VoiceCommandConfig.builder()
.id(102L)
.userId(2L)
.commandKey(VoiceCommandKey.SEND_SOS)
.triggerPhrase("kirim sos")
.enabled(false)
.build();
}
// ===== getAll TESTS =====
@Test
@DisplayName("getAll - harus return semua voice command milik user sebagai list response")
void getAll_shouldReturnAllVoiceCommandsForUser() {
when(voiceCommandConfigRepository.findByUserId(2L))
.thenReturn(List.of(vcOpenWalkguide, vcCallGuardian, vcSendSos));
List<VoiceCommandResponse> result = voiceCommandService.getAll(2L);
assertThat(result).hasSize(3);
VoiceCommandResponse first = result.get(0);
assertThat(first.getId()).isEqualTo(100L);
assertThat(first.getCommandKey()).isEqualTo("OPEN_WALKGUIDE");
assertThat(first.getTriggerPhrase()).isEqualTo("buka walkguide");
assertThat(first.getEnabled()).isTrue();
assertThat(first.getDescription()).isEqualTo("Buka menu WalkGuide");
VoiceCommandResponse second = result.get(1);
assertThat(second.getCommandKey()).isEqualTo("CALL_GUARDIAN");
assertThat(second.getDescription()).isEqualTo("Telepon Guardian");
VoiceCommandResponse third = result.get(2);
assertThat(third.getCommandKey()).isEqualTo("SEND_SOS");
assertThat(third.getEnabled()).isFalse();
assertThat(third.getDescription()).isEqualTo("Kirim sinyal darurat SOS");
}
@Test
@DisplayName("getAll - harus return list kosong jika user tidak punya voice command")
void getAll_emptyList_whenUserHasNoVoiceCommands() {
when(voiceCommandConfigRepository.findByUserId(99L)).thenReturn(List.of());
List<VoiceCommandResponse> result = voiceCommandService.getAll(99L);
assertThat(result).isEmpty();
verify(voiceCommandConfigRepository).findByUserId(99L);
}
@Test
@DisplayName("getAll - description harus ada untuk semua VoiceCommandKey yang terdefinisi")
void getAll_shouldProvideDescriptionForAllDefinedKeys() {
// Buat config dengan setiap key yang ada
List<VoiceCommandConfig> allConfigs = List.of(
buildVc(1L, 2L, VoiceCommandKey.OPEN_WALKGUIDE, "cmd1"),
buildVc(2L, 2L, VoiceCommandKey.START_WALKGUIDE, "cmd2"),
buildVc(3L, 2L, VoiceCommandKey.STOP_WALKGUIDE, "cmd3"),
buildVc(4L, 2L, VoiceCommandKey.CALL_GUARDIAN, "cmd4"),
buildVc(5L, 2L, VoiceCommandKey.OPEN_NOTIFICATION, "cmd5"),
buildVc(6L, 2L, VoiceCommandKey.READ_ALL_NOTIF, "cmd6"),
buildVc(7L, 2L, VoiceCommandKey.OPEN_SOS, "cmd7"),
buildVc(8L, 2L, VoiceCommandKey.SEND_SOS, "cmd8"),
buildVc(9L, 2L, VoiceCommandKey.WHERE_AM_I, "cmd9"),
buildVc(10L, 2L, VoiceCommandKey.OPEN_ACTIVITY, "cmd10"),
buildVc(11L, 2L, VoiceCommandKey.OPEN_NAVIGATION, "cmd11"),
buildVc(12L, 2L, VoiceCommandKey.OPEN_SETTINGS, "cmd12"),
buildVc(13L, 2L, VoiceCommandKey.REPEAT_LAST, "cmd13"),
buildVc(14L, 2L, VoiceCommandKey.STOP_TTS, "cmd14")
);
when(voiceCommandConfigRepository.findByUserId(2L)).thenReturn(allConfigs);
List<VoiceCommandResponse> result = voiceCommandService.getAll(2L);
assertThat(result).hasSize(14);
// Semua description tidak boleh kosong atau null
result.forEach(r -> assertThat(r.getDescription())
.as("Description untuk key %s tidak boleh kosong", r.getCommandKey())
.isNotNull()
.isNotBlank());
}
// ===== updateByGuardian TESTS =====
@Test
@DisplayName("updateByGuardian - harus update triggerPhrase dan enabled jika semua field diisi")
void updateByGuardian_allFields_shouldUpdateAll() {
VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
req.setCommandKey("OPEN_WALKGUIDE");
req.setTriggerPhrase("halo walkguide");
req.setEnabled(false);
when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
.thenReturn(Optional.of(activePairing));
when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.OPEN_WALKGUIDE))
.thenReturn(Optional.of(vcOpenWalkguide));
when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
.thenAnswer(inv -> inv.getArgument(0));
VoiceCommandResponse result = voiceCommandService.updateByGuardian(1L, req);
assertThat(result.getCommandKey()).isEqualTo("OPEN_WALKGUIDE");
assertThat(result.getTriggerPhrase()).isEqualTo("halo walkguide");
assertThat(result.getEnabled()).isFalse();
assertThat(result.getDescription()).isEqualTo("Buka menu WalkGuide");
}
@Test
@DisplayName("updateByGuardian - triggerPhrase null tidak boleh mengubah nilai yang sudah ada")
void updateByGuardian_nullTriggerPhrase_shouldKeepExistingPhrase() {
VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
req.setCommandKey("CALL_GUARDIAN");
req.setTriggerPhrase(null);
req.setEnabled(false);
when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
.thenReturn(Optional.of(activePairing));
when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.CALL_GUARDIAN))
.thenReturn(Optional.of(vcCallGuardian));
when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
.thenAnswer(inv -> inv.getArgument(0));
VoiceCommandResponse result = voiceCommandService.updateByGuardian(1L, req);
// triggerPhrase tidak berubah
assertThat(result.getTriggerPhrase()).isEqualTo("telepon guardian");
assertThat(result.getEnabled()).isFalse();
}
@Test
@DisplayName("updateByGuardian - triggerPhrase blank tidak boleh mengubah nilai yang sudah ada")
void updateByGuardian_blankTriggerPhrase_shouldKeepExistingPhrase() {
VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
req.setCommandKey("CALL_GUARDIAN");
req.setTriggerPhrase(" "); // blank string
req.setEnabled(null);
when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
.thenReturn(Optional.of(activePairing));
when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.CALL_GUARDIAN))
.thenReturn(Optional.of(vcCallGuardian));
when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
.thenAnswer(inv -> inv.getArgument(0));
VoiceCommandResponse result = voiceCommandService.updateByGuardian(1L, req);
assertThat(result.getTriggerPhrase()).isEqualTo("telepon guardian");
}
@Test
@DisplayName("updateByGuardian - enabled null tidak boleh mengubah nilai yang sudah ada")
void updateByGuardian_nullEnabled_shouldKeepExistingEnabled() {
VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
req.setCommandKey("SEND_SOS");
req.setTriggerPhrase("darurat sekarang");
req.setEnabled(null);
when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
.thenReturn(Optional.of(activePairing));
when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.SEND_SOS))
.thenReturn(Optional.of(vcSendSos));
when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
.thenAnswer(inv -> inv.getArgument(0));
VoiceCommandResponse result = voiceCommandService.updateByGuardian(1L, req);
// enabled tetap false (nilai awal vcSendSos)
assertThat(result.getEnabled()).isFalse();
assertThat(result.getTriggerPhrase()).isEqualTo("darurat sekarang");
}
@Test
@DisplayName("updateByGuardian - guardian tanpa pairing aktif harus throw PairingException")
void updateByGuardian_noActivePairing_shouldThrowPairingException() {
VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
req.setCommandKey("OPEN_WALKGUIDE");
when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> voiceCommandService.updateByGuardian(1L, req))
.isInstanceOf(PairingException.class)
.hasMessageContaining("Tidak ada user yang dipair");
verify(voiceCommandConfigRepository, never()).findByUserIdAndCommandKey(any(), any());
verify(voiceCommandConfigRepository, never()).save(any());
}
@Test
@DisplayName("updateByGuardian - commandKey tidak ditemukan harus throw ResourceNotFoundException")
void updateByGuardian_voiceCommandNotFound_shouldThrowResourceNotFoundException() {
VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
req.setCommandKey("WHERE_AM_I");
when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
.thenReturn(Optional.of(activePairing));
when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.WHERE_AM_I))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> voiceCommandService.updateByGuardian(1L, req))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessageContaining("Voice command tidak ditemukan");
verify(voiceCommandConfigRepository, never()).save(any());
}
@Test
@DisplayName("updateByGuardian - commandKey enum invalid harus throw IllegalArgumentException")
void updateByGuardian_invalidEnumKey_shouldThrowIllegalArgumentException() {
VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
req.setCommandKey("INVALID_COMMAND_XYZ");
when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
.thenReturn(Optional.of(activePairing));
assertThatThrownBy(() -> voiceCommandService.updateByGuardian(1L, req))
.isInstanceOf(IllegalArgumentException.class);
verify(voiceCommandConfigRepository, never()).save(any());
}
@Test
@DisplayName("updateByGuardian - harus menyimpan entitas yang sudah dimodifikasi ke repository")
void updateByGuardian_shouldPersistChangesToRepository() {
VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
req.setCommandKey("SEND_SOS");
req.setTriggerPhrase("minta tolong segera");
req.setEnabled(true);
when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
.thenReturn(Optional.of(activePairing));
when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.SEND_SOS))
.thenReturn(Optional.of(vcSendSos));
when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
.thenAnswer(inv -> inv.getArgument(0));
voiceCommandService.updateByGuardian(1L, req);
ArgumentCaptor<VoiceCommandConfig> captor = ArgumentCaptor.forClass(VoiceCommandConfig.class);
verify(voiceCommandConfigRepository).save(captor.capture());
VoiceCommandConfig saved = captor.getValue();
assertThat(saved.getCommandKey()).isEqualTo(VoiceCommandKey.SEND_SOS);
assertThat(saved.getTriggerPhrase()).isEqualTo("minta tolong segera");
assertThat(saved.getEnabled()).isTrue();
}
@Test
@DisplayName("updateByGuardian - harus mengirim FCM notification ke user setelah update berhasil")
void updateByGuardian_shouldSendFcmNotificationToUser() {
VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
req.setCommandKey("OPEN_WALKGUIDE");
req.setTriggerPhrase("ayo walkguide");
req.setEnabled(true);
when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
.thenReturn(Optional.of(activePairing));
when(voiceCommandConfigRepository.findByUserIdAndCommandKey(2L, VoiceCommandKey.OPEN_WALKGUIDE))
.thenReturn(Optional.of(vcOpenWalkguide));
when(voiceCommandConfigRepository.save(any(VoiceCommandConfig.class)))
.thenAnswer(inv -> inv.getArgument(0));
voiceCommandService.updateByGuardian(1L, req);
verify(fcmService).sendToToken(
eq("user-fcm-token-xyz"),
eq("Voice Command Diperbarui"),
eq("Guardian mengubah perintah suara kamu"),
argThat(data -> "SETTINGS_UPDATED".equals(data.get("type"))
&& "VOICE_COMMAND".equals(data.get("settingType")))
);
}
@Test
@DisplayName("updateByGuardian - FCM tidak boleh dipanggil jika update gagal karena pairing tidak ada")
void updateByGuardian_noPairing_fcmShouldNotBeCalled() {
VoiceCommandUpdateRequest req = new VoiceCommandUpdateRequest();
req.setCommandKey("OPEN_WALKGUIDE");
when(pairingRelationRepository.findByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> voiceCommandService.updateByGuardian(1L, req))
.isInstanceOf(PairingException.class);
verifyNoInteractions(fcmService);
}
// ===== Helper =====
private VoiceCommandConfig buildVc(Long id, Long userId, VoiceCommandKey key, String phrase) {
return VoiceCommandConfig.builder()
.id(id)
.userId(userId)
.commandKey(key)
.triggerPhrase(phrase)
.enabled(true)
.build();
}
}