diff --git a/backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java b/backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java new file mode 100644 index 0000000..33e136a --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java @@ -0,0 +1,32 @@ +package com.xly.erp.module.usr.controller; + +import com.xly.erp.common.response.Result; +import com.xly.erp.common.security.LoginContext; +import com.xly.erp.common.security.RequireSuperAdmin; +import com.xly.erp.module.usr.dto.CreateUserReq; +import com.xly.erp.module.usr.service.UserCreateService; +import com.xly.erp.module.usr.vo.CreateUserVo; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserCreateService userCreateService; + + @PostMapping + @RequireSuperAdmin + public ResponseEntity> create(@RequestBody @Valid CreateUserReq req) { + String operator = LoginContext.current().username(); + CreateUserVo vo = userCreateService.create(req, operator); + return ResponseEntity.status(HttpStatus.CREATED).body(Result.ok(vo)); + } +} diff --git a/backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerTest.java b/backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerTest.java new file mode 100644 index 0000000..c87a408 --- /dev/null +++ b/backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerTest.java @@ -0,0 +1,220 @@ +package com.xly.erp.module.usr.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.xly.erp.common.response.ErrorCode; +import com.xly.erp.common.security.JwtUtil; +import com.xly.erp.module.usr.dto.CreateUserReq; +import com.xly.erp.module.usr.dto.LoginReq; +import com.xly.erp.module.usr.support.LoginTestSeeder; +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.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class UserControllerTest { + + @Autowired private MockMvc mvc; + @Autowired private ObjectMapper json; + @Autowired private LoginTestSeeder seeder; + @Autowired private JwtUtil jwtUtil; + + private LoginTestSeeder.Fixture fx; + private String adminToken; + private String normalToken; + private String deletedToken; + + @BeforeEach + void setUp() { + fx = seeder.reset(); + adminToken = issue(LoginTestSeeder.USER_ADMIN, "SUPER_ADMIN"); + normalToken = issue(LoginTestSeeder.USER_OK, "NORMAL"); + deletedToken = issue(LoginTestSeeder.USER_DELETED, "NORMAL"); + } + + private String issue(String username, String userType) { + Map claims = new HashMap<>(); + claims.put("sub", 1); + claims.put("username", username); + claims.put("userType", userType); + claims.put("companyCode", LoginTestSeeder.COMPANY_OK); + claims.put("language", "zh-CN"); + return jwtUtil.issue(claims, 7200); + } + + private CreateUserReq buildReq() { + CreateUserReq r = new CreateUserReq(); + r.setUsername("newbie"); + r.setUserCode("U010"); + r.setUserType("NORMAL"); + r.setLanguage("zh-CN"); + r.setCanEditDocument(false); + return r; + } + + private String body(Object o) throws Exception { + return json.writeValueAsString(o); + } + + @Test + void post_users_success_returns201_andCreatedVo() throws Exception { + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body(buildReq()))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.code").value(ErrorCode.OK)) + .andExpect(jsonPath("$.data.userId").isNumber()) + .andExpect(jsonPath("$.data.username").value("newbie")) + .andExpect(jsonPath("$.data.userCode").value("U010")); + } + + @Test + void post_users_blankUsername_returns400_40001() throws Exception { + CreateUserReq r = buildReq(); + r.setUsername(""); + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body(r))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST)); + } + + @Test + void post_users_invalidUserType_returns400_40001() throws Exception { + CreateUserReq r = buildReq(); + r.setUserType("ROOT"); + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body(r))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST)); + } + + @Test + void post_users_unknownPropertyPassword_returns400_40001() throws Exception { + ObjectNode body = json.valueToTree(buildReq()); + body.put("password", "Password1!"); + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body.toString())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST)); + } + + @Test + void post_users_noAuthHeader_returns401_40101() throws Exception { + mvc.perform(post("/api/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(body(buildReq()))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_CREDENTIALS)); + } + + @Test + void post_users_normalUserToken_returns403_40301() throws Exception { + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + normalToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body(buildReq()))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(ErrorCode.FORBIDDEN)); + } + + @Test + void post_users_deletedUserToken_returns401_40101() throws Exception { + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + deletedToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body(buildReq()))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_CREDENTIALS)); + } + + @Test + void post_users_duplicateUsername_returns409_40901() throws Exception { + CreateUserReq r = buildReq(); + r.setUsername(LoginTestSeeder.USER_OK); + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body(r))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value(ErrorCode.CONFLICT_USERNAME)); + } + + @Test + void post_users_duplicateUserCode_returns409_40902() throws Exception { + CreateUserReq r = buildReq(); + r.setUserCode("U001"); + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body(r))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value(ErrorCode.CONFLICT_USERCODE)); + } + + @Test + void post_users_unknownEmployee_returns400_40004() throws Exception { + CreateUserReq r = buildReq(); + r.setEmployeeId(99999); + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body(r))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.COMPANY_NOT_FOUND)); + } + + @Test + void post_users_unknownPermissionCategory_returns400_40004() throws Exception { + CreateUserReq r = buildReq(); + r.setPermissionCategoryIds(List.of(99999)); + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body(r))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.COMPANY_NOT_FOUND)); + } + + @Test + void post_users_success_canLoginWithInitialPassword() throws Exception { + // 1) admin 创建用户 + mvc.perform(post("/api/v1/users") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(body(buildReq()))) + .andExpect(status().isCreated()); + + // 2) 新用户用初始密码 666666 登录应成功 + LoginReq login = new LoginReq(); + login.setUsername("newbie"); + login.setPassword("666666"); + login.setCompanyCode(LoginTestSeeder.COMPANY_OK); + mvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body(login))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(ErrorCode.OK)) + .andExpect(jsonPath("$.data.accessToken").isNotEmpty()); + } +}