Files
req/resx/res_permission_test.go
what f39adfa46c 测试: 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/按位组合)分别独立生效,作为这个
默认行为的回归防护。
2026-08-20 15:59:24 +08:00

131 lines
5.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}