Commit 943857d0ae5794feab71c40b7e2af07d1c1c913d

Authored by zichun
1 parent 83a1ea9c

feat(usr): POST /api/v1/users controller + 端到端测试 REQ-USR-002

backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java 0 → 100644
  1 +package com.xly.erp.module.usr.controller;
  2 +
  3 +import com.xly.erp.common.response.Result;
  4 +import com.xly.erp.common.security.LoginContext;
  5 +import com.xly.erp.common.security.RequireSuperAdmin;
  6 +import com.xly.erp.module.usr.dto.CreateUserReq;
  7 +import com.xly.erp.module.usr.service.UserCreateService;
  8 +import com.xly.erp.module.usr.vo.CreateUserVo;
  9 +import jakarta.validation.Valid;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.http.HttpStatus;
  12 +import org.springframework.http.ResponseEntity;
  13 +import org.springframework.web.bind.annotation.PostMapping;
  14 +import org.springframework.web.bind.annotation.RequestBody;
  15 +import org.springframework.web.bind.annotation.RequestMapping;
  16 +import org.springframework.web.bind.annotation.RestController;
  17 +
  18 +@RestController
  19 +@RequestMapping("/api/v1/users")
  20 +@RequiredArgsConstructor
  21 +public class UserController {
  22 +
  23 + private final UserCreateService userCreateService;
  24 +
  25 + @PostMapping
  26 + @RequireSuperAdmin
  27 + public ResponseEntity<Result<CreateUserVo>> create(@RequestBody @Valid CreateUserReq req) {
  28 + String operator = LoginContext.current().username();
  29 + CreateUserVo vo = userCreateService.create(req, operator);
  30 + return ResponseEntity.status(HttpStatus.CREATED).body(Result.ok(vo));
  31 + }
  32 +}
