userListLogic_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. package user
  2. import (
  3. "errors"
  4. "testing"
  5. "time"
  6. productMemberModel "perms-system-server/internal/model/productmember"
  7. userModelPkg "perms-system-server/internal/model/user"
  8. "perms-system-server/internal/response"
  9. "perms-system-server/internal/svc"
  10. "perms-system-server/internal/testutil"
  11. "perms-system-server/internal/testutil/ctxhelper"
  12. "perms-system-server/internal/types"
  13. "github.com/stretchr/testify/assert"
  14. "github.com/stretchr/testify/require"
  15. )
  16. // TC-0176: 含productCode
  17. func TestUserList_WithProductCode(t *testing.T) {
  18. ctx := ctxhelper.SuperAdminCtx()
  19. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  20. conn := testutil.GetTestSqlConn()
  21. username := testutil.UniqueId()
  22. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  23. productCode := testutil.UniqueId()
  24. now := time.Now().Unix()
  25. memberRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productMemberModel.SysProductMember{
  26. ProductCode: productCode,
  27. UserId: userId,
  28. MemberType: "ADMIN",
  29. Status: 1,
  30. CreateTime: now,
  31. UpdateTime: now,
  32. })
  33. require.NoError(t, err)
  34. memberId, _ := memberRes.LastInsertId()
  35. t.Cleanup(func() {
  36. testutil.CleanTable(ctx, conn, "`sys_product_member`", memberId)
  37. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  38. })
  39. logic := NewUserListLogic(ctx, svcCtx)
  40. resp, err := logic.UserList(&types.UserListReq{
  41. ProductCode: productCode,
  42. Page: 1,
  43. PageSize: 100,
  44. })
  45. require.NoError(t, err)
  46. require.NotNil(t, resp)
  47. assert.Greater(t, resp.Total, int64(0))
  48. items := resp.List.([]types.UserItem)
  49. found := false
  50. for _, item := range items {
  51. if item.Id == userId {
  52. found = true
  53. assert.Equal(t, "ADMIN", item.MemberType)
  54. break
  55. }
  56. }
  57. assert.True(t, found, "should find the inserted user in the list")
  58. }
  59. // TC-0177: 不含productCode
  60. func TestUserList_WithoutProductCode(t *testing.T) {
  61. ctx := ctxhelper.SuperAdminCtx()
  62. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  63. conn := testutil.GetTestSqlConn()
  64. username := testutil.UniqueId()
  65. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  66. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
  67. logic := NewUserListLogic(ctx, svcCtx)
  68. resp, err := logic.UserList(&types.UserListReq{
  69. Page: 1,
  70. PageSize: 100,
  71. })
  72. require.NoError(t, err)
  73. require.NotNil(t, resp)
  74. items := resp.List.([]types.UserItem)
  75. for _, item := range items {
  76. if item.Id == userId {
  77. assert.Equal(t, "", item.MemberType)
  78. return
  79. }
  80. }
  81. t.Fatal("should find inserted user in the list")
  82. }
  83. // TC-0178: pageSize超过上限
  84. func TestUserList_PageSizeOver100_Capped(t *testing.T) {
  85. ctx := ctxhelper.SuperAdminCtx()
  86. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  87. logic := NewUserListLogic(ctx, svcCtx)
  88. resp, err := logic.UserList(&types.UserListReq{
  89. Page: 1,
  90. PageSize: 200,
  91. })
  92. require.NoError(t, err)
  93. require.NotNil(t, resp)
  94. items := resp.List.([]types.UserItem)
  95. assert.LessOrEqual(t, len(items), 100)
  96. }
  97. // TC-0179: 用户不在产品中
  98. func TestUserList_PartialNonMember(t *testing.T) {
  99. ctx := ctxhelper.SuperAdminCtx()
  100. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  101. conn := testutil.GetTestSqlConn()
  102. now := time.Now().Unix()
  103. u1Name := testutil.UniqueId()
  104. u1Id := insertTestUser(t, ctx, u1Name, testutil.HashPassword("pass"))
  105. u2Name := testutil.UniqueId()
  106. u2Id := insertTestUser(t, ctx, u2Name, testutil.HashPassword("pass"))
  107. productCode := testutil.UniqueId()
  108. memberRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productMemberModel.SysProductMember{
  109. ProductCode: productCode,
  110. UserId: u1Id,
  111. MemberType: "MEMBER",
  112. Status: 1,
  113. CreateTime: now,
  114. UpdateTime: now,
  115. })
  116. require.NoError(t, err)
  117. memberId, _ := memberRes.LastInsertId()
  118. t.Cleanup(func() {
  119. testutil.CleanTable(ctx, conn, "`sys_product_member`", memberId)
  120. testutil.CleanTable(ctx, conn, "`sys_user`", u1Id, u2Id)
  121. })
  122. logic := NewUserListLogic(ctx, svcCtx)
  123. resp, err := logic.UserList(&types.UserListReq{
  124. ProductCode: productCode,
  125. Page: 1,
  126. PageSize: 100,
  127. })
  128. require.NoError(t, err)
  129. require.NotNil(t, resp)
  130. items := resp.List.([]types.UserItem)
  131. for _, item := range items {
  132. if item.Id == u1Id {
  133. assert.Equal(t, "MEMBER", item.MemberType)
  134. }
  135. if item.Id == u2Id {
  136. assert.Equal(t, "", item.MemberType)
  137. }
  138. }
  139. }
  140. // TC-0205: 非超管用户仅能看到产品成员(#1修复验证)
  141. func TestUserList_NonSuperAdminOnlySeeProductMembers(t *testing.T) {
  142. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  143. conn := testutil.GetTestSqlConn()
  144. superCtx := ctxhelper.SuperAdminCtx()
  145. now := time.Now().Unix()
  146. productCode := testutil.UniqueId()
  147. memberName := testutil.UniqueId()
  148. memberId := insertTestUser(t, superCtx, memberName, testutil.HashPassword("pass"))
  149. nonMemberName := testutil.UniqueId()
  150. nonMemberId := insertTestUser(t, superCtx, nonMemberName, testutil.HashPassword("pass"))
  151. pmRes, err := svcCtx.SysProductMemberModel.Insert(superCtx, &productMemberModel.SysProductMember{
  152. ProductCode: productCode,
  153. UserId: memberId,
  154. MemberType: "MEMBER",
  155. Status: 1,
  156. CreateTime: now,
  157. UpdateTime: now,
  158. })
  159. require.NoError(t, err)
  160. pmId, _ := pmRes.LastInsertId()
  161. t.Cleanup(func() {
  162. testutil.CleanTable(superCtx, conn, "`sys_product_member`", pmId)
  163. testutil.CleanTable(superCtx, conn, "`sys_user`", memberId, nonMemberId)
  164. })
  165. ctx := ctxhelper.AdminCtx(productCode)
  166. logic := NewUserListLogic(ctx, svcCtx)
  167. resp, err := logic.UserList(&types.UserListReq{
  168. ProductCode: productCode,
  169. Page: 1,
  170. PageSize: 100,
  171. })
  172. require.NoError(t, err)
  173. require.NotNil(t, resp)
  174. items := resp.List.([]types.UserItem)
  175. for _, item := range items {
  176. assert.NotEqual(t, nonMemberId, item.Id,
  177. "non-member user should NOT appear in product user list for non-super-admin (audit #1)")
  178. }
  179. }
  180. // TC-0206: 非超管不带productCode时返回403
  181. func TestUserList_NonSuperAdminWithoutProductCode_Rejected(t *testing.T) {
  182. ctx := ctxhelper.AdminCtx("test_product")
  183. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  184. logic := NewUserListLogic(ctx, svcCtx)
  185. _, err := logic.UserList(&types.UserListReq{
  186. Page: 1,
  187. PageSize: 10,
  188. })
  189. require.Error(t, err)
  190. var ce *response.CodeError
  191. require.True(t, errors.As(err, &ce))
  192. assert.Equal(t, 403, ce.Code())
  193. assert.Contains(t, ce.Error(), "非超管用户必须指定产品编码")
  194. }
  195. // TC-1167 / TC-1168 / TC-1169 统一使用这套 PII 种子,保证 UserList 三类调用者看到的
  196. // Email/Phone/Remark 与 UserDetail 口径完全对齐,避免"详情脱敏但列表不脱敏"的侧信道泄漏。
  197. func seedPIIMemberForList(t *testing.T, svcCtx *svc.ServiceContext, productCode string,
  198. memberType string) (callerUserId, targetUserId, targetMId, callerMId int64) {
  199. t.Helper()
  200. bootstrap := ctxhelper.SuperAdminCtx()
  201. now := time.Now().Unix()
  202. tRes, err := svcCtx.SysUserModel.Insert(bootstrap, &userModelPkg.SysUser{
  203. Username: "list_pii_tgt_" + testutil.UniqueId(), Password: testutil.HashPassword("pw"),
  204. Email: "[email protected]", Phone: "13922223333", Remark: "list-target-remark",
  205. IsSuperAdmin: 2, MustChangePassword: 2, Status: 1, DeptId: 1,
  206. CreateTime: now, UpdateTime: now,
  207. })
  208. require.NoError(t, err)
  209. targetUserId, _ = tRes.LastInsertId()
  210. mRes, err := svcCtx.SysProductMemberModel.Insert(bootstrap, &productMemberModel.SysProductMember{
  211. ProductCode: productCode, UserId: targetUserId, MemberType: "MEMBER",
  212. Status: 1, CreateTime: now, UpdateTime: now,
  213. })
  214. require.NoError(t, err)
  215. targetMId, _ = mRes.LastInsertId()
  216. // caller 只在 MEMBER 场景需要真实落库(和 UserDetail 对齐:非超管必须是产品成员才能 list)。
  217. // ADMIN / DEVELOPER 走 ctxhelper 预设的 UserId 即可,不必落库。
  218. if memberType == "MEMBER" {
  219. callerUserId = 4
  220. cRes, err := svcCtx.SysProductMemberModel.Insert(bootstrap, &productMemberModel.SysProductMember{
  221. ProductCode: productCode, UserId: callerUserId, MemberType: "MEMBER",
  222. Status: 1, CreateTime: now, UpdateTime: now,
  223. })
  224. require.NoError(t, err)
  225. callerMId, _ = cRes.LastInsertId()
  226. }
  227. return
  228. }
  229. // TC-1167: 产品 ADMIN 列表视角 —— 必须拿到所有成员的 Email/Phone/Remark 原值。
  230. func TestUserList_M_R16_1_ProductAdmin_KeepsPII(t *testing.T) {
  231. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  232. conn := testutil.GetTestSqlConn()
  233. bootstrap := ctxhelper.SuperAdminCtx()
  234. productCode := "list_m_r16_admin_" + testutil.UniqueId()
  235. _, targetId, targetMId, _ := seedPIIMemberForList(t, svcCtx, productCode, "ADMIN")
  236. t.Cleanup(func() {
  237. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", targetMId)
  238. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  239. })
  240. resp, err := NewUserListLogic(ctxhelper.AdminCtx(productCode), svcCtx).UserList(&types.UserListReq{
  241. ProductCode: productCode, Page: 1, PageSize: 100,
  242. })
  243. require.NoError(t, err)
  244. items := resp.List.([]types.UserItem)
  245. var found *types.UserItem
  246. for i := range items {
  247. if items[i].Id == targetId {
  248. found = &items[i]
  249. break
  250. }
  251. }
  252. require.NotNil(t, found, "ADMIN 列表必须能看到本产品下的 target 成员")
  253. assert.Equal(t, "[email protected]", found.Email)
  254. assert.Equal(t, "13922223333", found.Phone)
  255. assert.Equal(t, "list-target-remark", found.Remark)
  256. }
  257. // TC-1168: 产品 DEVELOPER 列表视角 —— 与 ADMIN 同口径,PII 完整返回。
  258. func TestUserList_M_R16_1_ProductDeveloper_KeepsPII(t *testing.T) {
  259. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  260. conn := testutil.GetTestSqlConn()
  261. bootstrap := ctxhelper.SuperAdminCtx()
  262. productCode := "list_m_r16_dev_" + testutil.UniqueId()
  263. _, targetId, targetMId, _ := seedPIIMemberForList(t, svcCtx, productCode, "DEVELOPER")
  264. t.Cleanup(func() {
  265. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", targetMId)
  266. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  267. })
  268. resp, err := NewUserListLogic(ctxhelper.DeveloperCtx(productCode), svcCtx).UserList(&types.UserListReq{
  269. ProductCode: productCode, Page: 1, PageSize: 100,
  270. })
  271. require.NoError(t, err)
  272. items := resp.List.([]types.UserItem)
  273. for _, item := range items {
  274. if item.Id == targetId {
  275. assert.Equal(t, "[email protected]", item.Email)
  276. assert.Equal(t, "13922223333", item.Phone)
  277. assert.Equal(t, "list-target-remark", item.Remark)
  278. return
  279. }
  280. }
  281. t.Fatal("DEVELOPER 列表必须能看到 target")
  282. }
  283. // TC-1169: 产品 MEMBER 列表视角 —— Email/Phone/Remark 必须为空字符串;
  284. // 其它字段(Username/Nickname/DeptId/Status 等)照常回填,保证 MEMBER 的"通讯录视图"退化为
  285. // "看得到谁在这个产品里",而不是"拉到每个人的私信渠道"。
  286. func TestUserList_M_R16_1_ProductMember_MasksPII(t *testing.T) {
  287. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  288. conn := testutil.GetTestSqlConn()
  289. bootstrap := ctxhelper.SuperAdminCtx()
  290. productCode := "list_m_r16_mb_" + testutil.UniqueId()
  291. _, targetId, targetMId, callerMId := seedPIIMemberForList(t, svcCtx, productCode, "MEMBER")
  292. t.Cleanup(func() {
  293. testutil.CleanTable(bootstrap, conn, "`sys_product_member`", targetMId, callerMId)
  294. testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId)
  295. })
  296. resp, err := NewUserListLogic(ctxhelper.MemberCtx(productCode), svcCtx).UserList(&types.UserListReq{
  297. ProductCode: productCode, Page: 1, PageSize: 100,
  298. })
  299. require.NoError(t, err)
  300. items := resp.List.([]types.UserItem)
  301. var found *types.UserItem
  302. for i := range items {
  303. if items[i].Id == targetId {
  304. found = &items[i]
  305. break
  306. }
  307. }
  308. require.NotNil(t, found, "MEMBER 列表仍应能看到 target 的存在,仅 PII 字段被清空")
  309. assert.Empty(t, found.Email, "MEMBER 视角下 Email 必须是空字符串")
  310. assert.Empty(t, found.Phone, "MEMBER 视角下 Phone 必须是空字符串")
  311. assert.Empty(t, found.Remark, "MEMBER 视角下 Remark 必须是空字符串")
  312. assert.NotEmpty(t, found.Username, "Username 不应被脱敏,保证 UI 还能渲染出成员行")
  313. }
  314. // TC-0207: 非超管访问其他产品数据被拒绝
  315. func TestUserList_NonSuperAdminWrongProductCode_Rejected(t *testing.T) {
  316. ctx := ctxhelper.AdminCtx("product_a")
  317. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  318. logic := NewUserListLogic(ctx, svcCtx)
  319. _, err := logic.UserList(&types.UserListReq{
  320. ProductCode: "product_b",
  321. Page: 1,
  322. PageSize: 10,
  323. })
  324. require.Error(t, err)
  325. var ce *response.CodeError
  326. require.True(t, errors.As(err, &ce))
  327. assert.Equal(t, 403, ce.Code())
  328. assert.Contains(t, ce.Error(), "无权访问该产品的数据")
  329. }