sysProductModel_test.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851
  1. package product
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "github.com/go-sql-driver/mysql"
  7. "github.com/stretchr/testify/assert"
  8. "github.com/stretchr/testify/require"
  9. "github.com/zeromicro/go-zero/core/stores/sqlx"
  10. "perms-system-server/internal/testutil"
  11. "strings"
  12. "sync"
  13. "sync/atomic"
  14. "testing"
  15. "time"
  16. )
  17. func newTestModel(t *testing.T) SysProductModel {
  18. t.Helper()
  19. return NewSysProductModel(
  20. testutil.GetTestSqlConn(),
  21. testutil.GetTestCacheConf(),
  22. testutil.GetTestCachePrefix(),
  23. )
  24. }
  25. func newSysProduct() *SysProduct {
  26. ts := time.Now().Unix()
  27. return &SysProduct{
  28. Code: testutil.UniqueId(),
  29. Name: "integration-product",
  30. AppKey: testutil.UniqueId(),
  31. AppSecret: "app-secret-" + testutil.UniqueId(),
  32. Remark: "remark",
  33. Status: 1,
  34. CreateTime: ts,
  35. UpdateTime: ts,
  36. }
  37. }
  38. // TC-0423: 正常分页
  39. func TestSysProductModel_Integration(t *testing.T) {
  40. ctx := context.Background()
  41. conn := testutil.GetTestSqlConn()
  42. m := newTestModel(t)
  43. t.Run("TableName", func(t *testing.T) {
  44. require.Equal(t, "`sys_product`", m.TableName())
  45. })
  46. t.Run("FindList_pagination", func(t *testing.T) {
  47. var beforeTotal int64
  48. _, beforeTotal, _ = m.FindList(ctx, 1, 1)
  49. const n = 5
  50. ids := make([]int64, 0, n)
  51. for i := 0; i < n; i++ {
  52. p := newSysProduct()
  53. p.Name = fmt.Sprintf("page-item-%d", i)
  54. res, err := m.Insert(ctx, p)
  55. require.NoError(t, err)
  56. id, err := res.LastInsertId()
  57. require.NoError(t, err)
  58. ids = append(ids, id)
  59. }
  60. defer func() {
  61. testutil.CleanTable(ctx, conn, "`sys_product`", ids...)
  62. }()
  63. expectedTotal := beforeTotal + int64(n)
  64. _, total, err := m.FindList(ctx, 1, 2)
  65. require.NoError(t, err)
  66. require.Equal(t, expectedTotal, total)
  67. var pageSize int64 = 2
  68. fullPages := expectedTotal / pageSize
  69. lastPageSize := expectedTotal % pageSize
  70. if lastPageSize > 0 {
  71. lastList, _, err := m.FindList(ctx, fullPages+1, pageSize)
  72. require.NoError(t, err)
  73. require.Len(t, lastList, int(lastPageSize))
  74. }
  75. })
  76. t.Run("CRUD_cycle", func(t *testing.T) {
  77. p := newSysProduct()
  78. p.Name = "crud-name"
  79. res, err := m.Insert(ctx, p)
  80. require.NoError(t, err)
  81. id, err := res.LastInsertId()
  82. require.NoError(t, err)
  83. defer testutil.CleanTable(ctx, conn, "`sys_product`", id)
  84. got, err := m.FindOne(ctx, id)
  85. require.NoError(t, err)
  86. require.Equal(t, id, got.Id)
  87. require.Equal(t, p.Code, got.Code)
  88. require.Equal(t, p.Name, got.Name)
  89. require.Equal(t, p.AppKey, got.AppKey)
  90. require.Equal(t, p.AppSecret, got.AppSecret)
  91. require.Equal(t, p.Remark, got.Remark)
  92. require.Equal(t, p.Status, got.Status)
  93. ts2 := time.Now().Unix()
  94. got.Name = "crud-name-updated"
  95. got.Remark = "updated-remark"
  96. got.Status = 2
  97. got.UpdateTime = ts2
  98. require.NoError(t, m.Update(ctx, got))
  99. after, err := m.FindOne(ctx, id)
  100. require.NoError(t, err)
  101. require.Equal(t, "crud-name-updated", after.Name)
  102. require.Equal(t, "updated-remark", after.Remark)
  103. require.Equal(t, int64(2), after.Status)
  104. require.Equal(t, ts2, after.UpdateTime)
  105. require.NoError(t, m.Delete(ctx, id))
  106. _, err = m.FindOne(ctx, id)
  107. require.ErrorIs(t, err, ErrNotFound)
  108. })
  109. t.Run("FindOneByAppKey_found_and_notFound", func(t *testing.T) {
  110. p := newSysProduct()
  111. res, err := m.Insert(ctx, p)
  112. require.NoError(t, err)
  113. id, err := res.LastInsertId()
  114. require.NoError(t, err)
  115. defer testutil.CleanTable(ctx, conn, "`sys_product`", id)
  116. got, err := m.FindOneByAppKey(ctx, p.AppKey)
  117. require.NoError(t, err)
  118. require.Equal(t, p.AppKey, got.AppKey)
  119. require.Equal(t, id, got.Id)
  120. _, err = m.FindOneByAppKey(ctx, "nonexistent-appkey-"+testutil.UniqueId())
  121. require.ErrorIs(t, err, ErrNotFound)
  122. })
  123. t.Run("FindOneByCode_found_and_notFound", func(t *testing.T) {
  124. p := newSysProduct()
  125. res, err := m.Insert(ctx, p)
  126. require.NoError(t, err)
  127. id, err := res.LastInsertId()
  128. require.NoError(t, err)
  129. defer testutil.CleanTable(ctx, conn, "`sys_product`", id)
  130. got, err := m.FindOneByCode(ctx, p.Code)
  131. require.NoError(t, err)
  132. require.Equal(t, p.Code, got.Code)
  133. require.Equal(t, id, got.Id)
  134. _, err = m.FindOneByCode(ctx, "nonexistent-code-"+testutil.UniqueId())
  135. require.ErrorIs(t, err, ErrNotFound)
  136. })
  137. t.Run("BatchInsert_and_BatchDelete", func(t *testing.T) {
  138. items := []*SysProduct{newSysProduct(), newSysProduct(), newSysProduct()}
  139. for i := range items {
  140. items[i].Name = fmt.Sprintf("batch-%d", i)
  141. }
  142. require.NoError(t, m.BatchInsert(ctx, items))
  143. var ids []int64
  144. for _, it := range items {
  145. row, err := m.FindOneByCode(ctx, it.Code)
  146. require.NoError(t, err)
  147. ids = append(ids, row.Id)
  148. }
  149. defer testutil.CleanTable(ctx, conn, "`sys_product`", ids...)
  150. require.NoError(t, m.BatchDelete(ctx, ids))
  151. for _, id := range ids {
  152. _, err := m.FindOne(ctx, id)
  153. require.ErrorIs(t, err, ErrNotFound)
  154. }
  155. })
  156. t.Run("TransactCtx_commit", func(t *testing.T) {
  157. p := newSysProduct()
  158. p.Name = "tx-commit"
  159. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  160. _, err := m.InsertWithTx(c, session, p)
  161. return err
  162. })
  163. require.NoError(t, err)
  164. row, err := m.FindOneByCode(ctx, p.Code)
  165. require.NoError(t, err)
  166. defer testutil.CleanTable(ctx, conn, "`sys_product`", row.Id)
  167. require.Equal(t, p.Code, row.Code)
  168. require.Equal(t, "tx-commit", row.Name)
  169. })
  170. t.Run("TransactCtx_rollback", func(t *testing.T) {
  171. p := newSysProduct()
  172. p.Name = "tx-rollback"
  173. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  174. if _, err := m.InsertWithTx(c, session, p); err != nil {
  175. return err
  176. }
  177. return fmt.Errorf("force rollback")
  178. })
  179. require.Error(t, err)
  180. _, err = m.FindOneByCode(ctx, p.Code)
  181. require.ErrorIs(t, err, ErrNotFound)
  182. })
  183. t.Run("Insert_duplicateCode", func(t *testing.T) {
  184. code := testutil.UniqueId()
  185. ts := time.Now().Unix()
  186. p1 := &SysProduct{
  187. Code: code,
  188. Name: "dup-a",
  189. AppKey: testutil.UniqueId(),
  190. AppSecret: "s1",
  191. Remark: "",
  192. Status: 1,
  193. CreateTime: ts,
  194. UpdateTime: ts,
  195. }
  196. res, err := m.Insert(ctx, p1)
  197. require.NoError(t, err)
  198. id1, err := res.LastInsertId()
  199. require.NoError(t, err)
  200. defer testutil.CleanTable(ctx, conn, "`sys_product`", id1)
  201. p2 := &SysProduct{
  202. Code: code,
  203. Name: "dup-b",
  204. AppKey: testutil.UniqueId(),
  205. AppSecret: "s2",
  206. Remark: "",
  207. Status: 1,
  208. CreateTime: ts,
  209. UpdateTime: ts,
  210. }
  211. _, err = m.Insert(ctx, p2)
  212. require.Error(t, err)
  213. var me *mysql.MySQLError
  214. require.True(t, errors.As(err, &me))
  215. require.Equal(t, uint16(1062), me.Number)
  216. })
  217. }
  218. // TC-0319: 记录不存在
  219. func TestSysProductModel_FindOne_NotFound(t *testing.T) {
  220. m := newTestModel(t)
  221. _, err := m.FindOne(context.Background(), 999999999999)
  222. require.ErrorIs(t, err, ErrNotFound)
  223. }
  224. // TC-0326: 记录不存在
  225. func TestSysProductModel_Update_NotFound(t *testing.T) {
  226. m := newTestModel(t)
  227. err := m.Update(context.Background(), &SysProduct{
  228. Id: 999999999999, Code: "x", Name: "n", AppKey: "k", AppSecret: "s",
  229. Status: 1, CreateTime: time.Now().Unix(), UpdateTime: time.Now().Unix(),
  230. })
  231. require.ErrorIs(t, err, ErrNotFound)
  232. }
  233. // TC-0329: 记录不存在
  234. func TestSysProductModel_Delete_NotFound(t *testing.T) {
  235. m := newTestModel(t)
  236. err := m.Delete(context.Background(), 999999999999)
  237. require.ErrorIs(t, err, ErrNotFound)
  238. }
  239. // TC-0334: 空列表
  240. func TestSysProductModel_BatchInsert_Empty(t *testing.T) {
  241. m := newTestModel(t)
  242. require.NoError(t, m.BatchInsert(context.Background(), nil))
  243. require.NoError(t, m.BatchInsert(context.Background(), []*SysProduct{}))
  244. }
  245. // TC-0353: 空ids
  246. func TestSysProductModel_BatchDelete_Empty(t *testing.T) {
  247. m := newTestModel(t)
  248. require.NoError(t, m.BatchDelete(context.Background(), nil))
  249. require.NoError(t, m.BatchDelete(context.Background(), []int64{}))
  250. }
  251. // TC-0327: 事务内更新
  252. func TestSysProductModel_UpdateWithTx(t *testing.T) {
  253. ctx := context.Background()
  254. conn := testutil.GetTestSqlConn()
  255. m := newTestModel(t)
  256. p := newSysProduct()
  257. res, err := m.Insert(ctx, p)
  258. require.NoError(t, err)
  259. id, err := res.LastInsertId()
  260. require.NoError(t, err)
  261. defer testutil.CleanTable(ctx, conn, m.TableName(), id)
  262. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  263. p.Id = id
  264. p.Name = "tx_updated_name"
  265. p.UpdateTime = time.Now().Unix()
  266. return m.UpdateWithTx(c, session, p)
  267. })
  268. require.NoError(t, err)
  269. got, err := m.FindOne(ctx, id)
  270. require.NoError(t, err)
  271. require.Equal(t, "tx_updated_name", got.Name)
  272. }
  273. // TC-0330: 事务内删除
  274. func TestSysProductModel_DeleteWithTx(t *testing.T) {
  275. ctx := context.Background()
  276. conn := testutil.GetTestSqlConn()
  277. m := newTestModel(t)
  278. p := newSysProduct()
  279. res, err := m.Insert(ctx, p)
  280. require.NoError(t, err)
  281. id, err := res.LastInsertId()
  282. require.NoError(t, err)
  283. defer testutil.CleanTable(ctx, conn, m.TableName(), id)
  284. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  285. return m.DeleteWithTx(c, session, id)
  286. })
  287. require.NoError(t, err)
  288. _, err = m.FindOne(ctx, id)
  289. require.ErrorIs(t, err, ErrNotFound)
  290. }
  291. // TC-0335: 单条记录
  292. func TestSysProductModel_BatchInsert_Single(t *testing.T) {
  293. ctx := context.Background()
  294. conn := testutil.GetTestSqlConn()
  295. m := newTestModel(t)
  296. p := newSysProduct()
  297. require.NoError(t, m.BatchInsert(ctx, []*SysProduct{p}))
  298. found, err := m.FindOneByCode(ctx, p.Code)
  299. require.NoError(t, err)
  300. defer testutil.CleanTable(ctx, conn, m.TableName(), found.Id)
  301. require.Equal(t, p.Code, found.Code)
  302. }
  303. // TC-0338: 唯一索引冲突
  304. func TestSysProductModel_BatchInsert_UniqueConflict(t *testing.T) {
  305. ctx := context.Background()
  306. conn := testutil.GetTestSqlConn()
  307. m := newTestModel(t)
  308. code := testutil.UniqueId()
  309. ts := time.Now().Unix()
  310. list := []*SysProduct{
  311. {Code: code, Name: "a", AppKey: testutil.UniqueId(), AppSecret: "s1", Status: 1, CreateTime: ts, UpdateTime: ts},
  312. {Code: code, Name: "b", AppKey: testutil.UniqueId(), AppSecret: "s2", Status: 1, CreateTime: ts, UpdateTime: ts},
  313. }
  314. err := m.BatchInsert(ctx, list)
  315. require.Error(t, err)
  316. t.Cleanup(func() {
  317. if found, e := m.FindOneByCode(ctx, code); e == nil {
  318. testutil.CleanTable(ctx, conn, m.TableName(), found.Id)
  319. }
  320. })
  321. var me *mysql.MySQLError
  322. require.True(t, errors.As(err, &me))
  323. require.Equal(t, uint16(1062), me.Number)
  324. }
  325. // TC-0343: 空列表
  326. func TestSysProductModel_BatchUpdate_Empty(t *testing.T) {
  327. m := newTestModel(t)
  328. require.NoError(t, m.BatchUpdate(context.Background(), nil))
  329. require.NoError(t, m.BatchUpdate(context.Background(), []*SysProduct{}))
  330. }
  331. // TC-0345: 多条记录(3条)
  332. func TestSysProductModel_BatchUpdate_Multi(t *testing.T) {
  333. ctx := context.Background()
  334. conn := testutil.GetTestSqlConn()
  335. m := newTestModel(t)
  336. p1 := newSysProduct()
  337. p2 := newSysProduct()
  338. r1, err := m.Insert(ctx, p1)
  339. require.NoError(t, err)
  340. id1, _ := r1.LastInsertId()
  341. r2, err := m.Insert(ctx, p2)
  342. require.NoError(t, err)
  343. id2, _ := r2.LastInsertId()
  344. defer testutil.CleanTable(ctx, conn, m.TableName(), id1, id2)
  345. now := time.Now().Unix()
  346. upd := []*SysProduct{
  347. {Id: id1, Code: p1.Code, Name: "upd1", AppKey: p1.AppKey, AppSecret: p1.AppSecret, Remark: "r1", Status: 1, CreateTime: p1.CreateTime, UpdateTime: now},
  348. {Id: id2, Code: p2.Code, Name: "upd2", AppKey: p2.AppKey, AppSecret: p2.AppSecret, Remark: "r2", Status: 2, CreateTime: p2.CreateTime, UpdateTime: now},
  349. }
  350. require.NoError(t, m.BatchUpdate(ctx, upd))
  351. g1, err := m.FindOne(ctx, id1)
  352. require.NoError(t, err)
  353. require.Equal(t, "upd1", g1.Name)
  354. g2, err := m.FindOne(ctx, id2)
  355. require.NoError(t, err)
  356. require.Equal(t, "upd2", g2.Name)
  357. require.Equal(t, int64(2), g2.Status)
  358. }
  359. // TC-0341: 正常多条
  360. func TestSysProductModel_BatchInsertWithTx_Normal(t *testing.T) {
  361. ctx := context.Background()
  362. conn := testutil.GetTestSqlConn()
  363. m := newTestModel(t)
  364. p1 := newSysProduct()
  365. p2 := newSysProduct()
  366. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  367. return m.BatchInsertWithTx(c, session, []*SysProduct{p1, p2})
  368. })
  369. require.NoError(t, err)
  370. f1, err := m.FindOneByCode(ctx, p1.Code)
  371. require.NoError(t, err)
  372. f2, err := m.FindOneByCode(ctx, p2.Code)
  373. require.NoError(t, err)
  374. defer testutil.CleanTable(ctx, conn, m.TableName(), f1.Id, f2.Id)
  375. require.Equal(t, p1.Code, f1.Code)
  376. require.Equal(t, p2.Code, f2.Code)
  377. }
  378. // TC-0340: 空列表
  379. func TestSysProductModel_BatchInsertWithTx_Empty(t *testing.T) {
  380. m := newTestModel(t)
  381. err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
  382. return m.BatchInsertWithTx(c, session, nil)
  383. })
  384. require.NoError(t, err)
  385. }
  386. // TC-0342: 事务回滚
  387. func TestSysProductModel_BatchInsertWithTx_Rollback(t *testing.T) {
  388. ctx := context.Background()
  389. m := newTestModel(t)
  390. p := newSysProduct()
  391. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  392. if e := m.BatchInsertWithTx(c, session, []*SysProduct{p}); e != nil {
  393. return e
  394. }
  395. return fmt.Errorf("force rollback")
  396. })
  397. require.Error(t, err)
  398. _, err = m.FindOneByCode(ctx, p.Code)
  399. require.ErrorIs(t, err, ErrNotFound)
  400. }
  401. // TC-0349: 正常多条
  402. func TestSysProductModel_BatchUpdateWithTx_Normal(t *testing.T) {
  403. ctx := context.Background()
  404. conn := testutil.GetTestSqlConn()
  405. m := newTestModel(t)
  406. p1 := newSysProduct()
  407. p2 := newSysProduct()
  408. r1, err := m.Insert(ctx, p1)
  409. require.NoError(t, err)
  410. id1, _ := r1.LastInsertId()
  411. r2, err := m.Insert(ctx, p2)
  412. require.NoError(t, err)
  413. id2, _ := r2.LastInsertId()
  414. defer testutil.CleanTable(ctx, conn, m.TableName(), id1, id2)
  415. now := time.Now().Unix()
  416. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  417. return m.BatchUpdateWithTx(c, session, []*SysProduct{
  418. {Id: id1, Code: p1.Code, Name: "tx_upd1", AppKey: p1.AppKey, AppSecret: p1.AppSecret, Status: 1, CreateTime: p1.CreateTime, UpdateTime: now},
  419. {Id: id2, Code: p2.Code, Name: "tx_upd2", AppKey: p2.AppKey, AppSecret: p2.AppSecret, Status: 1, CreateTime: p2.CreateTime, UpdateTime: now},
  420. })
  421. })
  422. require.NoError(t, err)
  423. g1, err := m.FindOne(ctx, id1)
  424. require.NoError(t, err)
  425. require.Equal(t, "tx_upd1", g1.Name)
  426. g2, err := m.FindOne(ctx, id2)
  427. require.NoError(t, err)
  428. require.Equal(t, "tx_upd2", g2.Name)
  429. }
  430. // TC-0348: 空列表
  431. func TestSysProductModel_BatchUpdateWithTx_Empty(t *testing.T) {
  432. m := newTestModel(t)
  433. err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
  434. return m.BatchUpdateWithTx(c, session, nil)
  435. })
  436. require.NoError(t, err)
  437. }
  438. // TC-0354: 单个id
  439. func TestSysProductModel_BatchDelete_Single(t *testing.T) {
  440. ctx := context.Background()
  441. conn := testutil.GetTestSqlConn()
  442. m := newTestModel(t)
  443. p := newSysProduct()
  444. res, err := m.Insert(ctx, p)
  445. require.NoError(t, err)
  446. id, _ := res.LastInsertId()
  447. defer testutil.CleanTable(ctx, conn, m.TableName(), id)
  448. require.NoError(t, m.BatchDelete(ctx, []int64{id}))
  449. _, err = m.FindOne(ctx, id)
  450. require.ErrorIs(t, err, ErrNotFound)
  451. }
  452. // TC-0356: 包含不存在id
  453. func TestSysProductModel_BatchDelete_ContainsNonExist(t *testing.T) {
  454. ctx := context.Background()
  455. conn := testutil.GetTestSqlConn()
  456. m := newTestModel(t)
  457. p := newSysProduct()
  458. res, err := m.Insert(ctx, p)
  459. require.NoError(t, err)
  460. id, _ := res.LastInsertId()
  461. defer testutil.CleanTable(ctx, conn, m.TableName(), id)
  462. require.NoError(t, m.BatchDelete(ctx, []int64{id, 999999999}))
  463. _, err = m.FindOne(ctx, id)
  464. require.ErrorIs(t, err, ErrNotFound)
  465. }
  466. // TC-0358: 正常多条
  467. func TestSysProductModel_BatchDeleteWithTx_Normal(t *testing.T) {
  468. ctx := context.Background()
  469. conn := testutil.GetTestSqlConn()
  470. m := newTestModel(t)
  471. p1 := newSysProduct()
  472. p2 := newSysProduct()
  473. r1, err := m.Insert(ctx, p1)
  474. require.NoError(t, err)
  475. id1, _ := r1.LastInsertId()
  476. r2, err := m.Insert(ctx, p2)
  477. require.NoError(t, err)
  478. id2, _ := r2.LastInsertId()
  479. defer testutil.CleanTable(ctx, conn, m.TableName(), id1, id2)
  480. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  481. return m.BatchDeleteWithTx(c, session, []int64{id1, id2})
  482. })
  483. require.NoError(t, err)
  484. _, err = m.FindOne(ctx, id1)
  485. require.ErrorIs(t, err, ErrNotFound)
  486. _, err = m.FindOne(ctx, id2)
  487. require.ErrorIs(t, err, ErrNotFound)
  488. }
  489. // TC-0357: 空ids
  490. func TestSysProductModel_BatchDeleteWithTx_Empty(t *testing.T) {
  491. m := newTestModel(t)
  492. err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
  493. return m.BatchDeleteWithTx(c, session, nil)
  494. })
  495. require.NoError(t, err)
  496. }
  497. // TC-0323: 事务内可见性
  498. func TestSysProductModel_FindOneWithTx_InsertThenFind(t *testing.T) {
  499. ctx := context.Background()
  500. conn := testutil.GetTestSqlConn()
  501. m := newTestModel(t)
  502. now := time.Now().Unix()
  503. var foundInTx *SysProduct
  504. var insertedId int64
  505. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  506. res, err := m.InsertWithTx(c, session, &SysProduct{
  507. Code: "ftx_code_" + testutil.UniqueId(),
  508. Name: "ftx_name_" + testutil.UniqueId(),
  509. AppKey: "ftx_ak_" + testutil.UniqueId(),
  510. AppSecret: "ftx_sec_" + testutil.UniqueId(),
  511. Remark: "",
  512. Status: 1,
  513. CreateTime: now,
  514. UpdateTime: now,
  515. })
  516. if err != nil {
  517. return err
  518. }
  519. insertedId, _ = res.LastInsertId()
  520. foundInTx, err = m.FindOneWithTx(c, session, insertedId)
  521. return err
  522. })
  523. require.NoError(t, err)
  524. t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), insertedId) })
  525. require.NotNil(t, foundInTx)
  526. assert.Equal(t, insertedId, foundInTx.Id)
  527. }
  528. // TC-0322: 事务内记录不存在
  529. func TestSysProductModel_FindOneWithTx_NotFound(t *testing.T) {
  530. ctx := context.Background()
  531. m := newTestModel(t)
  532. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  533. _, err := m.FindOneWithTx(c, session, 999999999999)
  534. return err
  535. })
  536. require.ErrorIs(t, err, ErrNotFound)
  537. }
  538. // TC-0365: FindOneByAppKeyWithTx
  539. func TestSysProductModel_FindOneByAppKeyWithTx_InsertThenFind(t *testing.T) {
  540. ctx := context.Background()
  541. conn := testutil.GetTestSqlConn()
  542. m := newTestModel(t)
  543. appKey := "ftx_ak_" + testutil.UniqueId()
  544. code := "ftx_code_" + testutil.UniqueId()
  545. now := time.Now().Unix()
  546. var foundByKey *SysProduct
  547. var insertedId int64
  548. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  549. res, err := m.InsertWithTx(c, session, &SysProduct{
  550. Code: code,
  551. Name: "ftx_name_" + testutil.UniqueId(),
  552. AppKey: appKey,
  553. AppSecret: "ftx_sec_" + testutil.UniqueId(),
  554. Remark: "",
  555. Status: 1,
  556. CreateTime: now,
  557. UpdateTime: now,
  558. })
  559. if err != nil {
  560. return err
  561. }
  562. insertedId, _ = res.LastInsertId()
  563. foundByKey, err = m.FindOneByAppKeyWithTx(c, session, appKey)
  564. return err
  565. })
  566. require.NoError(t, err)
  567. t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), insertedId) })
  568. require.NotNil(t, foundByKey)
  569. assert.Equal(t, insertedId, foundByKey.Id)
  570. assert.Equal(t, appKey, foundByKey.AppKey)
  571. }
  572. // TC-0366: FindOneByAppKeyWithTx
  573. func TestSysProductModel_FindOneByAppKeyWithTx_NotFound(t *testing.T) {
  574. ctx := context.Background()
  575. m := newTestModel(t)
  576. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  577. _, err := m.FindOneByAppKeyWithTx(c, session, "notexist_appkey_"+testutil.UniqueId())
  578. return err
  579. })
  580. require.ErrorIs(t, err, ErrNotFound)
  581. }
  582. // TC-0369: FindOneByCodeWithTx
  583. func TestSysProductModel_FindOneByCodeWithTx_InsertThenFind(t *testing.T) {
  584. ctx := context.Background()
  585. conn := testutil.GetTestSqlConn()
  586. m := newTestModel(t)
  587. code := "ftx_code_" + testutil.UniqueId()
  588. now := time.Now().Unix()
  589. var foundByCode *SysProduct
  590. var insertedId int64
  591. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  592. res, err := m.InsertWithTx(c, session, &SysProduct{
  593. Code: code,
  594. Name: "ftx_name_" + testutil.UniqueId(),
  595. AppKey: "ftx_ak_" + testutil.UniqueId(),
  596. AppSecret: "ftx_sec_" + testutil.UniqueId(),
  597. Remark: "",
  598. Status: 1,
  599. CreateTime: now,
  600. UpdateTime: now,
  601. })
  602. if err != nil {
  603. return err
  604. }
  605. insertedId, _ = res.LastInsertId()
  606. foundByCode, err = m.FindOneByCodeWithTx(c, session, code)
  607. return err
  608. })
  609. require.NoError(t, err)
  610. t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), insertedId) })
  611. require.NotNil(t, foundByCode)
  612. assert.Equal(t, insertedId, foundByCode.Id)
  613. assert.Equal(t, code, foundByCode.Code)
  614. }
  615. // TC-0370: FindOneByCodeWithTx
  616. func TestSysProductModel_FindOneByCodeWithTx_NotFound(t *testing.T) {
  617. ctx := context.Background()
  618. m := newTestModel(t)
  619. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  620. _, err := m.FindOneByCodeWithTx(c, session, "notexist_code_"+testutil.UniqueId())
  621. return err
  622. })
  623. require.ErrorIs(t, err, ErrNotFound)
  624. }
  625. // TC-0404: 多唯一索引前缀(SysProduct)
  626. func TestSysProductModel_CachePrefix_MultiUniqueIndex(t *testing.T) {
  627. oldId := cacheSysProductIdPrefix
  628. oldAppKey := cacheSysProductAppKeyPrefix
  629. oldCode := cacheSysProductCodePrefix
  630. defer func() {
  631. cacheSysProductIdPrefix = oldId
  632. cacheSysProductAppKeyPrefix = oldAppKey
  633. cacheSysProductCodePrefix = oldCode
  634. }()
  635. _ = newSysProductModel(testutil.GetTestSqlConn(), testutil.GetTestCacheConf(), "test")
  636. assert.True(t, strings.HasPrefix(cacheSysProductIdPrefix, "test:"))
  637. assert.True(t, strings.HasPrefix(cacheSysProductAppKeyPrefix, "test:"))
  638. assert.True(t, strings.HasPrefix(cacheSysProductCodePrefix, "test:"))
  639. }
  640. func TestSysProductModel_LockByCodeTx_Found(t *testing.T) {
  641. ctx := context.Background()
  642. conn := testutil.GetTestSqlConn()
  643. m := newTestModel(t)
  644. p := newSysProduct()
  645. res, err := m.Insert(ctx, p)
  646. require.NoError(t, err)
  647. id, _ := res.LastInsertId()
  648. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
  649. var got *SysProduct
  650. require.NoError(t, m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  651. got, err = m.LockByCodeTx(c, session, p.Code)
  652. return err
  653. }))
  654. require.NotNil(t, got)
  655. assert.Equal(t, id, got.Id)
  656. assert.Equal(t, p.Code, got.Code)
  657. assert.Equal(t, p.AppKey, got.AppKey)
  658. assert.Equal(t, p.Status, got.Status, "锁行时不得过滤禁用态,否则 SyncPermissions 无法为禁用产品正确 fail-close")
  659. }
  660. // TC-0810: LockByCodeTx 对不存在的 code 返回 sqlx.ErrNotFound。
  661. func TestSysProductModel_LockByCodeTx_NotFound(t *testing.T) {
  662. ctx := context.Background()
  663. m := newTestModel(t)
  664. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  665. _, e := m.LockByCodeTx(c, session, "definitely_no_such_code_"+testutil.UniqueId())
  666. return e
  667. })
  668. require.Error(t, err)
  669. assert.True(t, errors.Is(err, sqlx.ErrNotFound),
  670. "LockByCodeTx 对不存在的 code 必须返回 ErrNotFound,便于上层 fail-close 返回 401/404")
  671. }
  672. // TC-0811: FOR UPDATE 行锁真实生效 —— 两个事务同时尝试锁同一行时,
  673. // 后进者必须被阻塞直到先进者结束事务。
  674. // 测量方式:goroutine A 在 tx 内 Lock 住后 sleep 500ms 再 commit;
  675. //
  676. // goroutine B 等 100ms 后也尝试 Lock 同一行,记录耗时。B 的耗时必须≥400ms。
  677. func TestSysProductModel_LockByCodeTx_BlocksConcurrentWriter(t *testing.T) {
  678. ctx := context.Background()
  679. conn := testutil.GetTestSqlConn()
  680. m := newTestModel(t)
  681. p := newSysProduct()
  682. res, err := m.Insert(ctx, p)
  683. require.NoError(t, err)
  684. id, _ := res.LastInsertId()
  685. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
  686. var (
  687. wg sync.WaitGroup
  688. aHoldMs = int64(500)
  689. bStartDelayMs = int64(100)
  690. bElapsedNanos int64
  691. aFinishedNanos int64
  692. aErr, bErr error
  693. )
  694. wg.Add(2)
  695. go func() {
  696. defer wg.Done()
  697. aErr = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  698. if _, e := m.LockByCodeTx(c, session, p.Code); e != nil {
  699. return e
  700. }
  701. // A 拿到锁后故意延时,模拟一段业务处理期。期间 B 必须被阻塞。
  702. time.Sleep(time.Duration(aHoldMs) * time.Millisecond)
  703. atomic.StoreInt64(&aFinishedNanos, time.Now().UnixNano())
  704. return nil
  705. })
  706. }()
  707. go func() {
  708. defer wg.Done()
  709. time.Sleep(time.Duration(bStartDelayMs) * time.Millisecond)
  710. start := time.Now()
  711. bErr = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  712. _, e := m.LockByCodeTx(c, session, p.Code)
  713. return e
  714. })
  715. atomic.StoreInt64(&bElapsedNanos, time.Since(start).Nanoseconds())
  716. }()
  717. wg.Wait()
  718. require.NoError(t, aErr)
  719. require.NoError(t, bErr)
  720. // B 的耗时 ≥ A 的剩余持锁时间。A 持 500ms,B 延 100ms 后入场,
  721. // 因此 B 被阻塞的时间至少 (500-100)=400ms。给 DB 一点抖动放到 300ms。
  722. elapsedMs := atomic.LoadInt64(&bElapsedNanos) / int64(time.Millisecond)
  723. minBlockedMs := int64(300)
  724. assert.GreaterOrEqualf(t, elapsedMs, minBlockedMs, fmt.Sprintf(
  725. "B 的 LockByCodeTx 总耗时 %dms 明显低于预期最小阻塞 %dms —— "+
  726. "意味着 FOR UPDATE 行锁失效,声称的'按 product 串行化'不成立",
  727. elapsedMs, minBlockedMs))
  728. }