AuthzServiceTest.java 3.64 KB
package com.xly.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xly.agent.AgentIdentity;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.util.ReflectionTestUtils;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * 身份解析的**安全不变量**:身份只能来自 ERP 内省,失败路径一律 fail-closed。
 * 这几条断言对应审计里的 Critical #1(空 token → 超管)与「客户端自称身份」根因。
 */
class AuthzServiceTest {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    private AuthzService service(ErpClient erp) {
        JdbcTemplate jdbc = mock(JdbcTemplate.class);
        AuthzService s = new AuthzService(jdbc, erp);
        ReflectionTestUtils.setField(s, "devUserNo", "admin");
        ReflectionTestUtils.setField(s, "devBrand", "B1");
        ReflectionTestUtils.setField(s, "devSub", "S1");
        ReflectionTestUtils.setField(s, "devUserType", "sysadmin");
        ReflectionTestUtils.setField(s, "devUserIdOverride", "dev-user");
        return s;
    }

    @Test
    void noTokenInProductionIsRejected() {
        ErpClient erp = mock(ErpClient.class);
        when(erp.devLoginEnabled()).thenReturn(false);

        assertNull(service(erp).resolveIdentity(null), "生产环境下无 token 必须拒绝,不能回落 dev 身份");
        assertNull(service(erp).resolveIdentity("   "), "空白 token 同样必须拒绝");
    }

    @Test
    void invalidTokenIsRejectedEvenWhenDevLoginEnabled() {
        ErpClient erp = mock(ErpClient.class);
        when(erp.devLoginEnabled()).thenReturn(true);
        when(erp.whoami(anyString())).thenReturn(null); // 过期/伪造

        assertNull(service(erp).resolveIdentity("expired-token"),
                "无效 token 必须拒绝,绝不因为开了 dev-login 就降级成管理员");
    }

    @Test
    void identityComesFromIntrospectionNotFromClient() throws Exception {
        ErpClient erp = mock(ErpClient.class);
        when(erp.whoami(anyString())).thenReturn(MAPPER.readTree(
                "{\"sId\":\"real-user\",\"sBrandsId\":\"real-brand\",\"sSubsidiaryId\":\"real-sub\",\"sType\":\"sysadmin\"}"));

        AgentIdentity id = service(erp).resolveIdentity("good-token");

        assertNotNull(id);
        assertEquals("real-user", id.userId());
        assertEquals("real-brand", id.brandsId(), "租户必须取自服务端内省结果");
        assertEquals("good-token", id.token());
    }

    @Test
    void noTokenWithDevLoginEnabledYieldsDevIdentity() {
        ErpClient erp = mock(ErpClient.class);
        when(erp.devLoginEnabled()).thenReturn(true);
        JdbcTemplate jdbc = mock(JdbcTemplate.class);
        when(jdbc.queryForList(anyString(), (Object[]) any())).thenReturn(java.util.List.of());

        AuthzService s = new AuthzService(jdbc, erp);
        ReflectionTestUtils.setField(s, "devUserIdOverride", "dev-user");
        ReflectionTestUtils.setField(s, "devUserType", "sysadmin");
        ReflectionTestUtils.setField(s, "devBrand", "B1");
        ReflectionTestUtils.setField(s, "devSub", "S1");
        ReflectionTestUtils.setField(s, "devUserNo", "admin");

        AgentIdentity id = s.resolveIdentity(null);
        assertNotNull(id, "本地开发允许无 token");
        assertEquals("dev-user", id.userId());
    }
}