userListLogic_test.go 13 KB

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