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.
442 lines
25 KiB
442 lines
25 KiB
package com.mydreamjob.careerprogress;
|
|
|
|
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.CandidateDimensionScoreRepository;
|
|
import com.mydreamjob.candidatescore.CandidateScoringCalculator;
|
|
import com.mydreamjob.careerprogress.dto.CareerProgressResponse;
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import com.mydreamjob.company.Company;
|
|
import com.mydreamjob.company.CompanyRepository;
|
|
import com.mydreamjob.competition.CompetitionRankingCalculator;
|
|
import com.mydreamjob.interview.Interview;
|
|
import com.mydreamjob.interview.InterviewRepository;
|
|
import com.mydreamjob.interviewscore.InterviewScore;
|
|
import com.mydreamjob.interviewscore.InterviewScoreRepository;
|
|
import com.mydreamjob.job.Job;
|
|
import com.mydreamjob.job.JobRepository;
|
|
import com.mydreamjob.notification.NotificationRepository;
|
|
import com.mydreamjob.offer.Offer;
|
|
import com.mydreamjob.offer.OfferRepository;
|
|
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 org.springframework.data.domain.Page;
|
|
import org.springframework.data.domain.PageImpl;
|
|
import org.springframework.data.domain.PageRequest;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDateTime;
|
|
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.*;
|
|
|
|
/**
|
|
* 统一求职进度聚合服务单测(V34,编排 + 脱敏 + 聚合口径 + 无 N+1)。
|
|
*
|
|
* <p>排名/排序/百分位由真实 {@link CompetitionRankingCalculator}(配合 mock 的
|
|
* 快照仓库与综合分计算器)承载,与 V32 共用同一组件,保证口径一致。本类聚焦断言
|
|
* 权限、参数、投递分页、todo 计数、nextInterview、评分摘要、Offer 优先级、
|
|
* competition 摘要、通知仅计数与批量查询(无 N+1)。</p>
|
|
*/
|
|
@ExtendWith(MockitoExtension.class)
|
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
|
class CareerProgressServiceTest {
|
|
|
|
private static final Long ME = 100L;
|
|
private static final Long JOB = 9L;
|
|
private static final Long COMPANY = 1L;
|
|
private static final String PERIOD = yearMonth(LocalDate.now());
|
|
|
|
@Mock private ApplicationRepository applicationRepository;
|
|
@Mock private InterviewRepository interviewRepository;
|
|
@Mock private InterviewScoreRepository interviewScoreRepository;
|
|
@Mock private OfferRepository offerRepository;
|
|
@Mock private NotificationRepository notificationRepository;
|
|
@Mock private JobRepository jobRepository;
|
|
@Mock private CompanyRepository companyRepository;
|
|
@Mock private UserRepository userRepository;
|
|
@Mock private CandidateDimensionScoreRepository dimensionScoreRepository;
|
|
@Mock private CandidateScoringCalculator scoringCalculator;
|
|
|
|
private CompetitionRankingCalculator calculator;
|
|
private CareerProgressService service;
|
|
|
|
private Map<Long, BigDecimal> overallByStudent = Map.of();
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
calculator = spy(new CompetitionRankingCalculator(dimensionScoreRepository, scoringCalculator));
|
|
service = new CareerProgressService(applicationRepository, interviewRepository,
|
|
interviewScoreRepository, offerRepository, notificationRepository,
|
|
jobRepository, companyRepository, userRepository, calculator, scoringCalculator);
|
|
|
|
when(scoringCalculator.overallScoreFromSnapshots(anyList())).thenAnswer(inv -> {
|
|
List<?> snaps = inv.getArgument(0);
|
|
if (snaps == null || snaps.isEmpty()) return null;
|
|
Long sid = ((CandidateDimensionScore) snaps.get(0)).getStudentUserId();
|
|
return overallByStudent.get(sid);
|
|
});
|
|
}
|
|
|
|
// ── 权限与参数 ────────────────────────────────────────────
|
|
|
|
@Test
|
|
void companyRole_throwsForbidden() {
|
|
mockUser(ME, User.Role.COMPANY);
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> service.getCareerProgress(ME, 0, 20, null));
|
|
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCode());
|
|
}
|
|
|
|
@Test
|
|
void adminRole_throwsForbidden() {
|
|
mockUser(ME, User.Role.ADMIN);
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> service.getCareerProgress(ME, 0, 20, null));
|
|
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCode());
|
|
}
|
|
|
|
@Test
|
|
void negativePage_throwsBadRequest() {
|
|
mockUser(ME, User.Role.STUDENT);
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> service.getCareerProgress(ME, -1, 20, null));
|
|
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCode());
|
|
}
|
|
|
|
@Test
|
|
void sizeOutOfRange_throwsBadRequest() {
|
|
mockUser(ME, User.Role.STUDENT);
|
|
assertThrows(BusinessException.class,
|
|
() -> service.getCareerProgress(ME, 0, 0, null));
|
|
assertThrows(BusinessException.class,
|
|
() -> service.getCareerProgress(ME, 0, 51, null));
|
|
}
|
|
|
|
@Test
|
|
void invalidStatus_throwsBadRequest() {
|
|
mockUser(ME, User.Role.STUDENT);
|
|
BusinessException ex = assertThrows(BusinessException.class,
|
|
() -> service.getCareerProgress(ME, 0, 20, "NONEXISTENT"));
|
|
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCode());
|
|
}
|
|
|
|
// ── 空数据 ──────────────────────────────────────────────
|
|
|
|
@Test
|
|
void noApplications_returnsEmptyPage_andZeroTodo() {
|
|
mockUser(ME, User.Role.STUDENT);
|
|
when(applicationRepository.findByStudentUserIdOrderByCreatedAtDesc(eq(ME), any(PageRequest.class)))
|
|
.thenReturn(new PageImpl<>(List.of(), PageRequest.of(0, 20), 0));
|
|
when(interviewRepository.findByStudentUserIdAndStatusInOrderByStartAtAsc(anyLong(), anyCollection()))
|
|
.thenReturn(List.of());
|
|
when(offerRepository.findByCandidateIdAndStatusNotOrderByCreatedAtDesc(anyLong(), anyString()))
|
|
.thenReturn(List.of());
|
|
when(notificationRepository.countUnreadByUserId(ME)).thenReturn(0L);
|
|
when(notificationRepository.countUnreadByUserIdAndType(ME, "REPORT_DECISION")).thenReturn(0L);
|
|
|
|
CareerProgressResponse resp = service.getCareerProgress(ME, 0, 20, null);
|
|
|
|
assertEquals(0L, resp.getApplications().getItems().size());
|
|
assertEquals(0L, resp.getApplications().getTotal());
|
|
assertEquals(0L, resp.getTodo().getPendingInterviewResponseCount());
|
|
assertEquals(0L, resp.getTodo().getUpcomingConfirmedInterviewCount());
|
|
assertEquals(0L, resp.getTodo().getPendingOfferResponseCount());
|
|
assertEquals(0L, resp.getTodo().getUnreadNotificationCount());
|
|
assertEquals(0L, resp.getTodo().getUnreadReportDecisionCount());
|
|
}
|
|
|
|
// ── 聚合口径(完整 happy path)────────────────────────────
|
|
|
|
@Test
|
|
void fullAggregation_returnsCorrectSummaries() {
|
|
mockUser(ME, User.Role.STUDENT);
|
|
Application app = app(1L, ME, JOB, "SUBMITTED", "2026-08-20T10:00:00");
|
|
when(applicationRepository.findByStudentUserIdOrderByCreatedAtDesc(eq(ME), any(PageRequest.class)))
|
|
.thenReturn(new PageImpl<>(List.of(app), PageRequest.of(0, 20), 1));
|
|
when(jobRepository.findAllById(anyCollection()))
|
|
.thenReturn(List.of(Job.builder().id(JOB).companyId(COMPANY).title("Java 后端工程师")
|
|
.description("d").city("杭州").employmentType("FULL_TIME").status("PUBLISHED").build()));
|
|
when(companyRepository.findAllById(anyCollection()))
|
|
.thenReturn(List.of(Company.builder().id(COMPANY).ownerUserId(999L).name("示例科技").build()));
|
|
|
|
// 面试:两场 SCHEDULED(取最早为 nextInterview)+ 一场 CONFIRMED(未来)
|
|
Interview later = interview(2L, app.getId(), "SCHEDULED", "2026-08-30T14:00:00");
|
|
Interview earlier = interview(1L, app.getId(), "SCHEDULED", "2026-08-28T10:00:00");
|
|
Interview confirmed = interview(3L, app.getId(), "CONFIRMED", LocalDateTime.now().plusDays(3).toString());
|
|
when(interviewRepository.findByStudentUserIdAndStatusInOrderByStartAtAsc(anyLong(), anyCollection()))
|
|
.thenReturn(List.of(earlier, later, confirmed));
|
|
|
|
// 已提交评分:两条,取 submittedAt 最新
|
|
InterviewScore s1 = score(22L, app.getId(), "2026-08-25T16:00:00", new BigDecimal("3.60"));
|
|
InterviewScore s2 = score(23L, app.getId(), "2026-08-24T16:00:00", new BigDecimal("3.00"));
|
|
when(interviewScoreRepository.findByStudentUserIdAndApplicationIdInAndStatus(
|
|
anyLong(), anyCollection(), anyString())).thenReturn(List.of(s1, s2));
|
|
|
|
// Offer:一条 SENT 有效 + 一条 DRAFT(不应返回 DRAFT)
|
|
Offer sent = offer(14L, app.getId(), "SENT", "2026-09-05T23:59:59");
|
|
when(offerRepository.findByCandidateIdAndStatusNotOrderByCreatedAtDesc(anyLong(), anyString()))
|
|
.thenReturn(List.of(sent));
|
|
|
|
// 竞争力:同岗位 cohort 含本人 + 另一有分者
|
|
Application other = app(2L, 200L, JOB, "SUBMITTED", "2026-08-19T10:00:00");
|
|
when(applicationRepository.findByJobIdInAndStatusNotInOrderByCreatedAtAsc(anyCollection(), anyCollection()))
|
|
.thenReturn(List.of(other, app));
|
|
overallByStudent = Map.of(ME, new BigDecimal("77.50"), 200L, new BigDecimal("90.00"));
|
|
when(dimensionScoreRepository.findUpToPeriod(anyCollection(), anyString()))
|
|
.thenReturn(List.of(dim(ME, "77.50"), dim(200L, "90.00")));
|
|
|
|
when(notificationRepository.countUnreadByUserId(ME)).thenReturn(5L);
|
|
when(notificationRepository.countUnreadByUserIdAndType(ME, "REPORT_DECISION")).thenReturn(2L);
|
|
|
|
CareerProgressResponse resp = service.getCareerProgress(ME, 0, 20, null);
|
|
|
|
// todo
|
|
assertEquals(2L, resp.getTodo().getPendingInterviewResponseCount());
|
|
assertEquals(1L, resp.getTodo().getUpcomingConfirmedInterviewCount());
|
|
assertEquals(1L, resp.getTodo().getPendingOfferResponseCount());
|
|
assertEquals(5L, resp.getTodo().getUnreadNotificationCount());
|
|
assertEquals(2L, resp.getTodo().getUnreadReportDecisionCount());
|
|
|
|
// item
|
|
assertEquals(1L, resp.getApplications().getItems().size());
|
|
CareerProgressResponse.CareerProgressItem item = resp.getApplications().getItems().get(0);
|
|
assertEquals(1L, item.getApplicationId());
|
|
assertEquals("SUBMITTED", item.getApplicationStatus());
|
|
assertEquals("Java 后端工程师", item.getJob().getTitle());
|
|
assertEquals("示例科技", item.getJob().getCompanyName());
|
|
assertEquals("杭州", item.getJob().getCity());
|
|
assertEquals("FULL_TIME", item.getJob().getEmploymentType());
|
|
assertEquals("PUBLISHED", item.getJob().getJobStatus());
|
|
|
|
// nextInterview:取 startAt 最早 1L
|
|
assertEquals(1L, item.getNextInterview().getInterviewId());
|
|
// latest score:22(submittedAt 最新)
|
|
assertEquals(22L, item.getLatestSubmittedInterviewScore().getInterviewId());
|
|
assertEquals(new BigDecimal("3.60"), item.getLatestSubmittedInterviewScore().getOverallScore());
|
|
assertEquals("HIRE", item.getLatestSubmittedInterviewScore().getRecommendation());
|
|
// offer
|
|
assertEquals(14L, item.getOffer().getOfferId());
|
|
assertEquals("SENT", item.getOffer().getStatus());
|
|
|
|
// competition:cohort=2(other 90.00、me 77.50),me 排名 2;
|
|
// percentile = 严格低于 me 的人数(0) / (有分人数-1=1) * 100 = 0.00
|
|
assertTrue(item.getCompetition().isEligible());
|
|
assertEquals(2, item.getCompetition().getCohortSize());
|
|
assertEquals(2, item.getCompetition().getRank());
|
|
assertEquals(new BigDecimal("77.50"), item.getCompetition().getOverallScore());
|
|
assertEquals(new BigDecimal("0.00"), item.getCompetition().getPercentile());
|
|
assertEquals(PERIOD, item.getCompetition().getMySnapshotPeriod());
|
|
}
|
|
|
|
// ── DRAFT 不泄露 ────────────────────────────────────────
|
|
|
|
@Test
|
|
void draftScore_notReturned_andDraftOffer_notReturned() {
|
|
mockUser(ME, User.Role.STUDENT);
|
|
Application app = app(1L, ME, JOB, "SUBMITTED", "2026-08-20T10:00:00");
|
|
when(applicationRepository.findByStudentUserIdOrderByCreatedAtDesc(eq(ME), any(PageRequest.class)))
|
|
.thenReturn(new PageImpl<>(List.of(app), PageRequest.of(0, 20), 1));
|
|
when(jobRepository.findAllById(anyCollection()))
|
|
.thenReturn(List.of(Job.builder().id(JOB).companyId(COMPANY).title("t").description("d")
|
|
.city("c").employmentType("FULL_TIME").status("PUBLISHED").build()));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of());
|
|
when(interviewRepository.findByStudentUserIdAndStatusInOrderByStartAtAsc(anyLong(), anyCollection()))
|
|
.thenReturn(List.of());
|
|
// 只有 DRAFT 评分 → 不应出现在 latestSubmittedInterviewScore
|
|
InterviewScore draft = score(1L, app.getId(), null, null);
|
|
draft.setStatus(InterviewScore.STATUS_DRAFT);
|
|
when(interviewScoreRepository.findByStudentUserIdAndApplicationIdInAndStatus(
|
|
anyLong(), anyCollection(), anyString())).thenReturn(List.of());
|
|
// 只有 DRAFT offer → offer 为 null
|
|
when(offerRepository.findByCandidateIdAndStatusNotOrderByCreatedAtDesc(anyLong(), anyString()))
|
|
.thenReturn(List.of());
|
|
when(applicationRepository.findByJobIdInAndStatusNotInOrderByCreatedAtAsc(anyCollection(), anyCollection()))
|
|
.thenReturn(List.of(app));
|
|
when(dimensionScoreRepository.findUpToPeriod(anyCollection(), anyString())).thenReturn(List.of());
|
|
when(notificationRepository.countUnreadByUserId(ME)).thenReturn(0L);
|
|
when(notificationRepository.countUnreadByUserIdAndType(ME, "REPORT_DECISION")).thenReturn(0L);
|
|
|
|
CareerProgressResponse resp = service.getCareerProgress(ME, 0, 20, null);
|
|
CareerProgressResponse.CareerProgressItem item = resp.getApplications().getItems().get(0);
|
|
assertNull(item.getLatestSubmittedInterviewScore());
|
|
assertNull(item.getOffer());
|
|
}
|
|
|
|
// ── WITHDRAWN/REJECTED 保留历史但 eligible=false ─────────
|
|
|
|
@Test
|
|
void withdrawnApplication_eligibleFalse_andNoCompetitionFields() {
|
|
mockUser(ME, User.Role.STUDENT);
|
|
Application withdrawn = app(1L, ME, JOB, "WITHDRAWN", "2026-08-20T10:00:00");
|
|
when(applicationRepository.findByStudentUserIdOrderByCreatedAtDesc(eq(ME), any(PageRequest.class)))
|
|
.thenReturn(new PageImpl<>(List.of(withdrawn), PageRequest.of(0, 20), 1));
|
|
when(jobRepository.findAllById(anyCollection()))
|
|
.thenReturn(List.of(Job.builder().id(JOB).companyId(COMPANY).title("t").description("d")
|
|
.city("c").employmentType("FULL_TIME").status("PUBLISHED").build()));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of());
|
|
when(interviewRepository.findByStudentUserIdAndStatusInOrderByStartAtAsc(anyLong(), anyCollection()))
|
|
.thenReturn(List.of());
|
|
when(interviewScoreRepository.findByStudentUserIdAndApplicationIdInAndStatus(
|
|
anyLong(), anyCollection(), anyString())).thenReturn(List.of());
|
|
when(offerRepository.findByCandidateIdAndStatusNotOrderByCreatedAtDesc(anyLong(), anyString()))
|
|
.thenReturn(List.of());
|
|
when(notificationRepository.countUnreadByUserId(ME)).thenReturn(0L);
|
|
when(notificationRepository.countUnreadByUserIdAndType(ME, "REPORT_DECISION")).thenReturn(0L);
|
|
|
|
CareerProgressResponse resp = service.getCareerProgress(ME, 0, 20, null);
|
|
CareerProgressResponse.CareerProgressItem item = resp.getApplications().getItems().get(0);
|
|
// 保留历史
|
|
assertEquals(1L, item.getApplicationId());
|
|
assertEquals("WITHDRAWN", item.getApplicationStatus());
|
|
// competition 只返回 eligible=false,不返回其它排名字段
|
|
assertFalse(item.getCompetition().isEligible());
|
|
assertNull(item.getCompetition().getRank());
|
|
assertNull(item.getCompetition().getCohortSize());
|
|
assertNull(item.getCompetition().getOverallScore());
|
|
assertNull(item.getCompetition().getPercentile());
|
|
assertNull(item.getCompetition().getStatus());
|
|
assertNull(item.getCompetition().getMySnapshotPeriod());
|
|
assertNull(item.getCompetition().getRulesVersion());
|
|
}
|
|
|
|
// ── 有有效投递但无快照 → competition 分数/排名/百分位为 null ─
|
|
|
|
@Test
|
|
void validApplication_noSnapshot_competitionScoresNull_cohortSizeReturned() {
|
|
mockUser(ME, User.Role.STUDENT);
|
|
Application app = app(1L, ME, JOB, "SUBMITTED", "2026-08-20T10:00:00");
|
|
when(applicationRepository.findByStudentUserIdOrderByCreatedAtDesc(eq(ME), any(PageRequest.class)))
|
|
.thenReturn(new PageImpl<>(List.of(app), PageRequest.of(0, 20), 1));
|
|
when(jobRepository.findAllById(anyCollection()))
|
|
.thenReturn(List.of(Job.builder().id(JOB).companyId(COMPANY).title("t").description("d")
|
|
.city("c").employmentType("FULL_TIME").status("PUBLISHED").build()));
|
|
when(companyRepository.findAllById(anyCollection())).thenReturn(List.of());
|
|
when(interviewRepository.findByStudentUserIdAndStatusInOrderByStartAtAsc(anyLong(), anyCollection()))
|
|
.thenReturn(List.of());
|
|
when(interviewScoreRepository.findByStudentUserIdAndApplicationIdInAndStatus(
|
|
anyLong(), anyCollection(), anyString())).thenReturn(List.of());
|
|
when(offerRepository.findByCandidateIdAndStatusNotOrderByCreatedAtDesc(anyLong(), anyString()))
|
|
.thenReturn(List.of());
|
|
when(applicationRepository.findByJobIdInAndStatusNotInOrderByCreatedAtAsc(anyCollection(), anyCollection()))
|
|
.thenReturn(List.of(app));
|
|
when(dimensionScoreRepository.findUpToPeriod(anyCollection(), anyString())).thenReturn(List.of());
|
|
when(notificationRepository.countUnreadByUserId(ME)).thenReturn(0L);
|
|
when(notificationRepository.countUnreadByUserIdAndType(ME, "REPORT_DECISION")).thenReturn(0L);
|
|
|
|
CareerProgressResponse resp = service.getCareerProgress(ME, 0, 20, null);
|
|
CareerProgressResponse.CareerProgressItem item = resp.getApplications().getItems().get(0);
|
|
assertTrue(item.getCompetition().isEligible());
|
|
assertNull(item.getCompetition().getRank());
|
|
assertNull(item.getCompetition().getOverallScore());
|
|
assertNull(item.getCompetition().getPercentile());
|
|
assertNull(item.getCompetition().getMySnapshotPeriod());
|
|
assertEquals(1, item.getCompetition().getCohortSize());
|
|
}
|
|
|
|
// ── 无 N+1:所有批量查询各执行一次 ────────────────────────
|
|
|
|
@Test
|
|
void multipleApplications_noNPlusOne_batchQueriesSingleCallEach() {
|
|
mockUser(ME, User.Role.STUDENT);
|
|
Application a1 = app(1L, ME, 9L, "SUBMITTED", "2026-08-20T10:00:00");
|
|
Application a2 = app(2L, ME, 10L, "INTERVIEW", "2026-08-19T10:00:00");
|
|
Application a3 = app(3L, ME, 11L, "VIEWED", "2026-08-18T10:00:00");
|
|
when(applicationRepository.findByStudentUserIdOrderByCreatedAtDesc(eq(ME), any(PageRequest.class)))
|
|
.thenReturn(new PageImpl<>(List.of(a1, a2, a3), PageRequest.of(0, 20), 3));
|
|
when(jobRepository.findAllById(anyCollection())).thenReturn(List.of(
|
|
Job.builder().id(9L).companyId(COMPANY).title("t1").description("d").city("c")
|
|
.employmentType("FULL_TIME").status("PUBLISHED").build(),
|
|
Job.builder().id(10L).companyId(COMPANY).title("t2").description("d").city("c")
|
|
.employmentType("FULL_TIME").status("PUBLISHED").build(),
|
|
Job.builder().id(11L).companyId(COMPANY).title("t3").description("d").city("c")
|
|
.employmentType("FULL_TIME").status("PUBLISHED").build()));
|
|
when(companyRepository.findAllById(anyCollection()))
|
|
.thenReturn(List.of(Company.builder().id(COMPANY).ownerUserId(9L).name("c").build()));
|
|
when(interviewRepository.findByStudentUserIdAndStatusInOrderByStartAtAsc(anyLong(), anyCollection()))
|
|
.thenReturn(List.of());
|
|
when(interviewScoreRepository.findByStudentUserIdAndApplicationIdInAndStatus(
|
|
anyLong(), anyCollection(), anyString())).thenReturn(List.of());
|
|
when(offerRepository.findByCandidateIdAndStatusNotOrderByCreatedAtDesc(anyLong(), anyString()))
|
|
.thenReturn(List.of());
|
|
when(applicationRepository.findByJobIdInAndStatusNotInOrderByCreatedAtAsc(anyCollection(), anyCollection()))
|
|
.thenReturn(List.of(a1, a2, a3));
|
|
when(dimensionScoreRepository.findUpToPeriod(anyCollection(), anyString())).thenReturn(List.of());
|
|
when(notificationRepository.countUnreadByUserId(ME)).thenReturn(0L);
|
|
when(notificationRepository.countUnreadByUserIdAndType(ME, "REPORT_DECISION")).thenReturn(0L);
|
|
|
|
service.getCareerProgress(ME, 0, 20, null);
|
|
|
|
verify(jobRepository, times(1)).findAllById(anyCollection());
|
|
verify(companyRepository, times(1)).findAllById(anyCollection());
|
|
verify(interviewRepository, times(1)).findByStudentUserIdAndStatusInOrderByStartAtAsc(anyLong(), anyCollection());
|
|
verify(interviewScoreRepository, times(1)).findByStudentUserIdAndApplicationIdInAndStatus(anyLong(), anyCollection(), anyString());
|
|
verify(offerRepository, times(1)).findByCandidateIdAndStatusNotOrderByCreatedAtDesc(anyLong(), anyString());
|
|
verify(applicationRepository, times(1)).findByJobIdInAndStatusNotInOrderByCreatedAtAsc(anyCollection(), anyCollection());
|
|
verify(dimensionScoreRepository, times(1)).findUpToPeriod(anyCollection(), anyString());
|
|
verify(notificationRepository, times(1)).countUnreadByUserId(ME);
|
|
verify(notificationRepository, times(1)).countUnreadByUserIdAndType(ME, "REPORT_DECISION");
|
|
// 未按投递/面试/Offer 循环查询
|
|
verify(interviewRepository, never()).findByApplicationIdOrderByCreatedAtDesc(anyLong());
|
|
}
|
|
|
|
// ── 辅助 ────────────────────────────────────────────────
|
|
|
|
private void mockUser(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 Application app(Long id, Long sid, Long jobId, String status, String created) {
|
|
return Application.builder().id(id).jobId(jobId).studentUserId(sid).status(status)
|
|
.createdAt(LocalDateTime.parse(created)).updatedAt(LocalDateTime.parse(created)).build();
|
|
}
|
|
|
|
private Interview interview(Long id, Long appId, String status, String startAt) {
|
|
return Interview.builder().id(id).applicationId(appId).companyUserId(999L).studentUserId(ME)
|
|
.roundNo(1).title("技术面试").interviewType("ONLINE")
|
|
.startAt(LocalDateTime.parse(startAt)).endAt(LocalDateTime.parse(startAt).plusHours(1))
|
|
.status(status).build();
|
|
}
|
|
|
|
private InterviewScore score(Long interviewId, Long appId, String submittedAt, BigDecimal overall) {
|
|
LocalDateTime submitted = submittedAt == null ? null : LocalDateTime.parse(submittedAt);
|
|
return InterviewScore.builder().id(interviewId).interviewId(interviewId).applicationId(appId)
|
|
.companyUserId(999L).studentUserId(ME).status(InterviewScore.STATUS_SUBMITTED)
|
|
.overallScore(overall).recommendation("HIRE").submittedAt(submitted).version(0L).build();
|
|
}
|
|
|
|
private Offer offer(Long id, Long appId, String status, String expireAt) {
|
|
return Offer.builder().id(id).applicationId(appId).jobId(JOB).companyId(COMPANY).candidateId(ME)
|
|
.title("Java 后端工程师").description("d").expireAt(LocalDateTime.parse(expireAt))
|
|
.createdBy(999L).status(status).version(0L)
|
|
.createdAt(LocalDateTime.of(2026, 8, 25, 12, 0)).build();
|
|
}
|
|
|
|
private CandidateDimensionScore dim(Long sid, String score) {
|
|
return CandidateDimensionScore.builder().studentUserId(sid).dimension("EDUCATION").period(PERIOD)
|
|
.score(new BigDecimal(score)).status("OK").weight(new BigDecimal("12.50"))
|
|
.rulesVersion("candidate-scoring-v0.1").build();
|
|
}
|
|
|
|
private static String yearMonth(LocalDate d) {
|
|
return String.format("%04d-%02d", d.getYear(), d.getMonthValue());
|
|
}
|
|
}
|