forked from baskbuddy/zhangshun
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.
224 lines
10 KiB
224 lines
10 KiB
package com.mydreamjob.match;
|
|
|
|
import com.mydreamjob.audit.AuditService;
|
|
import com.mydreamjob.auth.User;
|
|
import com.mydreamjob.auth.UserRepository;
|
|
import com.mydreamjob.security.UserRoleGuard;
|
|
import com.mydreamjob.common.r01b.UserRoleGuardTestSupport;
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import com.mydreamjob.job.Job;
|
|
import com.mydreamjob.job.JobService;
|
|
import com.mydreamjob.preference.StudentPreference;
|
|
import com.mydreamjob.preference.StudentPreferenceRepository;
|
|
import com.mydreamjob.profile.StudentProfile;
|
|
import com.mydreamjob.profile.StudentProfileRepository;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.mockito.InjectMocks;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
|
import java.util.HashSet;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
@ExtendWith(MockitoExtension.class)
|
|
class MatchServiceTest {
|
|
|
|
@Mock private JobService jobService;
|
|
@Mock private StudentPreferenceRepository preferenceRepository;
|
|
@Mock private StudentProfileRepository profileRepository;
|
|
@Mock private AuditService auditService;
|
|
@Mock private UserRepository userRepository;
|
|
/** R01-B/B1:角色授权已委托给公共 guard;此处以委托桩保持既有断言语义。 */
|
|
@Mock
|
|
private UserRoleGuard userRoleGuard;
|
|
@InjectMocks private MatchService matchService;
|
|
|
|
private static final Long STUDENT_ID = 100L;
|
|
private static final Long COMPANY_ID = 200L;
|
|
private static final Long ADMIN_ID = 300L;
|
|
private static final Long JOB_ID = 10L;
|
|
|
|
private Job publishedJob;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
// R01-B/B1:让 guard 桩委托真实实现,保持既有 403/404 断言不变。
|
|
UserRoleGuardTestSupport.delegateToRealGuard(userRoleGuard, userRepository);
|
|
|
|
publishedJob = Job.builder().id(JOB_ID).companyId(1L).title("Java 后端")
|
|
.city("杭州").employmentType("FULL_TIME")
|
|
.salaryMax(20000).status("PUBLISHED").build();
|
|
}
|
|
|
|
private void mockRole(Long userId, User.Role role) {
|
|
User u = User.builder().id(userId).phone("1").role(role).build();
|
|
u.setRoles(new HashSet<>(Set.of(role)));
|
|
when(userRepository.findById(userId)).thenReturn(Optional.of(u));
|
|
}
|
|
|
|
private void mockPref() {
|
|
StudentPreference p = StudentPreference.builder().studentUserId(STUDENT_ID)
|
|
.preferredCities("[\"杭州\"]").employmentTypes("[\"FULL_TIME\"]")
|
|
.expectedSalaryMin(10000).preferredIndustries("[\"互联网\"]")
|
|
.preferredJobTypes("[\"Java\"]").remoteAllowed(false).build();
|
|
when(preferenceRepository.findByStudentUserId(STUDENT_ID)).thenReturn(Optional.of(p));
|
|
}
|
|
|
|
private void mockProfile() {
|
|
when(profileRepository.findByUserId(STUDENT_ID)).thenReturn(Optional.of(
|
|
StudentProfile.builder().userId(STUDENT_ID).build()));
|
|
}
|
|
|
|
// ── 角色校验 ─────────────────────────────────────────────
|
|
|
|
@Test void diagnose_asCompany_shouldReturn403() {
|
|
mockRole(COMPANY_ID, User.Role.COMPANY);
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> matchService.diagnose(COMPANY_ID, JOB_ID));
|
|
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCodeInt());
|
|
}
|
|
|
|
@Test void diagnose_asAdmin_shouldReturn403() {
|
|
mockRole(ADMIN_ID, User.Role.ADMIN);
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> matchService.diagnose(ADMIN_ID, JOB_ID));
|
|
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCodeInt());
|
|
}
|
|
|
|
// ── 完全匹配 ─────────────────────────────────────────────
|
|
|
|
@Test void diagnose_fullMatch_shouldBeEligible() {
|
|
mockRole(STUDENT_ID, User.Role.STUDENT);
|
|
when(jobService.getJob(JOB_ID)).thenReturn(publishedJob);
|
|
mockPref(); mockProfile();
|
|
|
|
Map<String, Object> r = matchService.diagnose(STUDENT_ID, JOB_ID);
|
|
|
|
assertEquals(true, r.get("eligible"));
|
|
assertEquals("RULES_V0_1", r.get("rulesVersion"));
|
|
assertTrue((Integer) r.get("matchScore") >= 70);
|
|
assertTrue(((java.util.List<String>) r.get("matchedConditions")).size() >= 3);
|
|
}
|
|
|
|
// ── 部分匹配 ─────────────────────────────────────────────
|
|
|
|
@Test void diagnose_partialMatch_shouldBeEligibleWithGaps() {
|
|
mockRole(STUDENT_ID, User.Role.STUDENT);
|
|
when(jobService.getJob(JOB_ID)).thenReturn(publishedJob);
|
|
StudentPreference p = StudentPreference.builder().studentUserId(STUDENT_ID)
|
|
.preferredCities("[\"上海\"]").employmentTypes("[\"PART_TIME\"]")
|
|
.expectedSalaryMin(15000).build();
|
|
when(preferenceRepository.findByStudentUserId(STUDENT_ID)).thenReturn(Optional.of(p));
|
|
when(profileRepository.findByUserId(STUDENT_ID)).thenReturn(Optional.of(
|
|
StudentProfile.builder().userId(STUDENT_ID).build()));
|
|
|
|
Map<String, Object> r = matchService.diagnose(STUDENT_ID, JOB_ID);
|
|
|
|
assertFalse(((java.util.List<String>) r.get("gaps")).isEmpty());
|
|
}
|
|
|
|
// ── 硬性条件不满足 ───────────────────────────────────────
|
|
|
|
@Test void diagnose_hardGap_salaryTooHigh_shouldBeIneligible() {
|
|
mockRole(STUDENT_ID, User.Role.STUDENT);
|
|
when(jobService.getJob(JOB_ID)).thenReturn(publishedJob);
|
|
StudentPreference p = StudentPreference.builder().studentUserId(STUDENT_ID)
|
|
.preferredCities("[\"杭州\"]").employmentTypes("[\"FULL_TIME\"]")
|
|
.expectedSalaryMin(30000).build(); // 期望下限 > 岗位上限 20000
|
|
when(preferenceRepository.findByStudentUserId(STUDENT_ID)).thenReturn(Optional.of(p));
|
|
when(profileRepository.findByUserId(STUDENT_ID)).thenReturn(Optional.of(
|
|
StudentProfile.builder().userId(STUDENT_ID).build()));
|
|
|
|
Map<String, Object> r = matchService.diagnose(STUDENT_ID, JOB_ID);
|
|
|
|
assertEquals(false, r.get("eligible"));
|
|
assertTrue(((String) r.get("explanation")).contains("硬性条件"));
|
|
}
|
|
|
|
// ── 空档案 ───────────────────────────────────────────────
|
|
|
|
@Test void diagnose_emptyProfile_shouldBeIneligibleWithZero() {
|
|
mockRole(STUDENT_ID, User.Role.STUDENT);
|
|
when(jobService.getJob(JOB_ID)).thenReturn(publishedJob);
|
|
when(preferenceRepository.findByStudentUserId(STUDENT_ID)).thenReturn(Optional.empty());
|
|
when(profileRepository.findByUserId(STUDENT_ID)).thenReturn(Optional.empty());
|
|
|
|
Map<String, Object> r = matchService.diagnose(STUDENT_ID, JOB_ID);
|
|
|
|
assertEquals(0, r.get("matchScore"));
|
|
assertEquals(false, r.get("eligible"));
|
|
assertTrue(((java.util.List<String>) r.get("gaps")).contains("尚未完善档案与偏好"));
|
|
}
|
|
|
|
// ── 阈值边界 ─────────────────────────────────────────────
|
|
|
|
@Test void diagnose_salaryBoundary_equal_shouldBeEligible() {
|
|
mockRole(STUDENT_ID, User.Role.STUDENT);
|
|
when(jobService.getJob(JOB_ID)).thenReturn(publishedJob); // salaryMax=20000
|
|
StudentPreference p = StudentPreference.builder().studentUserId(STUDENT_ID)
|
|
.preferredCities("[\"杭州\"]").employmentTypes("[\"FULL_TIME\"]")
|
|
.expectedSalaryMin(20000).build(); // 等于上限,边界
|
|
when(preferenceRepository.findByStudentUserId(STUDENT_ID)).thenReturn(Optional.of(p));
|
|
when(profileRepository.findByUserId(STUDENT_ID)).thenReturn(Optional.of(
|
|
StudentProfile.builder().userId(STUDENT_ID).build()));
|
|
|
|
Map<String, Object> r = matchService.diagnose(STUDENT_ID, JOB_ID);
|
|
|
|
assertEquals(true, r.get("eligible"));
|
|
}
|
|
|
|
// ── 岗位不存在 ───────────────────────────────────────────
|
|
|
|
@Test void diagnose_jobNotFound_shouldReturn404() {
|
|
mockRole(STUDENT_ID, User.Role.STUDENT);
|
|
when(jobService.getJob(999L)).thenThrow(new BusinessException(ErrorCode.JOB_NOT_FOUND));
|
|
|
|
assertThrows(BusinessException.class, () -> matchService.diagnose(STUDENT_ID, 999L));
|
|
}
|
|
|
|
// ── 不泄露敏感数据 ───────────────────────────────────────
|
|
|
|
@Test void diagnose_shouldNotIncludeSensitiveData() {
|
|
mockRole(STUDENT_ID, User.Role.STUDENT);
|
|
when(jobService.getJob(JOB_ID)).thenReturn(publishedJob);
|
|
mockPref(); mockProfile();
|
|
|
|
Map<String, Object> r = matchService.diagnose(STUDENT_ID, JOB_ID);
|
|
|
|
String json = r.toString();
|
|
assertFalse(json.contains("phone"));
|
|
assertFalse(json.contains("resume"));
|
|
}
|
|
|
|
// ── 审计记录 ─────────────────────────────────────────────
|
|
|
|
@Test void diagnose_shouldLogAudit() {
|
|
mockRole(STUDENT_ID, User.Role.STUDENT);
|
|
when(jobService.getJob(JOB_ID)).thenReturn(publishedJob);
|
|
mockPref(); mockProfile();
|
|
|
|
matchService.diagnose(STUDENT_ID, JOB_ID);
|
|
verify(auditService, times(1)).log(eq(STUDENT_ID), eq("MATCH_DIAGNOSE"), eq("JOB"), eq(JOB_ID),
|
|
anyString(), isNull());
|
|
}
|
|
|
|
@Test void diagnose_withContext_shouldPersistContext() {
|
|
mockRole(STUDENT_ID, User.Role.STUDENT);
|
|
when(jobService.getJob(JOB_ID)).thenReturn(publishedJob);
|
|
mockPref(); mockProfile();
|
|
com.mydreamjob.audit.AuditContext ctx = com.mydreamjob.audit.AuditContext.builder()
|
|
.requestId("req-ctx-1").ipAddress("10.1.1.1").userAgent("TestAgent").build();
|
|
|
|
matchService.diagnose(STUDENT_ID, JOB_ID, ctx);
|
|
verify(auditService, times(1)).log(eq(STUDENT_ID), eq("MATCH_DIAGNOSE"), eq("JOB"), eq(JOB_ID),
|
|
anyString(), eq(ctx));
|
|
}
|
|
}
|
|
|