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.
 
 
 

200 lines
8.2 KiB

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:候选人反馈的创建/修改/删除都必须登记评分刷新任务。
*
* <p>缺陷背景:{@code CandidateScoringCalculator#queryFeedback} 以
* {@code COUNT(*) / AVG(rating)} 参与 INTEGRITY 维度,但只有 createFeedback 登记了刷新,
* updateFeedback(改 rating 会改变 AVG)与 deleteFeedback(改变 COUNT 与 AVG)此前不登记。</p>
*/
@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<String, Object> 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<String, Object> 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 登记");
}
}