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 资源实现包。
701 lines
26 KiB
Go
701 lines
26 KiB
Go
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 或事务,这里会因为顺序对不上而报错。
|
||
// 实际生成的 SQL:UPDATE `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
|
||
},
|
||
)
|
||
|
||
// 实际生成的 SQL:DELETE 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()
|
||
// 实际生成的 SQL:INSERT 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 而不是 sqlmock:DB() 现在会自动建表(见 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 权限条件以子查询形式重写 WHERE:User.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,实际生成的 SQL:DELETE 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)
|
||
|
||
// 实际生成的 SQL(created_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 自动开的")
|
||
}
|