UsrUserCreateIT.java 8.48 KB
package com.xly.erp.modules.usr;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xly.erp.common.security.JwtUtil;
import com.xly.erp.modules.usr.entity.UsrPermission;
import com.xly.erp.modules.usr.entity.UsrUser;
import com.xly.erp.modules.usr.entity.UsrUserPermission;
import com.xly.erp.modules.usr.mapper.UsrPermissionMapper;
import com.xly.erp.modules.usr.mapper.UsrUserMapper;
import com.xly.erp.modules.usr.mapper.UsrUserPermissionMapper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
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.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

/**
 * REQ-USR-001 T8:新增用户端到端验收回归(spec § 7)。
 *
 * <p>@SpringBootTest + 真实 MockMvc 安全链 + test profile 连测试库(Flyway 已 apply V1)。
 * 认证态通过真实 JwtUtil 签发 token 走 JwtAuthenticationFilter 注入;
 * 真实 BCrypt 哈希 + 真实库写入做端到端确认。每个用例自管理 fixture 清理。</p>
 */
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class UsrUserCreateIT {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UsrUserMapper usrUserMapper;

    @Autowired
    private UsrUserPermissionMapper usrUserPermissionMapper;

    @Autowired
    private UsrPermissionMapper usrPermissionMapper;

    private static final String CALLER = "it_admin";

    @AfterEach
    void cleanup() {
        // 删除本测试创建的用户(及其权限授权由外键 CASCADE 清理)与 fixture 权限。
        usrUserMapper.delete(Wrappers.<UsrUser>lambdaQuery()
                .likeRight(UsrUser::getSUserName, "it_user_"));
        usrPermissionMapper.delete(Wrappers.<UsrPermission>lambdaQuery()
                .likeRight(UsrPermission::getSPermissionCode, "IT_PERM_"));
    }

    private String adminToken() {
        return "Bearer " + jwtUtil.generateToken(CALLER, "超级管理员");
    }

    private String normalToken() {
        return "Bearer " + jwtUtil.generateToken("it_normal", "普通用户");
    }

    private Map<String, Object> baseBody(String userName) {
        Map<String, Object> body = new HashMap<>();
        body.put("sUserName", userName);
        body.put("sLanguage", "中文");
        return body;
    }

    private int insertPermissionFixture(String codeSuffix) {
        UsrPermission perm = new UsrPermission();
        perm.setSPermissionName("IT权限" + codeSuffix);
        perm.setSPermissionCode("IT_PERM_" + codeSuffix);
        usrPermissionMapper.insert(perm);
        return perm.getIIncrement();
    }

    @Test
    void ac1NormalCreatePersistsHashedPassword() throws Exception {
        String userName = "it_user_ac1";
        MvcResult result = mockMvc.perform(post("/api/usr/users")
                        .header("Authorization", adminToken())
                        .contentType("application/json")
                        .content(objectMapper.writeValueAsString(baseBody(userName))))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.data.id").isNumber())
                .andReturn();

        Map<?, ?> resp = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class);
        Map<?, ?> data = (Map<?, ?>) resp.get("data");
        Integer newId = (Integer) data.get("id");

        UsrUser saved = usrUserMapper.selectById(newId);
        assertThat(saved).isNotNull();
        assertThat(saved.getSPassword()).isNotEqualTo("666666");
        assertThat(passwordEncoder.matches("666666", saved.getSPassword())).isTrue();
        assertThat(saved.getIIsVoid()).isZero();
        assertThat(saved.getSCreator()).isEqualTo(CALLER);
        assertThat(saved.getTCreateDate()).isNotNull();
    }

    @Test
    void ac2DuplicateUserNameRollsBack() throws Exception {
        String userName = "it_user_ac2";
        mockMvc.perform(post("/api/usr/users")
                        .header("Authorization", adminToken())
                        .contentType("application/json")
                        .content(objectMapper.writeValueAsString(baseBody(userName))))
                .andExpect(jsonPath("$.code").value(0));

        long before = usrUserMapper.selectCount(Wrappers.<UsrUser>lambdaQuery()
                .eq(UsrUser::getSUserName, userName));

        mockMvc.perform(post("/api/usr/users")
                        .header("Authorization", adminToken())
                        .contentType("application/json")
                        .content(objectMapper.writeValueAsString(baseBody(userName))))
                .andExpect(jsonPath("$.code").value(40901));

        long after = usrUserMapper.selectCount(Wrappers.<UsrUser>lambdaQuery()
                .eq(UsrUser::getSUserName, userName));
        assertThat(after).isEqualTo(before).isEqualTo(1L);
    }

    @Test
    void ac4PermissionGrantWritesRows() throws Exception {
        int permA = insertPermissionFixture("A");
        int permB = insertPermissionFixture("B");
        String userName = "it_user_ac4";
        Map<String, Object> body = baseBody(userName);
        body.put("permissionIds", List.of(permA, permB, permA));

        MvcResult result = mockMvc.perform(post("/api/usr/users")
                        .header("Authorization", adminToken())
                        .contentType("application/json")
                        .content(objectMapper.writeValueAsString(body)))
                .andExpect(jsonPath("$.code").value(0))
                .andReturn();

        Map<?, ?> resp = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class);
        Integer newId = (Integer) ((Map<?, ?>) resp.get("data")).get("id");

        long grants = usrUserPermissionMapper.selectCount(Wrappers.<UsrUserPermission>lambdaQuery()
                .eq(UsrUserPermission::getIUserId, newId));
        assertThat(grants).isEqualTo(2L);
    }

    @Test
    void ac5NonAdminForbidden() throws Exception {
        String userName = "it_user_ac5";

        // 普通用户 token → 40301
        mockMvc.perform(post("/api/usr/users")
                        .header("Authorization", normalToken())
                        .contentType("application/json")
                        .content(objectMapper.writeValueAsString(baseBody(userName))))
                .andExpect(jsonPath("$.code").value(40301));

        // 无 token → 401(安全链拦截,未认证)
        mockMvc.perform(post("/api/usr/users")
                        .contentType("application/json")
                        .content(objectMapper.writeValueAsString(baseBody(userName))))
                .andExpect(status().isUnauthorized());

        long created = usrUserMapper.selectCount(Wrappers.<UsrUser>lambdaQuery()
                .eq(UsrUser::getSUserName, userName));
        assertThat(created).isZero();
    }

    @Test
    void ac7ResponseHasNoPassword() throws Exception {
        String userName = "it_user_ac7";
        MvcResult result = mockMvc.perform(post("/api/usr/users")
                        .header("Authorization", adminToken())
                        .contentType("application/json")
                        .content(objectMapper.writeValueAsString(baseBody(userName))))
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.data.sPassword").doesNotExist())
                .andExpect(jsonPath("$.data.password").doesNotExist())
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        assertThat(responseBody).doesNotContain("666666");
        assertThat(responseBody.toLowerCase()).doesNotContain("password");
    }
}