package com.mydreamjob.common.r01a5; import com.mydreamjob.candidatescore.governance.CandidateScoringGovernanceService; import com.mydreamjob.interviewscore.InterviewScoreService; import com.mydreamjob.job.JobFavoriteService; import com.mydreamjob.review.AdminReviewReplyService; import com.mydreamjob.review.CompanyReviewManagementService; import com.mydreamjob.review.JobReviewService; import com.mydreamjob.review.StudentReviewWeightService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 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.jdbc.core.JdbcTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** * R01-A5 独立复验:**失败请求的业务副作用**。 * *

使用 H2 行级快照(显式列 + 稳定 ORDER BY,逐字段比较,非 COUNT/SUM), * 验证非法身份请求前后与本批端点相关的核心表完全不变。

* *

说明:本轮不启动 Docker,H2 独立复验;不声称真实 MySQL 结论。

*/ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("integration-test") class A5SideEffectSnapshotTest { @Autowired private MockMvc mockMvc; @Autowired private JdbcTemplate jdbcTemplate; @MockBean private CandidateScoringGovernanceService governanceService; @MockBean private InterviewScoreService interviewScoreService; @MockBean private JobFavoriteService favoriteService; @MockBean private JobReviewService reviewService; @MockBean private AdminReviewReplyService adminService; @MockBean private CompanyReviewManagementService managementService; @MockBean private StudentReviewWeightService weightService; @MockBean private RedisTemplate redisTemplate; /** (表, 主键列, 全列) —— 逐字段快照。 */ private static final List TABLES = List.of( new String[]{"JOB_FAVORITES", "ID", "ID, JOB_ID, STUDENT_USER_ID, CREATED_AT"}, new String[]{"JOB_REVIEWS", "ID", "ID, JOB_ID, STUDENT_USER_ID, RATING, CONTENT, STATUS, CREATED_AT, UPDATED_AT"}, new String[]{"COMPANY_REVIEW_REPLIES", "ID", "ID, REVIEW_ID, COMPANY_ID, CONTENT, MODERATION_STATUS, VERSION, CREATED_AT, UPDATED_AT"}, new String[]{"INTERVIEW_SCORES", "ID", "ID, INTERVIEW_ID, APPLICATION_ID, COMPANY_USER_ID, STUDENT_USER_ID, STATUS, CREATED_AT, UPDATED_AT"}, new String[]{"CANDIDATE_SCORING_APPEALS", "ID", "ID, CANDIDATE_USER_ID, DIMENSION, PERIOD, STATUS, REASON, CREATED_AT, UPDATED_AT"}, new String[]{"STUDENT_REVIEW_WEIGHT_PREFERENCES", "ID", "ID, STUDENT_USER_ID, SOURCE, TEMPLATE_CODE, VERSION, CREATED_AT, UPDATED_AT"}, new String[]{"NOTIFICATIONS", "ID", "ID, USER_ID, TYPE, TITLE, IS_READ, CREATED_AT"}, new String[]{"MESSAGES", "ID", "ID, CONVERSATION_ID, SENDER_USER_ID, CONTENT, CREATED_AT"}, new String[]{"AUDIT_LOGS", "ID", "ID, USER_ID, ACTION, ENTITY_TYPE, ENTITY_ID, CREATED_AT"}); private Map>> snapshot() { Map>> out = new LinkedHashMap<>(); for (String[] t : TABLES) { List> rows = jdbcTemplate.queryForList( "SELECT " + t[2] + " FROM " + t[0] + " ORDER BY " + t[2]); out.put(t[0], rows); } return out; } private static final List PATHS = List.of( "/profile/me/candidate-scoring/appeals", "/interviews/1/score", "/jobs/favorites", "/admin/reviews/pending", "/admin/review-replies/pending", "/companies/me/job-reviews", "/profile/me/review-weights"); private static final String DELETE_REVIEW = "/jobs/1/reviews/1"; @Test @DisplayName("独立复验:非法身份请求前后,九张核心表逐字段零变化") void illegalIdentityRequests_produceNoBusinessWrite() throws Exception { Map>> before = snapshot(); var bad = new UsernamePasswordAuthenticationToken("anonymousUser", "n/a", AuthorityUtils.NO_AUTHORITIES); for (String path : PATHS) { mockMvc.perform(get(path).with(authentication(bad))).andReturn(); mockMvc.perform(get(path).with(authentication( new UsernamePasswordAuthenticationToken("not-a-number", "n/a", AuthorityUtils.NO_AUTHORITIES)))).andReturn(); } mockMvc.perform(delete(DELETE_REVIEW).with(authentication(bad))).andReturn(); Map>> after = snapshot(); for (String[] t : TABLES) { assertThat(after.get(t[0])) .as("%s 必须逐字段零变化", t[0]) .isEqualTo(before.get(t[0])); } } @Test @DisplayName("独立复验:无 Token 请求前后同样零变化") void noTokenRequests_produceNoBusinessWrite() throws Exception { Map>> before = snapshot(); for (String path : PATHS) { mockMvc.perform(get(path)).andReturn(); } mockMvc.perform(delete(DELETE_REVIEW)).andReturn(); Map>> after = snapshot(); for (String[] t : TABLES) { assertThat(after.get(t[0])).as("%s", t[0]).isEqualTo(before.get(t[0])); } } }