package com.mydreamjob.competition; import com.mydreamjob.application.Application; import com.mydreamjob.candidatescore.CandidateDimensionScore; import com.mydreamjob.candidatescore.CandidateDimensionScoreRepository; import com.mydreamjob.candidatescore.CandidateScoringCalculator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; 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.time.LocalDateTime; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; /** * 共享排名计算器单测(V32/V34 复用核心)。 * *

覆盖稳定排序(有分优先 → overallScore DESC → createdAt ASC → studentUserId ASC → * 无分最后)、cohort、百分位、最新快照周期选择与 overviewStatus。综合分走真实 * CandidateScoringCalculator 语义(此处以 mock 模拟按 studentUserId 返回预设分)。

*/ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class CompetitionRankingCalculatorTest { private static final Long ME = 100L; @Mock private CandidateDimensionScoreRepository scoreRepository; @Mock private CandidateScoringCalculator scoringCalculator; private CompetitionRankingCalculator calculator; private Map overallByStudent; @BeforeEach void setUp() { calculator = new CompetitionRankingCalculator(scoreRepository, scoringCalculator); overallByStudent = Map.of(); when(scoringCalculator.overallScoreFromSnapshots(anyList())).thenAnswer(inv -> { List snaps = inv.getArgument(0); if (snaps == null || snaps.isEmpty()) return null; Number sid = ((CandidateDimensionScore) snaps.get(0)).getStudentUserId(); return overallByStudent.get(sid.longValue()); }); } @Test void rankCohort_sortsScoredFirstThenScoreDescThenCreatedAtThenUserId() { Application a = app(501L, "2026-01-01T10:00:00"); Application b = app(502L, "2026-01-02T10:00:00"); Application c = app(503L, "2026-01-03T10:00:00"); // 无分 Application me = app(ME, "2026-01-05T10:00:00"); Map> latest = Map.of( a.getStudentUserId(), List.of(dim(a.getStudentUserId(), "2026-07")), b.getStudentUserId(), List.of(dim(b.getStudentUserId(), "2026-07")), ME, List.of(dim(ME, "2026-07"))); overallByStudent = Map.of(a.getStudentUserId(), new BigDecimal("90.00"), b.getStudentUserId(), new BigDecimal("80.00"), ME, new BigDecimal("85.00")); CompetitionRankingCalculator.CohortRanking cr = calculator.rankCohort(List.of(me, a, b, c), latest, ME); assertEquals(4, cr.cohortSize); assertEquals(2, cr.rank.intValue()); assertEquals(new BigDecimal("85.00"), cr.overallScore); assertEquals(new BigDecimal("50.00"), cr.percentile); // lower=1, denom=2 // 排序:A(90) -> me(85) -> B(80) -> C(null) assertEquals(501L, cr.rows.get(0).studentUserId.longValue()); assertEquals(ME.longValue(), cr.rows.get(1).studentUserId.longValue()); assertEquals(502L, cr.rows.get(2).studentUserId.longValue()); assertEquals(503L, cr.rows.get(3).studentUserId.longValue()); assertNull(cr.rows.get(3).overallScore); assertEquals("OK", cr.status); assertEquals("2026-07", cr.mySnapshotPeriod); } @Test void rankCohort_tieBrokenByCreatedAtThenStudentUserId() { Application x = app(900L, "2026-04-01T10:00:00"); Application me = app(ME, "2026-04-02T10:00:00"); Map> latest = Map.of( x.getStudentUserId(), List.of(dim(x.getStudentUserId(), "2026-07")), ME, List.of(dim(ME, "2026-07"))); overallByStudent = Map.of(x.getStudentUserId(), new BigDecimal("60.00"), ME, new BigDecimal("60.00")); CompetitionRankingCalculator.CohortRanking cr = calculator.rankCohort(List.of(x, me), latest, ME); assertEquals(2, cr.rank.intValue()); // x first, me second assertEquals(900L, cr.rows.get(0).studentUserId.longValue()); } @Test void rankCohort_currentNoScore_rankScorePercentileNull() { Application a = app(501L, "2026-01-01T10:00:00"); Application me = app(ME, "2026-01-02T10:00:00"); Map> latest = Map.of( a.getStudentUserId(), List.of(dim(a.getStudentUserId(), "2026-07"))); overallByStudent = Map.of(a.getStudentUserId(), new BigDecimal("90.00")); CompetitionRankingCalculator.CohortRanking cr = calculator.rankCohort(List.of(a, me), latest, ME); assertEquals(2, cr.cohortSize); assertNull(cr.rank); assertNull(cr.overallScore); assertNull(cr.percentile); assertNull(cr.mySnapshotPeriod); assertEquals("INSUFFICIENT_DATA", cr.status); } @Test void rankCohort_scoredLessThan2_percentileNull() { Application a = app(501L, "2026-01-01T10:00:00"); Application me = app(ME, "2026-01-02T10:00:00"); Map> latest = Map.of(ME, List.of(dim(ME, "2026-07"))); overallByStudent = Map.of(ME, new BigDecimal("85.00")); CompetitionRankingCalculator.CohortRanking cr = calculator.rankCohort(List.of(a, me), latest, ME); assertNull(cr.percentile); } @Test void loadLatestSnapshots_picksLatestPeriodPerStudent() { when(scoreRepository.findUpToPeriod(anyCollection(), anyString())) .thenReturn(List.of( dim(ME, "2026-06"), dim(ME, "2026-07"), dim(501L, "2026-05"))); Map> latest = calculator.loadLatestSnapshots(List.of(ME, 501L), "2026-07"); assertEquals(1, latest.get(ME).size()); assertEquals("2026-07", latest.get(ME).get(0).getPeriod()); assertEquals("2026-05", latest.get(501L).get(0).getPeriod()); } @Test void overviewStatus_emptyIsInsufficientData() { assertEquals("INSUFFICIENT_DATA", calculator.overviewStatus(List.of())); } private Application app(Long sid, String created) { return Application.builder().studentUserId(sid) .status("SUBMITTED") .createdAt(LocalDateTime.parse(created)).build(); } private CandidateDimensionScore dim(Long sid, String period) { return CandidateDimensionScore.builder().studentUserId(sid).dimension("EDUCATION").period(period) .score(new BigDecimal("80")).status("OK") .weight(new BigDecimal("12.50")).rulesVersion("candidate-scoring-v0.1").build(); } }