syncPermsLogic_test.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. package pub
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. "github.com/zeromicro/go-zero/core/stores/redis"
  9. "github.com/zeromicro/go-zero/core/stores/sqlx"
  10. "go.uber.org/mock/gomock"
  11. "golang.org/x/crypto/bcrypt"
  12. permModel "perms-system-server/internal/model/perm"
  13. productModel "perms-system-server/internal/model/product"
  14. "perms-system-server/internal/response"
  15. "perms-system-server/internal/testutil"
  16. "perms-system-server/internal/testutil/mocks"
  17. "perms-system-server/internal/types"
  18. "testing"
  19. "time"
  20. )
  21. func insertSyncTestProduct(t *testing.T, ctx context.Context, code, appKey, appSecret string, status int64) (int64, func()) {
  22. t.Helper()
  23. svcCtx := newTestSvcCtx()
  24. conn := testutil.GetTestSqlConn()
  25. now := time.Now().Unix()
  26. hashedSecret, err := bcrypt.GenerateFromPassword([]byte(appSecret), bcrypt.MinCost)
  27. require.NoError(t, err)
  28. res, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  29. Code: code,
  30. Name: code,
  31. AppKey: appKey,
  32. AppSecret: string(hashedSecret),
  33. Status: status,
  34. CreateTime: now,
  35. UpdateTime: now,
  36. })
  37. require.NoError(t, err)
  38. id, _ := res.LastInsertId()
  39. cleanup := func() {
  40. testutil.CleanTable(ctx, conn, "`sys_product`", id)
  41. }
  42. return id, cleanup
  43. }
  44. // TC-0036: 全部新增
  45. func TestSyncPerms_AllNew(t *testing.T) {
  46. ctx := context.Background()
  47. svcCtx := newTestSvcCtx()
  48. conn := testutil.GetTestSqlConn()
  49. pc := testutil.UniqueId()
  50. appKey := testutil.UniqueId()
  51. appSecret := testutil.UniqueId()
  52. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  53. t.Cleanup(cleanProduct)
  54. t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
  55. logic := NewSyncPermsLogic(ctx, svcCtx)
  56. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  57. AppKey: appKey,
  58. AppSecret: appSecret,
  59. Perms: []types.SyncPermItem{
  60. {Code: "perm_a", Name: "Perm A", Remark: "remark a"},
  61. {Code: "perm_b", Name: "Perm B"},
  62. {Code: "perm_c", Name: "Perm C"},
  63. },
  64. })
  65. require.NoError(t, err)
  66. require.NotNil(t, resp)
  67. assert.Equal(t, int64(3), resp.Added)
  68. assert.Equal(t, int64(0), resp.Updated)
  69. assert.Equal(t, int64(0), resp.Disabled)
  70. }
  71. // TC-0037: 更新已有(名称变更)
  72. func TestSyncPerms_UpdateExisting(t *testing.T) {
  73. ctx := context.Background()
  74. svcCtx := newTestSvcCtx()
  75. conn := testutil.GetTestSqlConn()
  76. pc := testutil.UniqueId()
  77. appKey := testutil.UniqueId()
  78. appSecret := testutil.UniqueId()
  79. now := time.Now().Unix()
  80. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  81. t.Cleanup(cleanProduct)
  82. permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  83. ProductCode: pc, Name: "Old Name", Code: "upd_code", Remark: "old remark", Status: 1, CreateTime: now, UpdateTime: now,
  84. })
  85. require.NoError(t, err)
  86. permId, _ := permRes.LastInsertId()
  87. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) })
  88. logic := NewSyncPermsLogic(ctx, svcCtx)
  89. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  90. AppKey: appKey,
  91. AppSecret: appSecret,
  92. Perms: []types.SyncPermItem{
  93. {Code: "upd_code", Name: "New Name", Remark: "new remark"},
  94. },
  95. })
  96. require.NoError(t, err)
  97. require.NotNil(t, resp)
  98. assert.Equal(t, int64(0), resp.Added)
  99. assert.Equal(t, int64(1), resp.Updated)
  100. assert.Equal(t, int64(0), resp.Disabled)
  101. updated, err := svcCtx.SysPermModel.FindOne(ctx, permId)
  102. require.NoError(t, err)
  103. assert.Equal(t, "New Name", updated.Name)
  104. assert.Equal(t, "new remark", updated.Remark)
  105. assert.Equal(t, int64(1), updated.Status)
  106. }
  107. // TC-0038: 无变化
  108. func TestSyncPerms_NoChanges(t *testing.T) {
  109. ctx := context.Background()
  110. svcCtx := newTestSvcCtx()
  111. conn := testutil.GetTestSqlConn()
  112. pc := testutil.UniqueId()
  113. appKey := testutil.UniqueId()
  114. appSecret := testutil.UniqueId()
  115. now := time.Now().Unix()
  116. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  117. t.Cleanup(cleanProduct)
  118. permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  119. ProductCode: pc, Name: "Same Name", Code: "same_code", Remark: "same remark", Status: 1, CreateTime: now, UpdateTime: now,
  120. })
  121. require.NoError(t, err)
  122. permId, _ := permRes.LastInsertId()
  123. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) })
  124. logic := NewSyncPermsLogic(ctx, svcCtx)
  125. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  126. AppKey: appKey,
  127. AppSecret: appSecret,
  128. Perms: []types.SyncPermItem{
  129. {Code: "same_code", Name: "Same Name", Remark: "same remark"},
  130. },
  131. })
  132. require.NoError(t, err)
  133. require.NotNil(t, resp)
  134. assert.Equal(t, int64(0), resp.Added)
  135. assert.Equal(t, int64(0), resp.Updated)
  136. assert.Equal(t, int64(0), resp.Disabled)
  137. }
  138. // TC-0039: 禁用权限重启
  139. func TestSyncPerms_ReEnableDisabled(t *testing.T) {
  140. ctx := context.Background()
  141. svcCtx := newTestSvcCtx()
  142. conn := testutil.GetTestSqlConn()
  143. pc := testutil.UniqueId()
  144. appKey := testutil.UniqueId()
  145. appSecret := testutil.UniqueId()
  146. now := time.Now().Unix()
  147. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  148. t.Cleanup(cleanProduct)
  149. permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  150. ProductCode: pc, Name: "Disabled Perm", Code: "dis_code", Remark: "", Status: 2, CreateTime: now, UpdateTime: now,
  151. })
  152. require.NoError(t, err)
  153. permId, _ := permRes.LastInsertId()
  154. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) })
  155. logic := NewSyncPermsLogic(ctx, svcCtx)
  156. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  157. AppKey: appKey,
  158. AppSecret: appSecret,
  159. Perms: []types.SyncPermItem{
  160. {Code: "dis_code", Name: "Disabled Perm"},
  161. },
  162. })
  163. require.NoError(t, err)
  164. require.NotNil(t, resp)
  165. assert.Equal(t, int64(0), resp.Added)
  166. assert.Equal(t, int64(1), resp.Updated)
  167. reEnabled, err := svcCtx.SysPermModel.FindOne(ctx, permId)
  168. require.NoError(t, err)
  169. assert.Equal(t, int64(1), reEnabled.Status)
  170. }
  171. // TC-0040: 移除不在列表的权限
  172. func TestSyncPerms_DisableNotInList(t *testing.T) {
  173. ctx := context.Background()
  174. svcCtx := newTestSvcCtx()
  175. conn := testutil.GetTestSqlConn()
  176. pc := testutil.UniqueId()
  177. appKey := testutil.UniqueId()
  178. appSecret := testutil.UniqueId()
  179. now := time.Now().Unix()
  180. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  181. t.Cleanup(cleanProduct)
  182. keepCode := testutil.UniqueId()
  183. removeCode := testutil.UniqueId()
  184. keepRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  185. ProductCode: pc, Name: "Keep", Code: keepCode, Status: 1, CreateTime: now, UpdateTime: now,
  186. })
  187. require.NoError(t, err)
  188. keepId, _ := keepRes.LastInsertId()
  189. removeRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  190. ProductCode: pc, Name: "Remove", Code: removeCode, Status: 1, CreateTime: now, UpdateTime: now,
  191. })
  192. require.NoError(t, err)
  193. removeId, _ := removeRes.LastInsertId()
  194. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", keepId, removeId) })
  195. logic := NewSyncPermsLogic(ctx, svcCtx)
  196. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  197. AppKey: appKey,
  198. AppSecret: appSecret,
  199. Perms: []types.SyncPermItem{
  200. {Code: keepCode, Name: "Keep"},
  201. },
  202. })
  203. require.NoError(t, err)
  204. require.NotNil(t, resp)
  205. assert.Equal(t, int64(0), resp.Added)
  206. assert.Equal(t, int64(1), resp.Disabled)
  207. disabled, err := svcCtx.SysPermModel.FindOne(ctx, removeId)
  208. require.NoError(t, err)
  209. assert.Equal(t, int64(2), disabled.Status)
  210. kept, err := svcCtx.SysPermModel.FindOne(ctx, keepId)
  211. require.NoError(t, err)
  212. assert.Equal(t, int64(1), kept.Status)
  213. }
  214. // TC-0041: 空perms数组应被拒绝
  215. func TestSyncPerms_EmptyPermsRejected(t *testing.T) {
  216. ctx := context.Background()
  217. svcCtx := newTestSvcCtx()
  218. pc := testutil.UniqueId()
  219. appKey := testutil.UniqueId()
  220. appSecret := testutil.UniqueId()
  221. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  222. t.Cleanup(cleanProduct)
  223. logic := NewSyncPermsLogic(ctx, svcCtx)
  224. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  225. AppKey: appKey,
  226. AppSecret: appSecret,
  227. Perms: []types.SyncPermItem{},
  228. })
  229. require.Nil(t, resp)
  230. require.Error(t, err)
  231. var codeErr *response.CodeError
  232. require.True(t, errors.As(err, &codeErr))
  233. assert.Equal(t, 400, codeErr.Code())
  234. assert.Contains(t, codeErr.Error(), "权限列表不能为空")
  235. }
  236. // TC-0043: appKey无效
  237. func TestSyncPerms_InvalidAppKey(t *testing.T) {
  238. ctx := context.Background()
  239. svcCtx := newTestSvcCtx()
  240. logic := NewSyncPermsLogic(ctx, svcCtx)
  241. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  242. AppKey: "nonexistent_key_" + testutil.UniqueId(),
  243. AppSecret: "whatever",
  244. Perms: []types.SyncPermItem{{Code: "x", Name: "x"}},
  245. })
  246. require.Nil(t, resp)
  247. require.Error(t, err)
  248. var codeErr *response.CodeError
  249. require.True(t, errors.As(err, &codeErr))
  250. assert.Equal(t, 401, codeErr.Code())
  251. assert.Equal(t, "无效的appKey", codeErr.Error())
  252. }
  253. // TC-0044: appSecret错误
  254. func TestSyncPerms_WrongAppSecret(t *testing.T) {
  255. ctx := context.Background()
  256. svcCtx := newTestSvcCtx()
  257. pc := testutil.UniqueId()
  258. appKey := testutil.UniqueId()
  259. appSecret := testutil.UniqueId()
  260. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  261. t.Cleanup(cleanProduct)
  262. logic := NewSyncPermsLogic(ctx, svcCtx)
  263. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  264. AppKey: appKey,
  265. AppSecret: "wrong_secret",
  266. Perms: []types.SyncPermItem{{Code: "x", Name: "x"}},
  267. })
  268. require.Nil(t, resp)
  269. require.Error(t, err)
  270. var codeErr *response.CodeError
  271. require.True(t, errors.As(err, &codeErr))
  272. assert.Equal(t, 401, codeErr.Code())
  273. assert.Equal(t, "appSecret验证失败", codeErr.Error())
  274. }
  275. // TC-0045: 产品已禁用
  276. func TestSyncPerms_ProductDisabled(t *testing.T) {
  277. ctx := context.Background()
  278. svcCtx := newTestSvcCtx()
  279. pc := testutil.UniqueId()
  280. appKey := testutil.UniqueId()
  281. appSecret := testutil.UniqueId()
  282. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 2)
  283. t.Cleanup(cleanProduct)
  284. logic := NewSyncPermsLogic(ctx, svcCtx)
  285. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  286. AppKey: appKey,
  287. AppSecret: appSecret,
  288. Perms: []types.SyncPermItem{{Code: "x", Name: "x"}},
  289. })
  290. require.Nil(t, resp)
  291. require.Error(t, err)
  292. var codeErr *response.CodeError
  293. require.True(t, errors.As(err, &codeErr))
  294. assert.Equal(t, 403, codeErr.Code())
  295. assert.Equal(t, "产品已被禁用", codeErr.Error())
  296. }
  297. // TC-0046: 大批量(1000条)
  298. func TestSyncPerms_LargeBatch1000(t *testing.T) {
  299. ctx := context.Background()
  300. svcCtx := newTestSvcCtx()
  301. conn := testutil.GetTestSqlConn()
  302. pc := testutil.UniqueId()
  303. appKey := testutil.UniqueId()
  304. appSecret := testutil.UniqueId()
  305. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  306. t.Cleanup(cleanProduct)
  307. t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
  308. perms := make([]types.SyncPermItem, 1000)
  309. for i := 0; i < 1000; i++ {
  310. perms[i] = types.SyncPermItem{
  311. Code: fmt.Sprintf("batch_%s_%d", pc, i),
  312. Name: fmt.Sprintf("Perm_%d", i),
  313. }
  314. }
  315. logic := NewSyncPermsLogic(ctx, svcCtx)
  316. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  317. AppKey: appKey,
  318. AppSecret: appSecret,
  319. Perms: perms,
  320. })
  321. require.NoError(t, err)
  322. require.NotNil(t, resp)
  323. assert.Equal(t, int64(1000), resp.Added)
  324. assert.Equal(t, int64(0), resp.Updated)
  325. assert.Equal(t, int64(0), resp.Disabled)
  326. }
  327. // TC-0047: 重复code去重
  328. func TestSyncPerms_DeduplicateCodes(t *testing.T) {
  329. ctx := context.Background()
  330. svcCtx := newTestSvcCtx()
  331. conn := testutil.GetTestSqlConn()
  332. pc := testutil.UniqueId()
  333. appKey := testutil.UniqueId()
  334. appSecret := testutil.UniqueId()
  335. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  336. t.Cleanup(cleanProduct)
  337. t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
  338. logic := NewSyncPermsLogic(ctx, svcCtx)
  339. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  340. AppKey: appKey,
  341. AppSecret: appSecret,
  342. Perms: []types.SyncPermItem{
  343. {Code: "dup_code", Name: "Perm First"},
  344. {Code: "dup_code", Name: "Perm Duplicate"},
  345. {Code: "unique_code", Name: "Unique"},
  346. },
  347. })
  348. require.NoError(t, err)
  349. require.NotNil(t, resp)
  350. assert.Equal(t, int64(2), resp.Added, "重复code应被去重,只添加2条")
  351. }
  352. // TC-0042: 验证disabled返回值
  353. func TestSyncPerms_VerifyDisabledCount(t *testing.T) {
  354. ctx := context.Background()
  355. svcCtx := newTestSvcCtx()
  356. conn := testutil.GetTestSqlConn()
  357. pc := testutil.UniqueId()
  358. appKey := testutil.UniqueId()
  359. appSecret := testutil.UniqueId()
  360. now := time.Now().Unix()
  361. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  362. t.Cleanup(cleanProduct)
  363. var permIds []int64
  364. for i := 0; i < 5; i++ {
  365. res, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  366. ProductCode: pc, Name: fmt.Sprintf("p%d", i), Code: fmt.Sprintf("code_%s_%d", pc, i),
  367. Status: 1, CreateTime: now, UpdateTime: now,
  368. })
  369. require.NoError(t, err)
  370. id, _ := res.LastInsertId()
  371. permIds = append(permIds, id)
  372. }
  373. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permIds...) })
  374. logic := NewSyncPermsLogic(ctx, svcCtx)
  375. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  376. AppKey: appKey,
  377. AppSecret: appSecret,
  378. Perms: []types.SyncPermItem{
  379. {Code: fmt.Sprintf("code_%s_0", pc), Name: "p0"},
  380. {Code: fmt.Sprintf("code_%s_1", pc), Name: "p1"},
  381. },
  382. })
  383. require.NoError(t, err)
  384. require.NotNil(t, resp)
  385. assert.Equal(t, int64(0), resp.Added)
  386. assert.Equal(t, int64(3), resp.Disabled)
  387. }
  388. func TestSyncPerms_LockByCodeTxNotFound_MapsToHTTP404(t *testing.T) {
  389. ctrl := gomock.NewController(t)
  390. defer ctrl.Finish()
  391. hashedSecret, err := bcrypt.GenerateFromPassword([]byte("m2_secret"), bcrypt.MinCost)
  392. require.NoError(t, err)
  393. mockProduct := mocks.NewMockSysProductModel(ctrl)
  394. mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "m2_key").
  395. Return(&productModel.SysProduct{
  396. Id: 1, Code: "m2_prod", AppKey: "m2_key",
  397. AppSecret: string(hashedSecret), Status: 1,
  398. }, nil)
  399. // 关键:tx 内 LockByCodeTx 拿到 ErrNotFound → service 返回 SyncPermsError{Code:404, "产品不存在"}。
  400. mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "m2_prod").
  401. Return((*productModel.SysProduct)(nil), sqlx.ErrNotFound)
  402. mockPerm := mocks.NewMockSysPermModel(ctrl)
  403. mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  404. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  405. return fn(ctx, nil)
  406. })
  407. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: mockProduct, Perm: mockPerm})
  408. logic := NewSyncPermsLogic(context.Background(), svcCtx)
  409. resp, err := logic.SyncPerms(&types.SyncPermsReq{
  410. AppKey: "m2_key", AppSecret: "m2_secret",
  411. Perms: []types.SyncPermItem{{Code: "p1", Name: "P1"}},
  412. })
  413. assert.Nil(t, resp)
  414. require.Error(t, err, "tx 内产品消失必须返回错误")
  415. var ce *response.CodeError
  416. require.ErrorAs(t, err, &ce,
  417. "必须映射成 response.CodeError 结构化错误,不能透传 SyncPermsError 原文")
  418. assert.Equal(t, 404, ce.Code(),
  419. "SyncPermsError{Code:404} 必须落到 HTTP 404 分支;若仍是 500 说明 syncPermsLogic 的 switch 缺少 404 case")
  420. assert.Equal(t, "产品不存在", ce.Error(), "保留原始语义文案")
  421. }
  422. // TC-0980(负值域对称):未映射的 se.Code(例如 500)依旧走 default,原样透传,不得被误收进 404。
  423. // 防御未来有人想"把所有 SyncPermsError 都按 404 处理"的随手改动。
  424. func TestSyncPerms_UnmappedSyncPermsErrCode_StillFallsThroughDefault(t *testing.T) {
  425. ctrl := gomock.NewController(t)
  426. defer ctrl.Finish()
  427. hashedSecret, err := bcrypt.GenerateFromPassword([]byte("m2_secret"), bcrypt.MinCost)
  428. require.NoError(t, err)
  429. mockProduct := mocks.NewMockSysProductModel(ctrl)
  430. mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "m2_key2").
  431. Return(&productModel.SysProduct{
  432. Id: 1, Code: "m2_prod2", AppKey: "m2_key2",
  433. AppSecret: string(hashedSecret), Status: 1,
  434. }, nil)
  435. // LockByCodeTx 拿到的行必须 Status=1 才能继续进入 diff 逻辑
  436. mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "m2_prod2").
  437. Return(&productModel.SysProduct{Id: 1, Code: "m2_prod2", Status: 1}, nil)
  438. mockPerm := mocks.NewMockSysPermModel(ctrl)
  439. mockPerm.EXPECT().FindMapByProductCodeWithTx(gomock.Any(), gomock.Any(), "m2_prod2").
  440. Return(nil, assertAnyErr("internal storage bug"))
  441. mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  442. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  443. return fn(ctx, nil)
  444. })
  445. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: mockProduct, Perm: mockPerm})
  446. logic := NewSyncPermsLogic(context.Background(), svcCtx)
  447. _, err = logic.SyncPerms(&types.SyncPermsReq{
  448. AppKey: "m2_key2", AppSecret: "m2_secret",
  449. Perms: []types.SyncPermItem{{Code: "p1", Name: "P1"}},
  450. })
  451. require.Error(t, err)
  452. var se *SyncPermsError
  453. require.ErrorAs(t, err, &se, "未映射 code 走 default,原 SyncPermsError 被原样透传")
  454. assert.Equal(t, 500, se.Code, "500 必须保持 500 原语义,不得被误归类为 404")
  455. var ce *response.CodeError
  456. assert.False(t, assert.ObjectsAreEqual(err, ce),
  457. "500 分支绝不能被映射成 response.CodeError{Code:404}")
  458. }
  459. // assertAnyErr 构造任意错误,用来模拟 tx 内非业务分支错误。
  460. func assertAnyErr(msg string) error {
  461. return &localErr{s: msg}
  462. }
  463. type localErr struct{ s string }
  464. func (e *localErr) Error() string { return e.s }
  465. func primeProductIndex(t *testing.T, rds *redis.Redis, cachePrefix, productCode string) string {
  466. t.Helper()
  467. idxKey := fmt.Sprintf("%s:ud:idx:p:%s", cachePrefix, productCode)
  468. canary := fmt.Sprintf("%s:ud:probe:%s", cachePrefix, testutil.UniqueId())
  469. _, err := rds.Sadd(idxKey, canary)
  470. require.NoError(t, err, "SAdd 到 productIndexKey 失败,Redis 不可用,测试前置条件失败")
  471. members, err := rds.Smembers(idxKey)
  472. require.NoError(t, err)
  473. require.Contains(t, members, canary, "primeProductIndex: canary 必须先出现在集合里")
  474. return idxKey
  475. }
  476. // TC-1064: 纯新增不触发 CleanByProduct
  477. func TestSyncPerms_PureAddDoesNotTriggerCleanByProduct(t *testing.T) {
  478. ctx := context.Background()
  479. svcCtx := newTestSvcCtx()
  480. cfg := testutil.GetTestConfig()
  481. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  482. conn := testutil.GetTestSqlConn()
  483. pc := testutil.UniqueId()
  484. appKey := testutil.UniqueId()
  485. appSecret := testutil.UniqueId()
  486. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  487. t.Cleanup(cleanProduct)
  488. t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
  489. idxKey := primeProductIndex(t, rds, cfg.CacheRedis.KeyPrefix, pc)
  490. t.Cleanup(func() { _, _ = rds.Del(idxKey) })
  491. result, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{
  492. {Code: "r11_4_add_a", Name: "A"},
  493. {Code: "r11_4_add_b", Name: "B"},
  494. {Code: "r11_4_add_c", Name: "C"},
  495. })
  496. require.NoError(t, err)
  497. require.NotNil(t, result)
  498. assert.Equal(t, int64(3), result.Added)
  499. assert.Equal(t, int64(0), result.Updated)
  500. assert.Equal(t, int64(0), result.Disabled)
  501. // 纯新增路径:CleanByProduct 不得被调用,索引集合必须仍保留 canary。
  502. exists, err := rds.Exists(idxKey)
  503. require.NoError(t, err)
  504. assert.True(t, exists,
  505. "added=3 / updated=0 / disabled=0 属于纯新增,不得触发 CleanByProduct;"+
  506. "productIndexKey 若被删除说明 SyncPerms 仍在走全产品清缓存路径,回归:会把该产品"+
  507. "所有在线用户下一次请求同时打穿回 DB")
  508. }
  509. // TC-1065: updated > 0 时必须触发 CleanByProduct
  510. func TestSyncPerms_UpdateTriggersCleanByProduct(t *testing.T) {
  511. ctx := context.Background()
  512. svcCtx := newTestSvcCtx()
  513. cfg := testutil.GetTestConfig()
  514. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  515. conn := testutil.GetTestSqlConn()
  516. pc := testutil.UniqueId()
  517. appKey := testutil.UniqueId()
  518. appSecret := testutil.UniqueId()
  519. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  520. t.Cleanup(cleanProduct)
  521. t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
  522. // 第一次同步:纯新增,为随后的 update 打底;这次不触发 Clean,CleanByProduct 仍然可被后续触发。
  523. _, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{
  524. {Code: "r11_4_upd", Name: "OldName"},
  525. })
  526. require.NoError(t, err)
  527. idxKey := primeProductIndex(t, rds, cfg.CacheRedis.KeyPrefix, pc)
  528. t.Cleanup(func() { _, _ = rds.Del(idxKey) })
  529. // 第二次同步:同一 Code 改 Name → updated=1。
  530. result, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{
  531. {Code: "r11_4_upd", Name: "NewName"},
  532. })
  533. require.NoError(t, err)
  534. require.NotNil(t, result)
  535. assert.Equal(t, int64(1), result.Updated,
  536. "前置:同名 Code 改 Name 必须 updated=1,否则后续断言失去意义")
  537. exists, err := rds.Exists(idxKey)
  538. require.NoError(t, err)
  539. assert.False(t, exists,
  540. "updated>0 必须触发 CleanByProduct;若 canary 仍在,说明 Logic 把"+
  541. "updated 情况也误归入'纯新增'分支,已存在 UD 缓存中的旧 perms 将长期对外返回")
  542. }
  543. // TC-1066: disabled > 0 时必须触发 CleanByProduct
  544. func TestSyncPerms_DisableTriggersCleanByProduct(t *testing.T) {
  545. ctx := context.Background()
  546. svcCtx := newTestSvcCtx()
  547. cfg := testutil.GetTestConfig()
  548. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  549. conn := testutil.GetTestSqlConn()
  550. pc := testutil.UniqueId()
  551. appKey := testutil.UniqueId()
  552. appSecret := testutil.UniqueId()
  553. _, cleanProduct := insertSyncTestProduct(t, ctx, pc, appKey, appSecret, 1)
  554. t.Cleanup(cleanProduct)
  555. t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_perm`", "productCode", pc) })
  556. // 先注入两个 perm。
  557. _, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{
  558. {Code: "r11_4_keep", Name: "K"},
  559. {Code: "r11_4_drop", Name: "D"},
  560. })
  561. require.NoError(t, err)
  562. idxKey := primeProductIndex(t, rds, cfg.CacheRedis.KeyPrefix, pc)
  563. t.Cleanup(func() { _, _ = rds.Del(idxKey) })
  564. // 第二次只同步 r11_4_keep,r11_4_drop 会被 DisableNotInCodesWithTx 置 disabled。
  565. result, err := ExecuteSyncPerms(ctx, svcCtx, appKey, appSecret, []SyncPermItem{
  566. {Code: "r11_4_keep", Name: "K"},
  567. })
  568. require.NoError(t, err)
  569. assert.Equal(t, int64(1), result.Disabled,
  570. "前置:第二次只同步 keep,drop 必须被 disabled=1")
  571. exists, err := rds.Exists(idxKey)
  572. require.NoError(t, err)
  573. assert.False(t, exists,
  574. "disabled>0 必须触发 CleanByProduct,否则已缓存的 UD.perms 里仍挂着"+
  575. "已禁用权限,权限网关会把不再有效的权限判为 allow,产生权限残留")
  576. }
  577. func TestExecuteSyncPerms_DeduplicatesRequest(t *testing.T) {
  578. ctrl := gomock.NewController(t)
  579. t.Cleanup(ctrl.Finish)
  580. hashedSecret, err := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost)
  581. require.NoError(t, err)
  582. mockProduct := mocks.NewMockSysProductModel(ctrl)
  583. mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "ak").
  584. Return(&productModel.SysProduct{
  585. Id: 1, Code: "pc_dedup", AppKey: "ak", AppSecret: string(hashedSecret), Status: 1,
  586. }, nil)
  587. // LockByCodeTx 拿到的行必须 Status=1
  588. mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_dedup").
  589. Return(&productModel.SysProduct{Id: 1, Code: "pc_dedup", Status: 1}, nil)
  590. mockPerm := mocks.NewMockSysPermModel(ctrl)
  591. mockPerm.EXPECT().FindMapByProductCodeWithTx(gomock.Any(), gomock.Any(), "pc_dedup").
  592. Return(map[string]*permModel.SysPerm{}, nil)
  593. var captured []*permModel.SysPerm
  594. mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  595. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  596. return fn(ctx, nil)
  597. })
  598. mockPerm.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).
  599. DoAndReturn(func(ctx context.Context, s sqlx.Session, items []*permModel.SysPerm) error {
  600. captured = items
  601. return nil
  602. })
  603. // 去重后 codes 只剩一个,DisableNotInCodesWithTx 用去重后的集合做 NOT IN。
  604. mockPerm.EXPECT().DisableNotInCodesWithTx(gomock.Any(), nil, "pc_dedup", []string{"dup_code"}, gomock.Any()).
  605. Return(int64(0), nil)
  606. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  607. Product: mockProduct, Perm: mockPerm,
  608. })
  609. result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s", []SyncPermItem{
  610. {Code: "dup_code", Name: "A"},
  611. {Code: "dup_code", Name: "A-again"},
  612. {Code: "dup_code", Name: "A-yet-again"},
  613. })
  614. require.NoError(t, err)
  615. require.NotNil(t, result)
  616. require.Len(t, captured, 1, "入参内 code 重复必须去重为 1 条,避免自撞 1062")
  617. assert.Equal(t, "dup_code", captured[0].Code)
  618. assert.Equal(t, "A", captured[0].Name,
  619. "去重策略应稳定到首次出现,使行为可预测")
  620. }
  621. func newBaseProductMock(ctrl *gomock.Controller, code string) *mocks.MockSysProductModel {
  622. hashed, _ := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost)
  623. m := mocks.NewMockSysProductModel(ctrl)
  624. m.EXPECT().FindOneByAppKey(gomock.Any(), "ak").
  625. Return(&productModel.SysProduct{
  626. Id: 1, Code: code, AppKey: "ak", AppSecret: string(hashed), Status: 1,
  627. }, nil)
  628. return m
  629. }
  630. // TC-0843: 契约 —— 正常路径下 LockByCodeTx 必须先于 FindMapByProductCodeWithTx,
  631. // 且两者均在同一个 tx session 内被调用。
  632. func TestExecuteSyncPerms_LockBeforeMapReadInTx(t *testing.T) {
  633. ctrl := gomock.NewController(t)
  634. t.Cleanup(ctrl.Finish)
  635. productMock := newBaseProductMock(ctrl, "pc_tx_order")
  636. permMock := mocks.NewMockSysPermModel(ctrl)
  637. // 关键点 1:TransactCtx 必须真的传入一个 tx session,并把所有子调用都发生在其中。
  638. permMock.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  639. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  640. return fn(ctx, nil) // nil session 只是 mock 占位
  641. })
  642. // 关键点 2:gomock 的 Call.After 强制 LockByCodeTx 先于 FindMapByProductCodeWithTx 执行。
  643. // 顺序反过来的话 gomock 会在 Finish 时报错。
  644. // 事务内复核 Status 必须 Status=1,否则走 403 分支不写 perm
  645. lockCall := productMock.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_tx_order").
  646. Return(&productModel.SysProduct{Id: 1, Code: "pc_tx_order", Status: 1}, nil)
  647. permMock.EXPECT().FindMapByProductCodeWithTx(gomock.Any(), gomock.Any(), "pc_tx_order").
  648. Return(map[string]*permModel.SysPerm{}, nil).
  649. After(lockCall)
  650. // 一条简单的 INSERT + DisableNotIn 让流程走完;非本 TC 的主断言。
  651. permMock.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).Return(nil)
  652. permMock.EXPECT().DisableNotInCodesWithTx(gomock.Any(), nil, "pc_tx_order", []string{"x"}, gomock.Any()).
  653. Return(int64(0), nil)
  654. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: productMock, Perm: permMock})
  655. result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s",
  656. []SyncPermItem{{Code: "x", Name: "X"}})
  657. require.NoError(t, err)
  658. require.NotNil(t, result)
  659. assert.Equal(t, int64(1), result.Added, "lock 在 tx 内就位后应当能正常写入")
  660. }
  661. // TC-0844: 分支 —— tx 内 LockByCodeTx 返回 sqlx.ErrNotFound(产品在 tx 开启后被删),
  662. // 必须映射为 SyncPermsError{Code:404, Message:"产品不存在"},而非 500。
  663. func TestExecuteSyncPerms_LockNotFound_Maps404(t *testing.T) {
  664. ctrl := gomock.NewController(t)
  665. t.Cleanup(ctrl.Finish)
  666. productMock := newBaseProductMock(ctrl, "pc_tx_gone")
  667. permMock := mocks.NewMockSysPermModel(ctrl)
  668. permMock.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  669. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  670. return fn(ctx, nil)
  671. })
  672. productMock.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_tx_gone").
  673. Return(nil, sqlx.ErrNotFound)
  674. // 关键:锁失败后绝不能继续走 FindMapByProductCodeWithTx / BatchInsertWithTx。
  675. // gomock 默认严格模式会在 Finish 时报 "unexpected call",所以不为这些方法登记任何期望即可。
  676. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: productMock, Perm: permMock})
  677. result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s",
  678. []SyncPermItem{{Code: "x", Name: "X"}})
  679. assert.Nil(t, result)
  680. require.Error(t, err)
  681. var se *SyncPermsError
  682. require.True(t, errors.As(err, &se), "锁不到产品行必须产出 *SyncPermsError")
  683. assert.Equal(t, 404, se.Code,
  684. "tx 开启后 LockByCodeTx=ErrNotFound 意味着产品行在 tx 中不可见,应当返回 404 而非 500")
  685. assert.Contains(t, se.Message, "产品不存在",
  686. "文案应当能让调用方人眼秒懂是什么错误")
  687. }
  688. // TC-0845: 容错 —— tx 内 LockByCodeTx 冒出非 NotFound 的通用错误(driver/conn 异常),
  689. // 必须被事务回滚并被外层包裹为 SyncPermsError(500 级),而非原始 driver 错误直接冒出去。
  690. func TestExecuteSyncPerms_LockGenericError_WrappedAs500(t *testing.T) {
  691. ctrl := gomock.NewController(t)
  692. t.Cleanup(ctrl.Finish)
  693. productMock := newBaseProductMock(ctrl, "pc_tx_boom")
  694. permMock := mocks.NewMockSysPermModel(ctrl)
  695. permMock.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  696. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  697. return fn(ctx, nil)
  698. })
  699. boom := errors.New("driver: connection lost")
  700. productMock.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_tx_boom").
  701. Return(nil, boom)
  702. // 锁失败后同样不应调用后续方法。
  703. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: productMock, Perm: permMock})
  704. result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s",
  705. []SyncPermItem{{Code: "x", Name: "X"}})
  706. assert.Nil(t, result)
  707. require.Error(t, err)
  708. var se *SyncPermsError
  709. require.True(t, errors.As(err, &se), "底层错误必须被包成 *SyncPermsError,防止 driver 错误直接上抛")
  710. assert.Equal(t, 500, se.Code,
  711. "非 NotFound 的 DB 错误应当 fail-close 为 500,让接入方区别于 404/409")
  712. assert.NotContains(t, se.Message, "connection lost",
  713. "对外文案不能泄露原始 driver 错误(避免信息披露)")
  714. }