Commit 94d10c1349d8fe5580cfabdb5697c533256ebff4

Authored by zichun
1 parent c5286961

test(usr): 查询用户端到端验收回归 REQ-USR-003

backend/src/test/java/com/xly/erp/modules/usr/UsrUserQueryIT.java 0 → 100644
  1 +package com.xly.erp.modules.usr;
  2 +
  3 +import static org.assertj.core.api.Assertions.assertThat;
  4 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
  5 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
  6 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  7 +
  8 +import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  9 +import com.fasterxml.jackson.databind.ObjectMapper;
  10 +import com.xly.erp.common.security.JwtUtil;
  11 +import com.xly.erp.modules.usr.entity.UsrEmployee;
  12 +import com.xly.erp.modules.usr.entity.UsrUser;
  13 +import com.xly.erp.modules.usr.mapper.UsrEmployeeMapper;
  14 +import com.xly.erp.modules.usr.mapper.UsrUserMapper;
  15 +import java.time.LocalDateTime;
  16 +import org.junit.jupiter.api.AfterEach;
  17 +import org.junit.jupiter.api.BeforeEach;
  18 +import org.junit.jupiter.api.Test;
  19 +import org.springframework.beans.factory.annotation.Autowired;
  20 +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  21 +import org.springframework.boot.test.context.SpringBootTest;
  22 +import org.springframework.test.context.ActiveProfiles;
  23 +import org.springframework.test.web.servlet.MockMvc;
  24 +
  25 +/**
  26 + * REQ-USR-003 T6:查询用户端到端验收回归(spec § 7 AC1-AC13)。
  27 + *
  28 + * <p>@SpringBootTest + 真实 MockMvc 安全链 + test profile 连测试库(Flyway 已 apply V1)。
  29 + * 认证态通过真实 JwtUtil 签发 token 走 JwtAuthenticationFilter 注入;查询无管理员限制
  30 + * (任意已认证用户可调用,spec § 8 D5)。每个用例自管理 fixture 清理(命名前缀
  31 + * {@code it3_user_} / {@code IT3职员} / {@code IT3_EMP_})。</p>
  32 + */
  33 +@SpringBootTest
  34 +@AutoConfigureMockMvc
  35 +@ActiveProfiles("test")
  36 +class UsrUserQueryIT {
  37 +
  38 + private static final String USER_PREFIX = "it3_user_";
  39 + private static final String EMP_NAME_PREFIX = "IT3职员";
  40 + private static final String EMP_NO_PREFIX = "IT3_EMP_";
  41 +
  42 + @Autowired
  43 + private MockMvc mockMvc;
  44 +
  45 + @Autowired
  46 + private ObjectMapper objectMapper;
  47 +
  48 + @Autowired
  49 + private JwtUtil jwtUtil;
  50 +
  51 + @Autowired
  52 + private UsrUserMapper usrUserMapper;
  53 +
  54 + @Autowired
  55 + private UsrEmployeeMapper usrEmployeeMapper;
  56 +
  57 + @BeforeEach
  58 + void cleanupBefore() {
  59 + purge();
  60 + }
  61 +
  62 + @AfterEach
  63 + void cleanupAfter() {
  64 + purge();
  65 + }
  66 +
  67 + private void purge() {
  68 + usrUserMapper.delete(Wrappers.<UsrUser>lambdaQuery()
  69 + .likeRight(UsrUser::getSUserName, USER_PREFIX));
  70 + usrEmployeeMapper.delete(Wrappers.<UsrEmployee>lambdaQuery()
  71 + .likeRight(UsrEmployee::getSEmployeeName, EMP_NAME_PREFIX));
  72 + }
  73 +
  74 + private String token() {
  75 + return "Bearer " + jwtUtil.generateToken("it3_caller", "普通用户");
  76 + }
  77 +
  78 + private Integer seedEmployee(String nameSuffix, String dept) {
  79 + UsrEmployee emp = new UsrEmployee();
  80 + emp.setSEmployeeName(EMP_NAME_PREFIX + nameSuffix);
  81 + emp.setSEmployeeNo(EMP_NO_PREFIX + nameSuffix);
  82 + emp.setSDepartment(dept);
  83 + usrEmployeeMapper.insert(emp);
  84 + return emp.getIIncrement();
  85 + }
  86 +
  87 + private UsrUser seedUser(String nameSuffix, String creator, String userType) {
  88 + UsrUser u = new UsrUser();
  89 + u.setSUserName(USER_PREFIX + nameSuffix);
  90 + u.setSPassword("$2a$dummyhashvalue000000000000000000000");
  91 + u.setSUserType(userType);
  92 + u.setSLanguage("中文");
  93 + u.setICanModifyBill(0);
  94 + u.setIIsVoid(0);
  95 + u.setSCreator(creator);
  96 + usrUserMapper.insert(u);
  97 + return u;
  98 + }
  99 +
  100 + @Test
  101 + void ac1EmptyConditionFullPage() throws Exception {
  102 + seedUser("a1x", "it3_creator", "普通用户");
  103 + seedUser("a1y", "it3_creator", "普通用户");
  104 +
  105 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  106 + .param("pageNum", "1").param("pageSize", "10"))
  107 + .andExpect(status().isOk())
  108 + .andExpect(jsonPath("$.code").value(0))
  109 + .andExpect(jsonPath("$.data.pageNum").value(1))
  110 + .andExpect(jsonPath("$.data.pageSize").value(10))
  111 + .andExpect(jsonPath("$.data.records").isArray())
  112 + .andExpect(jsonPath("$.data.records[0].sUserName").exists())
  113 + .andExpect(jsonPath("$.data.records[0].sPassword").doesNotExist())
  114 + .andExpect(jsonPath("$.data.records[0].password").doesNotExist());
  115 + }
  116 +
  117 + @Test
  118 + void ac2TextContains() throws Exception {
  119 + seedUser("contains_target", "it3_creator", "普通用户");
  120 + seedUser("other_one", "it3_creator", "普通用户");
  121 +
  122 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  123 + .param("queryField", "用户名").param("matchType", "包含")
  124 + .param("queryValue", "contains_target"))
  125 + .andExpect(jsonPath("$.code").value(0))
  126 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "contains_target')]").exists())
  127 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "other_one')]").doesNotExist());
  128 + }
  129 +
  130 + @Test
  131 + void ac3TextEquals() throws Exception {
  132 + seedUser("eq", "it3_creator", "普通用户");
  133 + seedUser("eq_suffix", "it3_creator", "普通用户");
  134 + String exact = USER_PREFIX + "eq";
  135 +
  136 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  137 + .param("queryField", "用户名").param("matchType", "等于")
  138 + .param("queryValue", exact))
  139 + .andExpect(jsonPath("$.code").value(0))
  140 + .andExpect(jsonPath("$.data.total").value(1))
  141 + .andExpect(jsonPath("$.data.records[0].sUserName").value(exact));
  142 + }
  143 +
  144 + @Test
  145 + void ac4TextNotContains() throws Exception {
  146 + seedUser("nc1", "it3_creator_keep", "普通用户");
  147 + seedUser("nc2", "it3_creator_drop", "普通用户");
  148 +
  149 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  150 + .param("queryField", "制单人").param("matchType", "不包含")
  151 + .param("queryValue", "drop").param("pageSize", "100"))
  152 + .andExpect(jsonPath("$.code").value(0))
  153 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "nc1')]").exists())
  154 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "nc2')]").doesNotExist());
  155 + }
  156 +
  157 + @Test
  158 + void ac5EnumEquals() throws Exception {
  159 + seedUser("admin_u", "it3_creator", "超级管理员");
  160 + seedUser("normal_u", "it3_creator", "普通用户");
  161 +
  162 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  163 + .param("queryField", "用户类型").param("matchType", "等于")
  164 + .param("queryValue", "超级管理员").param("pageSize", "100"))
  165 + .andExpect(jsonPath("$.code").value(0))
  166 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "admin_u')]").exists())
  167 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "normal_u')]").doesNotExist());
  168 + }
  169 +
  170 + @Test
  171 + void ac6BoolFilter() throws Exception {
  172 + UsrUser voided = seedUser("voided", "it3_creator", "普通用户");
  173 + voided.setIIsVoid(1);
  174 + usrUserMapper.updateById(voided);
  175 + seedUser("active", "it3_creator", "普通用户");
  176 +
  177 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  178 + .param("queryField", "作废").param("queryValue", "1").param("pageSize", "100"))
  179 + .andExpect(jsonPath("$.code").value(0))
  180 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "voided')]").exists())
  181 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "active')]").doesNotExist());
  182 +
  183 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  184 + .param("queryField", "作废").param("queryValue", "0").param("pageSize", "100"))
  185 + .andExpect(jsonPath("$.code").value(0))
  186 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "active')]").exists())
  187 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "voided')]").doesNotExist());
  188 +
  189 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  190 + .param("queryField", "作废").param("queryValue", "abc"))
  191 + .andExpect(jsonPath("$.code").value(40001));
  192 + }
  193 +
  194 + @Test
  195 + void ac7DateFilter() throws Exception {
  196 + UsrUser dated = seedUser("dated", "it3_creator", "普通用户");
  197 + dated.setTLastLoginDate(LocalDateTime.of(2026, 6, 1, 12, 0, 0));
  198 + usrUserMapper.updateById(dated);
  199 +
  200 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  201 + .param("queryField", "登录日期").param("matchType", "等于")
  202 + .param("queryValue", "2026-06-01").param("pageSize", "100"))
  203 + .andExpect(jsonPath("$.code").value(0))
  204 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "dated')]").exists());
  205 +
  206 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  207 + .param("queryField", "登录日期").param("matchType", "等于")
  208 + .param("queryValue", "2026-13-99"))
  209 + .andExpect(jsonPath("$.code").value(40001));
  210 + }
  211 +
  212 + @Test
  213 + void ac8CrossTableEmployeeDept() throws Exception {
  214 + Integer empId = seedEmployee("fin", "财务部IT3");
  215 + UsrUser linked = seedUser("linked", "it3_creator", "普通用户");
  216 + linked.setIEmployeeId(empId);
  217 + usrUserMapper.updateById(linked);
  218 + seedUser("unlinked", "it3_creator", "普通用户");
  219 +
  220 + // 全量:关联用户带员工名 / 部门,未关联用户两列为 null。
  221 + mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageSize", "100"))
  222 + .andExpect(jsonPath("$.code").value(0))
  223 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX
  224 + + "linked')].department").value(org.hamcrest.Matchers.hasItem("财务部IT3")))
  225 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX
  226 + + "linked')].employeeName").value(org.hamcrest.Matchers.hasItem(EMP_NAME_PREFIX + "fin")));
  227 +
  228 + // 按部门跨表过滤:仅返回所属部门含「财务」的用户。
  229 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  230 + .param("queryField", "部门").param("matchType", "包含")
  231 + .param("queryValue", "财务").param("pageSize", "100"))
  232 + .andExpect(jsonPath("$.code").value(0))
  233 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "linked')]").exists())
  234 + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "unlinked')]").doesNotExist());
  235 + }
  236 +
  237 + @Test
  238 + void ac9NoMatchEmptyList() throws Exception {
  239 + seedUser("present", "it3_creator", "普通用户");
  240 +
  241 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  242 + .param("queryField", "用户名").param("matchType", "等于")
  243 + .param("queryValue", "it3_user_zzz_no_such_user_xyz"))
  244 + .andExpect(jsonPath("$.code").value(0))
  245 + .andExpect(jsonPath("$.data.total").value(0))
  246 + .andExpect(jsonPath("$.data.records").isEmpty());
  247 + }
  248 +
  249 + @Test
  250 + void ac10DataOutOfRangeLastPage() throws Exception {
  251 + // 插 3 条同制单人 fixture,按制单人精确过滤 → total=3,pageSize=2 → 2 页;请求 page99 钳到末页 2。
  252 + seedUser("clamp1", "it3_clamp_creator", "普通用户");
  253 + seedUser("clamp2", "it3_clamp_creator", "普通用户");
  254 + seedUser("clamp3", "it3_clamp_creator", "普通用户");
  255 +
  256 + mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  257 + .param("queryField", "制单人").param("matchType", "等于")
  258 + .param("queryValue", "it3_clamp_creator")
  259 + .param("pageNum", "99").param("pageSize", "2"))
  260 + .andExpect(jsonPath("$.code").value(0))
  261 + .andExpect(jsonPath("$.data.total").value(3))
  262 + .andExpect(jsonPath("$.data.pageNum").value(2))
  263 + .andExpect(jsonPath("$.data.records.length()").value(1));
  264 + }
  265 +
  266 + @Test
  267 + void ac11PageParamInvalid() throws Exception {
  268 + mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageNum", "0"))
  269 + .andExpect(jsonPath("$.code").value(42201));
  270 + mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageSize", "0"))
  271 + .andExpect(jsonPath("$.code").value(42201));
  272 + mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageSize", "500"))
  273 + .andExpect(jsonPath("$.code").value(42201));
  274 + }
  275 +
  276 + @Test
  277 + void ac12NoToken() throws Exception {
  278 + mockMvc.perform(get("/api/usr/users"))
  279 + .andExpect(status().isUnauthorized());
  280 + mockMvc.perform(get("/api/usr/users").header("Authorization", "Bearer invalid.token.value"))
  281 + .andExpect(status().isUnauthorized());
  282 + }
  283 +
  284 + @Test
  285 + void ac13PasswordNeverLeaks() throws Exception {
  286 + seedUser("leakcheck", "it3_creator", "普通用户");
  287 + String body = mockMvc.perform(get("/api/usr/users").header("Authorization", token())
  288 + .param("pageSize", "100"))
  289 + .andExpect(jsonPath("$.code").value(0))
  290 + .andReturn().getResponse().getContentAsString();
  291 +
  292 + assertThat(body).doesNotContain("sPassword");
  293 + assertThat(body.toLowerCase()).doesNotContain("password");
  294 + assertThat(body).doesNotContain("$2a$");
  295 + }
  296 +}
... ...