轻松聘AI开发
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

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