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.
201 lines
9.1 KiB
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();
|
|
}
|
|
}
|