diff --git a/walkguide-backend/demo/src/test/java/com/walkguide/Controller/PairingControllerTest.java b/walkguide-backend/demo/src/test/java/com/walkguide/Controller/PairingControllerTest.java new file mode 100644 index 0000000..6e5b1b3 --- /dev/null +++ b/walkguide-backend/demo/src/test/java/com/walkguide/Controller/PairingControllerTest.java @@ -0,0 +1,263 @@ +package com.walkguide.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.walkguide.dto.request.InviteUserRequest; +import com.walkguide.dto.request.PairingResponseRequest; +import com.walkguide.dto.response.PairingStatusResponse; +import com.walkguide.security.SecurityHelper; +import com.walkguide.service.PairingService; +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.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collection; +import java.util.List; + +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(PairingController.class) +@DisplayName("PairingController Unit Tests") +class PairingControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private PairingService pairingService; + + private PairingStatusResponse buildPairingStatus(String status) { + return PairingStatusResponse.builder() + .pairingId(1L) + .status(status) + .pairedWithName("Test Partner") + .pairedWithEmail("partner@test.com") + .uniqueUserId("ABC123") + .build(); + } + + // ===== INVITE ===== + + @Test + @WithMockUser(username = "2", roles = "GUARDIAN") + @DisplayName("POST /api/v1/shared/pairing/invite - Guardian undang User harus return 200") + void invite_asGuardian_shouldReturn200() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(2L); + + InviteUserRequest req = new InviteUserRequest(); + req.setUniqueUserId("ABC123DEF456"); + + PairingStatusResponse resp = buildPairingStatus("PENDING"); + when(pairingService.inviteUser(2L, "ABC123DEF456")).thenReturn(resp); + + mockMvc.perform(post("/api/v1/shared/pairing/invite") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Undangan dikirim ke user")) + .andExpect(jsonPath("$.data.status").value("PENDING")); + + verify(pairingService).inviteUser(2L, "ABC123DEF456"); + } + } + + @Test + @WithMockUser(username = "2", roles = "GUARDIAN") + @DisplayName("POST /api/v1/shared/pairing/invite - uniqueUserId tidak ada harus return 500") + void invite_userNotFound_shouldReturn500() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(2L); + + InviteUserRequest req = new InviteUserRequest(); + req.setUniqueUserId("INVALID999"); + + when(pairingService.inviteUser(2L, "INVALID999")) + .thenThrow(new RuntimeException("User dengan ID tersebut tidak ditemukan")); + + mockMvc.perform(post("/api/v1/shared/pairing/invite") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isInternalServerError()); + } + } + + // ===== RESPOND ===== + + @Test + @WithMockUser(username = "1", roles = "USER") + @DisplayName("POST /api/v1/shared/pairing/respond - User terima pairing harus return 200") + void respond_accept_shouldReturn200WithAcceptMessage() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + PairingResponseRequest req = new PairingResponseRequest(); + req.setPairingId(5L); + req.setAccept(true); + + PairingStatusResponse resp = buildPairingStatus("ACTIVE"); + when(pairingService.respondToPairing(1L, 5L, true)).thenReturn(resp); + + mockMvc.perform(post("/api/v1/shared/pairing/respond") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Pairing diterima")) + .andExpect(jsonPath("$.data.status").value("ACTIVE")); + + verify(pairingService).respondToPairing(1L, 5L, true); + } + } + + @Test + @WithMockUser(username = "1", roles = "USER") + @DisplayName("POST /api/v1/shared/pairing/respond - User tolak pairing harus return 200 pesan ditolak") + void respond_reject_shouldReturn200WithRejectMessage() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + PairingResponseRequest req = new PairingResponseRequest(); + req.setPairingId(5L); + req.setAccept(false); + + PairingStatusResponse resp = buildPairingStatus("REJECTED"); + when(pairingService.respondToPairing(1L, 5L, false)).thenReturn(resp); + + mockMvc.perform(post("/api/v1/shared/pairing/respond") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Pairing ditolak")) + .andExpect(jsonPath("$.data.status").value("REJECTED")); + + verify(pairingService).respondToPairing(1L, 5L, false); + } + } + + // ===== UNPAIR ===== + + @Test + @WithMockUser(username = "1", roles = "USER") + @DisplayName("DELETE /api/v1/shared/pairing/unpair - harus akhiri pairing dan return 200") + void unpair_shouldReturn200() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + doNothing().when(pairingService).unpair(1L); + + mockMvc.perform(delete("/api/v1/shared/pairing/unpair") + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Pairing diakhiri")); + + verify(pairingService).unpair(1L); + } + } + + @Test + @WithMockUser(username = "2", roles = "GUARDIAN") + @DisplayName("DELETE /api/v1/shared/pairing/unpair - Guardian juga bisa unpair") + void unpair_asGuardian_shouldReturn200() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(2L); + doNothing().when(pairingService).unpair(2L); + + mockMvc.perform(delete("/api/v1/shared/pairing/unpair") + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Pairing diakhiri")); + + verify(pairingService).unpair(2L); + } + } + + @Test + @WithMockUser(username = "1", roles = "USER") + @DisplayName("DELETE /api/v1/shared/pairing/unpair - service throw harus return 500") + void unpair_serviceThrows_shouldReturn500() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + doThrow(new RuntimeException("Tidak ada pairing aktif")).when(pairingService).unpair(1L); + + mockMvc.perform(delete("/api/v1/shared/pairing/unpair") + .with(csrf())) + .andExpect(status().isInternalServerError()); + } + } + + // ===== STATUS ===== + + @Test + @WithMockUser(username = "1", roles = "USER") + @DisplayName("GET /api/v1/shared/pairing/status - User harus return status pairing") + void status_asUser_shouldReturn200() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + PairingStatusResponse resp = buildPairingStatus("ACTIVE"); + when(pairingService.getStatus(eq(1L), eq("ROLE_USER"))).thenReturn(resp); + + mockMvc.perform(get("/api/v1/shared/pairing/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Status pairing")) + .andExpect(jsonPath("$.data.status").value("ACTIVE")) + .andExpect(jsonPath("$.data.pairingId").value(1)); + } + } + + @Test + @WithMockUser(username = "2", roles = "GUARDIAN") + @DisplayName("GET /api/v1/shared/pairing/status - Guardian harus return status pairing dengan role GUARDIAN") + void status_asGuardian_shouldReturn200() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(2L); + + PairingStatusResponse resp = buildPairingStatus("ACTIVE"); + when(pairingService.getStatus(eq(2L), eq("ROLE_GUARDIAN"))).thenReturn(resp); + + mockMvc.perform(get("/api/v1/shared/pairing/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Status pairing")); + } + } + + @Test + @WithMockUser(username = "1", roles = "USER") + @DisplayName("GET /api/v1/shared/pairing/status - belum ada pairing harus return status NONE") + void status_noPairing_shouldReturnNoneStatus() throws Exception { + try (MockedStatic sh = mockStatic(SecurityHelper.class)) { + sh.when(SecurityHelper::getCurrentUserId).thenReturn(1L); + + PairingStatusResponse resp = PairingStatusResponse.builder() + .status("NONE") + .build(); + when(pairingService.getStatus(eq(1L), anyString())).thenReturn(resp); + + mockMvc.perform(get("/api/v1/shared/pairing/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.status").value("NONE")); + } + } +}