package com.mydreamjob.feedback; import com.mydreamjob.application.Application; import com.mydreamjob.application.ApplicationRepository; import com.mydreamjob.auth.User; import com.mydreamjob.auth.UserRepository; import com.mydreamjob.company.Company; import com.mydreamjob.company.CompanyRepository; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.feedback.dto.CandidateFeedbackRequest; import com.mydreamjob.job.Job; import com.mydreamjob.job.JobRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * R01-D4-2 阻断缺陷 F2:候选人反馈的创建/修改/删除都必须登记评分刷新任务。 * *

缺陷背景:{@code CandidateScoringCalculator#queryFeedback} 以 * {@code COUNT(*) / AVG(rating)} 参与 INTEGRITY 维度,但只有 createFeedback 登记了刷新, * updateFeedback(改 rating 会改变 AVG)与 deleteFeedback(改变 COUNT 与 AVG)此前不登记。

*/ @SpringBootTest @ActiveProfiles("integration-test") class CandidateFeedbackRefreshRegistrationTest { @Autowired private CandidateFeedbackService service; @Autowired private CandidateFeedbackRepository feedbackRepository; @Autowired private ApplicationRepository applicationRepository; @Autowired private JobRepository jobRepository; @Autowired private CompanyRepository companyRepository; @Autowired private UserRepository userRepository; @Autowired private JdbcTemplate jdbcTemplate; @MockBean private RedisTemplate redisTemplate; private static final AtomicLong SEQ = new AtomicLong(5000); private Long companyUserId; private Long studentId; private Long applicationId; private long taskCount(Long studentUserId) { Long c = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM candidate_scoring_refresh_tasks WHERE student_user_id=?", Long.class, studentUserId); return c == null ? -1 : c; } private CandidateFeedbackRequest req(int rating, String content) { CandidateFeedbackRequest r = new CandidateFeedbackRequest(); r.setRating(rating); r.setContent(content); r.setTags(List.of("t")); return r; } @BeforeEach @SuppressWarnings("unchecked") void setUp() { ValueOperations ops = mock(ValueOperations.class); when(redisTemplate.opsForValue()).thenReturn(ops); when(redisTemplate.hasKey(any())).thenReturn(false); jdbcTemplate.update("DELETE FROM candidate_scoring_refresh_tasks"); feedbackRepository.deleteAll(); applicationRepository.deleteAll(); jobRepository.deleteAll(); companyRepository.deleteAll(); User student = userRepository.save(User.builder() .phone("196" + String.format("%08d", SEQ.incrementAndGet())) .nickname("F2学生").role(User.Role.STUDENT).active(true).verified(true).build()); student.setRoles(java.util.Set.of(User.Role.STUDENT)); studentId = student.getId(); User companyUser = userRepository.save(User.builder() .phone("195" + String.format("%08d", SEQ.incrementAndGet())) .nickname("F2企业").role(User.Role.COMPANY).active(true).verified(true).build()); companyUser.setRoles(java.util.Set.of(User.Role.COMPANY)); companyUserId = companyUser.getId(); Company company = companyRepository.save(Company.builder() .ownerUserId(companyUserId).name("F2公司").verified(true).build()); Job job = jobRepository.save(Job.builder() .companyId(company.getId()).title("F2岗位").description("d").city("上海") .employmentType("FULL_TIME").status("PUBLISHED").build()); Application app = applicationRepository.save(Application.builder() .jobId(job.getId()).studentUserId(studentId).status("SUBMITTED").build()); applicationId = app.getId(); } private void clearTasks() { jdbcTemplate.update("DELETE FROM candidate_scoring_refresh_tasks"); } /** 清理本类产生的全部行,避免污染同一 JVM 内共享的 H2 内存库。 */ @org.junit.jupiter.api.AfterEach void tearDown() { jdbcTemplate.update("DELETE FROM candidate_scoring_refresh_tasks"); jdbcTemplate.update("DELETE FROM candidate_feedback"); jdbcTemplate.update("DELETE FROM offers"); jdbcTemplate.update("DELETE FROM application_status_history"); jdbcTemplate.update("DELETE FROM applications"); jdbcTemplate.update("DELETE FROM jobs"); jdbcTemplate.update("DELETE FROM companies"); } @Test void createFeedback_registersRefreshTask() { clearTasks(); service.createFeedback(companyUserId, applicationId, req(5, "优秀")); assertEquals(1L, taskCount(studentId), "createFeedback 必须登记刷新任务"); } @Test void updateFeedback_registersRefreshTask() { service.createFeedback(companyUserId, applicationId, req(5, "优秀")); clearTasks(); // 只观察 update 自身是否登记 service.updateFeedback(companyUserId, applicationId, req(2, "较差")); assertEquals(1L, taskCount(studentId), "updateFeedback 改变 rating 会影响 AVG(rating),必须登记刷新任务"); } @Test void deleteFeedback_registersRefreshTask_afterRowRemoved() { service.createFeedback(companyUserId, applicationId, req(5, "优秀")); clearTasks(); service.deleteFeedback(companyUserId, applicationId); assertEquals(0L, feedbackRepository.count(), "反馈行应已删除"); assertEquals(1L, taskCount(studentId), "deleteFeedback 改变 COUNT(*) 与 AVG(rating),必须登记刷新任务"); } @Test void updateFeedback_ratingUnchanged_stillRegistersButConvergesToOneTask() { service.createFeedback(companyUserId, applicationId, req(4, "一般")); clearTasks(); service.updateFeedback(companyUserId, applicationId, req(4, "一般-改文案")); service.updateFeedback(companyUserId, applicationId, req(4, "一般-再改")); assertEquals(1L, taskCount(studentId), "重复登记必须收敛为同一学生同一周期的一条任务"); } @Test void updateFeedback_forbiddenCaller_leavesNoOrphanTask() { service.createFeedback(companyUserId, applicationId, req(5, "优秀")); clearTasks(); assertThrows(BusinessException.class, () -> service.updateFeedback(999999L, applicationId, req(1, "越权"))); assertEquals(0L, taskCount(studentId), "失败路径不得留下孤立刷新任务"); } @Test void deleteFeedback_missingFeedback_leavesNoOrphanTask() { clearTasks(); assertThrows(BusinessException.class, () -> service.deleteFeedback(companyUserId, applicationId)); assertEquals(0L, taskCount(studentId), "失败路径不得留下孤立刷新任务"); } @Test void feedbackLifecycle_createUpdateDelete_allRegisteredAndSnapshotRefreshable() { clearTasks(); service.createFeedback(companyUserId, applicationId, req(5, "优秀")); assertEquals(1L, taskCount(studentId), "create 登记"); clearTasks(); service.updateFeedback(companyUserId, applicationId, req(1, "很差")); assertEquals(1L, taskCount(studentId), "update 登记"); clearTasks(); service.deleteFeedback(companyUserId, applicationId); assertEquals(1L, taskCount(studentId), "delete 登记"); } }