package com.talentcard.server.business.admin.auth; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.List; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import tools.jackson.databind.json.JsonMapper; /** * 拦截器的「接口 → 权限码」映射。 * *

这层映射出过两次事故:一个接口服务多个页面时,推导出来的权限码在角色管理里根本勾不到, * 于是配好权限的员工账号也进不去模块;而权限 403 又被各控制器自带的兜底 advice 改写成 500, * 现场只看到「服务器内部错误」。这里不启动 Spring 上下文,直接盯住这两件事。 */ class AdminAuthInterceptorMappingTests { private final AdminAuthService service = mock(AdminAuthService.class); private final JdbcTemplate jdbc = mock(JdbcTemplate.class); private final List> requested = new ArrayList<>(); private AdminAuthInterceptor interceptor; @BeforeEach void setUp() { // 运行期注入的是 Jackson 3 的 ObjectMapper(tools.jackson),这里用同一套,别再用 Jackson 2 的。 interceptor = new AdminAuthInterceptor(service, jdbc, new JsonMapper()); when(service.authenticate(anyString())).thenReturn(1L); when(service.operatorName(anyLong())).thenReturn("测试员工"); when(service.hasAnyPermission(anyLong(), any())).thenAnswer(invocation -> { requested.add(invocation.getArgument(1)); return true; }); } @Test void serviceApplicationRequestsAreJudgedByTheirModulePage() { assertThat(pass(get("/api/admin/service-appointment-applications", "applicationType", "MEDICAL_RESERVATION"))) .containsExactly("admin.medical-reservations.view"); assertThat(pass(get("/api/admin/service-appointment-applications", "applicationType", "FUEL_SERVICE"))) .containsExactly("admin.fuel-services.view"); assertThat(pass(get("/api/admin/service-appointment-applications/export", "applicationType", "SPOUSE_EMPLOYMENT"))) .containsExactly("admin.spouse-employment.export"); assertThat(pass(patch("/api/admin/service-appointment-applications", "applicationType", "HOTEL_RESERVATION"))) .containsExactly("admin.hotel-reservations.audit"); } /** 详情页只带 id,得回表查类型,否则又会掉回没人能勾选的 appointment-management。 */ @Test void serviceApplicationDetailLooksUpItsTypeFromTheDatabase() { when(jdbc.queryForList(anyString(), eq(String.class), any())).thenReturn(List.of("MEDICAL_APPLICATION")); assertThat(pass(get("/api/admin/service-appointment-applications/42"))) .containsExactly("admin.medical-applications.view"); } @Test void unknownServiceApplicationTypeKeepsTheOldPermission() { assertThat(pass(get("/api/admin/service-appointment-applications"))) .containsExactly("admin.appointment-management.view"); } @Test void familyCardRequestsAcceptEitherOfTheTwoPagesThatUseThem() { assertThat(pass(get("/api/admin/dependent-card-applies", "management", "true"))) .containsExactly("admin.family-card-applications.view", "admin.family-cards.view"); assertThat(pass(patch("/api/admin/dependent-card-applies/15/unbind"))) .containsExactly("admin.family-card-applications.audit", "admin.family-cards.disable"); assertThat(pass(patch("/api/admin/dependent-card-applies/15/audit"))) .containsExactly("admin.family-card-applications.audit"); } /** 权限不足必须如实回 403,而不是被控制器兜底改写成 500「服务器内部错误」。 */ @Test void deniedPermissionIsWrittenAsFourZeroThree() throws Exception { when(service.hasAnyPermission(anyLong(), any())).thenReturn(false); MockHttpServletRequest request = get("/api/admin/service-appointment-applications", "applicationType", "MEDICAL_RESERVATION"); MockHttpServletResponse response = new MockHttpServletResponse(); assertThat(interceptor.preHandle(request, response, new Object())).isFalse(); assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getContentAsString()).contains("\"code\":403").contains("当前账号没有该操作权限"); } /** 会话失效要回 401,前端才会清会话并跳登录页;以前这批接口会把它写成 500。 */ @Test void expiredSessionIsWrittenAsFourZeroOne() throws Exception { when(service.authenticate(anyString())) .thenThrow(new BusinessException(ErrorCode.UNAUTHORIZED, "登录已失效,请重新登录")); MockHttpServletResponse response = new MockHttpServletResponse(); assertThat(interceptor.preHandle(get("/api/admin/dependent-card-applies/15"), response, new Object())).isFalse(); assertThat(response.getStatus()).isEqualTo(401); assertThat(response.getContentAsString()).contains("\"code\":401").contains("登录已失效"); } /** 不在这两个特殊前缀里的接口沿用原来的页面映射。 */ @Test void otherAdminRequestsKeepTheirPageMapping() { assertThat(pass(get("/api/admin/business/news"))).containsExactly("admin.news.view"); assertThat(pass(patch("/api/admin/parking-plate-submissions/9/audit"))) .containsExactly("admin.parking-plates.audit"); assertThat(pass(get("/api/admin/management/roles"))).containsExactly("admin.roles.view"); } private List pass(MockHttpServletRequest request) { requested.clear(); try { interceptor.preHandle(request, new MockHttpServletResponse(), new Object()); } catch (Exception exception) { throw new AssertionError(exception); } assertThat(requested).as("拦截器应该只校验一次权限").hasSize(1); return requested.get(0); } private MockHttpServletRequest get(String uri) { return get(uri, new String[0]); } private MockHttpServletRequest get(String uri, String... params) { MockHttpServletRequest request = new MockHttpServletRequest("GET", uri); request.addHeader("Authorization", "Bearer test-token"); for (int index = 0; index + 1 < params.length; index += 2) { request.setParameter(params[index], params[index + 1]); } return request; } private MockHttpServletRequest patch(String uri, String... params) { MockHttpServletRequest request = new MockHttpServletRequest("PATCH", uri); request.addHeader("Authorization", "Bearer test-token"); for (int index = 0; index + 1 < params.length; index += 2) { request.setParameter(params[index], params[index + 1]); } return request; } }