| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082 |
- package product
- import (
- "context"
- "errors"
- "fmt"
- "github.com/go-sql-driver/mysql"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/zeromicro/go-zero/core/stores/redis"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- "perms-system-server/internal/testutil"
- "strings"
- "sync"
- "sync/atomic"
- "testing"
- "time"
- )
- func newTestModel(t *testing.T) SysProductModel {
- t.Helper()
- return NewSysProductModel(
- testutil.GetTestSqlConn(),
- testutil.GetTestCacheConf(),
- testutil.GetTestCachePrefix(),
- )
- }
- func newSysProduct() *SysProduct {
- ts := time.Now().Unix()
- return &SysProduct{
- Code: testutil.UniqueId(),
- Name: "integration-product",
- AppKey: testutil.UniqueId(),
- AppSecret: "app-secret-" + testutil.UniqueId(),
- Remark: "remark",
- Status: 1,
- CreateTime: ts,
- UpdateTime: ts,
- }
- }
- // TC-0423: 正常分页
- func TestSysProductModel_Integration(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- t.Run("TableName", func(t *testing.T) {
- require.Equal(t, "`sys_product`", m.TableName())
- })
- t.Run("FindList_pagination", func(t *testing.T) {
- var beforeTotal int64
- _, beforeTotal, _ = m.FindList(ctx, 1, 1)
- const n = 5
- ids := make([]int64, 0, n)
- for i := 0; i < n; i++ {
- p := newSysProduct()
- p.Name = fmt.Sprintf("page-item-%d", i)
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- ids = append(ids, id)
- }
- defer func() {
- testutil.CleanTable(ctx, conn, "`sys_product`", ids...)
- }()
- expectedTotal := beforeTotal + int64(n)
- _, total, err := m.FindList(ctx, 1, 2)
- require.NoError(t, err)
- require.Equal(t, expectedTotal, total)
- var pageSize int64 = 2
- fullPages := expectedTotal / pageSize
- lastPageSize := expectedTotal % pageSize
- if lastPageSize > 0 {
- lastList, _, err := m.FindList(ctx, fullPages+1, pageSize)
- require.NoError(t, err)
- require.Len(t, lastList, int(lastPageSize))
- }
- })
- t.Run("CRUD_cycle", func(t *testing.T) {
- p := newSysProduct()
- p.Name = "crud-name"
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- defer testutil.CleanTable(ctx, conn, "`sys_product`", id)
- got, err := m.FindOne(ctx, id)
- require.NoError(t, err)
- require.Equal(t, id, got.Id)
- require.Equal(t, p.Code, got.Code)
- require.Equal(t, p.Name, got.Name)
- require.Equal(t, p.AppKey, got.AppKey)
- require.Equal(t, p.AppSecret, got.AppSecret)
- require.Equal(t, p.Remark, got.Remark)
- require.Equal(t, p.Status, got.Status)
- ts2 := time.Now().Unix()
- got.Name = "crud-name-updated"
- got.Remark = "updated-remark"
- got.Status = 2
- got.UpdateTime = ts2
- require.NoError(t, m.Update(ctx, got))
- after, err := m.FindOne(ctx, id)
- require.NoError(t, err)
- require.Equal(t, "crud-name-updated", after.Name)
- require.Equal(t, "updated-remark", after.Remark)
- require.Equal(t, int64(2), after.Status)
- require.Equal(t, ts2, after.UpdateTime)
- require.NoError(t, m.Delete(ctx, id))
- _, err = m.FindOne(ctx, id)
- require.ErrorIs(t, err, ErrNotFound)
- })
- t.Run("FindOneByAppKey_found_and_notFound", func(t *testing.T) {
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- defer testutil.CleanTable(ctx, conn, "`sys_product`", id)
- got, err := m.FindOneByAppKey(ctx, p.AppKey)
- require.NoError(t, err)
- require.Equal(t, p.AppKey, got.AppKey)
- require.Equal(t, id, got.Id)
- _, err = m.FindOneByAppKey(ctx, "nonexistent-appkey-"+testutil.UniqueId())
- require.ErrorIs(t, err, ErrNotFound)
- })
- t.Run("FindOneByCode_found_and_notFound", func(t *testing.T) {
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- defer testutil.CleanTable(ctx, conn, "`sys_product`", id)
- got, err := m.FindOneByCode(ctx, p.Code)
- require.NoError(t, err)
- require.Equal(t, p.Code, got.Code)
- require.Equal(t, id, got.Id)
- _, err = m.FindOneByCode(ctx, "nonexistent-code-"+testutil.UniqueId())
- require.ErrorIs(t, err, ErrNotFound)
- })
- t.Run("BatchInsert_and_BatchDelete", func(t *testing.T) {
- items := []*SysProduct{newSysProduct(), newSysProduct(), newSysProduct()}
- for i := range items {
- items[i].Name = fmt.Sprintf("batch-%d", i)
- }
- require.NoError(t, m.BatchInsert(ctx, items))
- var ids []int64
- for _, it := range items {
- row, err := m.FindOneByCode(ctx, it.Code)
- require.NoError(t, err)
- ids = append(ids, row.Id)
- }
- defer testutil.CleanTable(ctx, conn, "`sys_product`", ids...)
- require.NoError(t, m.BatchDelete(ctx, ids))
- for _, id := range ids {
- _, err := m.FindOne(ctx, id)
- require.ErrorIs(t, err, ErrNotFound)
- }
- })
- t.Run("TransactCtx_commit", func(t *testing.T) {
- p := newSysProduct()
- p.Name = "tx-commit"
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- _, err := m.InsertWithTx(c, session, p)
- return err
- })
- require.NoError(t, err)
- row, err := m.FindOneByCode(ctx, p.Code)
- require.NoError(t, err)
- defer testutil.CleanTable(ctx, conn, "`sys_product`", row.Id)
- require.Equal(t, p.Code, row.Code)
- require.Equal(t, "tx-commit", row.Name)
- })
- t.Run("TransactCtx_rollback", func(t *testing.T) {
- p := newSysProduct()
- p.Name = "tx-rollback"
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- if _, err := m.InsertWithTx(c, session, p); err != nil {
- return err
- }
- return fmt.Errorf("force rollback")
- })
- require.Error(t, err)
- _, err = m.FindOneByCode(ctx, p.Code)
- require.ErrorIs(t, err, ErrNotFound)
- })
- t.Run("Insert_duplicateCode", func(t *testing.T) {
- code := testutil.UniqueId()
- ts := time.Now().Unix()
- p1 := &SysProduct{
- Code: code,
- Name: "dup-a",
- AppKey: testutil.UniqueId(),
- AppSecret: "s1",
- Remark: "",
- Status: 1,
- CreateTime: ts,
- UpdateTime: ts,
- }
- res, err := m.Insert(ctx, p1)
- require.NoError(t, err)
- id1, err := res.LastInsertId()
- require.NoError(t, err)
- defer testutil.CleanTable(ctx, conn, "`sys_product`", id1)
- p2 := &SysProduct{
- Code: code,
- Name: "dup-b",
- AppKey: testutil.UniqueId(),
- AppSecret: "s2",
- Remark: "",
- Status: 1,
- CreateTime: ts,
- UpdateTime: ts,
- }
- _, err = m.Insert(ctx, p2)
- require.Error(t, err)
- var me *mysql.MySQLError
- require.True(t, errors.As(err, &me))
- require.Equal(t, uint16(1062), me.Number)
- })
- }
- // TC-0319: 记录不存在
- func TestSysProductModel_FindOne_NotFound(t *testing.T) {
- m := newTestModel(t)
- _, err := m.FindOne(context.Background(), 999999999999)
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0326: 记录不存在
- func TestSysProductModel_Update_NotFound(t *testing.T) {
- m := newTestModel(t)
- err := m.Update(context.Background(), &SysProduct{
- Id: 999999999999, Code: "x", Name: "n", AppKey: "k", AppSecret: "s",
- Status: 1, CreateTime: time.Now().Unix(), UpdateTime: time.Now().Unix(),
- })
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0329: 记录不存在
- func TestSysProductModel_Delete_NotFound(t *testing.T) {
- m := newTestModel(t)
- err := m.Delete(context.Background(), 999999999999)
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0334: 空列表
- func TestSysProductModel_BatchInsert_Empty(t *testing.T) {
- m := newTestModel(t)
- require.NoError(t, m.BatchInsert(context.Background(), nil))
- require.NoError(t, m.BatchInsert(context.Background(), []*SysProduct{}))
- }
- // TC-0353: 空ids
- func TestSysProductModel_BatchDelete_Empty(t *testing.T) {
- m := newTestModel(t)
- require.NoError(t, m.BatchDelete(context.Background(), nil))
- require.NoError(t, m.BatchDelete(context.Background(), []int64{}))
- }
- // TC-0327: 事务内更新
- func TestSysProductModel_UpdateWithTx(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- defer testutil.CleanTable(ctx, conn, m.TableName(), id)
- err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- p.Id = id
- p.Name = "tx_updated_name"
- p.UpdateTime = time.Now().Unix()
- return m.UpdateWithTx(c, session, p)
- })
- require.NoError(t, err)
- got, err := m.FindOne(ctx, id)
- require.NoError(t, err)
- require.Equal(t, "tx_updated_name", got.Name)
- }
- // TC-0330: 事务内删除
- func TestSysProductModel_DeleteWithTx(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, err := res.LastInsertId()
- require.NoError(t, err)
- defer testutil.CleanTable(ctx, conn, m.TableName(), id)
- err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- return m.DeleteWithTx(c, session, id)
- })
- require.NoError(t, err)
- _, err = m.FindOne(ctx, id)
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0335: 单条记录
- func TestSysProductModel_BatchInsert_Single(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- require.NoError(t, m.BatchInsert(ctx, []*SysProduct{p}))
- found, err := m.FindOneByCode(ctx, p.Code)
- require.NoError(t, err)
- defer testutil.CleanTable(ctx, conn, m.TableName(), found.Id)
- require.Equal(t, p.Code, found.Code)
- }
- // TC-0338: 唯一索引冲突
- func TestSysProductModel_BatchInsert_UniqueConflict(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- code := testutil.UniqueId()
- ts := time.Now().Unix()
- list := []*SysProduct{
- {Code: code, Name: "a", AppKey: testutil.UniqueId(), AppSecret: "s1", Status: 1, CreateTime: ts, UpdateTime: ts},
- {Code: code, Name: "b", AppKey: testutil.UniqueId(), AppSecret: "s2", Status: 1, CreateTime: ts, UpdateTime: ts},
- }
- err := m.BatchInsert(ctx, list)
- require.Error(t, err)
- t.Cleanup(func() {
- if found, e := m.FindOneByCode(ctx, code); e == nil {
- testutil.CleanTable(ctx, conn, m.TableName(), found.Id)
- }
- })
- var me *mysql.MySQLError
- require.True(t, errors.As(err, &me))
- require.Equal(t, uint16(1062), me.Number)
- }
- // TC-0343: 空列表
- func TestSysProductModel_BatchUpdate_Empty(t *testing.T) {
- m := newTestModel(t)
- require.NoError(t, m.BatchUpdate(context.Background(), nil))
- require.NoError(t, m.BatchUpdate(context.Background(), []*SysProduct{}))
- }
- // TC-0345: 多条记录(3条)
- func TestSysProductModel_BatchUpdate_Multi(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p1 := newSysProduct()
- p2 := newSysProduct()
- r1, err := m.Insert(ctx, p1)
- require.NoError(t, err)
- id1, _ := r1.LastInsertId()
- r2, err := m.Insert(ctx, p2)
- require.NoError(t, err)
- id2, _ := r2.LastInsertId()
- defer testutil.CleanTable(ctx, conn, m.TableName(), id1, id2)
- now := time.Now().Unix()
- upd := []*SysProduct{
- {Id: id1, Code: p1.Code, Name: "upd1", AppKey: p1.AppKey, AppSecret: p1.AppSecret, Remark: "r1", Status: 1, CreateTime: p1.CreateTime, UpdateTime: now},
- {Id: id2, Code: p2.Code, Name: "upd2", AppKey: p2.AppKey, AppSecret: p2.AppSecret, Remark: "r2", Status: 2, CreateTime: p2.CreateTime, UpdateTime: now},
- }
- require.NoError(t, m.BatchUpdate(ctx, upd))
- g1, err := m.FindOne(ctx, id1)
- require.NoError(t, err)
- require.Equal(t, "upd1", g1.Name)
- g2, err := m.FindOne(ctx, id2)
- require.NoError(t, err)
- require.Equal(t, "upd2", g2.Name)
- require.Equal(t, int64(2), g2.Status)
- }
- // TC-0341: 正常多条
- func TestSysProductModel_BatchInsertWithTx_Normal(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p1 := newSysProduct()
- p2 := newSysProduct()
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- return m.BatchInsertWithTx(c, session, []*SysProduct{p1, p2})
- })
- require.NoError(t, err)
- f1, err := m.FindOneByCode(ctx, p1.Code)
- require.NoError(t, err)
- f2, err := m.FindOneByCode(ctx, p2.Code)
- require.NoError(t, err)
- defer testutil.CleanTable(ctx, conn, m.TableName(), f1.Id, f2.Id)
- require.Equal(t, p1.Code, f1.Code)
- require.Equal(t, p2.Code, f2.Code)
- }
- // TC-0340: 空列表
- func TestSysProductModel_BatchInsertWithTx_Empty(t *testing.T) {
- m := newTestModel(t)
- err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
- return m.BatchInsertWithTx(c, session, nil)
- })
- require.NoError(t, err)
- }
- // TC-0342: 事务回滚
- func TestSysProductModel_BatchInsertWithTx_Rollback(t *testing.T) {
- ctx := context.Background()
- m := newTestModel(t)
- p := newSysProduct()
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- if e := m.BatchInsertWithTx(c, session, []*SysProduct{p}); e != nil {
- return e
- }
- return fmt.Errorf("force rollback")
- })
- require.Error(t, err)
- _, err = m.FindOneByCode(ctx, p.Code)
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0349: 正常多条
- func TestSysProductModel_BatchUpdateWithTx_Normal(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p1 := newSysProduct()
- p2 := newSysProduct()
- r1, err := m.Insert(ctx, p1)
- require.NoError(t, err)
- id1, _ := r1.LastInsertId()
- r2, err := m.Insert(ctx, p2)
- require.NoError(t, err)
- id2, _ := r2.LastInsertId()
- defer testutil.CleanTable(ctx, conn, m.TableName(), id1, id2)
- now := time.Now().Unix()
- err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- return m.BatchUpdateWithTx(c, session, []*SysProduct{
- {Id: id1, Code: p1.Code, Name: "tx_upd1", AppKey: p1.AppKey, AppSecret: p1.AppSecret, Status: 1, CreateTime: p1.CreateTime, UpdateTime: now},
- {Id: id2, Code: p2.Code, Name: "tx_upd2", AppKey: p2.AppKey, AppSecret: p2.AppSecret, Status: 1, CreateTime: p2.CreateTime, UpdateTime: now},
- })
- })
- require.NoError(t, err)
- g1, err := m.FindOne(ctx, id1)
- require.NoError(t, err)
- require.Equal(t, "tx_upd1", g1.Name)
- g2, err := m.FindOne(ctx, id2)
- require.NoError(t, err)
- require.Equal(t, "tx_upd2", g2.Name)
- }
- // TC-0348: 空列表
- func TestSysProductModel_BatchUpdateWithTx_Empty(t *testing.T) {
- m := newTestModel(t)
- err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
- return m.BatchUpdateWithTx(c, session, nil)
- })
- require.NoError(t, err)
- }
- // TC-0354: 单个id
- func TestSysProductModel_BatchDelete_Single(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- defer testutil.CleanTable(ctx, conn, m.TableName(), id)
- require.NoError(t, m.BatchDelete(ctx, []int64{id}))
- _, err = m.FindOne(ctx, id)
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0356: 包含不存在id
- func TestSysProductModel_BatchDelete_ContainsNonExist(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- defer testutil.CleanTable(ctx, conn, m.TableName(), id)
- require.NoError(t, m.BatchDelete(ctx, []int64{id, 999999999}))
- _, err = m.FindOne(ctx, id)
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0358: 正常多条
- func TestSysProductModel_BatchDeleteWithTx_Normal(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p1 := newSysProduct()
- p2 := newSysProduct()
- r1, err := m.Insert(ctx, p1)
- require.NoError(t, err)
- id1, _ := r1.LastInsertId()
- r2, err := m.Insert(ctx, p2)
- require.NoError(t, err)
- id2, _ := r2.LastInsertId()
- defer testutil.CleanTable(ctx, conn, m.TableName(), id1, id2)
- err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- return m.BatchDeleteWithTx(c, session, []int64{id1, id2})
- })
- require.NoError(t, err)
- _, err = m.FindOne(ctx, id1)
- require.ErrorIs(t, err, ErrNotFound)
- _, err = m.FindOne(ctx, id2)
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0357: 空ids
- func TestSysProductModel_BatchDeleteWithTx_Empty(t *testing.T) {
- m := newTestModel(t)
- err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
- return m.BatchDeleteWithTx(c, session, nil)
- })
- require.NoError(t, err)
- }
- // TC-0323: 事务内可见性
- func TestSysProductModel_FindOneWithTx_InsertThenFind(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- now := time.Now().Unix()
- var foundInTx *SysProduct
- var insertedId int64
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- res, err := m.InsertWithTx(c, session, &SysProduct{
- Code: "ftx_code_" + testutil.UniqueId(),
- Name: "ftx_name_" + testutil.UniqueId(),
- AppKey: "ftx_ak_" + testutil.UniqueId(),
- AppSecret: "ftx_sec_" + testutil.UniqueId(),
- Remark: "",
- Status: 1,
- CreateTime: now,
- UpdateTime: now,
- })
- if err != nil {
- return err
- }
- insertedId, _ = res.LastInsertId()
- foundInTx, err = m.FindOneWithTx(c, session, insertedId)
- return err
- })
- require.NoError(t, err)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), insertedId) })
- require.NotNil(t, foundInTx)
- assert.Equal(t, insertedId, foundInTx.Id)
- }
- // TC-0322: 事务内记录不存在
- func TestSysProductModel_FindOneWithTx_NotFound(t *testing.T) {
- ctx := context.Background()
- m := newTestModel(t)
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- _, err := m.FindOneWithTx(c, session, 999999999999)
- return err
- })
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0365: FindOneByAppKeyWithTx
- func TestSysProductModel_FindOneByAppKeyWithTx_InsertThenFind(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- appKey := "ftx_ak_" + testutil.UniqueId()
- code := "ftx_code_" + testutil.UniqueId()
- now := time.Now().Unix()
- var foundByKey *SysProduct
- var insertedId int64
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- res, err := m.InsertWithTx(c, session, &SysProduct{
- Code: code,
- Name: "ftx_name_" + testutil.UniqueId(),
- AppKey: appKey,
- AppSecret: "ftx_sec_" + testutil.UniqueId(),
- Remark: "",
- Status: 1,
- CreateTime: now,
- UpdateTime: now,
- })
- if err != nil {
- return err
- }
- insertedId, _ = res.LastInsertId()
- foundByKey, err = m.FindOneByAppKeyWithTx(c, session, appKey)
- return err
- })
- require.NoError(t, err)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), insertedId) })
- require.NotNil(t, foundByKey)
- assert.Equal(t, insertedId, foundByKey.Id)
- assert.Equal(t, appKey, foundByKey.AppKey)
- }
- // TC-0366: FindOneByAppKeyWithTx
- func TestSysProductModel_FindOneByAppKeyWithTx_NotFound(t *testing.T) {
- ctx := context.Background()
- m := newTestModel(t)
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- _, err := m.FindOneByAppKeyWithTx(c, session, "notexist_appkey_"+testutil.UniqueId())
- return err
- })
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0369: FindOneByCodeWithTx
- func TestSysProductModel_FindOneByCodeWithTx_InsertThenFind(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- code := "ftx_code_" + testutil.UniqueId()
- now := time.Now().Unix()
- var foundByCode *SysProduct
- var insertedId int64
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- res, err := m.InsertWithTx(c, session, &SysProduct{
- Code: code,
- Name: "ftx_name_" + testutil.UniqueId(),
- AppKey: "ftx_ak_" + testutil.UniqueId(),
- AppSecret: "ftx_sec_" + testutil.UniqueId(),
- Remark: "",
- Status: 1,
- CreateTime: now,
- UpdateTime: now,
- })
- if err != nil {
- return err
- }
- insertedId, _ = res.LastInsertId()
- foundByCode, err = m.FindOneByCodeWithTx(c, session, code)
- return err
- })
- require.NoError(t, err)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), insertedId) })
- require.NotNil(t, foundByCode)
- assert.Equal(t, insertedId, foundByCode.Id)
- assert.Equal(t, code, foundByCode.Code)
- }
- // TC-0370: FindOneByCodeWithTx
- func TestSysProductModel_FindOneByCodeWithTx_NotFound(t *testing.T) {
- ctx := context.Background()
- m := newTestModel(t)
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- _, err := m.FindOneByCodeWithTx(c, session, "notexist_code_"+testutil.UniqueId())
- return err
- })
- require.ErrorIs(t, err, ErrNotFound)
- }
- // TC-0404: 多唯一索引前缀(SysProduct)
- func TestSysProductModel_CachePrefix_MultiUniqueIndex(t *testing.T) {
- oldId := cacheSysProductIdPrefix
- oldAppKey := cacheSysProductAppKeyPrefix
- oldCode := cacheSysProductCodePrefix
- defer func() {
- cacheSysProductIdPrefix = oldId
- cacheSysProductAppKeyPrefix = oldAppKey
- cacheSysProductCodePrefix = oldCode
- }()
- _ = newSysProductModel(testutil.GetTestSqlConn(), testutil.GetTestCacheConf(), "test")
- assert.True(t, strings.HasPrefix(cacheSysProductIdPrefix, "test:"))
- assert.True(t, strings.HasPrefix(cacheSysProductAppKeyPrefix, "test:"))
- assert.True(t, strings.HasPrefix(cacheSysProductCodePrefix, "test:"))
- }
- func TestSysProductModel_LockByCodeTx_Found(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
- var got *SysProduct
- require.NoError(t, m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- got, err = m.LockByCodeTx(c, session, p.Code)
- return err
- }))
- require.NotNil(t, got)
- assert.Equal(t, id, got.Id)
- assert.Equal(t, p.Code, got.Code)
- assert.Equal(t, p.AppKey, got.AppKey)
- assert.Equal(t, p.Status, got.Status, "锁行时不得过滤禁用态,否则 SyncPermissions 无法为禁用产品正确 fail-close")
- }
- // TC-0810: LockByCodeTx 对不存在的 code 返回 sqlx.ErrNotFound。
- func TestSysProductModel_LockByCodeTx_NotFound(t *testing.T) {
- ctx := context.Background()
- m := newTestModel(t)
- err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- _, e := m.LockByCodeTx(c, session, "definitely_no_such_code_"+testutil.UniqueId())
- return e
- })
- require.Error(t, err)
- assert.True(t, errors.Is(err, sqlx.ErrNotFound),
- "LockByCodeTx 对不存在的 code 必须返回 ErrNotFound,便于上层 fail-close 返回 401/404")
- }
- // TC-0811: FOR UPDATE 行锁真实生效 —— 两个事务同时尝试锁同一行时,
- // 后进者必须被阻塞直到先进者结束事务。
- // 测量方式:goroutine A 在 tx 内 Lock 住后 sleep 500ms 再 commit;
- //
- // goroutine B 等 100ms 后也尝试 Lock 同一行,记录耗时。B 的耗时必须≥400ms。
- func TestSysProductModel_LockByCodeTx_BlocksConcurrentWriter(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
- var (
- wg sync.WaitGroup
- aHoldMs = int64(500)
- bStartDelayMs = int64(100)
- bElapsedNanos int64
- aFinishedNanos int64
- aErr, bErr error
- )
- wg.Add(2)
- go func() {
- defer wg.Done()
- aErr = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- if _, e := m.LockByCodeTx(c, session, p.Code); e != nil {
- return e
- }
- // A 拿到锁后故意延时,模拟一段业务处理期。期间 B 必须被阻塞。
- time.Sleep(time.Duration(aHoldMs) * time.Millisecond)
- atomic.StoreInt64(&aFinishedNanos, time.Now().UnixNano())
- return nil
- })
- }()
- go func() {
- defer wg.Done()
- time.Sleep(time.Duration(bStartDelayMs) * time.Millisecond)
- start := time.Now()
- bErr = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- _, e := m.LockByCodeTx(c, session, p.Code)
- return e
- })
- atomic.StoreInt64(&bElapsedNanos, time.Since(start).Nanoseconds())
- }()
- wg.Wait()
- require.NoError(t, aErr)
- require.NoError(t, bErr)
- // B 的耗时 ≥ A 的剩余持锁时间。A 持 500ms,B 延 100ms 后入场,
- // 因此 B 被阻塞的时间至少 (500-100)=400ms。给 DB 一点抖动放到 300ms。
- elapsedMs := atomic.LoadInt64(&bElapsedNanos) / int64(time.Millisecond)
- minBlockedMs := int64(300)
- assert.GreaterOrEqualf(t, elapsedMs, minBlockedMs, fmt.Sprintf(
- "B 的 LockByCodeTx 总耗时 %dms 明显低于预期最小阻塞 %dms —— "+
- "意味着 FOR UPDATE 行锁失效,声称的'按 product 串行化'不成立",
- elapsedMs, minBlockedMs))
- }
- // ---------------------------------------------------------------------------
- // L-R15-3 / L-R12-1:UpdateWithOptLockTx + InvalidateProductCache
- //
- // UpdateWithOptLockTx 语义契约:
- // - WHERE id=? AND updateTime=? 复现 UpdateWithOptLock 的 CAS;
- // - 必须走调用方 session,不得自身失效 sqlc 低层缓存(交给 post-commit 的 InvalidateProductCache);
- // - affected=0 → ErrUpdateConflict。
- //
- // InvalidateProductCache 语义契约:
- // - 必须一次失效 sysProduct 的 id / appKey / code 三把低层缓存 key;
- // - 不能依赖 session,只在 post-commit 调用。
- // ---------------------------------------------------------------------------
- // TC-1151: UpdateWithOptLockTx 正常路径——CAS 命中时 UPDATE 成功。
- func TestSysProductModel_UpdateWithOptLockTx_HappyPath(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
- orig, err := m.FindOne(ctx, id)
- require.NoError(t, err)
- newData := *orig
- newData.Name = "updated_name"
- newData.Remark = "updated_remark"
- newData.Status = 2
- newData.UpdateTime = orig.UpdateTime + 1
- require.NoError(t,
- m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- return m.UpdateWithOptLockTx(c, session, &newData, orig.UpdateTime)
- }))
- // 直读 DB 验证(绕过缓存——UpdateWithOptLockTx 不该动缓存,故 FindOne 可能仍看到旧值;
- // 这里是对"DB 确实被改写"的第一类证据)
- var dbName, dbRemark string
- var dbStatus int64
- require.NoError(t,
- conn.QueryRowCtx(ctx, &dbName,
- "SELECT `name` FROM `sys_product` WHERE `id` = ?", id))
- require.NoError(t,
- conn.QueryRowCtx(ctx, &dbRemark,
- "SELECT `remark` FROM `sys_product` WHERE `id` = ?", id))
- require.NoError(t,
- conn.QueryRowCtx(ctx, &dbStatus,
- "SELECT `status` FROM `sys_product` WHERE `id` = ?", id))
- assert.Equal(t, "updated_name", dbName)
- assert.Equal(t, "updated_remark", dbRemark)
- assert.Equal(t, int64(2), dbStatus)
- }
- // TC-1152: UpdateWithOptLockTx expectedUpdateTime 不匹配 → ErrUpdateConflict,DB 不变。
- func TestSysProductModel_UpdateWithOptLockTx_StaleExpectedUpdateTime_Conflict(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
- orig, err := m.FindOne(ctx, id)
- require.NoError(t, err)
- staleUpdateTime := orig.UpdateTime - 100
- newData := *orig
- newData.Name = "should_not_land"
- newData.UpdateTime = orig.UpdateTime + 1
- err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- return m.UpdateWithOptLockTx(c, session, &newData, staleUpdateTime)
- })
- require.ErrorIs(t, err, ErrUpdateConflict,
- "expectedUpdateTime 与 DB 当前值不一致 → ErrUpdateConflict")
- // DB 名称保持原值,证明 CAS 失败时 UPDATE 未落盘
- var dbName string
- require.NoError(t,
- conn.QueryRowCtx(ctx, &dbName,
- "SELECT `name` FROM `sys_product` WHERE `id` = ?", id))
- assert.Equal(t, p.Name, dbName,
- "CAS 未命中时 DB 必须保持原值,不得部分落盘")
- }
- // TC-1153: UpdateWithOptLockTx nil session → error(与 IncrementTokenVersionWithTx 同家族契约)。
- func TestSysProductModel_UpdateWithOptLockTx_NilSession_ReturnsError(t *testing.T) {
- m := newTestModel(t)
- err := m.UpdateWithOptLockTx(context.Background(), nil, &SysProduct{Id: 1}, 0)
- require.Error(t, err)
- assert.Contains(t, err.Error(), "non-nil session")
- }
- // TC-1154: UpdateWithOptLockTx 事务 rollback 后 DB 不落盘。
- func TestSysProductModel_UpdateWithOptLockTx_Rollback_NoPersistence(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
- orig, err := m.FindOne(ctx, id)
- require.NoError(t, err)
- newData := *orig
- newData.Name = "rolled_back_name"
- newData.UpdateTime = orig.UpdateTime + 1
- err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
- if e := m.UpdateWithOptLockTx(c, session, &newData, orig.UpdateTime); e != nil {
- return e
- }
- return errors.New("force rollback")
- })
- require.Error(t, err)
- var dbName string
- require.NoError(t,
- conn.QueryRowCtx(ctx, &dbName,
- "SELECT `name` FROM `sys_product` WHERE `id` = ?", id))
- assert.Equal(t, p.Name, dbName,
- "rollback 后 DB 必须保持原值——证明 UpdateWithOptLockTx 确实走 session 而非独立连接")
- }
- // TC-1155: InvalidateProductCache 必须一次失效 id / appKey / code 三把 key。
- // 对应 UpdateProduct 禁用后"sysProduct 低层缓存不准"风险。
- func TestSysProductModel_InvalidateProductCache_DelsAllThreeKeys(t *testing.T) {
- ctx := context.Background()
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- rds := redis.MustNewRedis(testutil.GetTestConfig().CacheRedis.Nodes[0].RedisConf)
- p := newSysProduct()
- res, err := m.Insert(ctx, p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
- // 预热三把 key
- _, err = m.FindOne(ctx, id)
- require.NoError(t, err)
- _, err = m.FindOneByAppKey(ctx, p.AppKey)
- require.NoError(t, err)
- _, err = m.FindOneByCode(ctx, p.Code)
- require.NoError(t, err)
- prefix := testutil.GetTestCachePrefix()
- keyId := fmt.Sprintf("%s:cache:sysProduct:id:%d", prefix, id)
- keyAppKey := fmt.Sprintf("%s:cache:sysProduct:appKey:%s", prefix, p.AppKey)
- keyCode := fmt.Sprintf("%s:cache:sysProduct:code:%s", prefix, p.Code)
- for _, k := range []string{keyId, keyAppKey, keyCode} {
- v, _ := rds.Get(k)
- require.NotEmpty(t, v, "预置:%s 已写入缓存", k)
- }
- m.InvalidateProductCache(ctx, id, p.AppKey, p.Code)
- for _, k := range []string{keyId, keyAppKey, keyCode} {
- v, _ := rds.Get(k)
- assert.Empty(t, v,
- "InvalidateProductCache 必须失效低层缓存 key %s", k)
- }
- }
- // TC-1156: InvalidateProductCache 在 ctx 已取消/超时下必须不 panic、不阻塞(best-effort 契约)。
- // 与 InvalidateProfileCache 的 L-R13-5 方案 B 完全同构,理由相同:事务已 commit,
- // 缓存清理失败必须吞错 + audit tag 日志,绝不能向上抛。
- func TestSysProductModel_InvalidateProductCache_CanceledCtxDoesNotPanicOrBlock(t *testing.T) {
- conn := testutil.GetTestSqlConn()
- m := newTestModel(t)
- p := newSysProduct()
- res, err := m.Insert(context.Background(), p)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- t.Cleanup(func() { testutil.CleanTable(context.Background(), conn, "`sys_product`", id) })
- cases := []struct {
- name string
- makeCtx func() (context.Context, context.CancelFunc)
- }{
- {
- name: "canceled",
- makeCtx: func() (context.Context, context.CancelFunc) {
- ctx, cancel := context.WithCancel(context.Background())
- cancel()
- return ctx, func() {}
- },
- },
- {
- name: "deadline_exceeded",
- makeCtx: func() (context.Context, context.CancelFunc) {
- ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
- return ctx, cancel
- },
- },
- }
- for _, tc := range cases {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- ctx, cancel := tc.makeCtx()
- defer cancel()
- done := make(chan struct{})
- go func() {
- defer close(done)
- assert.NotPanics(t, func() {
- m.InvalidateProductCache(ctx, id, p.AppKey, p.Code)
- })
- }()
- select {
- case <-done:
- case <-time.After(500 * time.Millisecond):
- t.Fatal("InvalidateProductCache 在 canceled ctx 下必须立即返回")
- }
- })
- }
- }
|