轻松聘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.
 
 
 

201 lines
9.1 KiB

package com.mydreamjob.competition;
import com.mydreamjob.application.Application;
import com.mydreamjob.application.ApplicationRepository;
import com.mydreamjob.auth.User;
import com.mydreamjob.auth.UserRepository;
import com.mydreamjob.candidatescore.CandidateDimensionScore;
import com.mydreamjob.candidatescore.CandidateScoringCalculator;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.competition.dto.CompetitionRankingResponse;
import com.mydreamjob.job.Job;
import com.mydreamjob.job.JobService;
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 org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 同岗位竞争力排名服务单测(V32,编排 + 响应组装 + 脱敏)。
*
* <p>排序/百分位/快照装载等算法已下沉到 {@link CompetitionRankingCalculator}
* (由 CompetitionRankingCalculatorTest 覆盖),本类用 mock 计算器聚焦断言
* 权限、参数、岗位状态、cohort 委托,以及 Top5 脱敏与维度/缺口的组装。</p>
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CompetitionRankingServiceTest {
private static final Long ME = 100L;
private static final Long JOB = 9L;
@Mock private UserRepository userRepository;
@Mock private JobService jobService;
@Mock private ApplicationRepository applicationRepository;
@Mock private CompetitionRankingCalculator rankingCalculator;
@Mock private CandidateScoringCalculator candidateScoringCalculator;
@InjectMocks private CompetitionRankingService service;
// ── 权限与参数校验 ─────────────────────────────────────────
@Test
void jobIdZeroOrNull_returnsBadRequest() {
BusinessException ex = assertThrows(BusinessException.class, () -> service.getRanking(ME, 0L));
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCode());
}
@Test
void nonStudent_returnsForbidden() {
mockRole(ME, User.Role.COMPANY);
BusinessException ex = assertThrows(BusinessException.class, () -> call());
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCode());
}
@Test
void jobNotFound_returnsJobNotFound() {
mockStudent(ME);
when(jobService.getJob(JOB)).thenThrow(new BusinessException(ErrorCode.JOB_NOT_FOUND));
BusinessException ex = assertThrows(BusinessException.class, () -> call());
assertEquals(ErrorCode.JOB_NOT_FOUND.getCode(), ex.getErrorCode());
}
@Test
void nonPublishedJob_returnsJobNotFound() {
mockStudent(ME);
Job draft = Job.builder().id(JOB).status("DRAFT").build();
when(jobService.getJob(JOB)).thenReturn(draft);
BusinessException ex = assertThrows(BusinessException.class, () -> call());
assertEquals(ErrorCode.JOB_NOT_FOUND.getCode(), ex.getErrorCode());
}
@Test
void notAppliedStudent_returnsForbidden() {
mockPublishedJob();
when(applicationRepository.existsByJobIdAndStudentUserIdAndStatusNotIn(eq(JOB), eq(ME), anySet())).thenReturn(false);
BusinessException ex = assertThrows(BusinessException.class, () -> call());
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCode());
}
@Test
void effectiveStatus_returns200_andAssemblesResponse() {
mockPublishedJob();
when(applicationRepository.existsByJobIdAndStudentUserIdAndStatusNotIn(eq(JOB), eq(ME), anySet())).thenReturn(true);
when(applicationRepository.findByJobIdAndStatusNotInOrderByCreatedAtAsc(eq(JOB), anyCollection()))
.thenReturn(List.of(app(ME, "2026-01-01T10:00:00")));
when(rankingCalculator.loadLatestSnapshots(anyList(), anyString())).thenReturn(Map.of());
when(rankingCalculator.rankCohort(anyList(), anyMap(), eq(ME)))
.thenReturn(cr(1, 1, new BigDecimal("85.00"), null, "OK", "2026-07", List.of()));
CompetitionRankingResponse resp = call();
assertEquals(JOB, resp.getJobId());
assertEquals(1, resp.getCohortSize());
assertEquals(1, resp.getRank());
assertEquals(new BigDecimal("85.00"), resp.getOverallScore());
assertNull(resp.getPercentile());
assertEquals("OK", resp.getStatus());
assertEquals("2026-07", resp.getMySnapshotPeriod());
}
// ── Top5 脱敏与上限、维度/缺口组装 ─────────────────────────
@Test
void topCandidates_cappedAt5_andNoIdentityFields() {
mockPublishedJob();
when(applicationRepository.existsByJobIdAndStudentUserIdAndStatusNotIn(eq(JOB), eq(ME), anySet())).thenReturn(true);
when(applicationRepository.findByJobIdAndStatusNotInOrderByCreatedAtAsc(eq(JOB), anyCollection()))
.thenReturn(List.of(app(ME, "2026-01-01T10:00:00")));
when(rankingCalculator.loadLatestSnapshots(anyList(), anyString())).thenReturn(Map.of());
when(rankingCalculator.rankCohort(anyList(), anyMap(), eq(ME)))
.thenReturn(cr(7, 3, new BigDecimal("77.00"), null, "PARTIAL", "2026-07", List.of()));
CompetitionRankingResponse resp = call();
assertEquals(5, resp.getTopCandidates().size());
resp.getTopCandidates().forEach(t -> {
assertNotNull(t.getRank());
});
}
@Test
void dimensionsAndGaps_builtFromSnapshots() {
mockPublishedJob();
when(applicationRepository.existsByJobIdAndStudentUserIdAndStatusNotIn(eq(JOB), eq(ME), anySet())).thenReturn(true);
when(applicationRepository.findByJobIdAndStatusNotInOrderByCreatedAtAsc(eq(JOB), anyCollection()))
.thenReturn(List.of(app(ME, "2026-01-01T10:00:00")));
when(rankingCalculator.loadLatestSnapshots(anyList(), anyString())).thenReturn(Map.of());
when(rankingCalculator.rankCohort(anyList(), anyMap(), eq(ME)))
.thenReturn(cr(1, 1, new BigDecimal("85.00"), null, "OK", "2026-07",
List.of(dim(ME, "EDUCATION", "OK", "85.00"), dim(ME, "ABILITY", "PENDING", null))));
CompetitionRankingResponse resp = call();
assertEquals(2, resp.getDimensions().size());
assertFalse(resp.getGaps().isEmpty());
}
// ── 辅助 ────────────────────────────────────────────────
private CompetitionRankingResponse call() {
return service.getRanking(ME, JOB);
}
private void mockStudent(Long uid) {
User u = User.builder().id(uid).phone("1").role(User.Role.STUDENT).build();
u.setRoles(new HashSet<>(Set.of(User.Role.STUDENT)));
when(userRepository.findById(uid)).thenReturn(Optional.of(u));
}
private void mockRole(Long uid, User.Role role) {
User u = User.builder().id(uid).phone("1").role(role).build();
u.setRoles(new HashSet<>(Set.of(role)));
when(userRepository.findById(uid)).thenReturn(Optional.of(u));
}
private void mockPublishedJob() {
mockStudent(ME);
Job j = Job.builder().id(JOB).status("PUBLISHED").build();
when(jobService.getJob(JOB)).thenReturn(j);
}
private CompetitionRankingCalculator.CohortRanking cr(int cohortSize, Integer rank, BigDecimal overall,
BigDecimal percentile, String status,
String period, List<CandidateDimensionScore> snaps) {
List<CompetitionRankingCalculator.Row> rows = new ArrayList<>();
for (int i = 0; i < cohortSize; i++) {
BigDecimal s = (i < cohortSize) ? new BigDecimal("90.00") : null;
rows.add(new CompetitionRankingCalculator.Row(ME + i, null, s, period));
}
CompetitionRankingCalculator.Row me = new CompetitionRankingCalculator.Row(ME, null, overall, period);
return new CompetitionRankingCalculator.CohortRanking(cohortSize, rows, me, rank,
overall, percentile, status, period, snaps);
}
private Application app(Long sid, String created) {
return Application.builder().studentUserId(sid).status("SUBMITTED")
.createdAt(java.time.LocalDateTime.parse(created)).build();
}
private CandidateDimensionScore dim(Long sid, String dimension, String status, String score) {
return CandidateDimensionScore.builder().studentUserId(sid).dimension(dimension).period("2026-07")
.score(score == null ? null : new BigDecimal(score)).status(status)
.weight(new BigDecimal("12.50")).rulesVersion("candidate-scoring-v0.1").build();
}
}