Files
req/resx/res_interceptor_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

700 lines
27 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 (
"database/sql"
"errors"
"fmt"
"testing"
"time"
"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"
"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"
_ "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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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
})
baseRes := resx.New(app, "User", "users", resx.WithConn("default"))
res := resx.NewVirtualResource(baseRes, "User", db.From(db.T("users")),
resx.WithFields(resx.NewResField("name", "User", resx.FieldWithName("姓名"), resx.FieldWithDataType(req.ResString))),
)
// 实际生成的 SQL(虚拟资源拿子查询拼表名,且没有 owned_user 权限条件):
// UPDATE (SELECT * FROM `users`) AS `User` SET `name`='新名字' WHERE (`id` = 1)
sql, _, _ := res.GetDBTable(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithPermission(req.ResAll)).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(userx.GetAnonymous(), req.WithTx(tx), req.WithPermission(req.ResAll)).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 自动开的")
}