You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

245 lines
12 KiB

package com.mydreamjob.bidirectionalmatch;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
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.web.servlet.MockMvc;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* 双向匹配接口集成测试:JWT、统一响应、权限、校验、OpenAPI 一致性。
* 激活 profile: integration-test(H2 内嵌数据库 + Flyway V16)
*/
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("integration-test")
class ApplicationMatchControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private com.mydreamjob.auth.JwtTokenProvider jwtTokenProvider;
@MockBean private BidirectionalMatchService matchService;
@MockBean private MatchRuleService matchRuleService;
@MockBean private MatchConversationService matchConversationService;
@MockBean private RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper mapper = new ObjectMapper();
private static final Long STUDENT_ID = 100L;
private static final Long COMPANY_ID = 10L;
private static final Long APPLICATION_ID = 5L;
@BeforeEach
@SuppressWarnings("unchecked")
void setUp() {
ValueOperations<String, Object> ops = mock(ValueOperations.class);
when(redisTemplate.opsForValue()).thenReturn(ops);
when(redisTemplate.hasKey(any())).thenReturn(false);
}
private String studentToken() {
return com.mydreamjob.auth.support.TestAuthTokens.bearer(STUDENT_ID, "STUDENT");
}
private String companyToken() {
return com.mydreamjob.auth.support.TestAuthTokens.bearer(COMPANY_ID, "COMPANY");
}
private ApplicationMatchRecord record() {
return ApplicationMatchRecord.builder().id(1L).applicationId(APPLICATION_ID)
.jobId(10L).companyId(1L).candidateId(STUDENT_ID)
.candidateInterestScore(80).hrSuitabilityScore(80)
.companyRequiredCandidateScore(60).studentRequiredHrScore(60)
.status(ApplicationMatchRecord.STATUS_MATCHED).build();
}
// ── 阈值 ──────────────────────────────────────────────────
@Test
void getCompanyRule_shouldReturn200() throws Exception {
when(matchRuleService.getCompanyRule(COMPANY_ID)).thenReturn(Map.of("companyId", 1L, "requiredCandidateScore", 60));
mockMvc.perform(get("/companies/me/match-rule").header("Authorization", companyToken()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.requiredCandidateScore").value(60));
}
@Test
void putCompanyRule_shouldReturn200() throws Exception {
when(matchRuleService.updateCompanyRule(eq(COMPANY_ID), eq(70))).thenReturn(Map.of("requiredCandidateScore", 70));
mockMvc.perform(put("/companies/me/match-rule")
.header("Authorization", companyToken())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"requiredCandidateScore\":70}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.requiredCandidateScore").value(70));
}
@Test
void putCompanyRule_invalidScore_shouldReturn400() throws Exception {
mockMvc.perform(put("/companies/me/match-rule")
.header("Authorization", companyToken())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"requiredCandidateScore\":101}"))
.andExpect(status().isBadRequest());
}
@Test
void getStudentRule_shouldReturn200() throws Exception {
when(matchRuleService.getStudentRule(STUDENT_ID)).thenReturn(Map.of("studentUserId", STUDENT_ID, "requiredHrScore", 60));
mockMvc.perform(get("/profile/me/match-rule").header("Authorization", studentToken()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
@Test
void putStudentRule_shouldReturn200() throws Exception {
when(matchRuleService.updateStudentRule(eq(STUDENT_ID), eq(75))).thenReturn(Map.of("requiredHrScore", 75));
mockMvc.perform(put("/profile/me/match-rule")
.header("Authorization", studentToken())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"requiredHrScore\":75}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.requiredHrScore").value(75));
}
// ── 评分 ──────────────────────────────────────────────────
@Test
void putCandidateInterestScore_shouldReturn200() throws Exception {
when(matchService.submitCandidateInterestScore(eq(STUDENT_ID), eq(APPLICATION_ID), eq(80), any()))
.thenReturn(record());
mockMvc.perform(put("/applications/" + APPLICATION_ID + "/candidate-interest-score")
.header("Authorization", studentToken())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"score\":80}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.status").value("MATCHED"));
}
@Test
void putCandidateInterestScore_invalidScore_shouldReturn400() throws Exception {
mockMvc.perform(put("/applications/" + APPLICATION_ID + "/candidate-interest-score")
.header("Authorization", studentToken())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"score\":150}"))
.andExpect(status().isBadRequest());
}
@Test
void putCandidateInterestScore_asCompany_shouldReturn403() throws Exception {
when(matchService.submitCandidateInterestScore(eq(COMPANY_ID), eq(APPLICATION_ID), eq(80), any()))
.thenThrow(new BusinessException(ErrorCode.FORBIDDEN));
mockMvc.perform(put("/applications/" + APPLICATION_ID + "/candidate-interest-score")
.header("Authorization", companyToken())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"score\":80}"))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.code").value(ErrorCode.FORBIDDEN.getCode()));
}
@Test
void putHrSuitabilityScore_shouldReturn200() throws Exception {
when(matchService.submitHrSuitabilityScore(eq(COMPANY_ID), eq(APPLICATION_ID), eq(80), any()))
.thenReturn(record());
mockMvc.perform(put("/applications/" + APPLICATION_ID + "/hr-suitability-score")
.header("Authorization", companyToken())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"score\":80}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.candidateInterestScore").value(80));
}
// ── 匹配状态 ──────────────────────────────────────────────
@Test
void getMatch_shouldReturn200() throws Exception {
when(matchService.getMatchView(STUDENT_ID, APPLICATION_ID)).thenReturn(Map.of(
"applicationId", APPLICATION_ID, "status", "MATCHED", "candidateInterestScore", 80));
mockMvc.perform(get("/applications/" + APPLICATION_ID + "/match").header("Authorization", studentToken()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("MATCHED"))
.andExpect(jsonPath("$.data.eventId").doesNotExist());
}
@Test
void getMatch_noToken_shouldReturn401() throws Exception {
mockMvc.perform(get("/applications/" + APPLICATION_ID + "/match"))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
@Test
void getMatch_notFound_shouldReturn404() throws Exception {
when(matchService.getMatchView(STUDENT_ID, 999L))
.thenThrow(new BusinessException(ErrorCode.APPLICATION_NOT_FOUND));
mockMvc.perform(get("/applications/999/match").header("Authorization", studentToken()))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(ErrorCode.APPLICATION_NOT_FOUND.getCode()));
}
// ── 会话解锁 ──────────────────────────────────────────────
@Test
void postConversation_shouldReturn200() throws Exception {
when(matchConversationService.createOrGetMatchConversation(STUDENT_ID, APPLICATION_ID))
.thenReturn(Map.of("id", 88L, "type", "DIRECT"));
mockMvc.perform(post("/applications/" + APPLICATION_ID + "/conversation")
.header("Authorization", studentToken()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.id").value(88L));
}
@Test
void postConversation_notMatched_shouldReturn409() throws Exception {
when(matchConversationService.createOrGetMatchConversation(STUDENT_ID, APPLICATION_ID))
.thenThrow(new BusinessException(ErrorCode.APPLICATION_NOT_MATCHED));
mockMvc.perform(post("/applications/" + APPLICATION_ID + "/conversation")
.header("Authorization", studentToken()))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.code").value(ErrorCode.APPLICATION_NOT_MATCHED.getCode()));
}
// ── OpenAPI 一致性 ────────────────────────────────────────
@Test
void openApi_shouldContainMatchEndpoints() throws Exception {
String json = mockMvc.perform(get("/v3/api-docs"))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
JsonNode paths = mapper.readTree(json).path("paths");
assertThat(paths.has("/companies/me/match-rule")).isTrue();
assertThat(paths.has("/profile/me/match-rule")).isTrue();
assertThat(paths.has("/applications/{id}/candidate-interest-score")).isTrue();
assertThat(paths.has("/applications/{id}/hr-suitability-score")).isTrue();
assertThat(paths.has("/applications/{id}/match")).isTrue();
assertThat(paths.has("/applications/{id}/conversation")).isTrue();
String snapshotDir = System.getProperty("offer.openapi.snapshot");
if (snapshotDir != null && !snapshotDir.isBlank()) {
java.nio.file.Files.write(
java.nio.file.Paths.get(snapshotDir, "openapi-rc.json"),
json.getBytes(StandardCharsets.UTF_8));
}
}
}