userDetailLogic_test.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. package user
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. "os"
  9. "perms-system-server/internal/consts"
  10. "perms-system-server/internal/loaders"
  11. "perms-system-server/internal/middleware"
  12. memberModel "perms-system-server/internal/model/productmember"
  13. userModel "perms-system-server/internal/model/user"
  14. "perms-system-server/internal/model/userrole"
  15. "perms-system-server/internal/response"
  16. "perms-system-server/internal/svc"
  17. "perms-system-server/internal/testutil"
  18. "perms-system-server/internal/testutil/ctxhelper"
  19. "perms-system-server/internal/types"
  20. "testing"
  21. "time"
  22. )
  23. func TestUserDetail_Success(t *testing.T) {
  24. ctx := ctxhelper.SuperAdminCtx()
  25. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  26. conn := testutil.GetTestSqlConn()
  27. username := testutil.UniqueId()
  28. userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
  29. // 插入两条"当前产品"下的真实 sys_role 以及一条属于其它产品的 sys_role,
  30. // 用户同时绑定这三个角色。超管在 test_product 上下文下应当只看到前两个。
  31. roleInCurrent1 := insertTestRole(t, svcCtx, "test_product", 1)
  32. roleInCurrent2 := insertTestRole(t, svcCtx, "test_product", 1)
  33. roleInOther := insertTestRole(t, svcCtx, "other_product", 1)
  34. now := time.Now().Unix()
  35. var roleRecordIds []int64
  36. for _, roleId := range []int64{roleInCurrent1, roleInCurrent2, roleInOther} {
  37. res, err := svcCtx.SysUserRoleModel.Insert(ctx, &userrole.SysUserRole{
  38. UserId: userId,
  39. RoleId: roleId,
  40. CreateTime: now,
  41. UpdateTime: now,
  42. })
  43. require.NoError(t, err)
  44. id, _ := res.LastInsertId()
  45. roleRecordIds = append(roleRecordIds, id)
  46. }
  47. t.Cleanup(func() {
  48. testutil.CleanTable(ctx, conn, "`sys_user_role`", roleRecordIds...)
  49. testutil.CleanTable(ctx, conn, "`sys_role`", roleInCurrent1, roleInCurrent2, roleInOther)
  50. testutil.CleanTable(ctx, conn, "`sys_user`", userId)
  51. })
  52. logic := NewUserDetailLogic(ctx, svcCtx)
  53. resp, err := logic.UserDetail(&types.UserDetailReq{Id: userId})
  54. require.NoError(t, err)
  55. require.NotNil(t, resp)
  56. assert.Equal(t, userId, resp.Id)
  57. assert.Equal(t, username, resp.Username)
  58. // 修复后:超管在产品上下文里只看到 test_product 的角色;other_product 的角色不应返回
  59. assert.ElementsMatch(t, []int64{roleInCurrent1, roleInCurrent2}, resp.RoleIds)
  60. assert.NotContains(t, resp.RoleIds, roleInOther, "超管在具体产品上下文不应返回其它产品的 roleIds")
  61. }
  62. // TC-0182: 正常查询-含Avatar
  63. func TestUserDetail_WithAvatar(t *testing.T) {
  64. ctx := ctxhelper.SuperAdminCtx()
  65. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  66. conn := testutil.GetTestSqlConn()
  67. userId := insertTestUserFull(t, ctx, &userModel.SysUser{
  68. Username: testutil.UniqueId(),
  69. Password: testutil.HashPassword("pass"),
  70. Nickname: "avatar_user",
  71. Avatar: sql.NullString{String: "https://example.com/avatar.png", Valid: true},
  72. IsSuperAdmin: 2,
  73. MustChangePassword: 2,
  74. Status: 1,
  75. })
  76. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
  77. logic := NewUserDetailLogic(ctx, svcCtx)
  78. resp, err := logic.UserDetail(&types.UserDetailReq{Id: userId})
  79. require.NoError(t, err)
  80. require.NotNil(t, resp)
  81. assert.Equal(t, "https://example.com/avatar.png", resp.Avatar)
  82. }
  83. // TC-0183: 不存在
  84. func TestUserDetail_NotFound(t *testing.T) {
  85. ctx := ctxhelper.SuperAdminCtx()
  86. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  87. logic := NewUserDetailLogic(ctx, svcCtx)
  88. _, err := logic.UserDetail(&types.UserDetailReq{Id: 999999999})
  89. require.Error(t, err)
  90. var codeErr *response.CodeError
  91. require.True(t, errors.As(err, &codeErr))
  92. assert.Equal(t, 404, codeErr.Code())
  93. assert.Equal(t, "用户不存在", codeErr.Error())
  94. }
  95. const auditPendingEnv = "AUDIT_RUN_PENDING"
  96. func skipPending(t *testing.T, marker, reason string) {
  97. t.Helper()
  98. if os.Getenv(auditPendingEnv) != "" {
  99. return
  100. }
  101. t.Skipf("AUDIT_PENDING %s (Round 8 fix 未落地) —— %s", marker, reason)
  102. }
  103. func insertH1Member(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext, productCode string, u *userModel.SysUser) (int64, int64) {
  104. t.Helper()
  105. id := insertTestUserFull(t, ctx, u)
  106. now := time.Now().Unix()
  107. res, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  108. ProductCode: productCode, UserId: id, MemberType: consts.MemberTypeMember,
  109. Status: 1, CreateTime: now, UpdateTime: now,
  110. })
  111. require.NoError(t, err)
  112. mId, _ := res.LastInsertId()
  113. return id, mId
  114. }
  115. // TC-0990: 对抗性 —— 同产品 MEMBER 互看,必须屏蔽 Email/Phone/Remark。
  116. func TestUserDetail_H1_MemberViewingPeer_MustMaskPII(t *testing.T) {
  117. skipPending(t, "H-1",
  118. "UserDetail 当前把 Email/Phone/Remark 原样回传同产品 MEMBER;"+
  119. "待 filterPIIForCaller + CanViewContact 落地后移除 Skip")
  120. ctx := context.Background()
  121. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  122. conn := testutil.GetTestSqlConn()
  123. productCode := "h1_" + testutil.UniqueId()
  124. target, mTarget := insertH1Member(t, ctx, svcCtx, productCode, &userModel.SysUser{
  125. Username: "t_" + testutil.UniqueId(),
  126. Password: testutil.HashPassword("pw"),
  127. Nickname: "target",
  128. Avatar: sql.NullString{},
  129. Email: "[email protected]",
  130. Phone: "13800001111",
  131. Remark: "内部岗位: 副总",
  132. DeptId: 1,
  133. IsSuperAdmin: 2,
  134. MustChangePassword: 2,
  135. Status: 1,
  136. })
  137. caller, mCaller := insertH1Member(t, ctx, svcCtx, productCode, &userModel.SysUser{
  138. Username: "c_" + testutil.UniqueId(),
  139. Password: testutil.HashPassword("pw"),
  140. Nickname: "caller",
  141. IsSuperAdmin: 2,
  142. MustChangePassword: 2,
  143. Status: 1,
  144. DeptId: 1,
  145. })
  146. t.Cleanup(func() {
  147. testutil.CleanTable(ctx, conn, "`sys_product_member`", mTarget, mCaller)
  148. testutil.CleanTable(ctx, conn, "`sys_user`", target, caller)
  149. })
  150. callerCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  151. UserId: caller, Username: "caller", MemberType: consts.MemberTypeMember,
  152. Status: 1, ProductCode: productCode, DeptId: 1, DeptPath: "/1/", MinPermsLevel: 100,
  153. })
  154. resp, err := NewUserDetailLogic(callerCtx, svcCtx).UserDetail(&types.UserDetailReq{Id: target})
  155. require.NoError(t, err)
  156. require.NotNil(t, resp)
  157. assert.NotEqual(t, "[email protected]", resp.Email,
  158. "同级 MEMBER 互看时 Email 必须脱敏(例:t***@example.com),禁止原文暴露")
  159. assert.NotEqual(t, "13800001111", resp.Phone,
  160. "同级 MEMBER 互看时 Phone 必须脱敏(例:138****1111)")
  161. assert.Empty(t, resp.Remark,
  162. "Remark 常含内部岗位/外部联络人,MEMBER 互看时必须清空")
  163. }
  164. // TC-0991: 正向——看自己时 Email/Phone/Remark 必须返回原值。
  165. func TestUserDetail_H1_ViewSelf_KeepsPII(t *testing.T) {
  166. skipPending(t, "H-1",
  167. "UserDetail 缺少 caller.UserId == target.Id 的 PII 放行短路;fix 落地后取消 Skip")
  168. ctx := context.Background()
  169. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  170. conn := testutil.GetTestSqlConn()
  171. productCode := "h1_self_" + testutil.UniqueId()
  172. selfId, mSelf := insertH1Member(t, ctx, svcCtx, productCode, &userModel.SysUser{
  173. Username: "self_" + testutil.UniqueId(),
  174. Password: testutil.HashPassword("pw"),
  175. Nickname: "self",
  176. Email: "[email protected]",
  177. Phone: "13900002222",
  178. Remark: "self-only note",
  179. IsSuperAdmin: 2,
  180. MustChangePassword: 2,
  181. Status: 1,
  182. DeptId: 1,
  183. })
  184. t.Cleanup(func() {
  185. testutil.CleanTable(ctx, conn, "`sys_product_member`", mSelf)
  186. testutil.CleanTable(ctx, conn, "`sys_user`", selfId)
  187. })
  188. selfCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{
  189. UserId: selfId, Username: "self", MemberType: consts.MemberTypeMember,
  190. Status: 1, ProductCode: productCode, DeptId: 1, DeptPath: "/1/", MinPermsLevel: 100,
  191. })
  192. resp, err := NewUserDetailLogic(selfCtx, svcCtx).UserDetail(&types.UserDetailReq{Id: selfId})
  193. require.NoError(t, err)
  194. assert.Equal(t, "[email protected]", resp.Email, "看自己必须返回 Email 原值")
  195. assert.Equal(t, "13900002222", resp.Phone, "看自己必须返回 Phone 原值")
  196. assert.Equal(t, "self-only note", resp.Remark, "看自己必须返回 Remark 原值")
  197. }
  198. // TC-0992: 超管分支 —— SuperAdmin 看任何用户都可以看到 PII 原值(工单兜底)。
  199. // 该分支本应通过;若 fix 改错把超管也一起脱敏了这条会挂,触发回归。
  200. func TestUserDetail_H1_SuperAdmin_KeepsPII(t *testing.T) {
  201. skipPending(t, "H-1",
  202. "当前无脱敏逻辑,超管天然看到原值;fix 落地后本测试用来防回归,确保超管不被误脱敏")
  203. ctx := ctxhelper.SuperAdminCtx()
  204. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  205. conn := testutil.GetTestSqlConn()
  206. userId := insertTestUserFull(t, ctx, &userModel.SysUser{
  207. Username: "sa_view_" + testutil.UniqueId(), Password: testutil.HashPassword("pw"),
  208. Nickname: "n", Email: "[email protected]", Phone: "13700000000", Remark: "nb",
  209. IsSuperAdmin: 2, MustChangePassword: 2, Status: 1,
  210. })
  211. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
  212. resp, err := NewUserDetailLogic(ctx, svcCtx).UserDetail(&types.UserDetailReq{Id: userId})
  213. require.NoError(t, err)
  214. assert.Equal(t, "[email protected]", resp.Email)
  215. assert.Equal(t, "13700000000", resp.Phone)
  216. assert.Equal(t, "nb", resp.Remark)
  217. }