| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- package user
- import (
- "context"
- "errors"
- "testing"
- "time"
- permModel "perms-system-server/internal/model/perm"
- "perms-system-server/internal/response"
- "perms-system-server/internal/svc"
- "perms-system-server/internal/testutil"
- "perms-system-server/internal/testutil/ctxhelper"
- "perms-system-server/internal/types"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // ---------------------------------------------------------------------------
- // 审计 L-4(第 8 轮)—— SetUserPerms 事务末 COUNT(*) 复核 sys_perm.status=1,
- // 把"FindByIds 通过 → 事务外某次 SyncPermissions 先把 permId 置为 DISABLED →
- // BatchInsertWithTx 把脏行写进 sys_user_perm"的 TOCTOU 窗口收紧。
- //
- // 测试思路:把 SysPermModel 用一个薄装饰器替换,让 FindByIds 说谎(返回 Enabled),
- // 但实际上 DB 里这批 permId 其实是 Disabled。这样:
- // - 前置 FindByIds 校验通过;
- // - 进入 TransactCtx,BatchInsertWithTx 成功;
- // - 事务末 COUNT(*) WHERE status=1 的真实 DB 读返回 0 ≠ 1 → 回滚,返回 409;
- // - sys_user_perm 必须一行脏数据都不剩。
- //
- // 如果 L-4 的复核被移除(或误改成 status != 0),COUNT 返回会≠0,脏行会被落盘,
- // 此测试自动失败。
- // ---------------------------------------------------------------------------
- // lyingSysPermModel 只重写 FindByIds:不管 DB 里 status 是什么,都声称是 Enabled。
- // 这是唯一一个能稳定模拟"前置 FindByIds → tx 内真实 status" 时序差的办法。
- type lyingSysPermModel struct {
- permModel.SysPermModel
- lyingProductCode string
- }
- func (m *lyingSysPermModel) FindByIds(ctx context.Context, ids []int64) ([]*permModel.SysPerm, error) {
- real, err := m.SysPermModel.FindByIds(ctx, ids)
- if err != nil {
- return nil, err
- }
- for _, p := range real {
- p.ProductCode = m.lyingProductCode
- p.Status = 1
- }
- return real, nil
- }
- // TC-0988: TOCTOU 复核 —— 前置检查通过但实际 Disabled,事务末 COUNT 必须触发 409 回滚。
- func TestSetUserPerms_L4_TOCTOU_CountMismatch_RollsBackWith409(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- // 直接在 DB 里塞一个 status=Disabled 的 perm,模拟 SyncPermissions 已经提交
- // 把这个 perm 落盘为 Disabled 的状态。
- now := time.Now().Unix()
- res, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
- ProductCode: "test_product",
- Name: "l4_disabled_" + testutil.UniqueId(),
- Code: "l4_dis_" + testutil.UniqueId(),
- Status: 2, // Disabled
- CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- disabledPermId, _ := res.LastInsertId()
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", disabledPermId)
- })
- // 装饰 SysPermModel:让 FindByIds 撒谎(Status=1, productCode=test_product)。
- svcCtx.SysPermModel = &lyingSysPermModel{
- SysPermModel: svcCtx.SysPermModel,
- lyingProductCode: "test_product",
- }
- err = NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{{PermId: disabledPermId, Effect: "ALLOW"}},
- })
- require.Error(t, err, "L-4:前置通过但 DB 实际 Disabled 时,事务末 COUNT 必须触发 409")
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 409, ce.Code(),
- "L-4:TOCTOU 复核必须返回 409 Conflict;若仍是 200/4xx 说明复核 COUNT 被移除,"+
- "脏 user_perm 会被真实落盘")
- assert.Contains(t, ce.Error(), "已被禁用",
- "L-4:错误文案必须明示'部分权限在提交时已被禁用',供前端判定是否重试")
- // 最关键的断言:脏行必须不可能落盘。
- leftover := findUserPerms(t, ctx, userId)
- assert.Empty(t, leftover,
- "L-4:事务必须回滚;如果发现 sys_user_perm 有脏行,说明 COUNT 复核失效或"+
- "事务隔离性被破坏,loadPerms 的 status=1 过滤能兜底但会绕开审计链")
- }
- // TC-0989: 正向基线 —— 所有 perm 真实 Enabled 时,不得被 L-4 复核误杀。
- // 这条显式"不回滚"的断言防止未来有人把 COUNT 改成 "!=" 逻辑或把阈值改错。
- func TestSetUserPerms_L4_AllEnabled_CountPasses(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass"))
- mId := insertTestMember(t, svcCtx, "test_product", userId)
- p1 := insertTestPerm(t, svcCtx, "test_product")
- p2 := insertTestPerm(t, svcCtx, "test_product")
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user_perm`", "userId", userId)
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", userId)
- testutil.CleanTable(ctx, conn, "`sys_perm`", p1, p2)
- })
- err := NewSetUserPermsLogic(ctx, svcCtx).SetUserPerms(&types.SetPermsReq{
- UserId: userId,
- Perms: []types.UserPermItem{
- {PermId: p1, Effect: "ALLOW"},
- {PermId: p2, Effect: "DENY"},
- },
- })
- require.NoError(t, err, "L-4 复核不得误杀正常写入;一旦误报会把正常管理操作变 409")
- rows := findUserPerms(t, ctx, userId)
- assert.Len(t, rows, 2, "两条 user_perm 必须落盘")
- }
|