package com.mydreamjob.talentdiscovery; import com.fasterxml.jackson.databind.ObjectMapper; import com.mydreamjob.auth.JwtTokenProvider; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import com.mydreamjob.talentdiscovery.dto.TalentDiscoveryCandidateResponse; import com.mydreamjob.talentdiscovery.dto.TalentDiscoveryPageResponse; import com.mydreamjob.talentdiscovery.dto.TalentDiscoveryPreferenceResponse; 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.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.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; import java.util.List; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("integration-test") @TestPropertySource(properties = "spring.jpa.open-in-view=false") class TalentDiscoveryControllerTest { @Autowired private MockMvc mockMvc; @Autowired private JwtTokenProvider jwtTokenProvider; @MockBean private TalentDiscoveryService service; @MockBean private RedisTemplate redisTemplate; private final ObjectMapper mapper = new ObjectMapper(); @BeforeEach @SuppressWarnings("unchecked") void setUp() { ValueOperations ops = org.mockito.Mockito.mock(ValueOperations.class); when(redisTemplate.opsForValue()).thenReturn(ops); when(redisTemplate.hasKey(any())).thenReturn(false); } private String token(long userId, String role) { return com.mydreamjob.auth.support.TestAuthTokens.bearer(userId, role); } @Test void getPreference_usesJwtIdentityAndReturnsPreference() throws Exception { when(service.getPreference(100L)).thenReturn(TalentDiscoveryPreferenceResponse.builder() .enabled(false).version(0L).build()); mockMvc.perform(get("/profile/me/talent-discovery") .header("Authorization", token(100L, "STUDENT"))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.enabled").value(false)) .andExpect(jsonPath("$.data.version").value(0)); } @Test void getPreference_withoutTokenReturns401() throws Exception { mockMvc.perform(get("/profile/me/talent-discovery")) .andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.code").value(401)); } @Test void updatePreference_studentCannotSupplyIdentityAndReturns200() throws Exception { when(service.updatePreference(anyLong(), any(), any())).thenReturn( TalentDiscoveryPreferenceResponse.builder().enabled(true).version(1L).build()); String body = mapper.writeValueAsString(java.util.Map.of( "enabled", true, "expectedVersion", 0, "studentUserId", 999999)); mockMvc.perform(put("/profile/me/talent-discovery") .header("Authorization", token(100L, "STUDENT")) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.enabled").value(true)); } @Test void companyRecommendationResponseContainsOnlyWhitelistedCardFields() throws Exception { TalentDiscoveryCandidateResponse candidate = TalentDiscoveryCandidateResponse.builder() .candidateKey("opaque-key") .candidateLabel("开放人才候选人") .city("杭州") .education("本科") .graduationYear(2026) .skills(List.of("Java", "Spring Boot")) .fitBand("HIGH") .summary("岗位画像与候选人公开资料存在较高匹配度") .signalsAvailable(List.of("PREFERENCE", "RIASEC", "ABILITY")) .build(); when(service.recommendations(anyLong(), anyLong(), anyInt(), anyInt())).thenReturn( TalentDiscoveryPageResponse.builder().mode("PERSONALIZED") .items(List.of(candidate)).total(1).page(0).size(20).totalPages(1).build()); mockMvc.perform(get("/companies/me/jobs/9/talent-recommendations") .header("Authorization", token(200L, "COMPANY"))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.items[0].candidateKey").value("opaque-key")) .andExpect(jsonPath("$.data.items[0].fitScore").doesNotExist()) .andExpect(content().string(not(containsString("studentUserId")))) .andExpect(content().string(not(containsString("dimensionScores")))) .andExpect(content().string(not(containsString("overallScore")))) .andExpect(content().string(not(containsString("fitScore")))); } @Test void companyRecommendationServiceForbiddenIsMappedTo403() throws Exception { doThrow(new BusinessException(ErrorCode.FORBIDDEN)) .when(service).recommendations(anyLong(), anyLong(), anyInt(), anyInt()); mockMvc.perform(get("/companies/me/jobs/9/talent-recommendations") .header("Authorization", token(201L, "COMPANY"))) .andExpect(status().isForbidden()) .andExpect(jsonPath("$.code").value(2002)); } }