From 785d883556d01cb918bc37d54e83ffa634d104ea Mon Sep 17 00:00:00 2001 From: 5803024019 Date: Sat, 16 May 2026 08:11:43 +0700 Subject: [PATCH] test: add CallControllerTest unit test --- .../Controller/CallControllerTest.java | 429 ++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 walkguide-backend/demo/src/test/java/com/walkguide/Controller/CallControllerTest.java diff --git a/walkguide-backend/demo/src/test/java/com/walkguide/Controller/CallControllerTest.java b/walkguide-backend/demo/src/test/java/com/walkguide/Controller/CallControllerTest.java new file mode 100644 index 0000000..aac6c20 --- /dev/null +++ b/walkguide-backend/demo/src/test/java/com/walkguide/Controller/CallControllerTest.java @@ -0,0 +1,429 @@ +package com.walkguide.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.walkguide.dto.request.CallNotifyRequest; +import com.walkguide.dto.request.CallTokenRequest; +import com.walkguide.dto.response.AgoraTokenResponse; +import com.walkguide.entity.User; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.UserRepository; +import com.walkguide.security.SecurityHelper; +import com.walkguide.service.AgoraTokenService; +import com.walkguide.service.FcmService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Map; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(CallController.class) +@WithMockUser(username = "1", roles = "USER") +@DisplayName("CallController Unit Tests") +class CallControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private AgoraTokenService agoraTokenService; + + @MockBean + private FcmService fcmService; + + @MockBean + private UserRepository userRepository; + + private User sampleCaller; + private User sampleReceiver; + + @BeforeEach + void setUp() { + sampleCaller = User.builder() + .id(1L) + .email("caller@test.com") + .displayName("Caller User") + .fcmToken("fcm-caller-token") + .build(); + + sampleReceiver = User.builder() + .id(2L) + .email("receiver@test.com") + .displayName("Receiver Guardian") + .fcmToken("fcm-receiver-token") + .build(); + } + + // ===== GENERATE TOKEN ===== + + @Test + @DisplayName("POST /api/v1/shared/call/token - valid request harus return Agora token") + void generateToken_validRequest_shouldReturn200() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + CallTokenRequest req = new CallTokenRequest(); + req.setReceiverId(2L); + + AgoraTokenResponse tokenResp = AgoraTokenResponse.builder() + .token("agora-rtc-token-xyz") + .channelName("call_1_2_1234567890") + .callerUid(1001) + .receiverUid(1002) + .build(); + + when(agoraTokenService.generateToken(1L, 2L)).thenReturn(tokenResp); + + mockMvc.perform(post("/api/v1/shared/call/token") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Token Agora berhasil digenerate")) + .andExpect(jsonPath("$.data.token").value("agora-rtc-token-xyz")) + .andExpect(jsonPath("$.data.channelName").value("call_1_2_1234567890")); + + verify(agoraTokenService).generateToken(1L, 2L); + } + } + + @Test + @DisplayName("POST /api/v1/shared/call/token - receiverId null harus return 400") + void generateToken_nullReceiverId_shouldReturn400() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + // CallTokenRequest dengan receiverId null — @Valid harus menolak + CallTokenRequest req = new CallTokenRequest(); + // receiverId tidak di-set (null) + + mockMvc.perform(post("/api/v1/shared/call/token") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()); + + verify(agoraTokenService, never()).generateToken(anyLong(), anyLong()); + } + } + + @Test + @DisplayName("POST /api/v1/shared/call/token - service throw harus return 500") + void generateToken_serviceThrows_shouldReturn500() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + CallTokenRequest req = new CallTokenRequest(); + req.setReceiverId(99L); + + when(agoraTokenService.generateToken(1L, 99L)) + .thenThrow(new RuntimeException("Receiver tidak ditemukan")); + + mockMvc.perform(post("/api/v1/shared/call/token") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isInternalServerError()); + } + } + + // ===== NOTIFY CALL ===== + + @Test + @DisplayName("POST /api/v1/shared/call/notify - receiver punya FCM token harus kirim notifikasi") + void notifyCall_receiverHasFcmToken_shouldReturn200AndSendFcm() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + when(userRepository.findById(1L)).thenReturn(Optional.of(sampleCaller)); + when(userRepository.findById(2L)).thenReturn(Optional.of(sampleReceiver)); + doNothing().when(fcmService).sendHighPriority(anyString(), anyString(), anyString(), anyMap()); + + CallNotifyRequest req = new CallNotifyRequest(); + req.setReceiverId(2L); + req.setChannelName("call_1_2_1234567890"); + req.setAgoraToken("agora-token-xyz"); + req.setReceiverUid(1002); + + mockMvc.perform(post("/api/v1/shared/call/notify") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Notifikasi panggilan berhasil dikirim")); + + verify(fcmService).sendHighPriority( + eq("fcm-receiver-token"), + eq("📞 Panggilan Masuk"), + contains("Caller User"), + anyMap() + ); + } + } + + @Test + @DisplayName("POST /api/v1/shared/call/notify - receiver tidak punya FCM token harus return 200 tanpa FCM") + void notifyCall_receiverNoFcmToken_shouldReturn200WithWarningMessage() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + User receiverNoFcm = User.builder() + .id(2L) + .email("receiver@test.com") + .displayName("Receiver") + .fcmToken(null) // tidak punya FCM token + .build(); + + when(userRepository.findById(1L)).thenReturn(Optional.of(sampleCaller)); + when(userRepository.findById(2L)).thenReturn(Optional.of(receiverNoFcm)); + + CallNotifyRequest req = new CallNotifyRequest(); + req.setReceiverId(2L); + req.setChannelName("call_1_2_abc"); + req.setAgoraToken("token-xyz"); + req.setReceiverUid(1002); + + mockMvc.perform(post("/api/v1/shared/call/notify") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value( + "Panggilan dikirim (receiver mungkin tidak menerima push notification)")); + + // FCM tidak boleh dipanggil karena tidak ada token + verify(fcmService, never()).sendHighPriority(anyString(), anyString(), anyString(), anyMap()); + } + } + + @Test + @DisplayName("POST /api/v1/shared/call/notify - receiver FCM token blank harus return 200 tanpa FCM") + void notifyCall_receiverBlankFcmToken_shouldReturn200WithoutFcm() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + User receiverBlankFcm = User.builder() + .id(2L) + .email("receiver@test.com") + .displayName("Receiver") + .fcmToken(" ") // blank + .build(); + + when(userRepository.findById(1L)).thenReturn(Optional.of(sampleCaller)); + when(userRepository.findById(2L)).thenReturn(Optional.of(receiverBlankFcm)); + + CallNotifyRequest req = new CallNotifyRequest(); + req.setReceiverId(2L); + req.setChannelName("channel-abc"); + req.setAgoraToken("token"); + req.setReceiverUid(1002); + + mockMvc.perform(post("/api/v1/shared/call/notify") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value( + "Panggilan dikirim (receiver mungkin tidak menerima push notification)")); + + verify(fcmService, never()).sendHighPriority(anyString(), anyString(), anyString(), anyMap()); + } + } + + @Test + @DisplayName("POST /api/v1/shared/call/notify - caller tidak ditemukan harus return 500") + void notifyCall_callerNotFound_shouldReturn500() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + when(userRepository.findById(1L)) + .thenThrow(new ResourceNotFoundException("Caller not found")); + + CallNotifyRequest req = new CallNotifyRequest(); + req.setReceiverId(2L); + req.setChannelName("channel-abc"); + req.setAgoraToken("token"); + req.setReceiverUid(1002); + + mockMvc.perform(post("/api/v1/shared/call/notify") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isInternalServerError()); + + verify(fcmService, never()).sendHighPriority(anyString(), anyString(), anyString(), anyMap()); + } + } + + @Test + @DisplayName("POST /api/v1/shared/call/notify - receiver tidak ditemukan harus return 500") + void notifyCall_receiverNotFound_shouldReturn500() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + when(userRepository.findById(1L)).thenReturn(Optional.of(sampleCaller)); + when(userRepository.findById(2L)) + .thenThrow(new ResourceNotFoundException("Receiver not found")); + + CallNotifyRequest req = new CallNotifyRequest(); + req.setReceiverId(2L); + req.setChannelName("channel-abc"); + req.setAgoraToken("token"); + req.setReceiverUid(1002); + + mockMvc.perform(post("/api/v1/shared/call/notify") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isInternalServerError()); + } + } + + @Test + @DisplayName("POST /api/v1/shared/call/notify - caller displayName null harus pakai email sebagai pengganti") + void notifyCall_callerNoDisplayName_shouldUseEmailAsFallback() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + User callerNoName = User.builder() + .id(1L) + .email("noreply@test.com") + .displayName(null) // tidak ada displayName + .build(); + + when(userRepository.findById(1L)).thenReturn(Optional.of(callerNoName)); + when(userRepository.findById(2L)).thenReturn(Optional.of(sampleReceiver)); + doNothing().when(fcmService).sendHighPriority(anyString(), anyString(), anyString(), anyMap()); + + CallNotifyRequest req = new CallNotifyRequest(); + req.setReceiverId(2L); + req.setChannelName("channel-abc"); + req.setAgoraToken("token"); + req.setReceiverUid(1002); + + mockMvc.perform(post("/api/v1/shared/call/notify") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()); + + // Pastikan body notifikasi menggunakan email sebagai fallback + verify(fcmService).sendHighPriority( + anyString(), + anyString(), + contains("noreply@test.com"), + anyMap() + ); + } + } + + // ===== END CALL ===== + + @Test + @DisplayName("POST /api/v1/shared/call/end - otherId valid dan punya FCM token harus kirim notifikasi berakhir") + void endCall_otherHasFcmToken_shouldSendEndNotification() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + when(userRepository.findById(2L)).thenReturn(Optional.of(sampleReceiver)); + doNothing().when(fcmService).sendToToken(anyString(), anyString(), anyString(), anyMap()); + + Map body = Map.of("otherId", 2L); + + mockMvc.perform(post("/api/v1/shared/call/end") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Call ended")); + + verify(fcmService).sendToToken( + eq("fcm-receiver-token"), + eq("Panggilan Berakhir"), + eq("Panggilan telah berakhir"), + anyMap() + ); + } + } + + @Test + @DisplayName("POST /api/v1/shared/call/end - otherId null harus return 200 tanpa kirim FCM") + void endCall_nullOtherId_shouldReturn200WithoutFcm() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + Map body = Map.of(); // tidak ada otherId + + mockMvc.perform(post("/api/v1/shared/call/end") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Call ended")); + + verify(fcmService, never()).sendToToken(anyString(), anyString(), anyString(), anyMap()); + } + } + + @Test + @DisplayName("POST /api/v1/shared/call/end - other tidak punya FCM token harus return 200 tanpa FCM") + void endCall_otherNoFcmToken_shouldReturn200WithoutFcm() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + User otherNoFcm = User.builder() + .id(2L) + .email("other@test.com") + .fcmToken(null) + .build(); + when(userRepository.findById(2L)).thenReturn(Optional.of(otherNoFcm)); + + Map body = Map.of("otherId", 2L); + + mockMvc.perform(post("/api/v1/shared/call/end") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Call ended")); + + verify(fcmService, never()).sendToToken(anyString(), anyString(), anyString(), anyMap()); + } + } + + @Test + @WithMockUser(username = "2", roles = "GUARDIAN") + @DisplayName("POST /api/v1/shared/call/end - Guardian juga bisa end call") + void endCall_asGuardian_shouldReturn200() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(2L); + when(userRepository.findById(1L)).thenReturn(Optional.of(sampleCaller)); + doNothing().when(fcmService).sendToToken(anyString(), anyString(), anyString(), anyMap()); + + Map body = Map.of("otherId", 1L); + + mockMvc.perform(post("/api/v1/shared/call/end") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Call ended")); + } + } +}