package com.mydreamjob.review; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageImpl; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import java.util.List; import java.util.Map; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("integration-test") class JobReviewControllerTest { @Autowired private MockMvc mockMvc; @Autowired private com.mydreamjob.auth.JwtTokenProvider jwtTokenProvider; @MockBean private JobReviewService reviewService; @MockBean private RedisTemplate redisTemplate; private final ObjectMapper mapper = new ObjectMapper(); private static final Long JOB_ID = 10L; private static final Long REVIEW_ID = 1L; @BeforeEach @SuppressWarnings("unchecked") void setUp() { ValueOperations ops = mock(ValueOperations.class); when(redisTemplate.opsForValue()).thenReturn(ops); when(redisTemplate.hasKey(any())).thenReturn(false); } private String token() { return "Bearer " + jwtTokenProvider.generateAccessToken(100L, "1", java.util.Map.of("roles", "STUDENT")); } @Test void createReview_shouldReturn200() throws Exception { when(reviewService.createReview(eq(100L), eq(JOB_ID), any(), any())) .thenReturn(Map.of("id", 1, "rating", 4)); String body = mapper.writeValueAsString(Map.of("rating", 4, "content", "ok")); mockMvc.perform(post("/jobs/" + JOB_ID + "/reviews") .header("Authorization", token()).contentType(MediaType.APPLICATION_JSON).content(body)) .andExpect(status().isOk()).andExpect(jsonPath("$.data.rating").value(4)); } @Test void createReview_noToken_shouldReturn401() throws Exception { String body = mapper.writeValueAsString(Map.of("rating", 4, "content", "ok")); mockMvc.perform(post("/jobs/" + JOB_ID + "/reviews") .contentType(MediaType.APPLICATION_JSON).content(body)) .andExpect(status().isUnauthorized()); } @Test void listReviews_public_shouldReturn200() throws Exception { when(reviewService.listReviews(eq(JOB_ID), any(), any(), any(), anyBoolean(), eq(0), eq(20))) .thenReturn(new PageImpl<>(List.of(Map.of("rating", 4)))); mockMvc.perform(get("/jobs/" + JOB_ID + "/reviews")) .andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200)); } @Test void updateReview_shouldReturn200() throws Exception { when(reviewService.updateReview(eq(100L), eq(JOB_ID), eq(REVIEW_ID), any(), any())) .thenReturn(Map.of("id", 1, "rating", 5)); String body = mapper.writeValueAsString(Map.of("rating", 5, "content", "updated")); mockMvc.perform(put("/jobs/" + JOB_ID + "/reviews/" + REVIEW_ID) .header("Authorization", token()).contentType(MediaType.APPLICATION_JSON).content(body)) .andExpect(status().isOk()).andExpect(jsonPath("$.data.rating").value(5)); } @Test void deleteReview_shouldReturn200() throws Exception { doNothing().when(reviewService).deleteReview(eq(100L), eq(JOB_ID), eq(REVIEW_ID), any()); mockMvc.perform(delete("/jobs/" + JOB_ID + "/reviews/" + REVIEW_ID) .header("Authorization", token())) .andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200)); } @Test void createReview_invalidRating_shouldReturn400() throws Exception { String body = mapper.writeValueAsString(Map.of("rating", 6, "content", "ok")); mockMvc.perform(post("/jobs/" + JOB_ID + "/reviews") .header("Authorization", token()).contentType(MediaType.APPLICATION_JSON).content(body)) .andExpect(status().isBadRequest()); } @Test void createReview_blankContent_shouldReturn400() throws Exception { String body = mapper.writeValueAsString(Map.of("rating", 4, "content", "")); mockMvc.perform(post("/jobs/" + JOB_ID + "/reviews") .header("Authorization", token()).contentType(MediaType.APPLICATION_JSON).content(body)) .andExpect(status().isBadRequest()); } @Test void updateReview_invalidStatus_shouldReturn400() throws Exception { String body = mapper.writeValueAsString(Map.of("status", "INVALID")); mockMvc.perform(put("/jobs/" + JOB_ID + "/reviews/" + REVIEW_ID) .header("Authorization", token()).contentType(MediaType.APPLICATION_JSON).content(body)) .andExpect(status().isBadRequest()); } @Test void summary_public_shouldReturn200() throws Exception { when(reviewService.getReviewSummary(JOB_ID)).thenReturn(Map.of( "jobId", JOB_ID, "totalReviews", 2, "averageRating", 4.5, "ratingDistribution", Map.of("rating1",0,"rating2",0,"rating3",0,"rating4",1,"rating5",1), "topTags", List.of("tag1"))); mockMvc.perform(get("/jobs/" + JOB_ID + "/reviews/summary")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.averageRating").value(4.5)) .andExpect(jsonPath("$.data.totalReviews").value(2)); } @Test void summary_noReviews_shouldReturnZeros() throws Exception { when(reviewService.getReviewSummary(JOB_ID)).thenReturn(Map.of( "jobId", JOB_ID, "totalReviews", 0, "averageRating", 0.0, "ratingDistribution", Map.of("rating1",0,"rating2",0,"rating3",0,"rating4",0,"rating5",0), "topTags", List.of())); mockMvc.perform(get("/jobs/" + JOB_ID + "/reviews/summary")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.totalReviews").value(0)); } }