sysProductModel_test.go 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082
  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/redis"
  10. "github.com/zeromicro/go-zero/core/stores/sqlx"
  11. "perms-system-server/internal/testutil"
  12. "strings"
  13. "sync"
  14. "sync/atomic"
  15. "testing"
  16. "time"
  17. )
  18. func newTestModel(t *testing.T) SysProductModel {
  19. t.Helper()
  20. return NewSysProductModel(
  21. testutil.GetTestSqlConn(),
  22. testutil.GetTestCacheConf(),
  23. testutil.GetTestCachePrefix(),
  24. )
  25. }
  26. func newSysProduct() *SysProduct {
  27. ts := time.Now().Unix()
  28. return &SysProduct{
  29. Code: testutil.UniqueId(),
  30. Name: "integration-product",
  31. AppKey: testutil.UniqueId(),
  32. AppSecret: "app-secret-" + testutil.UniqueId(),
  33. Remark: "remark",
  34. Status: 1,
  35. CreateTime: ts,
  36. UpdateTime: ts,
  37. }
  38. }
  39. // TC-0423: 正常分页
  40. func TestSysProductModel_Integration(t *testing.T) {
  41. ctx := context.Background()
  42. conn := testutil.GetTestSqlConn()
  43. m := newTestModel(t)
  44. t.Run("TableName", func(t *testing.T) {
  45. require.Equal(t, "`sys_product`", m.TableName())
  46. })
  47. t.Run("FindList_pagination", func(t *testing.T) {
  48. var beforeTotal int64
  49. _, beforeTotal, _ = m.FindList(ctx, 1, 1)
  50. const n = 5
  51. ids := make([]int64, 0, n)
  52. for i := 0; i < n; i++ {
  53. p := newSysProduct()
  54. p.Name = fmt.Sprintf("page-item-%d", i)
  55. res, err := m.Insert(ctx, p)
  56. require.NoError(t, err)
  57. id, err := res.LastInsertId()
  58. require.NoError(t, err)
  59. ids = append(ids, id)
  60. }
  61. defer func() {
  62. testutil.CleanTable(ctx, conn, "`sys_product`", ids...)
  63. }()
  64. expectedTotal := beforeTotal + int64(n)
  65. _, total, err := m.FindList(ctx, 1, 2)
  66. require.NoError(t, err)
  67. require.Equal(t, expectedTotal, total)
  68. var pageSize int64 = 2
  69. fullPages := expectedTotal / pageSize
  70. lastPageSize := expectedTotal % pageSize
  71. if lastPageSize > 0 {
  72. lastList, _, err := m.FindList(ctx, fullPages+1, pageSize)
  73. require.NoError(t, err)
  74. require.Len(t, lastList, int(lastPageSize))
  75. }
  76. })
  77. t.Run("CRUD_cycle", func(t *testing.T) {
  78. p := newSysProduct()
  79. p.Name = "crud-name"
  80. res, err := m.Insert(ctx, p)
  81. require.NoError(t, err)
  82. id, err := res.LastInsertId()
  83. require.NoError(t, err)
  84. defer testutil.CleanTable(ctx, conn, "`sys_product`", id)
  85. got, err := m.FindOne(ctx, id)
  86. require.NoError(t, err)
  87. require.Equal(t, id, got.Id)
  88. require.Equal(t, p.Code, got.Code)
  89. require.Equal(t, p.Name, got.Name)
  90. require.Equal(t, p.AppKey, got.AppKey)
  91. require.Equal(t, p.AppSecret, got.AppSecret)
  92. require.Equal(t, p.Remark, got.Remark)
  93. require.Equal(t, p.Status, got.Status)
  94. ts2 := time.Now().Unix()
  95. got.Name = "crud-name-updated"
  96. got.Remark = "updated-remark"
  97. got.Status = 2
  98. got.UpdateTime = ts2
  99. require.NoError(t, m.Update(ctx, got))
  100. after, err := m.FindOne(ctx, id)
  101. require.NoError(t, err)
  102. require.Equal(t, "crud-name-updated", after.Name)
  103. require.Equal(t, "updated-remark", after.Remark)
  104. require.Equal(t, int64(2), after.Status)
  105. require.Equal(t, ts2, after.UpdateTime)
  106. require.NoError(t, m.Delete(ctx, id))
  107. _, err = m.FindOne(ctx, id)
  108. require.ErrorIs(t, err, ErrNotFound)
  109. })
  110. t.Run("FindOneByAppKey_found_and_notFound", func(t *testing.T) {
  111. p := newSysProduct()
  112. res, err := m.Insert(ctx, p)
  113. require.NoError(t, err)
  114. id, err := res.LastInsertId()
  115. require.NoError(t, err)
  116. defer testutil.CleanTable(ctx, conn, "`sys_product`", id)
  117. got, err := m.FindOneByAppKey(ctx, p.AppKey)
  118. require.NoError(t, err)
  119. require.Equal(t, p.AppKey, got.AppKey)
  120. require.Equal(t, id, got.Id)
  121. _, err = m.FindOneByAppKey(ctx, "nonexistent-appkey-"+testutil.UniqueId())
  122. require.ErrorIs(t, err, ErrNotFound)
  123. })
  124. t.Run("FindOneByCode_found_and_notFound", func(t *testing.T) {
  125. p := newSysProduct()
  126. res, err := m.Insert(ctx, p)
  127. require.NoError(t, err)
  128. id, err := res.LastInsertId()
  129. require.NoError(t, err)
  130. defer testutil.CleanTable(ctx, conn, "`sys_product`", id)
  131. got, err := m.FindOneByCode(ctx, p.Code)
  132. require.NoError(t, err)
  133. require.Equal(t, p.Code, got.Code)
  134. require.Equal(t, id, got.Id)
  135. _, err = m.FindOneByCode(ctx, "nonexistent-code-"+testutil.UniqueId())
  136. require.ErrorIs(t, err, ErrNotFound)
  137. })
  138. t.Run("BatchInsert_and_BatchDelete", func(t *testing.T) {
  139. items := []*SysProduct{newSysProduct(), newSysProduct(), newSysProduct()}
  140. for i := range items {
  141. items[i].Name = fmt.Sprintf("batch-%d", i)
  142. }
  143. require.NoError(t, m.BatchInsert(ctx, items))
  144. var ids []int64
  145. for _, it := range items {
  146. row, err := m.FindOneByCode(ctx, it.Code)
  147. require.NoError(t, err)
  148. ids = append(ids, row.Id)
  149. }
  150. defer testutil.CleanTable(ctx, conn, "`sys_product`", ids...)
  151. require.NoError(t, m.BatchDelete(ctx, ids))
  152. for _, id := range ids {
  153. _, err := m.FindOne(ctx, id)
  154. require.ErrorIs(t, err, ErrNotFound)
  155. }
  156. })
  157. t.Run("TransactCtx_commit", func(t *testing.T) {
  158. p := newSysProduct()
  159. p.Name = "tx-commit"
  160. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  161. _, err := m.InsertWithTx(c, session, p)
  162. return err
  163. })
  164. require.NoError(t, err)
  165. row, err := m.FindOneByCode(ctx, p.Code)
  166. require.NoError(t, err)
  167. defer testutil.CleanTable(ctx, conn, "`sys_product`", row.Id)
  168. require.Equal(t, p.Code, row.Code)
  169. require.Equal(t, "tx-commit", row.Name)
  170. })
  171. t.Run("TransactCtx_rollback", func(t *testing.T) {
  172. p := newSysProduct()
  173. p.Name = "tx-rollback"
  174. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  175. if _, err := m.InsertWithTx(c, session, p); err != nil {
  176. return err
  177. }
  178. return fmt.Errorf("force rollback")
  179. })
  180. require.Error(t, err)
  181. _, err = m.FindOneByCode(ctx, p.Code)
  182. require.ErrorIs(t, err, ErrNotFound)
  183. })
  184. t.Run("Insert_duplicateCode", func(t *testing.T) {
  185. code := testutil.UniqueId()
  186. ts := time.Now().Unix()
  187. p1 := &SysProduct{
  188. Code: code,
  189. Name: "dup-a",
  190. AppKey: testutil.UniqueId(),
  191. AppSecret: "s1",
  192. Remark: "",
  193. Status: 1,
  194. CreateTime: ts,
  195. UpdateTime: ts,
  196. }
  197. res, err := m.Insert(ctx, p1)
  198. require.NoError(t, err)
  199. id1, err := res.LastInsertId()
  200. require.NoError(t, err)
  201. defer testutil.CleanTable(ctx, conn, "`sys_product`", id1)
  202. p2 := &SysProduct{
  203. Code: code,
  204. Name: "dup-b",
  205. AppKey: testutil.UniqueId(),
  206. AppSecret: "s2",
  207. Remark: "",
  208. Status: 1,
  209. CreateTime: ts,
  210. UpdateTime: ts,
  211. }
  212. _, err = m.Insert(ctx, p2)
  213. require.Error(t, err)
  214. var me *mysql.MySQLError
  215. require.True(t, errors.As(err, &me))
  216. require.Equal(t, uint16(1062), me.Number)
  217. })
  218. }
  219. // TC-0319: 记录不存在
  220. func TestSysProductModel_FindOne_NotFound(t *testing.T) {
  221. m := newTestModel(t)
  222. _, err := m.FindOne(context.Background(), 999999999999)
  223. require.ErrorIs(t, err, ErrNotFound)
  224. }
  225. // TC-0326: 记录不存在
  226. func TestSysProductModel_Update_NotFound(t *testing.T) {
  227. m := newTestModel(t)
  228. err := m.Update(context.Background(), &SysProduct{
  229. Id: 999999999999, Code: "x", Name: "n", AppKey: "k", AppSecret: "s",
  230. Status: 1, CreateTime: time.Now().Unix(), UpdateTime: time.Now().Unix(),
  231. })
  232. require.ErrorIs(t, err, ErrNotFound)
  233. }
  234. // TC-0329: 记录不存在
  235. func TestSysProductModel_Delete_NotFound(t *testing.T) {
  236. m := newTestModel(t)
  237. err := m.Delete(context.Background(), 999999999999)
  238. require.ErrorIs(t, err, ErrNotFound)
  239. }
  240. // TC-0334: 空列表
  241. func TestSysProductModel_BatchInsert_Empty(t *testing.T) {
  242. m := newTestModel(t)
  243. require.NoError(t, m.BatchInsert(context.Background(), nil))
  244. require.NoError(t, m.BatchInsert(context.Background(), []*SysProduct{}))
  245. }
  246. // TC-0353: 空ids
  247. func TestSysProductModel_BatchDelete_Empty(t *testing.T) {
  248. m := newTestModel(t)
  249. require.NoError(t, m.BatchDelete(context.Background(), nil))
  250. require.NoError(t, m.BatchDelete(context.Background(), []int64{}))
  251. }
  252. // TC-0327: 事务内更新
  253. func TestSysProductModel_UpdateWithTx(t *testing.T) {
  254. ctx := context.Background()
  255. conn := testutil.GetTestSqlConn()
  256. m := newTestModel(t)
  257. p := newSysProduct()
  258. res, err := m.Insert(ctx, p)
  259. require.NoError(t, err)
  260. id, err := res.LastInsertId()
  261. require.NoError(t, err)
  262. defer testutil.CleanTable(ctx, conn, m.TableName(), id)
  263. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  264. p.Id = id
  265. p.Name = "tx_updated_name"
  266. p.UpdateTime = time.Now().Unix()
  267. return m.UpdateWithTx(c, session, p)
  268. })
  269. require.NoError(t, err)
  270. got, err := m.FindOne(ctx, id)
  271. require.NoError(t, err)
  272. require.Equal(t, "tx_updated_name", got.Name)
  273. }
  274. // TC-0330: 事务内删除
  275. func TestSysProductModel_DeleteWithTx(t *testing.T) {
  276. ctx := context.Background()
  277. conn := testutil.GetTestSqlConn()
  278. m := newTestModel(t)
  279. p := newSysProduct()
  280. res, err := m.Insert(ctx, p)
  281. require.NoError(t, err)
  282. id, err := res.LastInsertId()
  283. require.NoError(t, err)
  284. defer testutil.CleanTable(ctx, conn, m.TableName(), id)
  285. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  286. return m.DeleteWithTx(c, session, id)
  287. })
  288. require.NoError(t, err)
  289. _, err = m.FindOne(ctx, id)
  290. require.ErrorIs(t, err, ErrNotFound)
  291. }
  292. // TC-0335: 单条记录
  293. func TestSysProductModel_BatchInsert_Single(t *testing.T) {
  294. ctx := context.Background()
  295. conn := testutil.GetTestSqlConn()
  296. m := newTestModel(t)
  297. p := newSysProduct()
  298. require.NoError(t, m.BatchInsert(ctx, []*SysProduct{p}))
  299. found, err := m.FindOneByCode(ctx, p.Code)
  300. require.NoError(t, err)
  301. defer testutil.CleanTable(ctx, conn, m.TableName(), found.Id)
  302. require.Equal(t, p.Code, found.Code)
  303. }
  304. // TC-0338: 唯一索引冲突
  305. func TestSysProductModel_BatchInsert_UniqueConflict(t *testing.T) {
  306. ctx := context.Background()
  307. conn := testutil.GetTestSqlConn()
  308. m := newTestModel(t)
  309. code := testutil.UniqueId()
  310. ts := time.Now().Unix()
  311. list := []*SysProduct{
  312. {Code: code, Name: "a", AppKey: testutil.UniqueId(), AppSecret: "s1", Status: 1, CreateTime: ts, UpdateTime: ts},
  313. {Code: code, Name: "b", AppKey: testutil.UniqueId(), AppSecret: "s2", Status: 1, CreateTime: ts, UpdateTime: ts},
  314. }
  315. err := m.BatchInsert(ctx, list)
  316. require.Error(t, err)
  317. t.Cleanup(func() {
  318. if found, e := m.FindOneByCode(ctx, code); e == nil {
  319. testutil.CleanTable(ctx, conn, m.TableName(), found.Id)
  320. }
  321. })
  322. var me *mysql.MySQLError
  323. require.True(t, errors.As(err, &me))
  324. require.Equal(t, uint16(1062), me.Number)
  325. }
  326. // TC-0343: 空列表
  327. func TestSysProductModel_BatchUpdate_Empty(t *testing.T) {
  328. m := newTestModel(t)
  329. require.NoError(t, m.BatchUpdate(context.Background(), nil))
  330. require.NoError(t, m.BatchUpdate(context.Background(), []*SysProduct{}))
  331. }
  332. // TC-0345: 多条记录(3条)
  333. func TestSysProductModel_BatchUpdate_Multi(t *testing.T) {
  334. ctx := context.Background()
  335. conn := testutil.GetTestSqlConn()
  336. m := newTestModel(t)
  337. p1 := newSysProduct()
  338. p2 := newSysProduct()
  339. r1, err := m.Insert(ctx, p1)
  340. require.NoError(t, err)
  341. id1, _ := r1.LastInsertId()
  342. r2, err := m.Insert(ctx, p2)
  343. require.NoError(t, err)
  344. id2, _ := r2.LastInsertId()
  345. defer testutil.CleanTable(ctx, conn, m.TableName(), id1, id2)
  346. now := time.Now().Unix()
  347. upd := []*SysProduct{
  348. {Id: id1, Code: p1.Code, Name: "upd1", AppKey: p1.AppKey, AppSecret: p1.AppSecret, Remark: "r1", Status: 1, CreateTime: p1.CreateTime, UpdateTime: now},
  349. {Id: id2, Code: p2.Code, Name: "upd2", AppKey: p2.AppKey, AppSecret: p2.AppSecret, Remark: "r2", Status: 2, CreateTime: p2.CreateTime, UpdateTime: now},
  350. }
  351. require.NoError(t, m.BatchUpdate(ctx, upd))
  352. g1, err := m.FindOne(ctx, id1)
  353. require.NoError(t, err)
  354. require.Equal(t, "upd1", g1.Name)
  355. g2, err := m.FindOne(ctx, id2)
  356. require.NoError(t, err)
  357. require.Equal(t, "upd2", g2.Name)
  358. require.Equal(t, int64(2), g2.Status)
  359. }
  360. // TC-0341: 正常多条
  361. func TestSysProductModel_BatchInsertWithTx_Normal(t *testing.T) {
  362. ctx := context.Background()
  363. conn := testutil.GetTestSqlConn()
  364. m := newTestModel(t)
  365. p1 := newSysProduct()
  366. p2 := newSysProduct()
  367. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  368. return m.BatchInsertWithTx(c, session, []*SysProduct{p1, p2})
  369. })
  370. require.NoError(t, err)
  371. f1, err := m.FindOneByCode(ctx, p1.Code)
  372. require.NoError(t, err)
  373. f2, err := m.FindOneByCode(ctx, p2.Code)
  374. require.NoError(t, err)
  375. defer testutil.CleanTable(ctx, conn, m.TableName(), f1.Id, f2.Id)
  376. require.Equal(t, p1.Code, f1.Code)
  377. require.Equal(t, p2.Code, f2.Code)
  378. }
  379. // TC-0340: 空列表
  380. func TestSysProductModel_BatchInsertWithTx_Empty(t *testing.T) {
  381. m := newTestModel(t)
  382. err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
  383. return m.BatchInsertWithTx(c, session, nil)
  384. })
  385. require.NoError(t, err)
  386. }
  387. // TC-0342: 事务回滚
  388. func TestSysProductModel_BatchInsertWithTx_Rollback(t *testing.T) {
  389. ctx := context.Background()
  390. m := newTestModel(t)
  391. p := newSysProduct()
  392. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  393. if e := m.BatchInsertWithTx(c, session, []*SysProduct{p}); e != nil {
  394. return e
  395. }
  396. return fmt.Errorf("force rollback")
  397. })
  398. require.Error(t, err)
  399. _, err = m.FindOneByCode(ctx, p.Code)
  400. require.ErrorIs(t, err, ErrNotFound)
  401. }
  402. // TC-0349: 正常多条
  403. func TestSysProductModel_BatchUpdateWithTx_Normal(t *testing.T) {
  404. ctx := context.Background()
  405. conn := testutil.GetTestSqlConn()
  406. m := newTestModel(t)
  407. p1 := newSysProduct()
  408. p2 := newSysProduct()
  409. r1, err := m.Insert(ctx, p1)
  410. require.NoError(t, err)
  411. id1, _ := r1.LastInsertId()
  412. r2, err := m.Insert(ctx, p2)
  413. require.NoError(t, err)
  414. id2, _ := r2.LastInsertId()
  415. defer testutil.CleanTable(ctx, conn, m.TableName(), id1, id2)
  416. now := time.Now().Unix()
  417. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  418. return m.BatchUpdateWithTx(c, session, []*SysProduct{
  419. {Id: id1, Code: p1.Code, Name: "tx_upd1", AppKey: p1.AppKey, AppSecret: p1.AppSecret, Status: 1, CreateTime: p1.CreateTime, UpdateTime: now},
  420. {Id: id2, Code: p2.Code, Name: "tx_upd2", AppKey: p2.AppKey, AppSecret: p2.AppSecret, Status: 1, CreateTime: p2.CreateTime, UpdateTime: now},
  421. })
  422. })
  423. require.NoError(t, err)
  424. g1, err := m.FindOne(ctx, id1)
  425. require.NoError(t, err)
  426. require.Equal(t, "tx_upd1", g1.Name)
  427. g2, err := m.FindOne(ctx, id2)
  428. require.NoError(t, err)
  429. require.Equal(t, "tx_upd2", g2.Name)
  430. }
  431. // TC-0348: 空列表
  432. func TestSysProductModel_BatchUpdateWithTx_Empty(t *testing.T) {
  433. m := newTestModel(t)
  434. err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
  435. return m.BatchUpdateWithTx(c, session, nil)
  436. })
  437. require.NoError(t, err)
  438. }
  439. // TC-0354: 单个id
  440. func TestSysProductModel_BatchDelete_Single(t *testing.T) {
  441. ctx := context.Background()
  442. conn := testutil.GetTestSqlConn()
  443. m := newTestModel(t)
  444. p := newSysProduct()
  445. res, err := m.Insert(ctx, p)
  446. require.NoError(t, err)
  447. id, _ := res.LastInsertId()
  448. defer testutil.CleanTable(ctx, conn, m.TableName(), id)
  449. require.NoError(t, m.BatchDelete(ctx, []int64{id}))
  450. _, err = m.FindOne(ctx, id)
  451. require.ErrorIs(t, err, ErrNotFound)
  452. }
  453. // TC-0356: 包含不存在id
  454. func TestSysProductModel_BatchDelete_ContainsNonExist(t *testing.T) {
  455. ctx := context.Background()
  456. conn := testutil.GetTestSqlConn()
  457. m := newTestModel(t)
  458. p := newSysProduct()
  459. res, err := m.Insert(ctx, p)
  460. require.NoError(t, err)
  461. id, _ := res.LastInsertId()
  462. defer testutil.CleanTable(ctx, conn, m.TableName(), id)
  463. require.NoError(t, m.BatchDelete(ctx, []int64{id, 999999999}))
  464. _, err = m.FindOne(ctx, id)
  465. require.ErrorIs(t, err, ErrNotFound)
  466. }
  467. // TC-0358: 正常多条
  468. func TestSysProductModel_BatchDeleteWithTx_Normal(t *testing.T) {
  469. ctx := context.Background()
  470. conn := testutil.GetTestSqlConn()
  471. m := newTestModel(t)
  472. p1 := newSysProduct()
  473. p2 := newSysProduct()
  474. r1, err := m.Insert(ctx, p1)
  475. require.NoError(t, err)
  476. id1, _ := r1.LastInsertId()
  477. r2, err := m.Insert(ctx, p2)
  478. require.NoError(t, err)
  479. id2, _ := r2.LastInsertId()
  480. defer testutil.CleanTable(ctx, conn, m.TableName(), id1, id2)
  481. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  482. return m.BatchDeleteWithTx(c, session, []int64{id1, id2})
  483. })
  484. require.NoError(t, err)
  485. _, err = m.FindOne(ctx, id1)
  486. require.ErrorIs(t, err, ErrNotFound)
  487. _, err = m.FindOne(ctx, id2)
  488. require.ErrorIs(t, err, ErrNotFound)
  489. }
  490. // TC-0357: 空ids
  491. func TestSysProductModel_BatchDeleteWithTx_Empty(t *testing.T) {
  492. m := newTestModel(t)
  493. err := m.TransactCtx(context.Background(), func(c context.Context, session sqlx.Session) error {
  494. return m.BatchDeleteWithTx(c, session, nil)
  495. })
  496. require.NoError(t, err)
  497. }
  498. // TC-0323: 事务内可见性
  499. func TestSysProductModel_FindOneWithTx_InsertThenFind(t *testing.T) {
  500. ctx := context.Background()
  501. conn := testutil.GetTestSqlConn()
  502. m := newTestModel(t)
  503. now := time.Now().Unix()
  504. var foundInTx *SysProduct
  505. var insertedId int64
  506. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  507. res, err := m.InsertWithTx(c, session, &SysProduct{
  508. Code: "ftx_code_" + testutil.UniqueId(),
  509. Name: "ftx_name_" + testutil.UniqueId(),
  510. AppKey: "ftx_ak_" + testutil.UniqueId(),
  511. AppSecret: "ftx_sec_" + testutil.UniqueId(),
  512. Remark: "",
  513. Status: 1,
  514. CreateTime: now,
  515. UpdateTime: now,
  516. })
  517. if err != nil {
  518. return err
  519. }
  520. insertedId, _ = res.LastInsertId()
  521. foundInTx, err = m.FindOneWithTx(c, session, insertedId)
  522. return err
  523. })
  524. require.NoError(t, err)
  525. t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), insertedId) })
  526. require.NotNil(t, foundInTx)
  527. assert.Equal(t, insertedId, foundInTx.Id)
  528. }
  529. // TC-0322: 事务内记录不存在
  530. func TestSysProductModel_FindOneWithTx_NotFound(t *testing.T) {
  531. ctx := context.Background()
  532. m := newTestModel(t)
  533. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  534. _, err := m.FindOneWithTx(c, session, 999999999999)
  535. return err
  536. })
  537. require.ErrorIs(t, err, ErrNotFound)
  538. }
  539. // TC-0365: FindOneByAppKeyWithTx
  540. func TestSysProductModel_FindOneByAppKeyWithTx_InsertThenFind(t *testing.T) {
  541. ctx := context.Background()
  542. conn := testutil.GetTestSqlConn()
  543. m := newTestModel(t)
  544. appKey := "ftx_ak_" + testutil.UniqueId()
  545. code := "ftx_code_" + testutil.UniqueId()
  546. now := time.Now().Unix()
  547. var foundByKey *SysProduct
  548. var insertedId int64
  549. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  550. res, err := m.InsertWithTx(c, session, &SysProduct{
  551. Code: code,
  552. Name: "ftx_name_" + testutil.UniqueId(),
  553. AppKey: appKey,
  554. AppSecret: "ftx_sec_" + testutil.UniqueId(),
  555. Remark: "",
  556. Status: 1,
  557. CreateTime: now,
  558. UpdateTime: now,
  559. })
  560. if err != nil {
  561. return err
  562. }
  563. insertedId, _ = res.LastInsertId()
  564. foundByKey, err = m.FindOneByAppKeyWithTx(c, session, appKey)
  565. return err
  566. })
  567. require.NoError(t, err)
  568. t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), insertedId) })
  569. require.NotNil(t, foundByKey)
  570. assert.Equal(t, insertedId, foundByKey.Id)
  571. assert.Equal(t, appKey, foundByKey.AppKey)
  572. }
  573. // TC-0366: FindOneByAppKeyWithTx
  574. func TestSysProductModel_FindOneByAppKeyWithTx_NotFound(t *testing.T) {
  575. ctx := context.Background()
  576. m := newTestModel(t)
  577. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  578. _, err := m.FindOneByAppKeyWithTx(c, session, "notexist_appkey_"+testutil.UniqueId())
  579. return err
  580. })
  581. require.ErrorIs(t, err, ErrNotFound)
  582. }
  583. // TC-0369: FindOneByCodeWithTx
  584. func TestSysProductModel_FindOneByCodeWithTx_InsertThenFind(t *testing.T) {
  585. ctx := context.Background()
  586. conn := testutil.GetTestSqlConn()
  587. m := newTestModel(t)
  588. code := "ftx_code_" + testutil.UniqueId()
  589. now := time.Now().Unix()
  590. var foundByCode *SysProduct
  591. var insertedId int64
  592. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  593. res, err := m.InsertWithTx(c, session, &SysProduct{
  594. Code: code,
  595. Name: "ftx_name_" + testutil.UniqueId(),
  596. AppKey: "ftx_ak_" + testutil.UniqueId(),
  597. AppSecret: "ftx_sec_" + testutil.UniqueId(),
  598. Remark: "",
  599. Status: 1,
  600. CreateTime: now,
  601. UpdateTime: now,
  602. })
  603. if err != nil {
  604. return err
  605. }
  606. insertedId, _ = res.LastInsertId()
  607. foundByCode, err = m.FindOneByCodeWithTx(c, session, code)
  608. return err
  609. })
  610. require.NoError(t, err)
  611. t.Cleanup(func() { testutil.CleanTable(ctx, conn, m.TableName(), insertedId) })
  612. require.NotNil(t, foundByCode)
  613. assert.Equal(t, insertedId, foundByCode.Id)
  614. assert.Equal(t, code, foundByCode.Code)
  615. }
  616. // TC-0370: FindOneByCodeWithTx
  617. func TestSysProductModel_FindOneByCodeWithTx_NotFound(t *testing.T) {
  618. ctx := context.Background()
  619. m := newTestModel(t)
  620. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  621. _, err := m.FindOneByCodeWithTx(c, session, "notexist_code_"+testutil.UniqueId())
  622. return err
  623. })
  624. require.ErrorIs(t, err, ErrNotFound)
  625. }
  626. // TC-0404: 多唯一索引前缀(SysProduct)
  627. func TestSysProductModel_CachePrefix_MultiUniqueIndex(t *testing.T) {
  628. oldId := cacheSysProductIdPrefix
  629. oldAppKey := cacheSysProductAppKeyPrefix
  630. oldCode := cacheSysProductCodePrefix
  631. defer func() {
  632. cacheSysProductIdPrefix = oldId
  633. cacheSysProductAppKeyPrefix = oldAppKey
  634. cacheSysProductCodePrefix = oldCode
  635. }()
  636. _ = newSysProductModel(testutil.GetTestSqlConn(), testutil.GetTestCacheConf(), "test")
  637. assert.True(t, strings.HasPrefix(cacheSysProductIdPrefix, "test:"))
  638. assert.True(t, strings.HasPrefix(cacheSysProductAppKeyPrefix, "test:"))
  639. assert.True(t, strings.HasPrefix(cacheSysProductCodePrefix, "test:"))
  640. }
  641. func TestSysProductModel_LockByCodeTx_Found(t *testing.T) {
  642. ctx := context.Background()
  643. conn := testutil.GetTestSqlConn()
  644. m := newTestModel(t)
  645. p := newSysProduct()
  646. res, err := m.Insert(ctx, p)
  647. require.NoError(t, err)
  648. id, _ := res.LastInsertId()
  649. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
  650. var got *SysProduct
  651. require.NoError(t, m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  652. got, err = m.LockByCodeTx(c, session, p.Code)
  653. return err
  654. }))
  655. require.NotNil(t, got)
  656. assert.Equal(t, id, got.Id)
  657. assert.Equal(t, p.Code, got.Code)
  658. assert.Equal(t, p.AppKey, got.AppKey)
  659. assert.Equal(t, p.Status, got.Status, "锁行时不得过滤禁用态,否则 SyncPermissions 无法为禁用产品正确 fail-close")
  660. }
  661. // TC-0810: LockByCodeTx 对不存在的 code 返回 sqlx.ErrNotFound。
  662. func TestSysProductModel_LockByCodeTx_NotFound(t *testing.T) {
  663. ctx := context.Background()
  664. m := newTestModel(t)
  665. err := m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  666. _, e := m.LockByCodeTx(c, session, "definitely_no_such_code_"+testutil.UniqueId())
  667. return e
  668. })
  669. require.Error(t, err)
  670. assert.True(t, errors.Is(err, sqlx.ErrNotFound),
  671. "LockByCodeTx 对不存在的 code 必须返回 ErrNotFound,便于上层 fail-close 返回 401/404")
  672. }
  673. // TC-0811: FOR UPDATE 行锁真实生效 —— 两个事务同时尝试锁同一行时,
  674. // 后进者必须被阻塞直到先进者结束事务。
  675. // 测量方式:goroutine A 在 tx 内 Lock 住后 sleep 500ms 再 commit;
  676. //
  677. // goroutine B 等 100ms 后也尝试 Lock 同一行,记录耗时。B 的耗时必须≥400ms。
  678. func TestSysProductModel_LockByCodeTx_BlocksConcurrentWriter(t *testing.T) {
  679. ctx := context.Background()
  680. conn := testutil.GetTestSqlConn()
  681. m := newTestModel(t)
  682. p := newSysProduct()
  683. res, err := m.Insert(ctx, p)
  684. require.NoError(t, err)
  685. id, _ := res.LastInsertId()
  686. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
  687. var (
  688. wg sync.WaitGroup
  689. aHoldMs = int64(500)
  690. bStartDelayMs = int64(100)
  691. bElapsedNanos int64
  692. aFinishedNanos int64
  693. aErr, bErr error
  694. )
  695. wg.Add(2)
  696. go func() {
  697. defer wg.Done()
  698. aErr = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  699. if _, e := m.LockByCodeTx(c, session, p.Code); e != nil {
  700. return e
  701. }
  702. // A 拿到锁后故意延时,模拟一段业务处理期。期间 B 必须被阻塞。
  703. time.Sleep(time.Duration(aHoldMs) * time.Millisecond)
  704. atomic.StoreInt64(&aFinishedNanos, time.Now().UnixNano())
  705. return nil
  706. })
  707. }()
  708. go func() {
  709. defer wg.Done()
  710. time.Sleep(time.Duration(bStartDelayMs) * time.Millisecond)
  711. start := time.Now()
  712. bErr = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  713. _, e := m.LockByCodeTx(c, session, p.Code)
  714. return e
  715. })
  716. atomic.StoreInt64(&bElapsedNanos, time.Since(start).Nanoseconds())
  717. }()
  718. wg.Wait()
  719. require.NoError(t, aErr)
  720. require.NoError(t, bErr)
  721. // B 的耗时 ≥ A 的剩余持锁时间。A 持 500ms,B 延 100ms 后入场,
  722. // 因此 B 被阻塞的时间至少 (500-100)=400ms。给 DB 一点抖动放到 300ms。
  723. elapsedMs := atomic.LoadInt64(&bElapsedNanos) / int64(time.Millisecond)
  724. minBlockedMs := int64(300)
  725. assert.GreaterOrEqualf(t, elapsedMs, minBlockedMs, fmt.Sprintf(
  726. "B 的 LockByCodeTx 总耗时 %dms 明显低于预期最小阻塞 %dms —— "+
  727. "意味着 FOR UPDATE 行锁失效,声称的'按 product 串行化'不成立",
  728. elapsedMs, minBlockedMs))
  729. }
  730. // ---------------------------------------------------------------------------
  731. // L-R15-3 / L-R12-1:UpdateWithOptLockTx + InvalidateProductCache
  732. //
  733. // UpdateWithOptLockTx 语义契约:
  734. // - WHERE id=? AND updateTime=? 复现 UpdateWithOptLock 的 CAS;
  735. // - 必须走调用方 session,不得自身失效 sqlc 低层缓存(交给 post-commit 的 InvalidateProductCache);
  736. // - affected=0 → ErrUpdateConflict。
  737. //
  738. // InvalidateProductCache 语义契约:
  739. // - 必须一次失效 sysProduct 的 id / appKey / code 三把低层缓存 key;
  740. // - 不能依赖 session,只在 post-commit 调用。
  741. // ---------------------------------------------------------------------------
  742. // TC-1151: UpdateWithOptLockTx 正常路径——CAS 命中时 UPDATE 成功。
  743. func TestSysProductModel_UpdateWithOptLockTx_HappyPath(t *testing.T) {
  744. ctx := context.Background()
  745. conn := testutil.GetTestSqlConn()
  746. m := newTestModel(t)
  747. p := newSysProduct()
  748. res, err := m.Insert(ctx, p)
  749. require.NoError(t, err)
  750. id, _ := res.LastInsertId()
  751. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
  752. orig, err := m.FindOne(ctx, id)
  753. require.NoError(t, err)
  754. newData := *orig
  755. newData.Name = "updated_name"
  756. newData.Remark = "updated_remark"
  757. newData.Status = 2
  758. newData.UpdateTime = orig.UpdateTime + 1
  759. require.NoError(t,
  760. m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  761. return m.UpdateWithOptLockTx(c, session, &newData, orig.UpdateTime)
  762. }))
  763. // 直读 DB 验证(绕过缓存——UpdateWithOptLockTx 不该动缓存,故 FindOne 可能仍看到旧值;
  764. // 这里是对"DB 确实被改写"的第一类证据)
  765. var dbName, dbRemark string
  766. var dbStatus int64
  767. require.NoError(t,
  768. conn.QueryRowCtx(ctx, &dbName,
  769. "SELECT `name` FROM `sys_product` WHERE `id` = ?", id))
  770. require.NoError(t,
  771. conn.QueryRowCtx(ctx, &dbRemark,
  772. "SELECT `remark` FROM `sys_product` WHERE `id` = ?", id))
  773. require.NoError(t,
  774. conn.QueryRowCtx(ctx, &dbStatus,
  775. "SELECT `status` FROM `sys_product` WHERE `id` = ?", id))
  776. assert.Equal(t, "updated_name", dbName)
  777. assert.Equal(t, "updated_remark", dbRemark)
  778. assert.Equal(t, int64(2), dbStatus)
  779. }
  780. // TC-1152: UpdateWithOptLockTx expectedUpdateTime 不匹配 → ErrUpdateConflict,DB 不变。
  781. func TestSysProductModel_UpdateWithOptLockTx_StaleExpectedUpdateTime_Conflict(t *testing.T) {
  782. ctx := context.Background()
  783. conn := testutil.GetTestSqlConn()
  784. m := newTestModel(t)
  785. p := newSysProduct()
  786. res, err := m.Insert(ctx, p)
  787. require.NoError(t, err)
  788. id, _ := res.LastInsertId()
  789. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
  790. orig, err := m.FindOne(ctx, id)
  791. require.NoError(t, err)
  792. staleUpdateTime := orig.UpdateTime - 100
  793. newData := *orig
  794. newData.Name = "should_not_land"
  795. newData.UpdateTime = orig.UpdateTime + 1
  796. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  797. return m.UpdateWithOptLockTx(c, session, &newData, staleUpdateTime)
  798. })
  799. require.ErrorIs(t, err, ErrUpdateConflict,
  800. "expectedUpdateTime 与 DB 当前值不一致 → ErrUpdateConflict")
  801. // DB 名称保持原值,证明 CAS 失败时 UPDATE 未落盘
  802. var dbName string
  803. require.NoError(t,
  804. conn.QueryRowCtx(ctx, &dbName,
  805. "SELECT `name` FROM `sys_product` WHERE `id` = ?", id))
  806. assert.Equal(t, p.Name, dbName,
  807. "CAS 未命中时 DB 必须保持原值,不得部分落盘")
  808. }
  809. // TC-1153: UpdateWithOptLockTx nil session → error(与 IncrementTokenVersionWithTx 同家族契约)。
  810. func TestSysProductModel_UpdateWithOptLockTx_NilSession_ReturnsError(t *testing.T) {
  811. m := newTestModel(t)
  812. err := m.UpdateWithOptLockTx(context.Background(), nil, &SysProduct{Id: 1}, 0)
  813. require.Error(t, err)
  814. assert.Contains(t, err.Error(), "non-nil session")
  815. }
  816. // TC-1154: UpdateWithOptLockTx 事务 rollback 后 DB 不落盘。
  817. func TestSysProductModel_UpdateWithOptLockTx_Rollback_NoPersistence(t *testing.T) {
  818. ctx := context.Background()
  819. conn := testutil.GetTestSqlConn()
  820. m := newTestModel(t)
  821. p := newSysProduct()
  822. res, err := m.Insert(ctx, p)
  823. require.NoError(t, err)
  824. id, _ := res.LastInsertId()
  825. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
  826. orig, err := m.FindOne(ctx, id)
  827. require.NoError(t, err)
  828. newData := *orig
  829. newData.Name = "rolled_back_name"
  830. newData.UpdateTime = orig.UpdateTime + 1
  831. err = m.TransactCtx(ctx, func(c context.Context, session sqlx.Session) error {
  832. if e := m.UpdateWithOptLockTx(c, session, &newData, orig.UpdateTime); e != nil {
  833. return e
  834. }
  835. return errors.New("force rollback")
  836. })
  837. require.Error(t, err)
  838. var dbName string
  839. require.NoError(t,
  840. conn.QueryRowCtx(ctx, &dbName,
  841. "SELECT `name` FROM `sys_product` WHERE `id` = ?", id))
  842. assert.Equal(t, p.Name, dbName,
  843. "rollback 后 DB 必须保持原值——证明 UpdateWithOptLockTx 确实走 session 而非独立连接")
  844. }
  845. // TC-1155: InvalidateProductCache 必须一次失效 id / appKey / code 三把 key。
  846. // 对应 UpdateProduct 禁用后"sysProduct 低层缓存不准"风险。
  847. func TestSysProductModel_InvalidateProductCache_DelsAllThreeKeys(t *testing.T) {
  848. ctx := context.Background()
  849. conn := testutil.GetTestSqlConn()
  850. m := newTestModel(t)
  851. rds := redis.MustNewRedis(testutil.GetTestConfig().CacheRedis.Nodes[0].RedisConf)
  852. p := newSysProduct()
  853. res, err := m.Insert(ctx, p)
  854. require.NoError(t, err)
  855. id, _ := res.LastInsertId()
  856. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", id) })
  857. // 预热三把 key
  858. _, err = m.FindOne(ctx, id)
  859. require.NoError(t, err)
  860. _, err = m.FindOneByAppKey(ctx, p.AppKey)
  861. require.NoError(t, err)
  862. _, err = m.FindOneByCode(ctx, p.Code)
  863. require.NoError(t, err)
  864. prefix := testutil.GetTestCachePrefix()
  865. keyId := fmt.Sprintf("%s:cache:sysProduct:id:%d", prefix, id)
  866. keyAppKey := fmt.Sprintf("%s:cache:sysProduct:appKey:%s", prefix, p.AppKey)
  867. keyCode := fmt.Sprintf("%s:cache:sysProduct:code:%s", prefix, p.Code)
  868. for _, k := range []string{keyId, keyAppKey, keyCode} {
  869. v, _ := rds.Get(k)
  870. require.NotEmpty(t, v, "预置:%s 已写入缓存", k)
  871. }
  872. m.InvalidateProductCache(ctx, id, p.AppKey, p.Code)
  873. for _, k := range []string{keyId, keyAppKey, keyCode} {
  874. v, _ := rds.Get(k)
  875. assert.Empty(t, v,
  876. "InvalidateProductCache 必须失效低层缓存 key %s", k)
  877. }
  878. }
  879. // TC-1156: InvalidateProductCache 在 ctx 已取消/超时下必须不 panic、不阻塞(best-effort 契约)。
  880. // 与 InvalidateProfileCache 的 L-R13-5 方案 B 完全同构,理由相同:事务已 commit,
  881. // 缓存清理失败必须吞错 + audit tag 日志,绝不能向上抛。
  882. func TestSysProductModel_InvalidateProductCache_CanceledCtxDoesNotPanicOrBlock(t *testing.T) {
  883. conn := testutil.GetTestSqlConn()
  884. m := newTestModel(t)
  885. p := newSysProduct()
  886. res, err := m.Insert(context.Background(), p)
  887. require.NoError(t, err)
  888. id, _ := res.LastInsertId()
  889. t.Cleanup(func() { testutil.CleanTable(context.Background(), conn, "`sys_product`", id) })
  890. cases := []struct {
  891. name string
  892. makeCtx func() (context.Context, context.CancelFunc)
  893. }{
  894. {
  895. name: "canceled",
  896. makeCtx: func() (context.Context, context.CancelFunc) {
  897. ctx, cancel := context.WithCancel(context.Background())
  898. cancel()
  899. return ctx, func() {}
  900. },
  901. },
  902. {
  903. name: "deadline_exceeded",
  904. makeCtx: func() (context.Context, context.CancelFunc) {
  905. ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
  906. return ctx, cancel
  907. },
  908. },
  909. }
  910. for _, tc := range cases {
  911. tc := tc
  912. t.Run(tc.name, func(t *testing.T) {
  913. ctx, cancel := tc.makeCtx()
  914. defer cancel()
  915. done := make(chan struct{})
  916. go func() {
  917. defer close(done)
  918. assert.NotPanics(t, func() {
  919. m.InvalidateProductCache(ctx, id, p.AppKey, p.Code)
  920. })
  921. }()
  922. select {
  923. case <-done:
  924. case <-time.After(500 * time.Millisecond):
  925. t.Fatal("InvalidateProductCache 在 canceled ctx 下必须立即返回")
  926. }
  927. })
  928. }
  929. }