Files
req/resx/res_normalize_test.go
T
what 3c757d53c5 重构: User 默认实现移到 req/userx,消除 req 对 contracts 的循环依赖
GetAnonymous/GetSystemUser 原来在 contracts/base 里实现,req 自己的测试
又要用它们构造测试用户,形成 req(测试)依赖 contracts、contracts(正式
代码)依赖 req 的循环——两边 go.mod 只能靠指向本地兄弟目录的 replace 绕过,
没法各自独立解析成真实版本号。

跟 resx 之于 req.Resource 一个思路:接口留在 req 包,默认实现挪到子包
req/userx,req 主包保持不引入实现细节所需的依赖(这里是 samber/lo)。
req 自己的测试文件改用 req/userx,不用触碰 contracts 了;contracts/base
里的 GetAnonymous/GetSystemUser 改成指向 req/userx 的函数别名,保持
兼容不用改调用方。
2026-08-20 18:00:40 +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/db"
"git.fsdpf.net/go/db/engine"
"git.fsdpf.net/go/req"
"git.fsdpf.net/go/req/resx"
"git.fsdpf.net/go/req/userx"
"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(userx.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(userx.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(userx.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(userx.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(userx.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(userx.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(userx.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(userx.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(userx.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(userx.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)")
}