测试: GetDBTable 显式传 WithPermission,补充权限组合覆盖

Permission 不传时按零值处理,行级过滤和字段脱敏都会跳过(见 options.go 里
Permission 字段的注释),这是既有设计,不是这次改的。但 resx 包自己的测试
之前几乎都没传 WithPermission,实际上一直跑在"权限检查关闭"的状态下——
字段脱敏、写权限过滤、行级过滤相关的断言全部失效但没有暴露出来。

给 res_mask_field_test.go/res_interceptor_test.go 里所有 GetDBTable 调用
显式加上 WithPermission(ResAll),让这些测试真正跑在权限检查打开的状态下。
另外新增 res_permission_test.go,单独验证 WithPermission 各个取值(不传/
显式传 0/单独 ResRow/单独 ResMask/ResAll/按位组合)分别独立生效,作为这个
默认行为的回归防护。
This commit is contained in:
2026-08-20 15:59:24 +08:00
parent 5cdf1cc00d
commit f39adfa46c
3 changed files with 177 additions and 47 deletions
+130
View File
@@ -0,0 +1,130 @@
package resx_test
import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/samber/do/v2"
"github.com/stretchr/testify/suite"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/engine"
"git.fsdpf.net/go/req"
"git.fsdpf.net/go/req/resx"
_ "git.fsdpf.net/go/db/dialect/mysql"
)
// permissionTest 验证 req.WithPermission(req.ResFlags) 各个取值(不传/传 0/单独 ResRow/单独
// ResMask/ResAll)分别独立生效:ResRow 控制行级过滤条件要不要拼进查询,ResMask 控制字段级脱敏
// 要不要生效,两者互不影响彼此。GetDBTable 不传 WithPermission 时按文档约定的零值处理(两者都
// 跳过),这里也显式测出来,作为这个默认行为的回归防护——之后如果谁想改这个默认值,这些测试会
// 先失败提醒。
type permissionTest struct {
suite.Suite
}
func TestPermission(t *testing.T) {
suite.Run(t, new(permissionTest))
}
// newPermissionResource 构造一个既配置了受限字段(salary,Roles=hr)、又注册了行级过滤条件
// owned_user = 当前用户)的资源,用同一份数据分别验证 ResRow/ResMask 两个开关。
func (t *permissionTest) newPermissionResource() (req.Resource, sqlmock.Sqlmock) {
mockDB, mock, err := sqlmock.New()
t.Require().NoError(err)
app := do.New()
do.Provide(app, func(i do.Injector) (engine.Engine, error) {
return engine.Mock(map[string]engine.MockDBConfig{
"default": {Driver: "mysql", Mock: mockDB},
}), nil
})
do.Provide(app, func(i do.Injector) (resx.DataInterceptor, error) {
return func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
return nil, db.C("owned_user").Eq(u.Uuid()), nil
}, nil
})
res := resx.New(app, "User", "users",
resx.WithConn("default"),
resx.WithFields(nameField, salaryField),
)
return res, mock
}
// TestNoPermissionOption_SkipsBothRowFilterAndMask 不传 WithPermissionPermission 零值,行级
// 过滤和字段脱敏都跳过——这是 options.go 里 Permission 字段注释明确写的默认行为
func (t *permissionTest) TestNoPermissionOption_SkipsBothRowFilterAndMask() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}).Select(db.Star()).Executor().ToSQL()
t.NotContains(sql, "owned_user", "不传 WithPermission 时行级过滤应该被跳过")
t.NotContains(sql, "-999999999", "不传 WithPermission 时字段脱敏应该被跳过")
}
// TestExplicitZeroPermission_SkipsBothRowFilterAndMask 显式传 WithPermission(0),效果跟不传一样
func (t *permissionTest) TestExplicitZeroPermission_SkipsBothRowFilterAndMask() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(0)).Select(db.Star()).Executor().ToSQL()
t.NotContains(sql, "owned_user")
t.NotContains(sql, "-999999999")
}
// TestResRowOnly_EnablesRowFilterOnlyNotMask 只传 ResRow:行级过滤生效,字段脱敏依旧跳过——两个
// 开关相互独立,不是绑在一起的
func (t *permissionTest) TestResRowOnly_EnablesRowFilterOnlyNotMask() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResRow)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "owned_user", "传了 ResRow,行级过滤应该生效")
t.NotContains(sql, "-999999999", "没传 ResMask,字段脱敏不该生效")
}
// TestResMaskOnly_EnablesMaskOnlyNotRowFilter 只传 ResMask:字段脱敏生效,行级过滤依旧跳过
func (t *permissionTest) TestResMaskOnly_EnablesMaskOnlyNotRowFilter() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResMask)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "-999999999 AS `salary`", "传了 ResMask,字段脱敏应该生效")
t.NotContains(sql, "owned_user", "没传 ResRow,行级过滤不该生效")
}
// TestResAll_EnablesBothRowFilterAndMask 传 ResAll:行级过滤和字段脱敏都生效
func (t *permissionTest) TestResAll_EnablesBothRowFilterAndMask() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "owned_user")
t.Contains(sql, "-999999999 AS `salary`")
}
// TestResRowOrResMaskBitwiseCombo_SameAsResAll 手动按位组合 ResRow|ResMask,效果跟直接传 ResAll
// 一样——ResAll 只是这个组合的一个别名,不是什么特殊值
func (t *permissionTest) TestResRowOrResMaskBitwiseCombo_SameAsResAll() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResRow|req.ResMask)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "owned_user")
t.Contains(sql, "-999999999 AS `salary`")
}
// TestUserHasRole_ResMaskStillOnButFieldPassesThrough ResMask 开启时,用户角色命中字段 Roles 的话
// 字段本身按真实值查询——ResMask 只是"开启检查",不是"无条件脱敏",命中权限时检查结果是放行
func (t *permissionTest) TestUserHasRole_ResMaskStillOnButFieldPassesThrough() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{roles: []string{"hr"}}, req.WithPermission(req.ResMask)).
Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "SELECT *", "没有字段需要脱敏时不会展开成显式列表")
t.NotContains(sql, "-999999999")
}