From 70487e6d0d0b822a645ab16099009a032d60a3a9 Mon Sep 17 00:00:00 2001 From: 5803024019 Date: Fri, 15 May 2026 21:36:44 +0700 Subject: [PATCH] test: add PairingServiceTest unit test --- .../walkguide/service/PairingServiceTest.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 walkguide-backend/demo/src/test/java/com/walkguide/service/PairingServiceTest.java diff --git a/walkguide-backend/demo/src/test/java/com/walkguide/service/PairingServiceTest.java b/walkguide-backend/demo/src/test/java/com/walkguide/service/PairingServiceTest.java new file mode 100644 index 0000000..4702932 --- /dev/null +++ b/walkguide-backend/demo/src/test/java/com/walkguide/service/PairingServiceTest.java @@ -0,0 +1,217 @@ +package com.walkguide.service; + +import com.walkguide.dto.response.PairingStatusResponse; +import com.walkguide.entity.PairingRelation; +import com.walkguide.entity.User; +import com.walkguide.enums.PairingStatus; +import com.walkguide.exception.PairingException; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.*; +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 java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("PairingService Unit Tests") +class PairingServiceTest { + + @Mock PairingRelationRepository pairingRelationRepository; + @Mock UserRepository userRepository; + @Mock VoiceCommandConfigRepository voiceCommandConfigRepository; + @Mock HardwareShortcutRepository hardwareShortcutRepository; + @Mock AiConfigRepository aiConfigRepository; + @Mock ActivityLogService activityLogService; + @Mock FcmService fcmService; + + @InjectMocks PairingService pairingService; + + private User guardian; + private User user; + + @BeforeEach + void setUp() { + guardian = User.builder() + .id(1L) + .email("guardian@test.com") + .role("ROLE_GUARDIAN") + .displayName("Guardian Test") + .fcmToken("guardian-fcm-token") + .build(); + + user = User.builder() + .id(2L) + .email("user@test.com") + .role("ROLE_USER") + .displayName("User Test") + .uniqueUserId("ABC123DEF456") + .fcmToken("user-fcm-token") + .build(); + } + + // ===== INVITE USER TESTS ===== + + @Test + @DisplayName("inviteUser - berhasil mengirim invite ke user yang valid") + void inviteUser_success_shouldSavePairingAndSendFcm() { + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)).thenReturn(false); + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.PENDING)).thenReturn(false); + when(userRepository.findById(1L)).thenReturn(Optional.of(guardian)); + when(userRepository.findByUniqueUserId("ABC123DEF456")).thenReturn(Optional.of(user)); + when(pairingRelationRepository.existsByUser_IdAndStatus(2L, PairingStatus.ACTIVE)).thenReturn(false); + when(pairingRelationRepository.save(any(PairingRelation.class))).thenAnswer(inv -> { + PairingRelation p = inv.getArgument(0); + p.setId(10L); + return p; + }); + + PairingStatusResponse result = pairingService.inviteUser(1L, "ABC123DEF456"); + + assertThat(result).isNotNull(); + verify(pairingRelationRepository).save(any(PairingRelation.class)); + verify(fcmService).sendToToken(eq("user-fcm-token"), anyString(), anyString(), anyMap()); + verify(activityLogService).createLog(eq(guardian), any(), anyString(), any()); + } + + @Test + @DisplayName("inviteUser - Guardian sudah ACTIVE pairing harus throw PairingException") + void inviteUser_alreadyActivePairing_shouldThrow() { + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)).thenReturn(true); + + assertThatThrownBy(() -> pairingService.inviteUser(1L, "ABC123DEF456")) + .isInstanceOf(PairingException.class) + .hasMessageContaining("sudah memiliki user yang dipair"); + + verify(pairingRelationRepository, never()).save(any()); + } + + @Test + @DisplayName("inviteUser - Guardian sudah PENDING invite harus throw PairingException") + void inviteUser_alreadyPendingInvite_shouldThrow() { + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)).thenReturn(false); + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.PENDING)).thenReturn(true); + + assertThatThrownBy(() -> pairingService.inviteUser(1L, "ABC123DEF456")) + .isInstanceOf(PairingException.class) + .hasMessageContaining("sudah punya invite yang menunggu"); + + verify(pairingRelationRepository, never()).save(any()); + } + + @Test + @DisplayName("inviteUser - uniqueUserId tidak ditemukan harus throw ResourceNotFoundException") + void inviteUser_userNotFound_shouldThrow() { + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)).thenReturn(false); + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.PENDING)).thenReturn(false); + when(userRepository.findById(1L)).thenReturn(Optional.of(guardian)); + when(userRepository.findByUniqueUserId("INVALID")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> pairingService.inviteUser(1L, "INVALID")) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("tidak ditemukan"); + } + + @Test + @DisplayName("inviteUser - target bukan ROLE_USER harus throw PairingException") + void inviteUser_targetNotUser_shouldThrow() { + User anotherGuardian = User.builder() + .id(3L).role("ROLE_GUARDIAN").uniqueUserId("GRD000000001").build(); + + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)).thenReturn(false); + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.PENDING)).thenReturn(false); + when(userRepository.findById(1L)).thenReturn(Optional.of(guardian)); + when(userRepository.findByUniqueUserId("GRD000000001")).thenReturn(Optional.of(anotherGuardian)); + + assertThatThrownBy(() -> pairingService.inviteUser(1L, "GRD000000001")) + .isInstanceOf(PairingException.class) + .hasMessageContaining("bukan milik User"); + } + + @Test + @DisplayName("inviteUser - User sudah ACTIVE dengan Guardian lain harus throw PairingException") + void inviteUser_userAlreadyPaired_shouldThrow() { + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.ACTIVE)).thenReturn(false); + when(pairingRelationRepository.existsByGuardian_IdAndStatus(1L, PairingStatus.PENDING)).thenReturn(false); + when(userRepository.findById(1L)).thenReturn(Optional.of(guardian)); + when(userRepository.findByUniqueUserId("ABC123DEF456")).thenReturn(Optional.of(user)); + when(pairingRelationRepository.existsByUser_IdAndStatus(2L, PairingStatus.ACTIVE)).thenReturn(true); + + assertThatThrownBy(() -> pairingService.inviteUser(1L, "ABC123DEF456")) + .isInstanceOf(PairingException.class) + .hasMessageContaining("sudah dipair dengan Guardian lain"); + } + + // ===== RESPOND TO PAIRING TESTS ===== + + @Test + @DisplayName("respondToPairing - accept: status harus jadi ACTIVE") + void respondToPairing_accept_shouldSetStatusActive() { + PairingRelation pairing = PairingRelation.builder() + .id(10L).guardian(guardian).user(user).status(PairingStatus.PENDING).build(); + + when(pairingRelationRepository.findById(10L)).thenReturn(Optional.of(pairing)); + when(pairingRelationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + // Stub untuk pembuatan config saat accept + when(voiceCommandConfigRepository.existsByUserId(2L)).thenReturn(false); + when(hardwareShortcutRepository.existsByUserId(2L)).thenReturn(false); + when(aiConfigRepository.findByUserId(2L)).thenReturn(Optional.empty()); + when(aiConfigRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + PairingStatusResponse result = pairingService.respondToPairing(2L, 10L, true); + + assertThat(result).isNotNull(); + assertThat(pairing.getStatus()).isEqualTo(PairingStatus.ACTIVE); + verify(pairingRelationRepository).save(pairing); + } + + @Test + @DisplayName("respondToPairing - reject: status harus jadi REJECTED") + void respondToPairing_reject_shouldSetStatusRejected() { + PairingRelation pairing = PairingRelation.builder() + .id(10L).guardian(guardian).user(user).status(PairingStatus.PENDING).build(); + + when(pairingRelationRepository.findById(10L)).thenReturn(Optional.of(pairing)); + when(pairingRelationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + PairingStatusResponse result = pairingService.respondToPairing(2L, 10L, false); + + assertThat(result).isNotNull(); + assertThat(pairing.getStatus()).isEqualTo(PairingStatus.REJECTED); + } + + @Test + @DisplayName("respondToPairing - user yang salah harus throw PairingException") + void respondToPairing_wrongUser_shouldThrow() { + PairingRelation pairing = PairingRelation.builder() + .id(10L).guardian(guardian).user(user).status(PairingStatus.PENDING).build(); + + when(pairingRelationRepository.findById(10L)).thenReturn(Optional.of(pairing)); + + // userId=99 bukan pemilik pairing ini (user.id=2) + assertThatThrownBy(() -> pairingService.respondToPairing(99L, 10L, true)) + .isInstanceOf(PairingException.class) + .hasMessageContaining("tidak berhak"); + } + + @Test + @DisplayName("respondToPairing - pairing yang sudah direspons harus throw PairingException") + void respondToPairing_alreadyResponded_shouldThrow() { + PairingRelation pairing = PairingRelation.builder() + .id(10L).guardian(guardian).user(user).status(PairingStatus.ACTIVE).build(); + + when(pairingRelationRepository.findById(10L)).thenReturn(Optional.of(pairing)); + + assertThatThrownBy(() -> pairingService.respondToPairing(2L, 10L, true)) + .isInstanceOf(PairingException.class) + .hasMessageContaining("sudah direspons"); + } +} \ No newline at end of file