新增 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
+700
View File
@@ -0,0 +1,700 @@
package resx_test
import (
"database/sql"
"errors"
"fmt"
"testing"
"time"
"git.fsdpf.net/go/contracts/base"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/engine"
"git.fsdpf.net/go/reflux"
"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"
_ "git.fsdpf.net/go/db/dialect/sqlite3"
_ "git.fsdpf.net/go/db/schema/dialect/sqlite3"
)
// hooksTest 用 sqlmock 验证 dataProcessor 的钩子编排:原来的 RolesConditionFunc(权限过滤)、
// NeedsChangeCaptureFunc(是否需要抓取)、AfterChangeFunc(变更内容)、OnWriteFunc(无条件触发)
// 四块能力现在是 DataInterceptor(权限过滤 + 是否需要抓取)+ ResChangeEventFunc(无条件触发)+
// ResChangeRowFunc(变更内容,仅需要抓取时调用),这里按原来的四块能力分别验证。
type hooksTest struct {
suite.Suite
}
func TestResourceHooks(t *testing.T) {
suite.Run(t, new(hooksTest))
}
// newResource 构造一个绑定了 sqlmock 的 User 资源,interceptor/event 为 nil 表示不注册对应钩子。
func (t *hooksTest) newResource(
interceptor resx.DataInterceptor,
event resx.ResChangeEventFunc,
) (req.Resource, sqlmock.Sqlmock) {
mockDB, mock, 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
})
if interceptor != nil {
do.Provide(app, func(i do.Injector) (resx.DataInterceptor, error) {
return interceptor, nil
})
}
if event != nil {
do.Provide(app, func(i do.Injector) (resx.ResChangeEventFunc, error) {
return event, nil
})
}
res := resx.New(app, "User", "users",
resx.WithUuid("4bf3a311-cbe1-4236-bdda-c64ab04ae9b1"),
resx.WithConn("default"),
resx.WithFields(
resx.NewResField("name", "User", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
),
)
return res, mock
}
// -------- 原 RolesConditionFunc:权限过滤 --------
// TestOnBefore_RolesCondition_Cond 权限条件直接拼进 WHERE
func (t *hooksTest) TestOnBefore_RolesCondition_Cond() {
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
t.Equal(resx.ResEventSelect, category, "SELECT 场景的 category 是 ResEventSelect")
return nil, db.C("owned_user").Eq(u.Uuid()), nil
},
nil,
)
// 实际生成的 SQL
// SELECT * FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000')
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "owned_user")
t.Contains(sql, "00000000-0000-0000-0000-000000000000")
mock.ExpectQuery("SELECT").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "张三"))
rows, err := res.GetDBTable(base.GetAnonymous()).Select(db.Star()).Executor().GetRecords()
t.NoError(err)
t.Len(rows, 1)
t.NoError(mock.ExpectationsWereMet())
}
// TestOnBefore_RolesCondition_Sub 权限条件以子查询的形式重写 FROM
func (t *hooksTest) TestOnBefore_RolesCondition_Sub() {
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
sub := res.DB().From(res.GetTableExpr().As(res.GetCode())).
Select(db.Star()).
Where(db.C("owned_user").Eq(u.Uuid()))
return sub, nil, nil
},
nil,
)
// 实际生成的 SQL
// SELECT * FROM (SELECT * FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000')) AS `User`
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "owned_user")
mock.ExpectQuery("SELECT").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(2, "李四"))
rows, err := res.GetDBTable(base.GetAnonymous()).Select(db.Star()).Executor().GetRecords()
t.NoError(err)
t.Len(rows, 1)
t.NoError(mock.ExpectationsWereMet())
}
// -------- 原 NeedsChangeCaptureFunc:是否需要抓取快照/变更内容 --------
// TestOnBefore_NeedsCapture_True 判定需要抓取时,UPDATE 前会多一次快照查询,ResChangeRowFunc 拿到完整的变更行;
// onChange 非 nil 时写操作还会被自动包一层事务(见 UseTx)
func (t *hooksTest) TestOnBefore_NeedsCapture_True() {
var gotRows []resx.ResChangeRow
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
if category != resx.ResEventUpdate {
return nil, nil, nil
}
return nil, nil, func(rows []resx.ResChangeRow) error {
gotRows = rows
return nil
}
},
nil,
)
// onChange 非 nil,写操作会被自动包一层事务
mock.ExpectBegin()
// 抓取快照的前置 SELECT,实际生成的 SQL:
// SELECT * FROM `users` AS `User` WHERE (`id` = 1)
mock.ExpectQuery("SELECT").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "旧名字"))
// 实际 UPDATE,实际生成的 SQL:
// UPDATE `users` AS `User` SET `name`='新名字' WHERE (`id` = 1)
mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Update().
Set(db.Record{"name": "新名字"}).
Where(db.C("id").Eq(1)).
Executor().Exec()
t.NoError(err)
t.NoError(mock.ExpectationsWereMet())
t.Require().Len(gotRows, 1)
t.Equal(resx.ResEventUpdate, gotRows[0].Type())
t.EqualValues(1, gotRows[0].ID())
t.Equal("旧名字", gotRows[0].Old().Get("name").String())
t.Equal("新名字", gotRows[0].New().Get("name").String())
}
// TestOnBefore_NeedsCapture_False 判定不需要抓取时,不会有额外的快照查询,也不会自动开事务;
// ResChangeEventFunc 依然无条件调用,但 ResChangeRowFunc 完全不会被调用
func (t *hooksTest) TestOnBefore_NeedsCapture_False() {
var eventCalled bool
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
return nil, nil, nil
},
func(event resx.ResChangeEvent) error {
eventCalled = true
return nil
},
)
// 没有安排快照查询、没有 Begin:如果 Before 阶段多发起一次 SELECT 或事务,这里会因为顺序对不上而报错。
// 实际生成的 SQLUPDATE `users` AS `User` SET `name`='新名字' WHERE (`id` = 1)
mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1))
_, err := res.GetDBTable(base.GetAnonymous()).Update().
Set(db.Record{"name": "新名字"}).
Where(db.C("id").Eq(1)).
Executor().Exec()
t.NoError(err)
t.NoError(mock.ExpectationsWereMet())
t.True(eventCalled, "ResChangeEventFunc 不受 needsCapture 影响,应该无条件调用")
}
// -------- 原 AfterChangeFunc:变更内容 --------
// TestOnAfter_RowsContent_Insert 验证 ResChangeRowFunc 收到的 INSERT 变更行内容:Type/Res/ID/Old/New/Tx
func (t *hooksTest) TestOnAfter_RowsContent_Insert() {
var gotRows []resx.ResChangeRow
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
if category != resx.ResEventInsert {
return nil, nil, nil
}
return nil, nil, func(rows []resx.ResChangeRow) error {
gotRows = rows
return nil
}
},
nil,
)
// onChange 非 nil,写操作会被自动包一层事务
mock.ExpectBegin()
// 实际生成的 SQL
// INSERT INTO `users` (`created_user`, `name`, `owned_user`) VALUES ('00000000-0000-0000-0000-000000000000', '张三', '00000000-0000-0000-0000-000000000000')
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(10, 1))
mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Insert().
Rows(db.Record{"name": "张三"}).
Executor().Exec()
t.NoError(err)
t.NoError(mock.ExpectationsWereMet())
t.Require().Len(gotRows, 1)
row := gotRows[0]
t.Equal(resx.ResEventInsert, row.Type())
t.Equal("User", row.Res().GetCode())
t.EqualValues(10, row.ID())
t.Equal(reflux.Nil, row.Old(), "INSERT 没有变更前快照")
t.Equal("张三", row.New().Get("name").String())
t.NotNil(row.Tx(), "onChange 非 nil 时会自动开启事务,Tx() 应该能拿到它")
}
// -------- 原 OnWriteFunc:无条件触发 --------
// TestOnAfter_UnconditionalOnWrite ResChangeEventFunc 不受 needsCapture 门槛限制,每次写操作成功后都会调用
func (t *hooksTest) TestOnAfter_UnconditionalOnWrite() {
var afterCalled bool
var gotResult sql.Result
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
return nil, nil, nil // 不需要留痕
},
func(event resx.ResChangeEvent) error {
afterCalled = true
gotResult = event.Result()
return nil
},
)
// 实际生成的 SQLDELETE FROM `users` AS `User` WHERE (`id` = 1)
mock.ExpectExec("DELETE").WillReturnResult(sqlmock.NewResult(0, 3))
_, err := res.GetDBTable(base.GetAnonymous()).Delete().
Where(db.C("id").Eq(1)).
Executor().Exec()
t.NoError(err)
t.NoError(mock.ExpectationsWereMet())
t.True(afterCalled, "ResChangeEventFunc 应该无条件触发,不受 needsCapture 门槛限制")
affected, _ := gotResult.RowsAffected()
t.EqualValues(3, affected)
}
// -------- INSERT ... SELECT:写入前拿不到具体行数据,写入后按 id 反查补全 --------
// TestInsertFromQuery_FetchesInsertedRowsAfterward beforeInsertFromQuery 是空实现,
// 真正的内容在 After 阶段:rh.rows 为空触发 fetchInsertedRows,按写入后拿到的自增 id 反查补全。
// fetchInsertedRows 走的是独立的 GetDBTable 连接,不在自动开启的事务里,所以顺序是
// Begin -> INSERT -> 反查 SELECT -> Commit。
func (t *hooksTest) TestInsertFromQuery_FetchesInsertedRowsAfterward() {
var gotRows []resx.ResChangeRow
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
if category != resx.ResEventInsert {
return nil, nil, nil
}
return nil, nil, func(rows []resx.ResChangeRow) error {
gotRows = rows
return nil
}
},
nil,
)
sub := res.DB().From(db.T("legacy_users")).Select(db.Star())
mock.ExpectBegin()
// 实际生成的 SQLINSERT INTO `users` SELECT * FROM `legacy_users`
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(10, 2))
// finalizeInsertedRows 反查补全,实际生成的 SQL:
// SELECT * FROM `users` AS `User` WHERE (`id` IN (10, 11))
mock.ExpectQuery("SELECT").WillReturnRows(
sqlmock.NewRows([]string{"id", "name"}).AddRow(10, "张三").AddRow(11, "李四"),
)
mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Insert().FromQuery(sub).Executor().Exec()
t.NoError(err)
t.NoError(mock.ExpectationsWereMet())
t.Require().Len(gotRows, 2)
ids := []int64{gotRows[0].ID(), gotRows[1].ID()}
t.ElementsMatch([]int64{10, 11}, ids)
names := []string{gotRows[0].New().Get("name").String(), gotRows[1].New().Get("name").String()}
t.ElementsMatch([]string{"张三", "李四"}, names)
}
// -------- 批量 INSERT 的自增 id 分配 --------
// TestBatchInsertIds_Mysql mysql: lastId 是第一条,后续行 id 依次递增
func (t *hooksTest) TestBatchInsertIds_Mysql() {
var gotRows []resx.ResChangeRow
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
return nil, nil, func(rows []resx.ResChangeRow) error {
gotRows = rows
return nil
}
},
nil,
)
mock.ExpectBegin()
// 实际生成的 SQL
// INSERT INTO `users` (`created_user`, `name`, `owned_user`) VALUES
// ('00000000-0000-0000-0000-000000000000', 'A', '00000000-0000-0000-0000-000000000000'),
// ('00000000-0000-0000-0000-000000000000', 'B', '00000000-0000-0000-0000-000000000000'),
// ('00000000-0000-0000-0000-000000000000', 'C', '00000000-0000-0000-0000-000000000000')
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(100, 3))
mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
db.Record{"name": "A"}, db.Record{"name": "B"}, db.Record{"name": "C"},
).Executor().Exec()
t.NoError(err)
t.NoError(mock.ExpectationsWereMet())
t.Require().Len(gotRows, 3)
t.EqualValues(100, gotRows[0].ID())
t.EqualValues(101, gotRows[1].ID())
t.EqualValues(102, gotRows[2].ID())
}
// TestBatchInsertIds_LocalDB sqlite3/duckdb 等本地库:lastId 是最后一条,需要倒推前面几条的 id。
// 这里用真实的内存 sqlite3 而不是 sqlmockDB() 现在会自动建表(见 resx.go 的
// autoCreateTable),sqlmock 没法真的执行 CREATE TABLE/INSERT,所以改用真实 SQLite 让建表和
// 插入都按真实语义跑一遍。连接名用独立的(不用 "default"),避免和其它用例共享 engine 包内部
// 的全局连接缓存。onChange 非 nil 会自动 Begin/Commit 一个真实事务,无需额外 mock。
func (t *hooksTest) TestBatchInsertIds_LocalDB() {
// engine 的连接缓存是包级单例,按名字缓存 *sql.DB;连接名必须每次唯一,
// 否则 -count=N 重跑时会复用上一轮建好表、插过数据的同一个内存库,自增 id 对不上
connName := fmt.Sprintf("test_batch_insert_local_db_%d", time.Now().UnixNano())
app := do.New()
do.Provide(app, func(i do.Injector) (engine.Engine, error) {
return engine.Open(map[string]engine.DBConfig{
connName: engine.NewDBConfig("sqlite3", engine.WithSQLiteFile(":memory:")),
}), nil
})
var gotRows []resx.ResChangeRow
do.Provide(app, func(i do.Injector) (resx.DataInterceptor, error) {
return func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
return nil, nil, func(rows []resx.ResChangeRow) error {
gotRows = rows
return nil
}
}, nil
})
// id/created_user/owned_user 是 autoCreateTable 建表、以及 applyDefaultValue 无条件填充
// 用到的系统字段,真实建表场景下必须显式声明,否则真实 INSERT 会报"列不存在"
res := resx.New(app, "User", "users",
resx.WithConn(connName),
resx.WithFields(
resx.NewResField("id", "User", resx.FieldWithName("ID"), resx.FieldWithDataType(req.ResInteger)),
resx.NewResField("name", "User", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString)),
resx.NewResField("created_user", "User", resx.FieldWithName("创建者"), resx.FieldWithDataType(req.ResString)),
resx.NewResField("owned_user", "User", resx.FieldWithName("拥有者"), resx.FieldWithDataType(req.ResString)),
),
)
_, err := res.GetDBTable(base.GetAnonymous()).Insert().Rows(
db.Record{"name": "A"}, db.Record{"name": "B"}, db.Record{"name": "C"},
).Executor().Exec()
t.Require().NoError(err)
// 真实自增:一条 INSERT 里的 lastId 是最后一条(3),insertRowIds 按 isLocalDB 分支倒推出 1/2/3
t.Require().Len(gotRows, 3)
t.EqualValues(1, gotRows[0].ID())
t.EqualValues(2, gotRows[1].ID())
t.EqualValues(3, gotRows[2].ID())
}
// -------- UPDATE/DELETE 权限过滤的子查询改写 --------
// TestUpdate_RolesConditionSub 权限条件以子查询形式重写 WHEREUser.id IN (子查询)
func (t *hooksTest) TestUpdate_RolesConditionSub() {
res, _ := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
sub := res.DB().From(res.GetTableExpr().As(res.GetCode())).
Select(db.Star()).
Where(db.C("owned_user").Eq(u.Uuid()))
return sub, nil, nil
},
nil,
)
// 实际生成的 SQL
// UPDATE `users` AS `User` SET `name`='新名字' WHERE ((`id` = 1) AND (`id` IN ((SELECT `User`.`id` FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000')))))
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Update().
Set(db.Record{"name": "新名字"}).
Where(db.C("id").Eq(1)).
Executor().ToSQL()
t.Contains(sql, "`id` IN ((SELECT")
t.Contains(sql, "owned_user")
// 原有的 WHERE 条件依然保留
t.Contains(sql, "`id` = 1")
}
// TestDelete_RolesConditionSub DELETE 场景下同样会被改写成子查询形式
func (t *hooksTest) TestDelete_RolesConditionSub() {
res, _ := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
sub := res.DB().From(res.GetTableExpr().As(res.GetCode())).
Select(db.Star()).
Where(db.C("owned_user").Eq(u.Uuid()))
return sub, nil, nil
},
nil,
)
// 实际生成的 SQL
// DELETE FROM `users` AS `User` WHERE ((`id` = 1) AND (`id` IN ((SELECT `User`.`id` FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000')))))
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Delete().
Where(db.C("id").Eq(1)).
Executor().ToSQL()
t.Contains(sql, "`id` IN ((SELECT")
t.Contains(sql, "owned_user")
t.Contains(sql, "`id` = 1")
}
// -------- DELETE 的快照捕获 --------
// TestDelete_CapturesSnapshot 判定需要抓取时,DELETE 前会先查一次快照,ResChangeRowFunc 能拿到被删行的 Old()
func (t *hooksTest) TestDelete_CapturesSnapshot() {
var gotRows []resx.ResChangeRow
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
if category != resx.ResEventDelete {
return nil, nil, nil
}
return nil, nil, func(rows []resx.ResChangeRow) error {
gotRows = rows
return nil
}
},
nil,
)
mock.ExpectBegin()
// 抓取快照的前置 SELECT,实际生成的 SQL:
// SELECT * FROM `users` AS `User` WHERE (`id` = 5)
mock.ExpectQuery("SELECT").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(5, "旧值"))
// 实际 DELETE,实际生成的 SQLDELETE FROM `users` AS `User` WHERE (`id` = 5)
mock.ExpectExec("DELETE").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Delete().
Where(db.C("id").Eq(5)).
Executor().Exec()
t.NoError(err)
t.NoError(mock.ExpectationsWereMet())
t.Require().Len(gotRows, 1)
t.Equal(resx.ResEventDelete, gotRows[0].Type())
t.EqualValues(5, gotRows[0].ID())
t.Equal("旧值", gotRows[0].Old().Get("name").String())
}
// -------- Cols/Vals 插入路径 --------
// TestInsertColsVals_Capture Cols/Vals 路径同样支持 capture,能拿到本次写入的字段值
func (t *hooksTest) TestInsertColsVals_Capture() {
var gotRows []resx.ResChangeRow
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
if category != resx.ResEventInsert {
return nil, nil, nil
}
return nil, nil, func(rows []resx.ResChangeRow) error {
gotRows = rows
return nil
}
},
nil,
)
mock.ExpectBegin()
// 实际生成的 SQL
// INSERT INTO `users` (`name`, `owned_user`, `created_user`) VALUES ('赵六', '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000')
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Insert().Cols("name").Vals(
db.Vals{"赵六"},
).Executor().Exec()
t.NoError(err)
t.NoError(mock.ExpectationsWereMet())
t.Require().Len(gotRows, 1)
t.Equal("赵六", gotRows[0].New().Get("name").String())
}
// TestInsertColsVals_DoesNotStripSystemColumns Cols/Vals 路径不会调用 normalizeSaveValue
// 调用方显式把 created_user 放进 Cols 时不会像 Rows() 路径那样被剔除/改写——这是两条路径
// 目前存在的行为差异,用测试把它钉住,避免以后被误当成两条路径完全等价。
func (t *hooksTest) TestInsertColsVals_DoesNotStripSystemColumns() {
res, _ := t.newResource(nil, nil)
// 实际生成的 SQLcreated_user 没有被剔除/改写):
// INSERT INTO `users` (`name`, `created_user`, `owned_user`) VALUES ('赵六', 'attacker-uuid', '00000000-0000-0000-0000-000000000000')
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Cols("name", "created_user").Vals(
db.Vals{"赵六", "attacker-uuid"},
).Executor().ToSQL()
t.Contains(sql, "attacker-uuid")
}
// -------- 虚拟资源短路 --------
// TestVirtualResource_SkipsRolesConditionAndOnAfter 虚拟资源的 UPDATE/DELETE 不做权限过滤,
// 写操作完成后也不会触发 ResChangeEventFunc/ResChangeRowFunc,也不会自动开事务
func (t *hooksTest) TestVirtualResource_SkipsRolesConditionAndOnAfter() {
var eventCalled, changeCalled bool
mockDB, mock, 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
})
do.Provide(app, func(i do.Injector) (resx.DataInterceptor, error) {
return func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
return nil, db.C("owned_user").Eq(u.Uuid()), func(rows []resx.ResChangeRow) error {
changeCalled = true
return nil
}
}, nil
})
do.Provide(app, func(i do.Injector) (resx.ResChangeEventFunc, error) {
return func(event resx.ResChangeEvent) error {
eventCalled = true
return nil
}, nil
})
res := resx.New(app, "User", "users",
resx.WithConn("default"),
resx.WithVirtual(true),
resx.WithFields(resx.NewResField("name", "User", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString))),
)
// 实际生成的 SQL(虚拟资源用字面量拼表名,且没有 owned_user 权限条件):
// UPDATE (users) AS `User` SET `name`='新名字' WHERE (`id` = 1)
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Update().
Set(db.Record{"name": "新名字"}).
Where(db.C("id").Eq(1)).
Executor().ToSQL()
t.NotContains(sql, "owned_user", "虚拟资源不应该走权限过滤改写")
// 没有安排 Begin:虚拟资源在 UseTx 里直接短路,不会自动开事务
mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1))
_, err = res.GetDBTable(base.GetAnonymous()).Update().
Set(db.Record{"name": "新名字"}).
Where(db.C("id").Eq(1)).
Executor().Exec()
t.NoError(err)
t.NoError(mock.ExpectationsWereMet())
t.False(eventCalled, "虚拟资源写操作后不应该触发 ResChangeEventFunc")
t.False(changeCalled, "虚拟资源写操作后不应该触发 ResChangeRowFunc")
}
// -------- 变更内容里字面量表达式的还原 --------
// TestBuildChanges_ResolvesNonPlaceholderLiteral resolveLiteralValue 对形如 db.L("NOW()") 这种
// 没有占位符的字面量表达式,应该还原成它的原始文本,而不是占位符对应的参数
func (t *hooksTest) TestBuildChanges_ResolvesNonPlaceholderLiteral() {
var gotRows []resx.ResChangeRow
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
if category != resx.ResEventUpdate {
return nil, nil, nil
}
return nil, nil, func(rows []resx.ResChangeRow) error {
gotRows = rows
return nil
}
},
nil,
)
mock.ExpectBegin()
// 抓取快照的前置 SELECT,实际生成的 SQL:
// SELECT * FROM `users` AS `User` WHERE (`id` = 1)
mock.ExpectQuery("SELECT").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "旧名字"))
// 实际 UPDATE,注意 NOW() 是原样拼进 SQL 的字面量,不是占位符参数:
// UPDATE `users` AS `User` SET `name`=NOW() WHERE (`id` = 1)
mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Update().
Set(db.Record{"name": db.L("NOW()")}).
Where(db.C("id").Eq(1)).
Executor().Exec()
t.NoError(err)
t.NoError(mock.ExpectationsWereMet())
t.Require().Len(gotRows, 1)
t.Equal("NOW()", gotRows[0].New().Get("name").String())
}
// -------- 事务托底:UseTx 自动开事务 + Commit/Rollback --------
// TestUseTx_RollsBackOnChangeError onChange 失败时,自动开启的事务要整体回滚,连带这次写操作
// 本身一起撤销——而不是"数据写成功了、只是留痕/watcher 失败"
func (t *hooksTest) TestUseTx_RollsBackOnChangeError() {
onChangeErr := errors.New("write change log failed")
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
return nil, nil, func(rows []resx.ResChangeRow) error {
return onChangeErr
}
},
nil,
)
mock.ExpectBegin()
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectRollback()
_, err := res.GetDBTable(base.GetAnonymous()).Insert().
Rows(db.Record{"name": "赵六"}).
Executor().Exec()
t.Error(err)
var hookErr *db.HookError
t.True(errors.As(err, &hookErr), "error 应该能用 errors.As 解出 db.HookError")
t.ErrorIs(hookErr.Unwrap(), onChangeErr)
t.NoError(mock.ExpectationsWereMet(), "应该是 Begin -> INSERT -> Rollback,而不是 Commit")
}
// TestUseTx_ReusesCallerProvidedTx 调用方已经显式传入事务时,UseTx 不会再自动开一个新的,
// After 也不会自动 Commit/Rollback 它——事务的生命周期仍然由调用方自己负责
func (t *hooksTest) TestUseTx_ReusesCallerProvidedTx() {
var gotRows []resx.ResChangeRow
res, mock := t.newResource(
func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
return nil, nil, func(rows []resx.ResChangeRow) error {
gotRows = rows
return nil
}
},
nil,
)
// 模拟调用方自己已经开了一个事务
mock.ExpectBegin()
tx, err := res.DB().Begin()
t.Require().NoError(err)
// 这里不再有第二次 ExpectBegin:证明 UseTx 发现 dp.tx 已经非 nil 就不会再自己开一个
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(1, 1))
_, err = res.GetDBTable(base.GetAnonymous(), req.WithTx(tx)).Insert().
Rows(db.Record{"name": "赵六"}).
Executor().Exec()
t.NoError(err)
// After 不会自动提交,调用方自己提交
mock.ExpectCommit()
t.NoError(tx.Commit())
t.NoError(mock.ExpectationsWereMet())
t.Require().Len(gotRows, 1)
t.Same(tx, gotRows[0].Tx(), "row.Tx() 应该是调用方传入的那个事务,不是 UseTx 自动开的")
}