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 的函数别名,保持 兼容不用改调用方。
235 lines
8.6 KiB
Go
235 lines
8.6 KiB
Go
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)")
|
||
}
|