Commit d931bbcb78e01a84a8f57b7fb470de8037903d2a

Authored by zichun
1 parent 89786804

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

backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java
1 1 package com.xly.erp.module.usr.controller;
2 2  
  3 +import com.xly.erp.common.response.PageResult;
3 4 import com.xly.erp.common.response.Result;
4 5 import com.xly.erp.common.security.LoginContext;
5 6 import com.xly.erp.common.security.RequireSuperAdmin;
6 7 import com.xly.erp.module.usr.dto.CreateUserReq;
7 8 import com.xly.erp.module.usr.dto.UpdateUserReq;
  9 +import com.xly.erp.module.usr.dto.UserQueryReq;
8 10 import com.xly.erp.module.usr.service.UserCreateService;
9 11 import com.xly.erp.module.usr.service.UserDetailService;
  12 +import com.xly.erp.module.usr.service.UserListService;
10 13 import com.xly.erp.module.usr.service.UserUpdateService;
11 14 import com.xly.erp.module.usr.vo.CreateUserVo;
12 15 import com.xly.erp.module.usr.vo.UserDetailVo;
  16 +import com.xly.erp.module.usr.vo.UserListItemVo;
13 17 import jakarta.validation.Valid;
14 18 import lombok.RequiredArgsConstructor;
15 19 import org.springframework.http.HttpStatus;
... ... @@ -30,6 +34,7 @@ public class UserController {
30 34 private final UserCreateService userCreateService;
31 35 private final UserDetailService userDetailService;
32 36 private final UserUpdateService userUpdateService;
  37 + private final UserListService userListService;
33 38  
34 39 @PostMapping
35 40 @RequireSuperAdmin
... ... @@ -39,6 +44,12 @@ public class UserController {
39 44 return ResponseEntity.status(HttpStatus.CREATED).body(Result.ok(vo));
40 45 }
41 46  
  47 + @GetMapping
  48 + @RequireSuperAdmin
  49 + public Result<PageResult<UserListItemVo>> list(@Valid UserQueryReq req) {
  50 + return Result.ok(userListService.list(req));
  51 + }
  52 +
42 53 @GetMapping("/{userId}")
43 54 @RequireSuperAdmin
44 55 public Result<UserDetailVo> getById(@PathVariable Integer userId) {
... ...
backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerListTest.java 0 → 100644
  1 +package com.xly.erp.module.usr.controller;
  2 +
  3 +import com.xly.erp.common.response.ErrorCode;
  4 +import com.xly.erp.common.security.JwtUtil;
  5 +import com.xly.erp.module.usr.support.LoginTestSeeder;
  6 +import org.junit.jupiter.api.BeforeEach;
  7 +import org.junit.jupiter.api.Test;
  8 +import org.springframework.beans.factory.annotation.Autowired;
  9 +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  10 +import org.springframework.boot.test.context.SpringBootTest;
  11 +import org.springframework.jdbc.core.JdbcTemplate;
  12 +import org.springframework.test.context.ActiveProfiles;
  13 +import org.springframework.test.web.servlet.MockMvc;
  14 +
  15 +import java.util.HashMap;
  16 +import java.util.Map;
  17 +
  18 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
  19 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  20 +
  21 +@SpringBootTest
  22 +@AutoConfigureMockMvc
  23 +@ActiveProfiles("test")
  24 +class UserControllerListTest {
  25 +
  26 + @Autowired private MockMvc mvc;
  27 + @Autowired private LoginTestSeeder seeder;
  28 + @Autowired private JwtUtil jwtUtil;
  29 + @Autowired private JdbcTemplate jdbc;
  30 +
  31 + private LoginTestSeeder.Fixture fx;
  32 + private String adminToken;
  33 + private String normalToken;
  34 +
  35 + @BeforeEach
  36 + void setUp() {
  37 + fx = seeder.reset();
  38 + adminToken = issue(LoginTestSeeder.USER_ADMIN, "SUPER_ADMIN", fx.adminId());
  39 + normalToken = issue(LoginTestSeeder.USER_OK, "NORMAL", fx.aliceId());
  40 + }
  41 +
  42 + private String issue(String username, String userType, Integer userId) {
  43 + Map<String, Object> c = new HashMap<>();
  44 + c.put("sub", userId);
  45 + c.put("username", username);
  46 + c.put("userType", userType);
  47 + c.put("companyCode", LoginTestSeeder.COMPANY_OK);
  48 + c.put("language", "zh-CN");
  49 + return jwtUtil.issue(c, 7200);
  50 + }
  51 +
  52 + @Test
  53 + void list_default_returnsAllUsers() throws Exception {
  54 + mvc.perform(get("/api/v1/users").header("Authorization", "Bearer " + adminToken))
  55 + .andExpect(status().isOk())
  56 + .andExpect(jsonPath("$.code").value(ErrorCode.OK))
  57 + .andExpect(jsonPath("$.data.total").value(3))
  58 + .andExpect(jsonPath("$.data.records.length()").value(3))
  59 + .andExpect(jsonPath("$.data.page").value(1))
  60 + .andExpect(jsonPath("$.data.size").value(20));
  61 + }
  62 +
  63 + @Test
  64 + void list_sizeOver100_returns400_40001() throws Exception {
  65 + mvc.perform(get("/api/v1/users")
  66 + .param("size", "200")
  67 + .header("Authorization", "Bearer " + adminToken))
  68 + .andExpect(status().isBadRequest())
  69 + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST));
  70 + }
  71 +
  72 + @Test
  73 + void list_pageZero_returns400_40001() throws Exception {
  74 + mvc.perform(get("/api/v1/users")
  75 + .param("page", "0")
  76 + .header("Authorization", "Bearer " + adminToken))
  77 + .andExpect(status().isBadRequest())
  78 + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST));
  79 + }
  80 +
  81 + @Test
  82 + void list_sortByUsernameAsc() throws Exception {
  83 + mvc.perform(get("/api/v1/users")
  84 + .param("sortField", "sUsername")
  85 + .param("sortOrder", "asc")
  86 + .header("Authorization", "Bearer " + adminToken))
  87 + .andExpect(status().isOk())
  88 + .andExpect(jsonPath("$.data.records[0].username").value(LoginTestSeeder.USER_ADMIN));
  89 + }
  90 +
  91 + @Test
  92 + void list_sortFieldInvalid_returns400_40003() throws Exception {
  93 + mvc.perform(get("/api/v1/users")
  94 + .param("sortField", "badField")
  95 + .header("Authorization", "Bearer " + adminToken))
  96 + .andExpect(status().isBadRequest())
  97 + .andExpect(jsonPath("$.code").value(ErrorCode.INVALID_ENUM_PARAM));
  98 + }
  99 +
  100 + @Test
  101 + void list_sortOrderInvalid_returns400_40001() throws Exception {
  102 + mvc.perform(get("/api/v1/users")
  103 + .param("sortOrder", "foo")
  104 + .header("Authorization", "Bearer " + adminToken))
  105 + .andExpect(status().isBadRequest())
  106 + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST));
  107 + }
  108 +
  109 + @Test
  110 + void list_queryByUsernameContains() throws Exception {
  111 + mvc.perform(get("/api/v1/users")
  112 + .param("queryField", "username")
  113 + .param("matchMode", "contains")
  114 + .param("queryValue", "ali")
  115 + .header("Authorization", "Bearer " + adminToken))
  116 + .andExpect(status().isOk())
  117 + .andExpect(jsonPath("$.data.total").value(1))
  118 + .andExpect(jsonPath("$.data.records[0].username").value(LoginTestSeeder.USER_OK));
  119 + }
  120 +
  121 + @Test
  122 + void list_queryByDepartmentName_multiJoin() throws Exception {
  123 + mvc.perform(get("/api/v1/users")
  124 + .param("queryField", "departmentName")
  125 + .param("matchMode", "equals")
  126 + .param("queryValue", "技术部")
  127 + .header("Authorization", "Bearer " + adminToken))
  128 + .andExpect(status().isOk())
  129 + .andExpect(jsonPath("$.data.total").value(1))
  130 + .andExpect(jsonPath("$.data.records[0].departmentName").value("技术部"));
  131 + }
  132 +
  133 + @Test
  134 + void list_queryByIsDeletedTrue() throws Exception {
  135 + mvc.perform(get("/api/v1/users")
  136 + .param("queryField", "isDeleted")
  137 + .param("matchMode", "equals")
  138 + .param("queryValue", "true")
  139 + .header("Authorization", "Bearer " + adminToken))
  140 + .andExpect(status().isOk())
  141 + .andExpect(jsonPath("$.data.total").value(1))
  142 + .andExpect(jsonPath("$.data.records[0].username").value(LoginTestSeeder.USER_DELETED));
  143 + }
  144 +
  145 + @Test
  146 + void list_queryFieldInvalid_returns400_40003() throws Exception {
  147 + mvc.perform(get("/api/v1/users")
  148 + .param("queryField", "badField")
  149 + .param("queryValue", "x")
  150 + .header("Authorization", "Bearer " + adminToken))
  151 + .andExpect(status().isBadRequest())
  152 + .andExpect(jsonPath("$.code").value(ErrorCode.INVALID_ENUM_PARAM));
  153 + }
  154 +
  155 + @Test
  156 + void list_matchModeInvalid_returns400_40003() throws Exception {
  157 + mvc.perform(get("/api/v1/users")
  158 + .param("matchMode", "startsWith")
  159 + .header("Authorization", "Bearer " + adminToken))
  160 + .andExpect(status().isBadRequest())
  161 + .andExpect(jsonPath("$.code").value(ErrorCode.INVALID_ENUM_PARAM));
  162 + }
  163 +
  164 + @Test
  165 + void list_explicitUserTypeFilter() throws Exception {
  166 + mvc.perform(get("/api/v1/users")
  167 + .param("userType", "NORMAL")
  168 + .header("Authorization", "Bearer " + adminToken))
  169 + .andExpect(status().isOk())
  170 + .andExpect(jsonPath("$.data.total").value(2)); // alice + bob_deleted
  171 + }
  172 +
  173 + @Test
  174 + void list_explicitUserTypeInvalid_returns400_40001() throws Exception {
  175 + mvc.perform(get("/api/v1/users")
  176 + .param("userType", "HACKER")
  177 + .header("Authorization", "Bearer " + adminToken))
  178 + .andExpect(status().isBadRequest())
  179 + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST));
  180 + }
  181 +
  182 + @Test
  183 + void list_explicitIsDeletedFalse_filtersActive() throws Exception {
  184 + mvc.perform(get("/api/v1/users")
  185 + .param("isDeleted", "false")
  186 + .header("Authorization", "Bearer " + adminToken))
  187 + .andExpect(status().isOk())
  188 + .andExpect(jsonPath("$.data.total").value(2)); // alice + admin
  189 + }
  190 +
  191 + @Test
  192 + void list_composedFilters_andSemantics() throws Exception {
  193 + mvc.perform(get("/api/v1/users")
  194 + .param("queryField", "username")
  195 + .param("queryValue", "a")
  196 + .param("userType", "NORMAL")
  197 + .param("isDeleted", "false")
  198 + .header("Authorization", "Bearer " + adminToken))
  199 + .andExpect(status().isOk())
  200 + .andExpect(jsonPath("$.data.total").value(1))
  201 + .andExpect(jsonPath("$.data.records[0].username").value(LoginTestSeeder.USER_OK));
  202 + }
  203 +
  204 + @Test
  205 + void list_pageBeyondTotal_returnsLastPage() throws Exception {
  206 + mvc.perform(get("/api/v1/users")
  207 + .param("page", "999")
  208 + .param("size", "10")
  209 + .header("Authorization", "Bearer " + adminToken))
  210 + .andExpect(status().isOk())
  211 + .andExpect(jsonPath("$.data.total").value(3))
  212 + .andExpect(jsonPath("$.data.page").value(1))
  213 + .andExpect(jsonPath("$.data.records.length()").value(3));
  214 + }
  215 +
  216 + @Test
  217 + void list_normalUserToken_returns403_40301() throws Exception {
  218 + mvc.perform(get("/api/v1/users").header("Authorization", "Bearer " + normalToken))
  219 + .andExpect(status().isForbidden())
  220 + .andExpect(jsonPath("$.code").value(ErrorCode.FORBIDDEN));
  221 + }
  222 +
  223 + @Test
  224 + void list_noAuthHeader_returns401_40101() throws Exception {
  225 + mvc.perform(get("/api/v1/users"))
  226 + .andExpect(status().isUnauthorized())
  227 + .andExpect(jsonPath("$.code").value(ErrorCode.BAD_CREDENTIALS));
  228 + }
  229 +
  230 + @Test
  231 + void list_responseRecordDoesNotIncludePasswordField() throws Exception {
  232 + mvc.perform(get("/api/v1/users").header("Authorization", "Bearer " + adminToken))
  233 + .andExpect(status().isOk())
  234 + .andExpect(jsonPath("$.data.records[0].password").doesNotExist())
  235 + .andExpect(jsonPath("$.data.records[0].passwordHash").doesNotExist())
  236 + .andExpect(jsonPath("$.data.records[0].sPasswordHash").doesNotExist());
  237 + }
  238 +
  239 + @Test
  240 + void list_emptyTable_returnsZeroTotal() throws Exception {
  241 + jdbc.update("DELETE FROM sys_user_permission_category");
  242 + jdbc.update("DELETE FROM sys_user");
  243 + // 但鉴权需要 token 关联用户存在,所以保留 admin 用户即可
  244 + // 重做:只删 alice / bob
  245 + // 重置后用一个独立 seed
  246 + seeder.reset();
  247 + jdbc.update("DELETE FROM sys_user WHERE sUsername IN (?, ?)",
  248 + LoginTestSeeder.USER_OK, LoginTestSeeder.USER_DELETED);
  249 +
  250 + mvc.perform(get("/api/v1/users").header("Authorization", "Bearer " + adminToken))
  251 + .andExpect(status().isOk())
  252 + .andExpect(jsonPath("$.data.total").value(1)); // 只剩 admin
  253 + }
  254 +}
... ...