| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- package productmember
- import (
- "context"
- "testing"
- "time"
- "perms-system-server/internal/consts"
- "perms-system-server/internal/testutil"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- )
- // ---------------------------------------------------------------------------
- // 覆盖目标:审计第 6 轮 L-5 修复回归 —— CountOtherActiveAdminsTx 排除目标自己后计数。
- //
- // 原做法:removeMember / 降级时调用 CountActiveAdminsTx,再在业务层做 <=1 判断。
- // 问题:业务语义模糊、边界条件容易算错("算不算目标自己"),出事故后排查痛苦。
- // 修复后的契约:
- // - CountOtherActiveAdminsTx(ctx, session, productCode, excludeId)
- // 返回 productCode 下 memberType=ADMIN 且 status=Enabled 且 id != excludeId 的行数。
- // - 不再依赖调用方做减法,0 就是"最后一个 admin",1 就是"还有 1 个 backup admin"。
- // - 作用于事务 session,且为 FOR UPDATE,避免并发场景重复判断、双双放过。
- // ---------------------------------------------------------------------------
- // TC-0867: 唯一 admin 场景,排除自己后返回 0(代表"删掉/降级此人后没有启用 admin")。
- func TestCountOtherActiveAdminsTx_SoleAdmin_ReturnsZero(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
- pc := "t_pm_coaa_sole_" + testutil.UniqueId()
- adminUser := randProductMemberUserId()
- ts := time.Now().Unix()
- res, err := m.Insert(ctx, &SysProductMember{
- ProductCode: pc, UserId: adminUser,
- MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
- CreateTime: ts, UpdateTime: ts,
- })
- require.NoError(t, err)
- adminId, _ := res.LastInsertId()
- defer testutil.CleanTable(ctx, conn, "sys_product_member", adminId)
- err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- n, e := m.CountOtherActiveAdminsTx(c, session, pc, adminId)
- require.NoError(t, e)
- assert.Equal(t, int64(0), n,
- "L-5:唯一 admin 排除自己后必须为 0,调用方据此才能阻止删除最后一个 admin")
- return nil
- })
- require.NoError(t, err)
- }
- // TC-0868: 多 admin 场景,排除 A 后返回剩余 backup admin 数量。
- func TestCountOtherActiveAdminsTx_MultipleAdmins_ExcludesSelf(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
- pc := "t_pm_coaa_multi_" + testutil.UniqueId()
- ts := time.Now().Unix()
- // 插三个启用 ADMIN + 一个启用 MEMBER + 一个禁用 ADMIN,用来检验 WHERE 条件完整性。
- type row struct {
- mt string
- status int64
- }
- rows := []row{
- {consts.MemberTypeAdmin, consts.StatusEnabled},
- {consts.MemberTypeAdmin, consts.StatusEnabled},
- {consts.MemberTypeAdmin, consts.StatusEnabled},
- {consts.MemberTypeMember, consts.StatusEnabled}, // 不计入
- {consts.MemberTypeAdmin, consts.StatusDisabled}, // 不计入
- }
- ids := make([]int64, 0, len(rows))
- for _, r := range rows {
- uid := randProductMemberUserId()
- res, err := m.Insert(ctx, &SysProductMember{
- ProductCode: pc, UserId: uid,
- MemberType: r.mt, Status: r.status,
- CreateTime: ts, UpdateTime: ts,
- })
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- ids = append(ids, id)
- }
- t.Cleanup(func() {
- for _, id := range ids {
- testutil.CleanTable(ctx, conn, "sys_product_member", id)
- }
- })
- // 排除 ids[0]:剩下应还有两个启用 ADMIN。
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- n, e := m.CountOtherActiveAdminsTx(c, session, pc, ids[0])
- require.NoError(t, e)
- assert.Equal(t, int64(2), n,
- "L-5:MEMBER 与 Disabled 行不得被计入;排除自己后剩余 admin 数必须等于 2")
- return nil
- })
- require.NoError(t, err)
- }
- // TC-0869: 排除一个根本不存在的 id,结果与正向 CountActiveAdminsTx 一致(自洽性校验)。
- func TestCountOtherActiveAdminsTx_NonExistentExclude_EqualsTotal(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
- pc := "t_pm_coaa_none_" + testutil.UniqueId()
- ts := time.Now().Unix()
- var ids []int64
- for i := 0; i < 2; i++ {
- res, err := m.Insert(ctx, &SysProductMember{
- ProductCode: pc, UserId: randProductMemberUserId(),
- MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
- CreateTime: ts, UpdateTime: ts,
- })
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- ids = append(ids, id)
- }
- t.Cleanup(func() {
- for _, id := range ids {
- testutil.CleanTable(ctx, conn, "sys_product_member", id)
- }
- })
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- total, e1 := m.CountActiveAdminsTx(c, session, pc)
- require.NoError(t, e1)
- other, e2 := m.CountOtherActiveAdminsTx(c, session, pc, -1) // -1 不存在
- require.NoError(t, e2)
- assert.Equal(t, total, other,
- "L-5:excludeId 不存在时,排除计数必须等于总计数;否则说明 WHERE 条件里多写或少写了什么")
- assert.Equal(t, int64(2), total, "前置数据校验")
- return nil
- })
- require.NoError(t, err)
- }
- // TC-0870: 空 productCode 不会串库 —— 不同产品线互不影响。
- func TestCountOtherActiveAdminsTx_ScopedByProductCode(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := NewSysProductMemberModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix())
- ts := time.Now().Unix()
- pcA := "t_pm_coaa_A_" + testutil.UniqueId()
- pcB := "t_pm_coaa_B_" + testutil.UniqueId()
- // 产品 A 有 1 个 admin(自己),排除后应为 0;产品 B 有 2 个 admin,这条查询不应拉到产品 B。
- resA, err := m.Insert(ctx, &SysProductMember{
- ProductCode: pcA, UserId: randProductMemberUserId(),
- MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
- CreateTime: ts, UpdateTime: ts,
- })
- require.NoError(t, err)
- aId, _ := resA.LastInsertId()
- defer testutil.CleanTable(ctx, conn, "sys_product_member", aId)
- var bIds []int64
- for i := 0; i < 2; i++ {
- r, err := m.Insert(ctx, &SysProductMember{
- ProductCode: pcB, UserId: randProductMemberUserId(),
- MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled,
- CreateTime: ts, UpdateTime: ts,
- })
- require.NoError(t, err)
- id, _ := r.LastInsertId()
- bIds = append(bIds, id)
- }
- t.Cleanup(func() {
- for _, id := range bIds {
- testutil.CleanTable(ctx, conn, "sys_product_member", id)
- }
- })
- err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- n, e := m.CountOtherActiveAdminsTx(c, session, pcA, aId)
- require.NoError(t, e)
- assert.Equal(t, int64(0), n,
- "L-5:pcA 的排除计数必须只看 pcA,绝不能把 pcB 的 2 个 admin 误计入")
- return nil
- })
- require.NoError(t, err)
- }
|