新增 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:
2026-07-22 09:11:50 +08:00
parent 1868c8dd15
commit 09f4c40d67
10 changed files with 2427 additions and 0 deletions
+234
View File
@@ -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)")
}