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
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));
|
|
}
|
|
}
|
|
}
|
|
|