... ...
backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerTest.java 0 → 100644
  1 +package com.xly.erp.module.usr.controller;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +import com.fasterxml.jackson.databind.node.ObjectNode;
  5 +import com.xly.erp.common.response.ErrorCode;
  6 +import com.xly.erp.common.security.JwtUtil;
  7 +import com.xly.erp.module.usr.dto.CreateUserReq;
  8 +import com.xly.erp.module.usr.dto.LoginReq;
  9 +import com.xly.erp.module.usr.support.LoginTestSeeder;
  10 +import org.junit.jupiter.api.BeforeEach;
  11 +import org.junit.jupiter.api.Test;
  12 +import org.springframework.beans.factory.annotation.Autowired;
  13 +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  14 +import org.springframework.boot.test.context.SpringBootTest;
  15 +import org.springframework.http.MediaType;
  16 +import org.springframework.test.context.ActiveProfiles;
  17 +import org.springframework.test.web.servlet.MockMvc;
  18 +
  19 +import java.util.HashMap;
  20 +import java.util.List;
  21 +import java.util.Map;
  22 +
  23 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
  24 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  25 +
  26 +@SpringBootTest
  27 +@AutoConfigureMockMvc
  28 +@ActiveProfiles("test")
  29 +class UserControllerTest {
  30 +
  31 + @Autowired private MockMvc mvc;
  32 + @Autowired private ObjectMapper json;
  33 + @Autowired private LoginTestSeeder seeder;
  34 + @Autowired private JwtUtil jwtUtil;
  35 +
  36 + private LoginTestSeeder.Fixture fx;
  37 + private String adminToken;
  38 + private String normalToken;
  39 + private String deletedToken;
  40 +
  41 + @BeforeEach
  42 + void setUp() {
  43 + fx = seeder.reset();
  44 + adminToken = issue(LoginTestSeeder.USER_ADMIN, "SUPER_ADMIN");
  45 + normalToken = issue(LoginTestSeeder.USER_OK, "NORMAL");
  46 + deletedToken = issue(LoginTestSeeder.USER_DELETED, "NORMAL");
  47 + }
  48 +
  49 + private String issue(String username, String userType) {
  50 + Map<String, Object> claims = new HashMap<>();
  51 + claims.put("sub", 1);
  52 + claims.put("username", username);
  53 + claims.put("userType", userType);
  54 + claims.put("companyCode", LoginTestSeeder.COMPANY_OK);
  55 + claims.put("language", "zh-CN");
  56 + return jwtUtil.issue(claims, 7200);
  57 + }
  58 +
  59 + private CreateUserReq buildReq() {
  60 + CreateUserReq r = new CreateUserReq();
  61 + r.setUsername("newbie");
  62 + r.setUserCode("U010");
  63 + r.setUserType("NORMAL");
  64 + r.setLanguage("zh-CN");
  65 + r.setCanEditDocument(false);
  66 + return r;
  67 + }
  68 +
  69 + private String body(Object o) throws Exception {
  70 + return json.writeValueAsString(o);
  71 + }
  72 +
  73 + @Test
  74 + void post_users_success_returns201_andCreatedVo() throws Exception {
  75 + mvc.perform(post("/api/v1/users")
  76 + .header("Authorization", "Bearer " + adminToken)
  77 + .contentType(MediaType.APPLICATION_JSON)
  78 + .content(body(buildReq())))
  79 + .andExpect(status().isCreated())
  80 + .andExpect(jsonPath("$.code").value(ErrorCode.OK))
  81 + .andExpect(jsonPath("$.data.userId").isNumber())
  82 + .andExpect(jsonPath("$.data.username").value("newbie"))
  83 + .andExpect(jsonPath("$.data.userCode").value("U010"));
  84 + }
  85 +
  86 + @Test
  87 + void post_users_blankUsername_returns400_40001() throws Exception {
  88 + CreateUserReq r = buildReq();
  89 + r.setUsername("");
  90 + mvc.perform(post("/api/v1/users")
  91 + .header("Authorization", "Bearer " + adminToken)
  92 + .contentType(MediaType.APPLICATION_JSON)
  93 + .content(body(r)))
  94 + .andExpect(status().isBadRequest())
  95 + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST));
  96 + }
  97 +
  98 + @Test
  99 + void post_users_invalidUserType_returns400_40001() throws Exception {
  100 + CreateUserReq r = buildReq();
  101 + r.setUserType("ROOT");
  102 + mvc.perform(post("/api/v1/users")
  103 + .header("Authorization", "Bearer " + adminToken)
  104 + .contentType(MediaType.APPLICATION_JSON)
  105 + .content(body(r)))
  106 + .andExpect(status().isBadRequest())
  107 + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST));
  108 + }
  109 +
  110 + @Test
  111 + void post_users_unknownPropertyPassword_returns400_40001() throws Exception {
  112 + ObjectNode body = json.valueToTree(buildReq());
  113 + body.put("password", "Password1!");
  114 + mvc.perform(post("/api/v1/users")
  115 + .header("Authorization", "Bearer " + adminToken)
  116 + .contentType(MediaType.APPLICATION_JSON)
  117 + .content(body.toString()))
  118 + .andExpect(status().isBadRequest())
  119 + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST));
  120 + }
  121 +
  122 + @Test
  123 + void post_users_noAuthHeader_returns401_40101() throws Exception {
  124 + mvc.perform(post("/api/v1/users")
  125 + .contentType(MediaType.APPLICATION_JSON)
  126 + .content(body(buildReq())))
  127 + .andExpect(status().isUnauthorized())
  128 + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_CREDENTIALS));
  129 + }
  130 +
  131 + @Test
  132 + void post_users_normalUserToken_returns403_40301() throws Exception {
  133 + mvc.perform(post("/api/v1/users")
  134 + .header("Authorization", "Bearer " + normalToken)
  135 + .contentType(MediaType.APPLICATION_JSON)
  136 + .content(body(buildReq())))
  137 + .andExpect(status().isForbidden())
  138 + .andExpect(jsonPath("$.code").value(ErrorCode.FORBIDDEN));
  139 + }
  140 +
  141 + @Test
  142 + void post_users_deletedUserToken_returns401_40101() throws Exception {
  143 + mvc.perform(post("/api/v1/users")
  144 + .header("Authorization", "Bearer " + deletedToken)
  145 + .contentType(MediaType.APPLICATION_JSON)
  146 + .content(body(buildReq())))
  147 + .andExpect(status().isUnauthorized())
  148 + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_CREDENTIALS));
  149 + }
  150 +
  151 + @Test
  152 + void post_users_duplicateUsername_returns409_40901() throws Exception {
  153 + CreateUserReq r = buildReq();
  154 + r.setUsername(LoginTestSeeder.USER_OK);
  155 + mvc.perform(post("/api/v1/users")
  156 + .header("Authorization", "Bearer " + adminToken)
  157 + .contentType(MediaType.APPLICATION_JSON)
  158 + .content(body(r)))
  159 + .andExpect(status().isConflict())
  160 + .andExpect(jsonPath("$.code").value(ErrorCode.CONFLICT_USERNAME));
  161 + }
  162 +
  163 + @Test
  164 + void post_users_duplicateUserCode_returns409_40902() throws Exception {
  165 + CreateUserReq r = buildReq();
  166 + r.setUserCode("U001");
  167 + mvc.perform(post("/api/v1/users")
  168 + .header("Authorization", "Bearer " + adminToken)
  169 + .contentType(MediaType.APPLICATION_JSON)
  170 + .content(body(r)))
  171 + .andExpect(status().isConflict())
  172 + .andExpect(jsonPath("$.code").value(ErrorCode.CONFLICT_USERCODE));
  173 + }
  174 +
  175 + @Test
  176 + void post_users_unknownEmployee_returns400_40004() throws Exception {
  177 + CreateUserReq r = buildReq();
  178 + r.setEmployeeId(99999);
  179 + mvc.perform(post("/api/v1/users")
  180 + .header("Authorization", "Bearer " + adminToken)
  181 + .contentType(MediaType.APPLICATION_JSON)
  182 + .content(body(r)))
  183 + .andExpect(status().isBadRequest())
  184 + .andExpect(jsonPath("$.code").value(ErrorCode.COMPANY_NOT_FOUND));
  185 + }
  186 +
  187 + @Test
  188 + void post_users_unknownPermissionCategory_returns400_40004() throws Exception {
  189 + CreateUserReq r = buildReq();
  190 + r.setPermissionCategoryIds(List.of(99999));
  191 + mvc.perform(post("/api/v1/users")
  192 + .header("Authorization", "Bearer " + adminToken)
  193 + .contentType(MediaType.APPLICATION_JSON)
  194 + .content(body(r)))
  195 + .andExpect(status().isBadRequest())
  196 + .andExpect(jsonPath("$.code").value(ErrorCode.COMPANY_NOT_FOUND));
  197 + }
  198 +
  199 + @Test
  200 + void post_users_success_canLoginWithInitialPassword() throws Exception {
  201 + // 1) admin 创建用户
  202 + mvc.perform(post("/api/v1/users")
  203 + .header("Authorization", "Bearer " + adminToken)
  204 + .contentType(MediaType.APPLICATION_JSON)
  205 + .content(body(buildReq())))
  206 + .andExpect(status().isCreated());
  207 +
  208 + // 2) 新用户用初始密码 666666 登录应成功
  209 + LoginReq login = new LoginReq();
  210 + login.setUsername("newbie");
  211 + login.setPassword("666666");
  212 + login.setCompanyCode(LoginTestSeeder.COMPANY_OK);
  213 + mvc.perform(post("/api/v1/auth/login")
  214 + .contentType(MediaType.APPLICATION_JSON)
  215 + .content(body(login)))
  216 + .andExpect(status().isOk())
  217 + .andExpect(jsonPath("$.code").value(ErrorCode.OK))
  218 + .andExpect(jsonPath("$.data.accessToken").isNotEmpty());
  219 + }
  220 +}
... ...