| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851 |
- 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/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))
- }
|