From 94d10c1349d8fe5580cfabdb5697c533256ebff4 Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 1 Jun 2026 14:39:46 +0800 Subject: [PATCH] test(usr): 查询用户端到端验收回归 REQ-USR-003 --- backend/src/test/java/com/xly/erp/modules/usr/UsrUserQueryIT.java | 296 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+), 0 deletions(-) create mode 100644 backend/src/test/java/com/xly/erp/modules/usr/UsrUserQueryIT.java diff --git a/backend/src/test/java/com/xly/erp/modules/usr/UsrUserQueryIT.java b/backend/src/test/java/com/xly/erp/modules/usr/UsrUserQueryIT.java new file mode 100644 index 0000000..9e2ed6c --- /dev/null +++ b/backend/src/test/java/com/xly/erp/modules/usr/UsrUserQueryIT.java @@ -0,0 +1,296 @@ +package com.xly.erp.modules.usr; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +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.UsrEmployee; +import com.xly.erp.modules.usr.entity.UsrUser; +import com.xly.erp.modules.usr.mapper.UsrEmployeeMapper; +import com.xly.erp.modules.usr.mapper.UsrUserMapper; +import java.time.LocalDateTime; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +/** + * REQ-USR-003 T6:查询用户端到端验收回归(spec § 7 AC1-AC13)。 + * + *

@SpringBootTest + 真实 MockMvc 安全链 + test profile 连测试库(Flyway 已 apply V1)。 + * 认证态通过真实 JwtUtil 签发 token 走 JwtAuthenticationFilter 注入;查询无管理员限制 + * (任意已认证用户可调用,spec § 8 D5)。每个用例自管理 fixture 清理(命名前缀 + * {@code it3_user_} / {@code IT3职员} / {@code IT3_EMP_})。

+ */ +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class UsrUserQueryIT { + + private static final String USER_PREFIX = "it3_user_"; + private static final String EMP_NAME_PREFIX = "IT3职员"; + private static final String EMP_NO_PREFIX = "IT3_EMP_"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private UsrUserMapper usrUserMapper; + + @Autowired + private UsrEmployeeMapper usrEmployeeMapper; + + @BeforeEach + void cleanupBefore() { + purge(); + } + + @AfterEach + void cleanupAfter() { + purge(); + } + + private void purge() { + usrUserMapper.delete(Wrappers.lambdaQuery() + .likeRight(UsrUser::getSUserName, USER_PREFIX)); + usrEmployeeMapper.delete(Wrappers.lambdaQuery() + .likeRight(UsrEmployee::getSEmployeeName, EMP_NAME_PREFIX)); + } + + private String token() { + return "Bearer " + jwtUtil.generateToken("it3_caller", "普通用户"); + } + + private Integer seedEmployee(String nameSuffix, String dept) { + UsrEmployee emp = new UsrEmployee(); + emp.setSEmployeeName(EMP_NAME_PREFIX + nameSuffix); + emp.setSEmployeeNo(EMP_NO_PREFIX + nameSuffix); + emp.setSDepartment(dept); + usrEmployeeMapper.insert(emp); + return emp.getIIncrement(); + } + + private UsrUser seedUser(String nameSuffix, String creator, String userType) { + UsrUser u = new UsrUser(); + u.setSUserName(USER_PREFIX + nameSuffix); + u.setSPassword("$2a$dummyhashvalue000000000000000000000"); + u.setSUserType(userType); + u.setSLanguage("中文"); + u.setICanModifyBill(0); + u.setIIsVoid(0); + u.setSCreator(creator); + usrUserMapper.insert(u); + return u; + } + + @Test + void ac1EmptyConditionFullPage() throws Exception { + seedUser("a1x", "it3_creator", "普通用户"); + seedUser("a1y", "it3_creator", "普通用户"); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("pageNum", "1").param("pageSize", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.pageNum").value(1)) + .andExpect(jsonPath("$.data.pageSize").value(10)) + .andExpect(jsonPath("$.data.records").isArray()) + .andExpect(jsonPath("$.data.records[0].sUserName").exists()) + .andExpect(jsonPath("$.data.records[0].sPassword").doesNotExist()) + .andExpect(jsonPath("$.data.records[0].password").doesNotExist()); + } + + @Test + void ac2TextContains() throws Exception { + seedUser("contains_target", "it3_creator", "普通用户"); + seedUser("other_one", "it3_creator", "普通用户"); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "用户名").param("matchType", "包含") + .param("queryValue", "contains_target")) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "contains_target')]").exists()) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "other_one')]").doesNotExist()); + } + + @Test + void ac3TextEquals() throws Exception { + seedUser("eq", "it3_creator", "普通用户"); + seedUser("eq_suffix", "it3_creator", "普通用户"); + String exact = USER_PREFIX + "eq"; + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "用户名").param("matchType", "等于") + .param("queryValue", exact)) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.total").value(1)) + .andExpect(jsonPath("$.data.records[0].sUserName").value(exact)); + } + + @Test + void ac4TextNotContains() throws Exception { + seedUser("nc1", "it3_creator_keep", "普通用户"); + seedUser("nc2", "it3_creator_drop", "普通用户"); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "制单人").param("matchType", "不包含") + .param("queryValue", "drop").param("pageSize", "100")) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "nc1')]").exists()) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "nc2')]").doesNotExist()); + } + + @Test + void ac5EnumEquals() throws Exception { + seedUser("admin_u", "it3_creator", "超级管理员"); + seedUser("normal_u", "it3_creator", "普通用户"); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "用户类型").param("matchType", "等于") + .param("queryValue", "超级管理员").param("pageSize", "100")) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "admin_u')]").exists()) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "normal_u')]").doesNotExist()); + } + + @Test + void ac6BoolFilter() throws Exception { + UsrUser voided = seedUser("voided", "it3_creator", "普通用户"); + voided.setIIsVoid(1); + usrUserMapper.updateById(voided); + seedUser("active", "it3_creator", "普通用户"); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "作废").param("queryValue", "1").param("pageSize", "100")) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "voided')]").exists()) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "active')]").doesNotExist()); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "作废").param("queryValue", "0").param("pageSize", "100")) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "active')]").exists()) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "voided')]").doesNotExist()); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "作废").param("queryValue", "abc")) + .andExpect(jsonPath("$.code").value(40001)); + } + + @Test + void ac7DateFilter() throws Exception { + UsrUser dated = seedUser("dated", "it3_creator", "普通用户"); + dated.setTLastLoginDate(LocalDateTime.of(2026, 6, 1, 12, 0, 0)); + usrUserMapper.updateById(dated); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "登录日期").param("matchType", "等于") + .param("queryValue", "2026-06-01").param("pageSize", "100")) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "dated')]").exists()); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "登录日期").param("matchType", "等于") + .param("queryValue", "2026-13-99")) + .andExpect(jsonPath("$.code").value(40001)); + } + + @Test + void ac8CrossTableEmployeeDept() throws Exception { + Integer empId = seedEmployee("fin", "财务部IT3"); + UsrUser linked = seedUser("linked", "it3_creator", "普通用户"); + linked.setIEmployeeId(empId); + usrUserMapper.updateById(linked); + seedUser("unlinked", "it3_creator", "普通用户"); + + // 全量:关联用户带员工名 / 部门,未关联用户两列为 null。 + mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageSize", "100")) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + + "linked')].department").value(org.hamcrest.Matchers.hasItem("财务部IT3"))) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + + "linked')].employeeName").value(org.hamcrest.Matchers.hasItem(EMP_NAME_PREFIX + "fin"))); + + // 按部门跨表过滤:仅返回所属部门含「财务」的用户。 + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "部门").param("matchType", "包含") + .param("queryValue", "财务").param("pageSize", "100")) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "linked')]").exists()) + .andExpect(jsonPath("$.data.records[?(@.sUserName=='" + USER_PREFIX + "unlinked')]").doesNotExist()); + } + + @Test + void ac9NoMatchEmptyList() throws Exception { + seedUser("present", "it3_creator", "普通用户"); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "用户名").param("matchType", "等于") + .param("queryValue", "it3_user_zzz_no_such_user_xyz")) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.total").value(0)) + .andExpect(jsonPath("$.data.records").isEmpty()); + } + + @Test + void ac10DataOutOfRangeLastPage() throws Exception { + // 插 3 条同制单人 fixture,按制单人精确过滤 → total=3,pageSize=2 → 2 页;请求 page99 钳到末页 2。 + seedUser("clamp1", "it3_clamp_creator", "普通用户"); + seedUser("clamp2", "it3_clamp_creator", "普通用户"); + seedUser("clamp3", "it3_clamp_creator", "普通用户"); + + mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("queryField", "制单人").param("matchType", "等于") + .param("queryValue", "it3_clamp_creator") + .param("pageNum", "99").param("pageSize", "2")) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.total").value(3)) + .andExpect(jsonPath("$.data.pageNum").value(2)) + .andExpect(jsonPath("$.data.records.length()").value(1)); + } + + @Test + void ac11PageParamInvalid() throws Exception { + mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageNum", "0")) + .andExpect(jsonPath("$.code").value(42201)); + mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageSize", "0")) + .andExpect(jsonPath("$.code").value(42201)); + mockMvc.perform(get("/api/usr/users").header("Authorization", token()).param("pageSize", "500")) + .andExpect(jsonPath("$.code").value(42201)); + } + + @Test + void ac12NoToken() throws Exception { + mockMvc.perform(get("/api/usr/users")) + .andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/usr/users").header("Authorization", "Bearer invalid.token.value")) + .andExpect(status().isUnauthorized()); + } + + @Test + void ac13PasswordNeverLeaks() throws Exception { + seedUser("leakcheck", "it3_creator", "普通用户"); + String body = mockMvc.perform(get("/api/usr/users").header("Authorization", token()) + .param("pageSize", "100")) + .andExpect(jsonPath("$.code").value(0)) + .andReturn().getResponse().getContentAsString(); + + assertThat(body).doesNotContain("sPassword"); + assertThat(body.toLowerCase()).doesNotContain("password"); + assertThat(body).doesNotContain("$2a$"); + } +} -- libgit2 0.22.2