countOtherActiveAdmins_audit_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. package productmember
  2. import (
  3. "context"
  4. "testing"
  5. "time"
  6. "perms-system-server/internal/consts"
  7. "perms-system-server/internal/testutil"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/require"
  10. "github.com/zeromicro/go-zero/core/stores/sqlx"
  11. )
  12. // ---------------------------------------------------------------------------
  13. // 覆盖目标:审计第 6 轮 L-5 修复回归 —— CountOtherActiveAdminsTx 排除目标自己后计数。
  14. //
  15. // 原做法:removeMember / 降级时调用 CountActiveAdminsTx,再在业务层做 <=1 判断。
  16. // 问题:业务语义模糊、边界条件容易算错("算不算目标自己"),出事故后排查痛苦。
  17. // 修复后的契约:
  18. // - CountOtherActiveAdminsTx(ctx, session, productCode, excludeId)
  19. // 返回 productCode 下 memberType=ADMIN 且 status=Enabled 且 id != excludeId 的行数。
  20. // - 不再依赖调用方做减法,0 就是"最后一个 admin",1 就是"还有 1 个 backup admin"。
  21. // - 作用于事务 session,且为 FOR UPDATE,避免并发场景重复判断、双双放过。
  22. // ---------------------------------------------------------------------------
  23. // TC-0867: 唯一 admin 场景,排除自己后返回 0(代表"删掉/降级此人后没有启用 admin")。
  24. func TestCountOtherActiveAdminsTx_SoleAdmin_ReturnsZero(t *testing.T) {
  25. ctx := context.Background()
  26. conn := testutil.GetTestSqlConn()
  27. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  28. pc := "t_pm_coaa_sole_" + testutil.UniqueId()
  29. adminUser := randProductMemberUserId()
  30. ts := time.Now().Unix()
  31. res, err := m.Insert(ctx, &SysProductMember{
  32. ProductCode: pc, UserId: adminUser,
  33. MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
  34. CreateTime: ts, UpdateTime: ts,
  35. })
  36. require.NoError(t, err)
  37. adminId, _ := res.LastInsertId()
  38. defer testutil.CleanTable(ctx, conn, "sys_product_member", adminId)
  39. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  40. n, e := m.CountOtherActiveAdminsTx(c, session, pc, adminId)
  41. require.NoError(t, e)
  42. assert.Equal(t, int64(0), n,
  43. "L-5:唯一 admin 排除自己后必须为 0,调用方据此才能阻止删除最后一个 admin")
  44. return nil
  45. })
  46. require.NoError(t, err)
  47. }
  48. // TC-0868: 多 admin 场景,排除 A 后返回剩余 backup admin 数量。
  49. func TestCountOtherActiveAdminsTx_MultipleAdmins_ExcludesSelf(t *testing.T) {
  50. ctx := context.Background()
  51. conn := testutil.GetTestSqlConn()
  52. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  53. pc := "t_pm_coaa_multi_" + testutil.UniqueId()
  54. ts := time.Now().Unix()
  55. // 插三个启用 ADMIN + 一个启用 MEMBER + 一个禁用 ADMIN,用来检验 WHERE 条件完整性。
  56. type row struct {
  57. mt string
  58. status int64
  59. }
  60. rows := []row{
  61. {consts.MemberTypeAdmin, consts.StatusEnabled},
  62. {consts.MemberTypeAdmin, consts.StatusEnabled},
  63. {consts.MemberTypeAdmin, consts.StatusEnabled},
  64. {consts.MemberTypeMember, consts.StatusEnabled}, // 不计入
  65. {consts.MemberTypeAdmin, consts.StatusDisabled}, // 不计入
  66. }
  67. ids := make([]int64, 0, len(rows))
  68. for _, r := range rows {
  69. uid := randProductMemberUserId()
  70. res, err := m.Insert(ctx, &SysProductMember{
  71. ProductCode: pc, UserId: uid,
  72. MemberType: r.mt, Status: r.status,
  73. CreateTime: ts, UpdateTime: ts,
  74. })
  75. require.NoError(t, err)
  76. id, _ := res.LastInsertId()
  77. ids = append(ids, id)
  78. }
  79. t.Cleanup(func() {
  80. for _, id := range ids {
  81. testutil.CleanTable(ctx, conn, "sys_product_member", id)
  82. }
  83. })
  84. // 排除 ids[0]:剩下应还有两个启用 ADMIN。
  85. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  86. n, e := m.CountOtherActiveAdminsTx(c, session, pc, ids[0])
  87. require.NoError(t, e)
  88. assert.Equal(t, int64(2), n,
  89. "L-5:MEMBER 与 Disabled 行不得被计入;排除自己后剩余 admin 数必须等于 2")
  90. return nil
  91. })
  92. require.NoError(t, err)
  93. }
  94. // TC-0869: 排除一个根本不存在的 id,结果与正向 CountActiveAdminsTx 一致(自洽性校验)。
  95. func TestCountOtherActiveAdminsTx_NonExistentExclude_EqualsTotal(t *testing.T) {
  96. ctx := context.Background()
  97. conn := testutil.GetTestSqlConn()
  98. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  99. pc := "t_pm_coaa_none_" + testutil.UniqueId()
  100. ts := time.Now().Unix()
  101. var ids []int64
  102. for i := 0; i < 2; i++ {
  103. res, err := m.Insert(ctx, &SysProductMember{
  104. ProductCode: pc, UserId: randProductMemberUserId(),
  105. MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
  106. CreateTime: ts, UpdateTime: ts,
  107. })
  108. require.NoError(t, err)
  109. id, _ := res.LastInsertId()
  110. ids = append(ids, id)
  111. }
  112. t.Cleanup(func() {
  113. for _, id := range ids {
  114. testutil.CleanTable(ctx, conn, "sys_product_member", id)
  115. }
  116. })
  117. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  118. total, e1 := m.CountActiveAdminsTx(c, session, pc)
  119. require.NoError(t, e1)
  120. other, e2 := m.CountOtherActiveAdminsTx(c, session, pc, -1) // -1 不存在
  121. require.NoError(t, e2)
  122. assert.Equal(t, total, other,
  123. "L-5:excludeId 不存在时,排除计数必须等于总计数;否则说明 WHERE 条件里多写或少写了什么")
  124. assert.Equal(t, int64(2), total, "前置数据校验")
  125. return nil
  126. })
  127. require.NoError(t, err)
  128. }
  129. // TC-0870: 空 productCode 不会串库 —— 不同产品线互不影响。
  130. func TestCountOtherActiveAdminsTx_ScopedByProductCode(t *testing.T) {
  131. ctx := context.Background()
  132. conn := testutil.GetTestSqlConn()
  133. m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
  134. ts := time.Now().Unix()
  135. pcA := "t_pm_coaa_A_" + testutil.UniqueId()
  136. pcB := "t_pm_coaa_B_" + testutil.UniqueId()
  137. // 产品 A 有 1 个 admin(自己),排除后应为 0;产品 B 有 2 个 admin,这条查询不应拉到产品 B。
  138. resA, err := m.Insert(ctx, &SysProductMember{
  139. ProductCode: pcA, UserId: randProductMemberUserId(),
  140. MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
  141. CreateTime: ts, UpdateTime: ts,
  142. })
  143. require.NoError(t, err)
  144. aId, _ := resA.LastInsertId()
  145. defer testutil.CleanTable(ctx, conn, "sys_product_member", aId)
  146. var bIds []int64
  147. for i := 0; i < 2; i++ {
  148. r, err := m.Insert(ctx, &SysProductMember{
  149. ProductCode: pcB, UserId: randProductMemberUserId(),
  150. MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
  151. CreateTime: ts, UpdateTime: ts,
  152. })
  153. require.NoError(t, err)
  154. id, _ := r.LastInsertId()
  155. bIds = append(bIds, id)
  156. }
  157. t.Cleanup(func() {
  158. for _, id := range bIds {
  159. testutil.CleanTable(ctx, conn, "sys_product_member", id)
  160. }
  161. })
  162. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  163. n, e := m.CountOtherActiveAdminsTx(c, session, pcA, aId)
  164. require.NoError(t, e)
  165. assert.Equal(t, int64(0), n,
  166. "L-5:pcA 的排除计数必须只看 pcA,绝不能把 pcB 的 2 个 admin 误计入")
  167. return nil
  168. })
  169. require.NoError(t, err)
  170. }