新增 resx 包剩余实现:字段/查询字段/脱敏/变更行等类型及测试
res_field.go/res_field_option.go(字段定义)、res_query_field.go/res_query_field_option.go(查询字段)、res_change_row.go(变更行)、res_mask_field.go(字段脱敏标记)及对应测试文件,配合此前已提交的 resource.go/res_interceptor.go 组成完整的 resx 资源实现包。
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
package resx_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/contracts/base"
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/db/engine"
|
||||
"git.fsdpf.net/go/req"
|
||||
"git.fsdpf.net/go/req/resx"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
_ "git.fsdpf.net/go/db/dialect/mysql"
|
||||
)
|
||||
|
||||
// normalizeTest 针对 dataProcessor 的 normalizeSaveValue / applyDefaultValue 两个私有方法做黑盒验证:
|
||||
// 由于它们只在 INSERT/UPDATE 前被调用,行为都会体现在最终生成的 SQL 上,所以借助 ToSQL() 而不必真正
|
||||
// 执行 Exec,也不需要 sqlmock 的 Expect* 断言。
|
||||
type normalizeTest struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestResourceNormalize(t *testing.T) {
|
||||
suite.Run(t, new(normalizeTest))
|
||||
}
|
||||
|
||||
const anonUUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
// newNormalizeResource 构造一个绑定了 sqlmock 的 User 资源,不设置 onBefore/onAfter:
|
||||
// normalizeSaveValue/applyDefaultValue 在 INSERT/UPDATE 前无条件执行,不依赖这两个钩子。
|
||||
func (t *normalizeTest) newNormalizeResource(fields ...req.ResField) req.Resource {
|
||||
mockDB, _, 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
|
||||
})
|
||||
|
||||
return resx.New(app, "User", "users",
|
||||
resx.WithConn("default"),
|
||||
resx.WithFields(fields...),
|
||||
)
|
||||
}
|
||||
|
||||
// -------- normalizeSaveValue --------
|
||||
|
||||
// TestStripsSystemFields id/created_at/deleted_at/updated_at 无条件剔除,created_user 被强制改写为当前用户
|
||||
func (t *normalizeTest) TestStripsSystemFields() {
|
||||
res := t.newNormalizeResource(
|
||||
resx.NewResField("name", "", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
|
||||
)
|
||||
|
||||
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
||||
db.Record{
|
||||
"id": 999,
|
||||
"name": "张三",
|
||||
"created_at": "2020-01-01",
|
||||
"deleted_at": "2020-01-01",
|
||||
"updated_at": "2020-01-01",
|
||||
"created_user": "attacker-uuid",
|
||||
},
|
||||
).Executor().ToSQL()
|
||||
|
||||
t.NotContains(sql, "attacker-uuid")
|
||||
t.NotContains(sql, "`id`")
|
||||
t.NotContains(sql, "`created_at`")
|
||||
t.NotContains(sql, "`deleted_at`")
|
||||
t.NotContains(sql, "`updated_at`")
|
||||
t.Contains(sql, "`created_user`")
|
||||
// created_user、owned_user 都会落到当前用户 uuid 上
|
||||
t.GreaterOrEqual(strings.Count(sql, anonUUID), 2)
|
||||
}
|
||||
|
||||
// TestDropsUndeclaredFields 未在 WithFields 中声明的字段会被静默丢弃
|
||||
func (t *normalizeTest) TestDropsUndeclaredFields() {
|
||||
res := t.newNormalizeResource(
|
||||
resx.NewResField("name", "", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
|
||||
)
|
||||
|
||||
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
||||
db.Record{"name": "李四", "hacker_field": "evil"},
|
||||
).Executor().ToSQL()
|
||||
|
||||
t.NotContains(sql, "hacker_field")
|
||||
t.NotContains(sql, "evil")
|
||||
t.Contains(sql, "李四")
|
||||
}
|
||||
|
||||
// TestExpressionValuePassesThroughEvenIfUndeclared db.Expression 类型的值会原样保留,
|
||||
// 既不会被 ToValue 转换,也不受"未声明字段丢弃"规则约束
|
||||
func (t *normalizeTest) TestExpressionValuePassesThroughEvenIfUndeclared() {
|
||||
res := t.newNormalizeResource(
|
||||
resx.NewResField("name", "", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
|
||||
)
|
||||
|
||||
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
||||
db.Record{"name": "王五", "login_at": db.L("NOW()")},
|
||||
).Executor().ToSQL()
|
||||
|
||||
t.Contains(sql, "`login_at`")
|
||||
t.Contains(sql, "NOW()")
|
||||
}
|
||||
|
||||
// TestJSONFieldNilUsesFieldDefault 显式传 nil 的 json 字段会被 field.ToValue 换成该字段的默认值
|
||||
func (t *normalizeTest) TestJSONFieldNilUsesFieldDefault() {
|
||||
res := t.newNormalizeResource(
|
||||
resx.NewResField("name", "", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
|
||||
resx.NewResField("fields", "", resx.FieldWithName("字段"), resx.FieldWithDefault("[]"), resx.FieldWithDataType(req.ResJson)),
|
||||
)
|
||||
|
||||
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
||||
db.Record{"name": "A", "fields": nil},
|
||||
).Executor().ToSQL()
|
||||
|
||||
t.Contains(sql, "`fields`")
|
||||
t.Contains(sql, "'[]'")
|
||||
}
|
||||
|
||||
// -------- applyDefaultValue --------
|
||||
|
||||
// TestJSONFieldDefaultFilledWhenOmitted 完全不传 json 字段时,applyDefaultValue 会按字段默认值补全
|
||||
func (t *normalizeTest) TestJSONFieldDefaultFilledWhenOmitted() {
|
||||
res := t.newNormalizeResource(
|
||||
resx.NewResField("name", "", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
|
||||
resx.NewResField("fields", "", resx.FieldWithName("字段"), resx.FieldWithDefault("{}"), resx.FieldWithDataType(req.ResJson)),
|
||||
)
|
||||
|
||||
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
||||
db.Record{"name": "B"},
|
||||
).Executor().ToSQL()
|
||||
|
||||
t.Contains(sql, "`fields`")
|
||||
t.Contains(sql, "'{}'")
|
||||
}
|
||||
|
||||
// TestNonJSONFieldDefaultNotAutoFilled 非 json 类型字段的 Default 不会被 applyDefaultValue 自动补全
|
||||
// (getFieldsDefaultValue 只处理 json 字段和 sql: 前缀的默认值),完全依赖数据库表结构自身的默认值
|
||||
func (t *normalizeTest) TestNonJSONFieldDefaultNotAutoFilled() {
|
||||
res := t.newNormalizeResource(
|
||||
resx.NewResField("name", "", resx.FieldWithName("姓名"), resx.FieldWithDefault("匿名"), resx.FieldWithDataType(req.ResString)),
|
||||
)
|
||||
|
||||
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
||||
db.Record{},
|
||||
).Executor().ToSQL()
|
||||
|
||||
t.NotContains(sql, "`name`")
|
||||
t.NotContains(sql, "匿名")
|
||||
}
|
||||
|
||||
// TestOwnedUserDefaultsToCurrentUserWhenUndeclared owned_user 未声明为资源字段时,
|
||||
// 调用方传入的值会被 normalizeSaveValue 当作未知字段丢弃,再由 applyDefaultValue 强制填充为当前用户
|
||||
func (t *normalizeTest) TestOwnedUserDefaultsToCurrentUserWhenUndeclared() {
|
||||
res := t.newNormalizeResource(
|
||||
resx.NewResField("name", "", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
|
||||
)
|
||||
|
||||
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
||||
db.Record{"name": "C", "owned_user": "someone-else-uuid"},
|
||||
).Executor().ToSQL()
|
||||
|
||||
t.NotContains(sql, "someone-else-uuid")
|
||||
t.Contains(sql, "`owned_user`")
|
||||
t.Contains(sql, anonUUID)
|
||||
}
|
||||
|
||||
// TestOwnedUserPreservedWhenDeclaredAsField 一旦把 owned_user 声明成资源字段,
|
||||
// 调用方传入的值会被 field.ToValue 保留,applyDefaultValue 见 row 里已有值就不会覆盖
|
||||
func (t *normalizeTest) TestOwnedUserPreservedWhenDeclaredAsField() {
|
||||
res := t.newNormalizeResource(
|
||||
resx.NewResField("name", "", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
|
||||
resx.NewResField("owned_user", "", resx.FieldWithName("拥有者"), resx.FieldWithDataType(req.ResString)),
|
||||
)
|
||||
|
||||
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
||||
db.Record{"name": "D", "owned_user": "someone-else-uuid"},
|
||||
).Executor().ToSQL()
|
||||
|
||||
t.Contains(sql, "someone-else-uuid")
|
||||
}
|
||||
|
||||
// TestCreatedUserAlwaysForcedToCurrentUser created_user 属于系统字段,无论是否声明为资源字段,
|
||||
// 都会被 normalizeSaveValue 无条件剔除,再由 applyDefaultValue 强制写入当前用户
|
||||
func (t *normalizeTest) TestCreatedUserAlwaysForcedToCurrentUser() {
|
||||
res := t.newNormalizeResource(
|
||||
resx.NewResField("name", "", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
|
||||
resx.NewResField("created_user", "", resx.FieldWithName("创建者"), resx.FieldWithDataType(req.ResString)),
|
||||
)
|
||||
|
||||
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
||||
db.Record{"name": "E", "created_user": "someone-else-uuid"},
|
||||
).Executor().ToSQL()
|
||||
|
||||
t.NotContains(sql, "someone-else-uuid")
|
||||
t.Contains(sql, "`created_user`")
|
||||
t.Contains(sql, anonUUID)
|
||||
}
|
||||
|
||||
// -------- UPDATE 场景下的 normalizeSaveValue --------
|
||||
|
||||
// TestNormalizeSaveValue_Update Set() 传入的数据同样会经过 normalizeSaveValue:
|
||||
// 剔除系统字段、丢弃未声明字段、对声明字段做 ToValue 转换
|
||||
func (t *normalizeTest) TestNormalizeSaveValue_Update() {
|
||||
res := t.newNormalizeResource(
|
||||
resx.NewResField("name", "", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
|
||||
resx.NewResField("fields", "", resx.FieldWithName("字段"), resx.FieldWithDefault("{}"), resx.FieldWithDataType(req.ResJson)),
|
||||
)
|
||||
|
||||
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Update().
|
||||
Set(db.Record{
|
||||
"id": 1,
|
||||
"name": "F",
|
||||
"fields": map[string]any{"x": 1},
|
||||
"hacker": "y",
|
||||
"updated_at": "ignored",
|
||||
}).
|
||||
Where(db.C("id").Eq(1)).
|
||||
Executor().ToSQL()
|
||||
|
||||
t.NotContains(sql, "`hacker`")
|
||||
t.NotContains(sql, "ignored")
|
||||
t.NotContains(sql, "`updated_at`=")
|
||||
t.NotContains(sql, "`id`=")
|
||||
t.Contains(sql, "`name`='F'")
|
||||
t.Contains(sql, "`fields`='{\\\"x\\\":1}'")
|
||||
// WHERE 条件里的 id 不受影响
|
||||
t.Contains(sql, "WHERE (`id` = 1)")
|
||||
}
|
||||
Reference in New Issue
Block a user