1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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;
 
/**
 * 拦截器的「接口 → 权限码」映射。
 *
 * <p>这层映射出过两次事故:一个接口服务多个页面时,推导出来的权限码在角色管理里根本勾不到,
 * 于是配好权限的员工账号也进不去模块;而权限 403 又被各控制器自带的兜底 advice 改写成 500,
 * 现场只看到「服务器内部错误」。这里不启动 Spring 上下文,直接盯住这两件事。
 */
class AdminAuthInterceptorMappingTests {
 
    private final AdminAuthService service = mock(AdminAuthService.class);
    private final JdbcTemplate jdbc = mock(JdbcTemplate.class);
    private final List<List<String>> 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<String> 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;
    }
}