Files
req/resx/res_normalize_test.go
T
what 09f4c40d67 新增 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 资源实现包。
2026-07-22 09:11:50 +08:00

235 lines
8.6 KiB
Go
Raw 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 (
"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)")
}