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)") }