Commit f5a0442e475b6c69a826fdc7d073549827dd23fb

Authored by zichun
1 parent 5ebb96ab

test(usr): 修改用户端到端验收回归 REQ-USR-002

backend/src/test/java/com/xly/erp/modules/usr/UsrUserUpdateIT.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.post;
  5 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
  6 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
  7 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  8 +
  9 +import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  10 +import com.fasterxml.jackson.databind.ObjectMapper;
  11 +import com.xly.erp.common.security.JwtUtil;
  12 +import com.xly.erp.modules.usr.entity.UsrPermission;
  13 +import com.xly.erp.modules.usr.entity.UsrUser;
  14 +import com.xly.erp.modules.usr.entity.UsrUserPermission;
  15 +import com.xly.erp.modules.usr.mapper.UsrPermissionMapper;
  16 +import com.xly.erp.modules.usr.mapper.UsrUserMapper;
  17 +import com.xly.erp.modules.usr.mapper.UsrUserPermissionMapper;
  18 +import java.util.HashMap;
  19 +import java.util.List;
  20 +import java.util.Map;
  21 +import org.junit.jupiter.api.AfterEach;
  22 +import org.junit.jupiter.api.Test;
  23 +import org.springframework.beans.factory.annotation.Autowired;
  24 +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  25 +import org.springframework.boot.test.context.SpringBootTest;
  26 +import org.springframework.test.context.ActiveProfiles;
  27 +import org.springframework.test.web.servlet.MockMvc;
  28 +import org.springframework.test.web.servlet.MvcResult;
  29 +
  30 +/**
  31 + * REQ-USR-002 T5:修改用户端到端验收回归(spec § 7)。
  32 + *
  33 + * <p>@SpringBootTest + 真实 MockMvc 安全链 + test profile 连测试库(Flyway 已 apply V1)。
  34 + * 认证态通过真实 JwtUtil 签发 token 走 JwtAuthenticationFilter 注入;真实库读写做端到端确认。
  35 + * 每个用例自管理 fixture 清理(命名前缀 {@code it2_user_} / {@code IT2_PERM_})。</p>
  36 + *
  37 + * <p>边界说明(spec § 7):AC4「禁用实时生效」、AC5「角色变更实时生效」依赖 REQ-USR-004 登录 /
  38 + * REQ-USR-003 查询接口,尚未实现;本 IT 以「PUT iIsVoid=1 / 改 sUserType 后 selectById
  39 + * 读回库内 iIsVoid==1 / sUserType 为新值」做后端落库层等价验证,登录 / 查询联动留待对应 REQ
  40 + * 的 IT 覆盖。</p>
  41 + */
  42 +@SpringBootTest
  43 +@AutoConfigureMockMvc
  44 +@ActiveProfiles("test")
  45 +class UsrUserUpdateIT {
  46 +
  47 + @Autowired
  48 + private MockMvc mockMvc;
  49 +
  50 + @Autowired
  51 + private ObjectMapper objectMapper;
  52 +
  53 + @Autowired
  54 + private JwtUtil jwtUtil;
  55 +
  56 + @Autowired
  57 + private UsrUserMapper usrUserMapper;
  58 +
  59 + @Autowired
  60 + private UsrUserPermissionMapper usrUserPermissionMapper;
  61 +
  62 + @Autowired
  63 + private UsrPermissionMapper usrPermissionMapper;
  64 +
  65 + private static final String CALLER = "it2_admin";
  66 +
  67 + @AfterEach
  68 + void cleanup() {
  69 + usrUserMapper.delete(Wrappers.<UsrUser>lambdaQuery()
  70 + .likeRight(UsrUser::getSUserName, "it2_user_"));
  71 + usrPermissionMapper.delete(Wrappers.<UsrPermission>lambdaQuery()
  72 + .likeRight(UsrPermission::getSPermissionCode, "IT2_PERM_"));
  73 + }
  74 +
  75 + private String adminToken() {
  76 + return "Bearer " + jwtUtil.generateToken(CALLER, "超级管理员");
  77 + }
  78 +
  79 + private String normalToken() {
  80 + return "Bearer " + jwtUtil.generateToken("it2_normal", "普通用户");
  81 + }
  82 +
  83 + private Map<String, Object> createBody(String userName) {
  84 + Map<String, Object> body = new HashMap<>();
  85 + body.put("sUserName", userName);
  86 + body.put("sLanguage", "中文");
  87 + return body;
  88 + }
  89 +
  90 + private int createUser(String userName) throws Exception {
  91 + MvcResult result = mockMvc.perform(post("/api/usr/users")
  92 + .header("Authorization", adminToken())
  93 + .contentType("application/json")
  94 + .content(objectMapper.writeValueAsString(createBody(userName))))
  95 + .andExpect(jsonPath("$.code").value(0))
  96 + .andReturn();
  97 + Map<?, ?> resp = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class);
  98 + return (Integer) ((Map<?, ?>) resp.get("data")).get("id");
  99 + }
  100 +
  101 + private int insertPermissionFixture(String codeSuffix) {
  102 + UsrPermission perm = new UsrPermission();
  103 + perm.setSPermissionName("IT2权限" + codeSuffix);
  104 + perm.setSPermissionCode("IT2_PERM_" + codeSuffix);
  105 + usrPermissionMapper.insert(perm);
  106 + return perm.getIIncrement();
  107 + }
  108 +
  109 + // ---------------- AC1:基本信息修改落库,且身份 / 密码 / 审计列不变 ----------------
  110 +
  111 + @Test
  112 + void ac1UpdateBasicInfoPersists() throws Exception {
  113 + int id = createUser("it2_user_ac1");
  114 + UsrUser before = usrUserMapper.selectById(id);
  115 +
  116 + Map<String, Object> body = new HashMap<>();
  117 + body.put("sUserType", "超级管理员");
  118 + body.put("sLanguage", "英文");
  119 + body.put("iCanModifyBill", 1);
  120 + body.put("sUserNo", "NO-AC1");
  121 +
  122 + mockMvc.perform(put("/api/usr/users/" + id)
  123 + .header("Authorization", adminToken())
  124 + .contentType("application/json")
  125 + .content(objectMapper.writeValueAsString(body)))
  126 + .andExpect(status().isOk())
  127 + .andExpect(jsonPath("$.code").value(0))
  128 + .andExpect(jsonPath("$.data.id").value(id));
  129 +
  130 + UsrUser after = usrUserMapper.selectById(id);
  131 + assertThat(after.getSUserType()).isEqualTo("超级管理员");
  132 + assertThat(after.getSLanguage()).isEqualTo("英文");
  133 + assertThat(after.getICanModifyBill()).isEqualTo(1);
  134 + assertThat(after.getSUserNo()).isEqualTo("NO-AC1");
  135 + // 身份 / 密码 / 审计列字节级不变。
  136 + assertThat(after.getSUserName()).isEqualTo(before.getSUserName());
  137 + assertThat(after.getSPassword()).isEqualTo(before.getSPassword());
  138 + assertThat(after.getSCreator()).isEqualTo(before.getSCreator());
  139 + assertThat(after.getTCreateDate()).isEqualTo(before.getTCreateDate());
  140 + }
  141 +
  142 + // ---------------- AC2:目标用户不存在 → 40401,无写入 ----------------
  143 +
  144 + @Test
  145 + void ac2UpdateNonExistentReturns40401() throws Exception {
  146 + Map<String, Object> body = new HashMap<>();
  147 + body.put("sUserType", "普通用户");
  148 + body.put("sLanguage", "中文");
  149 +
  150 + mockMvc.perform(put("/api/usr/users/2000000001")
  151 + .header("Authorization", adminToken())
  152 + .contentType("application/json")
  153 + .content(objectMapper.writeValueAsString(body)))
  154 + .andExpect(status().isOk())
  155 + .andExpect(jsonPath("$.code").value(40401));
  156 +
  157 + assertThat(usrUserMapper.selectById(2000000001)).isNull();
  158 + }
  159 +
  160 + // ---------------- AC3:非法参数(不存在职员)→ 40001,整体回滚无副作用 ----------------
  161 +
  162 + @Test
  163 + void ac3InvalidParamRollsBack() throws Exception {
  164 + int id = createUser("it2_user_ac3");
  165 + UsrUser before = usrUserMapper.selectById(id);
  166 +
  167 + Map<String, Object> body = new HashMap<>();
  168 + body.put("sUserType", "超级管理员");
  169 + body.put("sLanguage", "英文");
  170 + body.put("iEmployeeId", 2000000002); // 不存在的职员
  171 +
  172 + mockMvc.perform(put("/api/usr/users/" + id)
  173 + .header("Authorization", adminToken())
  174 + .contentType("application/json")
  175 + .content(objectMapper.writeValueAsString(body)))
  176 + .andExpect(status().isOk())
  177 + .andExpect(jsonPath("$.code").value(40001));
  178 +
  179 + UsrUser after = usrUserMapper.selectById(id);
  180 + // 目标用户行各列与调用前一致(无副作用)。
  181 + assertThat(after.getSUserType()).isEqualTo(before.getSUserType());
  182 + assertThat(after.getSLanguage()).isEqualTo(before.getSLanguage());
  183 + assertThat(after.getIEmployeeId()).isEqualTo(before.getIEmployeeId());
  184 + }
  185 +
  186 + // ---------------- AC6:权限组全量覆盖 / 清空 / 不改 ----------------
  187 +
  188 + @Test
  189 + void ac6PermissionOverwrite() throws Exception {
  190 + int id = createUser("it2_user_ac6");
  191 + int permA = insertPermissionFixture("A");
  192 + int permB = insertPermissionFixture("B");
  193 + int permC = insertPermissionFixture("C");
  194 + // 预置该用户授权为 {a, c}
  195 + usrUserPermissionMapper.insert(new UsrUserPermission(id, permA));
  196 + usrUserPermissionMapper.insert(new UsrUserPermission(id, permC));
  197 +
  198 + Map<String, Object> body = new HashMap<>();
  199 + body.put("sUserType", "普通用户");
  200 + body.put("sLanguage", "中文");
  201 + body.put("permissionIds", List.of(permA, permB, permA)); // 去重后 {a, b}
  202 +
  203 + mockMvc.perform(put("/api/usr/users/" + id)
  204 + .header("Authorization", adminToken())
  205 + .contentType("application/json")
  206 + .content(objectMapper.writeValueAsString(body)))
  207 + .andExpect(jsonPath("$.code").value(0));
  208 +
  209 + List<UsrUserPermission> grants = usrUserPermissionMapper.selectList(
  210 + Wrappers.<UsrUserPermission>lambdaQuery().eq(UsrUserPermission::getIUserId, id));
  211 + assertThat(grants).extracting(UsrUserPermission::getIPermissionId)
  212 + .containsExactlyInAnyOrder(permA, permB); // c 被删、b 新增、a 去重一次
  213 +
  214 + // 传 [] → 清空全部授权
  215 + Map<String, Object> clearBody = new HashMap<>();
  216 + clearBody.put("sUserType", "普通用户");
  217 + clearBody.put("sLanguage", "中文");
  218 + clearBody.put("permissionIds", List.of());
  219 + mockMvc.perform(put("/api/usr/users/" + id)
  220 + .header("Authorization", adminToken())
  221 + .contentType("application/json")
  222 + .content(objectMapper.writeValueAsString(clearBody)))
  223 + .andExpect(jsonPath("$.code").value(0));
  224 + long afterClear = usrUserPermissionMapper.selectCount(
  225 + Wrappers.<UsrUserPermission>lambdaQuery().eq(UsrUserPermission::getIUserId, id));
  226 + assertThat(afterClear).isZero();
  227 +
  228 + // 预置一条授权后,不传 permissionIds → 授权不变
  229 + usrUserPermissionMapper.insert(new UsrUserPermission(id, permA));
  230 + Map<String, Object> noPermBody = new HashMap<>();
  231 + noPermBody.put("sUserType", "普通用户");
  232 + noPermBody.put("sLanguage", "中文");
  233 + mockMvc.perform(put("/api/usr/users/" + id)
  234 + .header("Authorization", adminToken())
  235 + .contentType("application/json")
  236 + .content(objectMapper.writeValueAsString(noPermBody)))
  237 + .andExpect(jsonPath("$.code").value(0));
  238 + long afterNoChange = usrUserPermissionMapper.selectCount(
  239 + Wrappers.<UsrUserPermission>lambdaQuery().eq(UsrUserPermission::getIUserId, id));
  240 + assertThat(afterNoChange).isEqualTo(1L);
  241 + }
  242 +
  243 + // ---------------- AC7:非管理员 / 无 token 被拦截,目标行未被修改 ----------------
  244 +
  245 + @Test
  246 + void ac7NonAdminAndNoTokenBlocked() throws Exception {
  247 + int id = createUser("it2_user_ac7");
  248 + UsrUser before = usrUserMapper.selectById(id);
  249 +
  250 + Map<String, Object> body = new HashMap<>();
  251 + body.put("sUserType", "超级管理员");
  252 + body.put("sLanguage", "英文");
  253 +
  254 + // 普通用户 token → 40301
  255 + mockMvc.perform(put("/api/usr/users/" + id)
  256 + .header("Authorization", normalToken())
  257 + .contentType("application/json")
  258 + .content(objectMapper.writeValueAsString(body)))
  259 + .andExpect(jsonPath("$.code").value(40301));
  260 +
  261 + // 无 token → 401(安全链拦截)
  262 + mockMvc.perform(put("/api/usr/users/" + id)
  263 + .contentType("application/json")
  264 + .content(objectMapper.writeValueAsString(body)))
  265 + .andExpect(status().isUnauthorized());
  266 +
  267 + UsrUser after = usrUserMapper.selectById(id);
  268 + assertThat(after.getSUserType()).isEqualTo(before.getSUserType());
  269 + assertThat(after.getSLanguage()).isEqualTo(before.getSLanguage());
  270 + }
  271 +
  272 + // ---------------- AC8:密码不变且不出现在响应;iIsVoid 落库(AC4/AC5 落库层等价验证) ----------------
  273 +
  274 + @Test
  275 + void ac8PasswordUnchangedAndAbsentFromResponse() throws Exception {
  276 + int id = createUser("it2_user_ac8");
  277 + UsrUser before = usrUserMapper.selectById(id);
  278 +
  279 + Map<String, Object> body = new HashMap<>();
  280 + body.put("sUserType", "超级管理员"); // AC5 角色变更落库层等价验证
  281 + body.put("sLanguage", "中文");
  282 + body.put("iIsVoid", 1); // AC4 禁用落库层等价验证
  283 +
  284 + MvcResult result = mockMvc.perform(put("/api/usr/users/" + id)
  285 + .header("Authorization", adminToken())
  286 + .contentType("application/json")
  287 + .content(objectMapper.writeValueAsString(body)))
  288 + .andExpect(jsonPath("$.code").value(0))
  289 + .andExpect(jsonPath("$.data.id").value(id))
  290 + .andExpect(jsonPath("$.data.sPassword").doesNotExist())
  291 + .andExpect(jsonPath("$.data.password").doesNotExist())
  292 + .andReturn();
  293 +
  294 + String responseBody = result.getResponse().getContentAsString();
  295 + assertThat(responseBody.toLowerCase()).doesNotContain("password");
  296 +
  297 + UsrUser after = usrUserMapper.selectById(id);
  298 + // 密码列字节级不变。
  299 + assertThat(after.getSPassword()).isEqualTo(before.getSPassword());
  300 + // AC4 / AC5 落库层等价验证:禁用状态 / 角色变更读回库内为新值。
  301 + assertThat(after.getIIsVoid()).isEqualTo(1);
  302 + assertThat(after.getSUserType()).isEqualTo("超级管理员");
  303 + }
  304 +}