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.
146 lines
6.8 KiB
146 lines
6.8 KiB
package com.mydreamjob.common.r01b;
|
|
|
|
import com.mydreamjob.auth.support.TestAuthTokens;
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import com.mydreamjob.risk.RiskGovernanceService;
|
|
import com.mydreamjob.security.UserRoleGuard;
|
|
import org.junit.jupiter.api.AfterEach;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
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.test.context.ActiveProfiles;
|
|
import org.springframework.test.web.servlet.MockMvc;
|
|
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.anyLong;
|
|
import static org.mockito.Mockito.doThrow;
|
|
import static org.mockito.Mockito.never;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
|
/**
|
|
* R01-B 独立复验:**接线变异检测**。
|
|
*
|
|
* <p>用 {@code @MockBean} 替换公共 guard,使"角色决策"完全由测试掌控。
|
|
* 由于端点自身的角色判定已被收束到该 guard,两个方向都可被观测:</p>
|
|
* <ul>
|
|
* <li>guard 拒绝 → 端点 403 且**下游业务方法零调用**;</li>
|
|
* <li>guard 放行 → 即使调用方是 STUDENT,ADMIN 端点也返回 200。</li>
|
|
* </ul>
|
|
* <p>第二条是关键的变异证明:若端点仍保留自有的角色比较(未委托 guard),
|
|
* STUDENT 无论 guard 怎么桩都不可能通过 ADMIN 端点,本测试必然失败。</p>
|
|
*/
|
|
@SpringBootTest
|
|
@AutoConfigureMockMvc
|
|
@ActiveProfiles("integration-test")
|
|
class R01BWiringMutationTest {
|
|
|
|
private static final long STUDENT_ID = 9_340_001L;
|
|
private static final long ADMIN_ID = 9_340_002L;
|
|
|
|
private static final String ADMIN_ENDPOINT = "/admin/scoring/risk/overview";
|
|
private static final String COMPANY_CANDIDATE_ENDPOINT = "/company/candidates/999999999";
|
|
|
|
@Autowired private MockMvc mockMvc;
|
|
@Autowired private JdbcTemplate jdbcTemplate;
|
|
|
|
@MockBean private UserRoleGuard userRoleGuard;
|
|
@MockBean private RiskGovernanceService riskGovernanceService;
|
|
@MockBean private RedisTemplate<String, Object> redisTemplate;
|
|
|
|
@BeforeEach
|
|
void provision() {
|
|
cleanup();
|
|
insertUser(STUDENT_ID, "r01bwm-student", "STUDENT");
|
|
insertUser(ADMIN_ID, "r01bwm-admin", "ADMIN");
|
|
}
|
|
|
|
@AfterEach
|
|
void cleanup() {
|
|
for (long id : new long[]{STUDENT_ID, ADMIN_ID}) {
|
|
jdbcTemplate.update("DELETE FROM auth_tokens WHERE user_id = ?", id);
|
|
jdbcTemplate.update("DELETE FROM user_roles WHERE user_id = ?", id);
|
|
jdbcTemplate.update("DELETE FROM users WHERE id = ?", id);
|
|
}
|
|
}
|
|
|
|
private void insertUser(long id, String phone, String role) {
|
|
jdbcTemplate.update("INSERT INTO users (id, phone, nickname, role, is_active, is_verified) "
|
|
+ "VALUES (?, ?, ?, ?, 1, 1)", id, phone, "r01bwm-" + role, role);
|
|
jdbcTemplate.update("INSERT INTO user_roles (user_id, role) VALUES (?, ?)", id, role);
|
|
}
|
|
|
|
// ── guard 拒绝 → 业务不继续 ───────────────────────────────
|
|
|
|
@Test
|
|
@DisplayName("变异检测:guard 抛 FORBIDDEN 时端点 403,且下游业务方法零调用")
|
|
void guardDenyStopsBusinessFlow() throws Exception {
|
|
doThrow(new BusinessException(ErrorCode.FORBIDDEN))
|
|
.when(userRoleGuard).requireAdmin(anyLong());
|
|
|
|
mockMvc.perform(get(ADMIN_ENDPOINT).header("Authorization", TestAuthTokens.bearer(ADMIN_ID, "ADMIN")))
|
|
.andExpect(status().isForbidden());
|
|
|
|
verify(riskGovernanceService, never()).overview(any(), any());
|
|
verify(userRoleGuard).requireAdmin(ADMIN_ID);
|
|
}
|
|
|
|
// ── guard 放行 → 业务继续;且证明端点不自带角色比较 ────────
|
|
|
|
@Test
|
|
@DisplayName("变异检测:guard 放行时业务继续;STUDENT 亦可通过 ADMIN 端点(证明判定只来自 guard)")
|
|
void guardAllowLetsFlowContinueEvenForStudent() throws Exception {
|
|
// guard 为 mock,void 方法默认放行
|
|
mockMvc.perform(get(ADMIN_ENDPOINT).header("Authorization", TestAuthTokens.bearer(STUDENT_ID, "STUDENT")))
|
|
.andExpect(status().isOk());
|
|
|
|
verify(riskGovernanceService).overview(any(), any());
|
|
// 端点把**真实调用方身份**交给 guard;若端点自带角色比较,STUDENT 不可能走到这里
|
|
verify(userRoleGuard).requireAdmin(STUDENT_ID);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("变异检测:guard 收到的 userId 与 Token 主体一致(无身份替换)")
|
|
void guardReceivesCallerIdentity() throws Exception {
|
|
mockMvc.perform(get(ADMIN_ENDPOINT).header("Authorization", TestAuthTokens.bearer(ADMIN_ID, "ADMIN")))
|
|
.andExpect(status().isOk());
|
|
verify(userRoleGuard).requireAdmin(ADMIN_ID);
|
|
verify(userRoleGuard, never()).requireAdmin(STUDENT_ID);
|
|
}
|
|
|
|
// ── 资源归属仍由业务层负责,且错误码未被角色层吞掉 ─────────
|
|
|
|
@Test
|
|
@DisplayName("变异检测:guard 放行后,归属/存在性失败仍由业务层返回原错误码(404 而非 403)")
|
|
void ownershipFailureStillComesFromBusinessLayer() throws Exception {
|
|
// 该 STUDENT 用户没有企业实体;guard(mock)放行后由业务层给出 404/COMPANY_NOT_FOUND 语义
|
|
mockMvc.perform(get(COMPANY_CANDIDATE_ENDPOINT)
|
|
.header("Authorization", TestAuthTokens.bearer(STUDENT_ID, "STUDENT")))
|
|
.andExpect(status().isNotFound());
|
|
|
|
// 该端点路径上 Company guard 会被调用多次(Controller 直调 + 下游 Service 委托),
|
|
// 故只断言"至少一次且身份正确",不臆造次数。
|
|
verify(userRoleGuard, org.mockito.Mockito.atLeastOnce()).requireCompany(STUDENT_ID);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("变异检测:guard 在 COMPANY 端点上同样按调用方身份被调用")
|
|
void companyEndpointAlsoDelegatesToGuard() throws Exception {
|
|
doThrow(new BusinessException(ErrorCode.FORBIDDEN))
|
|
.when(userRoleGuard).requireCompany(anyLong());
|
|
|
|
mockMvc.perform(get(COMPANY_CANDIDATE_ENDPOINT)
|
|
.header("Authorization", TestAuthTokens.bearer(ADMIN_ID, "ADMIN")))
|
|
.andExpect(status().isForbidden());
|
|
|
|
verify(userRoleGuard).requireCompany(ADMIN_ID);
|
|
}
|
|
}
|
|
|