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
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 登记");
|
|
}
|
|
}
|
|
|