From f5a0442e475b6c69a826fdc7d073549827dd23fb Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 1 Jun 2026 14:04:18 +0800 Subject: [PATCH] test(usr): 修改用户端到端验收回归 REQ-USR-002 --- backend/src/test/java/com/xly/erp/modules/usr/UsrUserUpdateIT.java | 304 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 304 insertions(+), 0 deletions(-) create mode 100644 backend/src/test/java/com/xly/erp/modules/usr/UsrUserUpdateIT.java diff --git a/backend/src/test/java/com/xly/erp/modules/usr/UsrUserUpdateIT.java b/backend/src/test/java/com/xly/erp/modules/usr/UsrUserUpdateIT.java new file mode 100644 index 0000000..3c3ee1a --- /dev/null +++ b/backend/src/test/java/com/xly/erp/modules/usr/UsrUserUpdateIT.java @@ -0,0 +1,304 @@ +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.request.MockMvcRequestBuilders.put; +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.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +/** + * REQ-USR-002 T5:修改用户端到端验收回归(spec § 7)。 + * + *

@SpringBootTest + 真实 MockMvc 安全链 + test profile 连测试库(Flyway 已 apply V1)。 + * 认证态通过真实 JwtUtil 签发 token 走 JwtAuthenticationFilter 注入;真实库读写做端到端确认。 + * 每个用例自管理 fixture 清理(命名前缀 {@code it2_user_} / {@code IT2_PERM_})。

+ * + *

边界说明(spec § 7):AC4「禁用实时生效」、AC5「角色变更实时生效」依赖 REQ-USR-004 登录 / + * REQ-USR-003 查询接口,尚未实现;本 IT 以「PUT iIsVoid=1 / 改 sUserType 后 selectById + * 读回库内 iIsVoid==1 / sUserType 为新值」做后端落库层等价验证,登录 / 查询联动留待对应 REQ + * 的 IT 覆盖。

+ */ +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class UsrUserUpdateIT { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private UsrUserMapper usrUserMapper; + + @Autowired + private UsrUserPermissionMapper usrUserPermissionMapper; + + @Autowired + private UsrPermissionMapper usrPermissionMapper; + + private static final String CALLER = "it2_admin"; + + @AfterEach + void cleanup() { + usrUserMapper.delete(Wrappers.lambdaQuery() + .likeRight(UsrUser::getSUserName, "it2_user_")); + usrPermissionMapper.delete(Wrappers.lambdaQuery() + .likeRight(UsrPermission::getSPermissionCode, "IT2_PERM_")); + } + + private String adminToken() { + return "Bearer " + jwtUtil.generateToken(CALLER, "超级管理员"); + } + + private String normalToken() { + return "Bearer " + jwtUtil.generateToken("it2_normal", "普通用户"); + } + + private Map createBody(String userName) { + Map body = new HashMap<>(); + body.put("sUserName", userName); + body.put("sLanguage", "中文"); + return body; + } + + private int createUser(String userName) throws Exception { + MvcResult result = mockMvc.perform(post("/api/usr/users") + .header("Authorization", adminToken()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(createBody(userName)))) + .andExpect(jsonPath("$.code").value(0)) + .andReturn(); + Map resp = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class); + return (Integer) ((Map) resp.get("data")).get("id"); + } + + private int insertPermissionFixture(String codeSuffix) { + UsrPermission perm = new UsrPermission(); + perm.setSPermissionName("IT2权限" + codeSuffix); + perm.setSPermissionCode("IT2_PERM_" + codeSuffix); + usrPermissionMapper.insert(perm); + return perm.getIIncrement(); + } + + // ---------------- AC1:基本信息修改落库,且身份 / 密码 / 审计列不变 ---------------- + + @Test + void ac1UpdateBasicInfoPersists() throws Exception { + int id = createUser("it2_user_ac1"); + UsrUser before = usrUserMapper.selectById(id); + + Map body = new HashMap<>(); + body.put("sUserType", "超级管理员"); + body.put("sLanguage", "英文"); + body.put("iCanModifyBill", 1); + body.put("sUserNo", "NO-AC1"); + + mockMvc.perform(put("/api/usr/users/" + id) + .header("Authorization", adminToken()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.id").value(id)); + + UsrUser after = usrUserMapper.selectById(id); + assertThat(after.getSUserType()).isEqualTo("超级管理员"); + assertThat(after.getSLanguage()).isEqualTo("英文"); + assertThat(after.getICanModifyBill()).isEqualTo(1); + assertThat(after.getSUserNo()).isEqualTo("NO-AC1"); + // 身份 / 密码 / 审计列字节级不变。 + assertThat(after.getSUserName()).isEqualTo(before.getSUserName()); + assertThat(after.getSPassword()).isEqualTo(before.getSPassword()); + assertThat(after.getSCreator()).isEqualTo(before.getSCreator()); + assertThat(after.getTCreateDate()).isEqualTo(before.getTCreateDate()); + } + + // ---------------- AC2:目标用户不存在 → 40401,无写入 ---------------- + + @Test + void ac2UpdateNonExistentReturns40401() throws Exception { + Map body = new HashMap<>(); + body.put("sUserType", "普通用户"); + body.put("sLanguage", "中文"); + + mockMvc.perform(put("/api/usr/users/2000000001") + .header("Authorization", adminToken()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40401)); + + assertThat(usrUserMapper.selectById(2000000001)).isNull(); + } + + // ---------------- AC3:非法参数(不存在职员)→ 40001,整体回滚无副作用 ---------------- + + @Test + void ac3InvalidParamRollsBack() throws Exception { + int id = createUser("it2_user_ac3"); + UsrUser before = usrUserMapper.selectById(id); + + Map body = new HashMap<>(); + body.put("sUserType", "超级管理员"); + body.put("sLanguage", "英文"); + body.put("iEmployeeId", 2000000002); // 不存在的职员 + + mockMvc.perform(put("/api/usr/users/" + id) + .header("Authorization", adminToken()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(40001)); + + UsrUser after = usrUserMapper.selectById(id); + // 目标用户行各列与调用前一致(无副作用)。 + assertThat(after.getSUserType()).isEqualTo(before.getSUserType()); + assertThat(after.getSLanguage()).isEqualTo(before.getSLanguage()); + assertThat(after.getIEmployeeId()).isEqualTo(before.getIEmployeeId()); + } + + // ---------------- AC6:权限组全量覆盖 / 清空 / 不改 ---------------- + + @Test + void ac6PermissionOverwrite() throws Exception { + int id = createUser("it2_user_ac6"); + int permA = insertPermissionFixture("A"); + int permB = insertPermissionFixture("B"); + int permC = insertPermissionFixture("C"); + // 预置该用户授权为 {a, c} + usrUserPermissionMapper.insert(new UsrUserPermission(id, permA)); + usrUserPermissionMapper.insert(new UsrUserPermission(id, permC)); + + Map body = new HashMap<>(); + body.put("sUserType", "普通用户"); + body.put("sLanguage", "中文"); + body.put("permissionIds", List.of(permA, permB, permA)); // 去重后 {a, b} + + mockMvc.perform(put("/api/usr/users/" + id) + .header("Authorization", adminToken()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(body))) + .andExpect(jsonPath("$.code").value(0)); + + List grants = usrUserPermissionMapper.selectList( + Wrappers.lambdaQuery().eq(UsrUserPermission::getIUserId, id)); + assertThat(grants).extracting(UsrUserPermission::getIPermissionId) + .containsExactlyInAnyOrder(permA, permB); // c 被删、b 新增、a 去重一次 + + // 传 [] → 清空全部授权 + Map clearBody = new HashMap<>(); + clearBody.put("sUserType", "普通用户"); + clearBody.put("sLanguage", "中文"); + clearBody.put("permissionIds", List.of()); + mockMvc.perform(put("/api/usr/users/" + id) + .header("Authorization", adminToken()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(clearBody))) + .andExpect(jsonPath("$.code").value(0)); + long afterClear = usrUserPermissionMapper.selectCount( + Wrappers.lambdaQuery().eq(UsrUserPermission::getIUserId, id)); + assertThat(afterClear).isZero(); + + // 预置一条授权后,不传 permissionIds → 授权不变 + usrUserPermissionMapper.insert(new UsrUserPermission(id, permA)); + Map noPermBody = new HashMap<>(); + noPermBody.put("sUserType", "普通用户"); + noPermBody.put("sLanguage", "中文"); + mockMvc.perform(put("/api/usr/users/" + id) + .header("Authorization", adminToken()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(noPermBody))) + .andExpect(jsonPath("$.code").value(0)); + long afterNoChange = usrUserPermissionMapper.selectCount( + Wrappers.lambdaQuery().eq(UsrUserPermission::getIUserId, id)); + assertThat(afterNoChange).isEqualTo(1L); + } + + // ---------------- AC7:非管理员 / 无 token 被拦截,目标行未被修改 ---------------- + + @Test + void ac7NonAdminAndNoTokenBlocked() throws Exception { + int id = createUser("it2_user_ac7"); + UsrUser before = usrUserMapper.selectById(id); + + Map body = new HashMap<>(); + body.put("sUserType", "超级管理员"); + body.put("sLanguage", "英文"); + + // 普通用户 token → 40301 + mockMvc.perform(put("/api/usr/users/" + id) + .header("Authorization", normalToken()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(body))) + .andExpect(jsonPath("$.code").value(40301)); + + // 无 token → 401(安全链拦截) + mockMvc.perform(put("/api/usr/users/" + id) + .contentType("application/json") + .content(objectMapper.writeValueAsString(body))) + .andExpect(status().isUnauthorized()); + + UsrUser after = usrUserMapper.selectById(id); + assertThat(after.getSUserType()).isEqualTo(before.getSUserType()); + assertThat(after.getSLanguage()).isEqualTo(before.getSLanguage()); + } + + // ---------------- AC8:密码不变且不出现在响应;iIsVoid 落库(AC4/AC5 落库层等价验证) ---------------- + + @Test + void ac8PasswordUnchangedAndAbsentFromResponse() throws Exception { + int id = createUser("it2_user_ac8"); + UsrUser before = usrUserMapper.selectById(id); + + Map body = new HashMap<>(); + body.put("sUserType", "超级管理员"); // AC5 角色变更落库层等价验证 + body.put("sLanguage", "中文"); + body.put("iIsVoid", 1); // AC4 禁用落库层等价验证 + + MvcResult result = mockMvc.perform(put("/api/usr/users/" + id) + .header("Authorization", adminToken()) + .contentType("application/json") + .content(objectMapper.writeValueAsString(body))) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.id").value(id)) + .andExpect(jsonPath("$.data.sPassword").doesNotExist()) + .andExpect(jsonPath("$.data.password").doesNotExist()) + .andReturn(); + + String responseBody = result.getResponse().getContentAsString(); + assertThat(responseBody.toLowerCase()).doesNotContain("password"); + + UsrUser after = usrUserMapper.selectById(id); + // 密码列字节级不变。 + assertThat(after.getSPassword()).isEqualTo(before.getSPassword()); + // AC4 / AC5 落库层等价验证:禁用状态 / 角色变更读回库内为新值。 + assertThat(after.getIIsVoid()).isEqualTo(1); + assertThat(after.getSUserType()).isEqualTo("超级管理员"); + } +} -- libgit2 0.22.2