addMemberLogic_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. package member
  2. import (
  3. "database/sql"
  4. "errors"
  5. "sync"
  6. "testing"
  7. "time"
  8. productModel "perms-system-server/internal/model/product"
  9. memberModel "perms-system-server/internal/model/productmember"
  10. userModel "perms-system-server/internal/model/user"
  11. "perms-system-server/internal/response"
  12. "perms-system-server/internal/svc"
  13. "perms-system-server/internal/testutil"
  14. "perms-system-server/internal/testutil/ctxhelper"
  15. "perms-system-server/internal/types"
  16. "github.com/stretchr/testify/assert"
  17. "github.com/stretchr/testify/require"
  18. )
  19. // TC-0213: 正常添加
  20. func TestAddMember_Normal(t *testing.T) {
  21. ctx := ctxhelper.SuperAdminCtx()
  22. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  23. conn := testutil.GetTestSqlConn()
  24. now := time.Now().Unix()
  25. uid := testutil.UniqueId()
  26. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  27. Code: uid, Name: "test_prod", AppKey: uid, AppSecret: "s1",
  28. Status: 1, CreateTime: now, UpdateTime: now,
  29. })
  30. require.NoError(t, err)
  31. pId, _ := pRes.LastInsertId()
  32. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  33. Username: uid, Password: testutil.HashPassword("pass123"), Nickname: "nick",
  34. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  35. Status: 1, CreateTime: now, UpdateTime: now,
  36. })
  37. require.NoError(t, err)
  38. uId, _ := uRes.LastInsertId()
  39. t.Cleanup(func() {
  40. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", uid)
  41. testutil.CleanTable(ctx, conn, "`sys_user`", uId)
  42. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  43. })
  44. logic := NewAddMemberLogic(ctx, svcCtx)
  45. resp, err := logic.AddMember(&types.AddMemberReq{
  46. ProductCode: uid,
  47. UserId: uId,
  48. MemberType: "MEMBER",
  49. })
  50. require.NoError(t, err)
  51. assert.True(t, resp.Id > 0)
  52. member, err := svcCtx.SysProductMemberModel.FindOne(ctx, resp.Id)
  53. require.NoError(t, err)
  54. assert.Equal(t, uid, member.ProductCode)
  55. assert.Equal(t, uId, member.UserId)
  56. assert.Equal(t, "MEMBER", member.MemberType)
  57. assert.Equal(t, int64(1), member.Status)
  58. }
  59. // TC-0214: 产品不存在
  60. func TestAddMember_ProductNotFound(t *testing.T) {
  61. ctx := ctxhelper.SuperAdminCtx()
  62. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  63. logic := NewAddMemberLogic(ctx, svcCtx)
  64. _, err := logic.AddMember(&types.AddMemberReq{
  65. ProductCode: "nonexistent_product_xyz",
  66. UserId: 1,
  67. MemberType: "MEMBER",
  68. })
  69. require.Error(t, err)
  70. ce, ok := err.(*response.CodeError)
  71. require.True(t, ok)
  72. assert.Equal(t, 404, ce.Code())
  73. assert.Equal(t, "产品不存在", ce.Error())
  74. }
  75. // TC-0215: 用户不存在
  76. func TestAddMember_UserNotFound(t *testing.T) {
  77. ctx := ctxhelper.SuperAdminCtx()
  78. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  79. conn := testutil.GetTestSqlConn()
  80. now := time.Now().Unix()
  81. uid := testutil.UniqueId()
  82. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  83. Code: uid, Name: "test_prod", AppKey: uid, AppSecret: "s1",
  84. Status: 1, CreateTime: now, UpdateTime: now,
  85. })
  86. require.NoError(t, err)
  87. pId, _ := pRes.LastInsertId()
  88. t.Cleanup(func() {
  89. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  90. })
  91. logic := NewAddMemberLogic(ctx, svcCtx)
  92. _, err = logic.AddMember(&types.AddMemberReq{
  93. ProductCode: uid,
  94. UserId: 999999999,
  95. MemberType: "MEMBER",
  96. })
  97. require.Error(t, err)
  98. ce, ok := err.(*response.CodeError)
  99. require.True(t, ok)
  100. assert.Equal(t, 404, ce.Code())
  101. assert.Equal(t, "用户不存在", ce.Error())
  102. }
  103. // TC-0216: 已是成员
  104. func TestAddMember_AlreadyMember(t *testing.T) {
  105. ctx := ctxhelper.SuperAdminCtx()
  106. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  107. conn := testutil.GetTestSqlConn()
  108. now := time.Now().Unix()
  109. uid := testutil.UniqueId()
  110. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  111. Code: uid, Name: "test_prod", AppKey: uid, AppSecret: "s1",
  112. Status: 1, CreateTime: now, UpdateTime: now,
  113. })
  114. require.NoError(t, err)
  115. pId, _ := pRes.LastInsertId()
  116. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  117. Username: uid, Password: testutil.HashPassword("pass123"), Nickname: "nick",
  118. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  119. Status: 1, CreateTime: now, UpdateTime: now,
  120. })
  121. require.NoError(t, err)
  122. uId, _ := uRes.LastInsertId()
  123. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  124. ProductCode: uid, UserId: uId, MemberType: "MEMBER",
  125. Status: 1, CreateTime: now, UpdateTime: now,
  126. })
  127. require.NoError(t, err)
  128. mId, _ := mRes.LastInsertId()
  129. t.Cleanup(func() {
  130. testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
  131. testutil.CleanTable(ctx, conn, "`sys_user`", uId)
  132. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  133. })
  134. logic := NewAddMemberLogic(ctx, svcCtx)
  135. _, err = logic.AddMember(&types.AddMemberReq{
  136. ProductCode: uid,
  137. UserId: uId,
  138. MemberType: "ADMIN",
  139. })
  140. require.Error(t, err)
  141. ce, ok := err.(*response.CodeError)
  142. require.True(t, ok)
  143. assert.Equal(t, 409, ce.Code())
  144. assert.Equal(t, "该用户已是该产品成员", ce.Error())
  145. }
  146. // TC-0218: 无效MemberType
  147. func TestAddMember_InvalidMemberType(t *testing.T) {
  148. ctx := ctxhelper.SuperAdminCtx()
  149. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  150. conn := testutil.GetTestSqlConn()
  151. now := time.Now().Unix()
  152. uid := testutil.UniqueId()
  153. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  154. Code: uid, Name: "test_prod", AppKey: uid, AppSecret: "s1",
  155. Status: 1, CreateTime: now, UpdateTime: now,
  156. })
  157. require.NoError(t, err)
  158. pId, _ := pRes.LastInsertId()
  159. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  160. Username: uid, Password: testutil.HashPassword("pass123"), Nickname: "nick",
  161. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  162. Status: 1, CreateTime: now, UpdateTime: now,
  163. })
  164. require.NoError(t, err)
  165. uId, _ := uRes.LastInsertId()
  166. t.Cleanup(func() {
  167. testutil.CleanTable(ctx, conn, "`sys_user`", uId)
  168. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  169. })
  170. logic := NewAddMemberLogic(ctx, svcCtx)
  171. _, err = logic.AddMember(&types.AddMemberReq{
  172. ProductCode: uid,
  173. UserId: uId,
  174. MemberType: "INVALID",
  175. })
  176. require.Error(t, err)
  177. ce, ok := err.(*response.CodeError)
  178. require.True(t, ok)
  179. assert.Equal(t, 400, ce.Code())
  180. assert.Equal(t, "无效的成员类型", ce.Error())
  181. }
  182. // TC-0217: 并发添加
  183. func TestAddMember_ConcurrentSameUserProduct(t *testing.T) {
  184. ctx := ctxhelper.SuperAdminCtx()
  185. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  186. conn := testutil.GetTestSqlConn()
  187. now := time.Now().Unix()
  188. uid := testutil.UniqueId()
  189. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  190. Code: uid, Name: "concurrent_prod", AppKey: uid, AppSecret: "s1",
  191. Status: 1, CreateTime: now, UpdateTime: now,
  192. })
  193. require.NoError(t, err)
  194. pId, _ := pRes.LastInsertId()
  195. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  196. Username: uid, Password: testutil.HashPassword("pass123"), Nickname: "nick",
  197. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  198. Status: 1, CreateTime: now, UpdateTime: now,
  199. })
  200. require.NoError(t, err)
  201. uId, _ := uRes.LastInsertId()
  202. t.Cleanup(func() {
  203. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", uid)
  204. testutil.CleanTable(ctx, conn, "`sys_user`", uId)
  205. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  206. })
  207. var wg sync.WaitGroup
  208. results := make(chan error, 2)
  209. for i := 0; i < 2; i++ {
  210. wg.Add(1)
  211. go func() {
  212. defer wg.Done()
  213. logic := NewAddMemberLogic(ctx, svcCtx)
  214. _, err := logic.AddMember(&types.AddMemberReq{
  215. ProductCode: uid,
  216. UserId: uId,
  217. MemberType: "MEMBER",
  218. })
  219. results <- err
  220. }()
  221. }
  222. wg.Wait()
  223. close(results)
  224. var errs []error
  225. for e := range results {
  226. errs = append(errs, e)
  227. }
  228. require.Len(t, errs, 2)
  229. successCount := 0
  230. failCount := 0
  231. for _, e := range errs {
  232. if e == nil {
  233. successCount++
  234. } else {
  235. failCount++
  236. }
  237. }
  238. assert.Equal(t, 1, successCount, "exactly one goroutine should succeed")
  239. assert.Equal(t, 1, failCount, "exactly one goroutine should fail (409 or DB duplicate)")
  240. }
  241. // TC-0950: 修复 —— AddMember 必须显式拒绝把 SuperAdmin 作为普通产品成员加入。
  242. // 背景:loadMembership 会把 SuperAdmin 的 MemberType 固定为 SuperAdmin 让其实际权限不受影响,
  243. // 但若 sys_product_member 里仍落一条记录,会污染日志 / 权限推理工具,且给产品 ADMIN
  244. // "纳管了 superadmin" 的错觉。必须在 AddMember 入口就 403。
  245. func TestAddMember_SuperAdminTargetRejected(t *testing.T) {
  246. ctx := ctxhelper.SuperAdminCtx()
  247. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  248. conn := testutil.GetTestSqlConn()
  249. now := time.Now().Unix()
  250. code := testutil.UniqueId()
  251. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  252. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  253. Status: 1, CreateTime: now, UpdateTime: now,
  254. })
  255. require.NoError(t, err)
  256. pId, _ := pRes.LastInsertId()
  257. // target 是 SuperAdmin(IsSuperAdmin=1)
  258. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  259. Username: "h3_su_" + code, Password: testutil.HashPassword("pw"),
  260. Avatar: sql.NullString{}, IsSuperAdmin: 1, MustChangePassword: 2,
  261. Status: 1, CreateTime: now, UpdateTime: now,
  262. })
  263. require.NoError(t, err)
  264. uId, _ := uRes.LastInsertId()
  265. t.Cleanup(func() {
  266. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  267. testutil.CleanTable(ctx, conn, "`sys_user`", uId)
  268. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  269. })
  270. _, err = NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
  271. ProductCode: code, UserId: uId, MemberType: "MEMBER",
  272. })
  273. require.Error(t, err, "禁止把 SuperAdmin 加入具体产品为普通成员")
  274. var ce *response.CodeError
  275. require.True(t, errors.As(err, &ce))
  276. assert.Equal(t, 403, ce.Code())
  277. assert.Contains(t, ce.Error(), "超级管理员")
  278. // DB 侧必须没有落下 SuperAdmin 的成员记录(regression:确保 AddMember 未短路在插入之后)
  279. _, findErr := svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(ctx, code, uId)
  280. require.Error(t, findErr, "SuperAdmin 不得被落入 sys_product_member")
  281. }
  282. // TC-0729: 修复:禁用产品不允许添加成员
  283. func TestAddMember_DisabledProductRejected(t *testing.T) {
  284. ctx := ctxhelper.SuperAdminCtx()
  285. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  286. conn := testutil.GetTestSqlConn()
  287. now := time.Now().Unix()
  288. code := testutil.UniqueId()
  289. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  290. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  291. Status: 2, CreateTime: now, UpdateTime: now, // 禁用
  292. })
  293. require.NoError(t, err)
  294. pId, _ := pRes.LastInsertId()
  295. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  296. Username: code, Password: testutil.HashPassword("pw"),
  297. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  298. Status: 1, CreateTime: now, UpdateTime: now,
  299. })
  300. require.NoError(t, err)
  301. uId, _ := uRes.LastInsertId()
  302. t.Cleanup(func() {
  303. testutil.CleanTable(ctx, conn, "`sys_user`", uId)
  304. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  305. })
  306. _, err = NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
  307. ProductCode: code, UserId: uId, MemberType: "MEMBER",
  308. })
  309. require.Error(t, err)
  310. var ce *response.CodeError
  311. require.True(t, errors.As(err, &ce))
  312. assert.Equal(t, 400, ce.Code())
  313. assert.Contains(t, ce.Error(), "禁用")
  314. }
  315. // TC-1107: 非超管 req.ProductCode 被忽略,枚举攻击路径从根本上被堵死。
  316. // L-R13-1 改造后:非超管的 productCode 统一从 JWT 获取,req.ProductCode 无论传什么都被忽略,
  317. // 因此无法通过 404/403 差分枚举产品 code。此测试验证传入不存在的 productCode 时被忽略,
  318. // 实际使用 JWT 的 productCode(some_other_product),因该产品不存在而返回 404。
  319. func TestAddMember_L_R13_1_ProductEnumerationBlocked(t *testing.T) {
  320. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  321. conn := testutil.GetTestSqlConn()
  322. callerCtx := ctxhelper.AdminCtx("some_other_product")
  323. _, err := NewAddMemberLogic(callerCtx, svcCtx).AddMember(&types.AddMemberReq{
  324. ProductCode: "definitely_does_not_exist_" + testutil.UniqueId(),
  325. UserId: 999999999,
  326. MemberType: "MEMBER",
  327. })
  328. require.Error(t, err)
  329. var ce *response.CodeError
  330. require.True(t, errors.As(err, &ce))
  331. assert.Equal(t, 404, ce.Code(),
  332. "L-R13-1:非超管 req.ProductCode 被忽略,使用 JWT 的 productCode 查询产品,不存在则 404")
  333. var count int64
  334. _ = conn.QueryRowCtx(callerCtx, &count,
  335. "SELECT COUNT(*) FROM `sys_product_member` WHERE `userId` = ?", int64(999999999))
  336. assert.Equal(t, int64(0), count)
  337. }
  338. // TC-1108: 非 ADMIN caller + 非法 MemberType —— 必须 403 而不是 400(权限先于字面校验),
  339. // 防御通过 400 "无效的成员类型" 和 404 "产品不存在" 的差分探测 productCode 是否在线。
  340. func TestAddMember_L_R13_1_InvalidMemberTypeBeforeAuth(t *testing.T) {
  341. ctx := ctxhelper.MemberCtx("test_product")
  342. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  343. _, err := NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
  344. ProductCode: "test_product",
  345. UserId: 1,
  346. MemberType: "INVALID",
  347. })
  348. require.Error(t, err)
  349. var ce *response.CodeError
  350. require.True(t, errors.As(err, &ce))
  351. assert.Equal(t, 403, ce.Code(),
  352. "L-R13-1:MEMBER 无 ADMIN 权限必须先于 MemberType 字面校验 403,不得返 400")
  353. }
  354. // TC-1109: 超管 + 非法 MemberType:权限通过后仍必须命中 400 字面校验,
  355. // 回归 L-R13-1 改动没有把合法路径的 400 语义也吃掉。
  356. func TestAddMember_L_R13_1_SuperAdminStillGets400ForInvalidType(t *testing.T) {
  357. ctx := ctxhelper.SuperAdminCtx()
  358. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  359. conn := testutil.GetTestSqlConn()
  360. now := time.Now().Unix()
  361. uid := testutil.UniqueId()
  362. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  363. Code: uid, Name: "test_prod", AppKey: uid, AppSecret: "s1",
  364. Status: 1, CreateTime: now, UpdateTime: now,
  365. })
  366. require.NoError(t, err)
  367. pId, _ := pRes.LastInsertId()
  368. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", pId) })
  369. _, err = NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
  370. ProductCode: uid,
  371. UserId: 999999999,
  372. MemberType: "INVALID",
  373. })
  374. require.Error(t, err)
  375. var ce *response.CodeError
  376. require.True(t, errors.As(err, &ce))
  377. assert.Equal(t, 400, ce.Code(),
  378. "超管权限通过后必须继续走字面 400 检查,不得因 L-R13-1 改动被吞掉")
  379. assert.Equal(t, "无效的成员类型", ce.Error())
  380. }
  381. // TC-1314: 非超管不传productCode时从JWT获取并正常添加
  382. func TestAddMember_NonSuperAdminUsesJWTProductCode(t *testing.T) {
  383. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  384. conn := testutil.GetTestSqlConn()
  385. now := time.Now().Unix()
  386. pc := testutil.UniqueId()
  387. pRes, err := svcCtx.SysProductModel.Insert(ctxhelper.SuperAdminCtx(), &productModel.SysProduct{
  388. Code: pc, Name: "test_prod", AppKey: pc, AppSecret: "s1",
  389. Status: 1, CreateTime: now, UpdateTime: now,
  390. })
  391. require.NoError(t, err)
  392. pId, _ := pRes.LastInsertId()
  393. uRes, err := svcCtx.SysUserModel.Insert(ctxhelper.SuperAdminCtx(), &userModel.SysUser{
  394. Username: testutil.UniqueId(), Password: testutil.HashPassword("pass123"), Nickname: "nick",
  395. Avatar: sql.NullString{}, DeptId: 1, IsSuperAdmin: 2, MustChangePassword: 2,
  396. Status: 1, CreateTime: now, UpdateTime: now,
  397. })
  398. require.NoError(t, err)
  399. uId, _ := uRes.LastInsertId()
  400. t.Cleanup(func() {
  401. testutil.CleanTableByField(ctxhelper.SuperAdminCtx(), conn, "`sys_product_member`", "productCode", pc)
  402. testutil.CleanTable(ctxhelper.SuperAdminCtx(), conn, "`sys_user`", uId)
  403. testutil.CleanTable(ctxhelper.SuperAdminCtx(), conn, "`sys_product`", pId)
  404. })
  405. ctx := ctxhelper.AdminCtx(pc)
  406. logic := NewAddMemberLogic(ctx, svcCtx)
  407. resp, err := logic.AddMember(&types.AddMemberReq{
  408. UserId: uId,
  409. MemberType: "MEMBER",
  410. })
  411. require.NoError(t, err)
  412. assert.Greater(t, resp.Id, int64(0))
  413. member, err := svcCtx.SysProductMemberModel.FindOne(ctxhelper.SuperAdminCtx(), resp.Id)
  414. require.NoError(t, err)
  415. assert.Equal(t, pc, member.ProductCode)
  416. }
  417. // TC-1315: 超管不传productCode时返回400
  418. func TestAddMember_SuperAdminNoProductCodeReturns400(t *testing.T) {
  419. ctx := ctxhelper.SuperAdminCtx()
  420. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  421. logic := NewAddMemberLogic(ctx, svcCtx)
  422. _, err := logic.AddMember(&types.AddMemberReq{
  423. UserId: 1,
  424. MemberType: "MEMBER",
  425. })
  426. require.Error(t, err)
  427. ce, ok := err.(*response.CodeError)
  428. require.True(t, ok)
  429. assert.Equal(t, 400, ce.Code())
  430. assert.Equal(t, "必须指定产品编码", ce.Error())
  431. }