syncPermsLogic_test.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  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. // TC-1118: M-R14-1 —— ExecuteSyncPerms 的 post-commit CleanByProduct 必须跑在
  578. // DetachCacheCleanCtx 独立 ctx 上。用例口径:
  579. // 1. 用 `parent = WithCancel(bg)` 作为调用方 ctx;
  580. // 2. 先走一次纯新增做打底;
  581. // 3. primeProductIndex 置 canary,随后第二次 sync 改同 code 的 Name → updated=1;
  582. // 4. 在 ExecuteSyncPerms 返回后 **立即** `cancel(parent)`;
  583. // 5. canary 必须已被删除——说明 CleanByProduct 的 Redis DEL 跑在独立 ctx 上、
  584. // 不依赖 parent 存活。若回退为"直接传 parent ctx",cancel 会与 DEL 竞争,
  585. // 实际落到生产时 5min TTL 内的 UD 缓存仍挂着被禁用 / 变更的 perm。
  586. func TestSyncPerms_M_R14_1_CleanByProductDetachedFromRequestCtx(t *testing.T) {
  587. parent, cancel := context.WithCancel(context.Background())
  588. defer cancel()
  589. svcCtx := newTestSvcCtx()
  590. cfg := testutil.GetTestConfig()
  591. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  592. conn := testutil.GetTestSqlConn()
  593. pc := testutil.UniqueId()
  594. appKey := testutil.UniqueId()
  595. appSecret := testutil.UniqueId()
  596. _, cleanProduct := insertSyncTestProduct(t, parent, pc, appKey, appSecret, 1)
  597. t.Cleanup(cleanProduct)
  598. t.Cleanup(func() { testutil.CleanTableByField(parent, conn, "`sys_perm`", "productCode", pc) })
  599. // 第一次:纯新增打底;此次不应触发 Clean(见 TC-1064),所以 canary 放在此之后。
  600. _, err := ExecuteSyncPerms(parent, svcCtx, appKey, appSecret, []SyncPermItem{
  601. {Code: "r14_detach_upd", Name: "OldName"},
  602. })
  603. require.NoError(t, err)
  604. idxKey := primeProductIndex(t, rds, cfg.CacheRedis.KeyPrefix, pc)
  605. t.Cleanup(func() { _, _ = rds.Del(idxKey) })
  606. // 第二次:同 code 改 Name → updated=1 → 必触发 CleanByProduct。
  607. result, err := ExecuteSyncPerms(parent, svcCtx, appKey, appSecret, []SyncPermItem{
  608. {Code: "r14_detach_upd", Name: "NewName"},
  609. })
  610. require.NoError(t, err)
  611. require.NotNil(t, result)
  612. assert.Equal(t, int64(1), result.Updated,
  613. "前置:同名 Code 改 Name 必须 updated=1,否则后续 Clean 断言失去意义")
  614. // 模拟"HTTP ctx 在函数返回同时/之后被 cancel"。若未 detach,生产环境里此 cancel
  615. // 在 Clean 的 DEL 之前触达,Redis 里的 canary 会留存;若已 detach,cancel 只影响
  616. // parent,CleanByProduct 用的 cleanCtx 独立存活 3s,DEL 正常落地。
  617. cancel()
  618. exists, err := rds.Exists(idxKey)
  619. require.NoError(t, err)
  620. assert.False(t, exists,
  621. "M-R14-1:productIndexKey 必须被 CleanByProduct 抹掉;若仍存在,说明 CleanByProduct "+
  622. "被请求 ctx 的 cancel 拖死,对应生产事务已 commit 但 UD 缓存仍挂着被禁用 perm 的 5min 窗口")
  623. }
  624. func TestExecuteSyncPerms_DeduplicatesRequest(t *testing.T) {
  625. ctrl := gomock.NewController(t)
  626. t.Cleanup(ctrl.Finish)
  627. hashedSecret, err := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost)
  628. require.NoError(t, err)
  629. mockProduct := mocks.NewMockSysProductModel(ctrl)
  630. mockProduct.EXPECT().FindOneByAppKey(gomock.Any(), "ak").
  631. Return(&productModel.SysProduct{
  632. Id: 1, Code: "pc_dedup", AppKey: "ak", AppSecret: string(hashedSecret), Status: 1,
  633. }, nil)
  634. // LockByCodeTx 拿到的行必须 Status=1
  635. mockProduct.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_dedup").
  636. Return(&productModel.SysProduct{Id: 1, Code: "pc_dedup", Status: 1}, nil)
  637. mockPerm := mocks.NewMockSysPermModel(ctrl)
  638. mockPerm.EXPECT().FindMapByProductCodeWithTx(gomock.Any(), gomock.Any(), "pc_dedup").
  639. Return(map[string]*permModel.SysPerm{}, nil)
  640. var captured []*permModel.SysPerm
  641. mockPerm.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  642. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  643. return fn(ctx, nil)
  644. })
  645. mockPerm.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).
  646. DoAndReturn(func(ctx context.Context, s sqlx.Session, items []*permModel.SysPerm) error {
  647. captured = items
  648. return nil
  649. })
  650. // 去重后 codes 只剩一个,DisableNotInCodesWithTx 用去重后的集合做 NOT IN。
  651. mockPerm.EXPECT().DisableNotInCodesWithTx(gomock.Any(), nil, "pc_dedup", []string{"dup_code"}, gomock.Any()).
  652. Return(int64(0), nil)
  653. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  654. Product: mockProduct, Perm: mockPerm,
  655. })
  656. result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s", []SyncPermItem{
  657. {Code: "dup_code", Name: "A"},
  658. {Code: "dup_code", Name: "A-again"},
  659. {Code: "dup_code", Name: "A-yet-again"},
  660. })
  661. require.NoError(t, err)
  662. require.NotNil(t, result)
  663. require.Len(t, captured, 1, "入参内 code 重复必须去重为 1 条,避免自撞 1062")
  664. assert.Equal(t, "dup_code", captured[0].Code)
  665. assert.Equal(t, "A", captured[0].Name,
  666. "去重策略应稳定到首次出现,使行为可预测")
  667. }
  668. func newBaseProductMock(ctrl *gomock.Controller, code string) *mocks.MockSysProductModel {
  669. hashed, _ := bcrypt.GenerateFromPassword([]byte("s"), bcrypt.MinCost)
  670. m := mocks.NewMockSysProductModel(ctrl)
  671. m.EXPECT().FindOneByAppKey(gomock.Any(), "ak").
  672. Return(&productModel.SysProduct{
  673. Id: 1, Code: code, AppKey: "ak", AppSecret: string(hashed), Status: 1,
  674. }, nil)
  675. return m
  676. }
  677. // TC-0843: 契约 —— 正常路径下 LockByCodeTx 必须先于 FindMapByProductCodeWithTx,
  678. // 且两者均在同一个 tx session 内被调用。
  679. func TestExecuteSyncPerms_LockBeforeMapReadInTx(t *testing.T) {
  680. ctrl := gomock.NewController(t)
  681. t.Cleanup(ctrl.Finish)
  682. productMock := newBaseProductMock(ctrl, "pc_tx_order")
  683. permMock := mocks.NewMockSysPermModel(ctrl)
  684. // 关键点 1:TransactCtx 必须真的传入一个 tx session,并把所有子调用都发生在其中。
  685. permMock.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  686. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  687. return fn(ctx, nil) // nil session 只是 mock 占位
  688. })
  689. // 关键点 2:gomock 的 Call.After 强制 LockByCodeTx 先于 FindMapByProductCodeWithTx 执行。
  690. // 顺序反过来的话 gomock 会在 Finish 时报错。
  691. // 事务内复核 Status 必须 Status=1,否则走 403 分支不写 perm
  692. lockCall := productMock.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_tx_order").
  693. Return(&productModel.SysProduct{Id: 1, Code: "pc_tx_order", Status: 1}, nil)
  694. permMock.EXPECT().FindMapByProductCodeWithTx(gomock.Any(), gomock.Any(), "pc_tx_order").
  695. Return(map[string]*permModel.SysPerm{}, nil).
  696. After(lockCall)
  697. // 一条简单的 INSERT + DisableNotIn 让流程走完;非本 TC 的主断言。
  698. permMock.EXPECT().BatchInsertWithTx(gomock.Any(), nil, gomock.Any()).Return(nil)
  699. permMock.EXPECT().DisableNotInCodesWithTx(gomock.Any(), nil, "pc_tx_order", []string{"x"}, gomock.Any()).
  700. Return(int64(0), nil)
  701. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: productMock, Perm: permMock})
  702. result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s",
  703. []SyncPermItem{{Code: "x", Name: "X"}})
  704. require.NoError(t, err)
  705. require.NotNil(t, result)
  706. assert.Equal(t, int64(1), result.Added, "lock 在 tx 内就位后应当能正常写入")
  707. }
  708. // TC-0844: 分支 —— tx 内 LockByCodeTx 返回 sqlx.ErrNotFound(产品在 tx 开启后被删),
  709. // 必须映射为 SyncPermsError{Code:404, Message:"产品不存在"},而非 500。
  710. func TestExecuteSyncPerms_LockNotFound_Maps404(t *testing.T) {
  711. ctrl := gomock.NewController(t)
  712. t.Cleanup(ctrl.Finish)
  713. productMock := newBaseProductMock(ctrl, "pc_tx_gone")
  714. permMock := mocks.NewMockSysPermModel(ctrl)
  715. permMock.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  716. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  717. return fn(ctx, nil)
  718. })
  719. productMock.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_tx_gone").
  720. Return(nil, sqlx.ErrNotFound)
  721. // 关键:锁失败后绝不能继续走 FindMapByProductCodeWithTx / BatchInsertWithTx。
  722. // gomock 默认严格模式会在 Finish 时报 "unexpected call",所以不为这些方法登记任何期望即可。
  723. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: productMock, Perm: permMock})
  724. result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s",
  725. []SyncPermItem{{Code: "x", Name: "X"}})
  726. assert.Nil(t, result)
  727. require.Error(t, err)
  728. var se *SyncPermsError
  729. require.True(t, errors.As(err, &se), "锁不到产品行必须产出 *SyncPermsError")
  730. assert.Equal(t, 404, se.Code,
  731. "tx 开启后 LockByCodeTx=ErrNotFound 意味着产品行在 tx 中不可见,应当返回 404 而非 500")
  732. assert.Contains(t, se.Message, "产品不存在",
  733. "文案应当能让调用方人眼秒懂是什么错误")
  734. }
  735. // TC-0845: 容错 —— tx 内 LockByCodeTx 冒出非 NotFound 的通用错误(driver/conn 异常),
  736. // 必须被事务回滚并被外层包裹为 SyncPermsError(500 级),而非原始 driver 错误直接冒出去。
  737. func TestExecuteSyncPerms_LockGenericError_WrappedAs500(t *testing.T) {
  738. ctrl := gomock.NewController(t)
  739. t.Cleanup(ctrl.Finish)
  740. productMock := newBaseProductMock(ctrl, "pc_tx_boom")
  741. permMock := mocks.NewMockSysPermModel(ctrl)
  742. permMock.EXPECT().TransactCtx(gomock.Any(), gomock.Any()).
  743. DoAndReturn(func(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
  744. return fn(ctx, nil)
  745. })
  746. boom := errors.New("driver: connection lost")
  747. productMock.EXPECT().LockByCodeTx(gomock.Any(), gomock.Any(), "pc_tx_boom").
  748. Return(nil, boom)
  749. // 锁失败后同样不应调用后续方法。
  750. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Product: productMock, Perm: permMock})
  751. result, err := ExecuteSyncPerms(context.Background(), svcCtx, "ak", "s",
  752. []SyncPermItem{{Code: "x", Name: "X"}})
  753. assert.Nil(t, result)
  754. require.Error(t, err)
  755. var se *SyncPermsError
  756. require.True(t, errors.As(err, &se), "底层错误必须被包成 *SyncPermsError,防止 driver 错误直接上抛")
  757. assert.Equal(t, 500, se.Code,
  758. "非 NotFound 的 DB 错误应当 fail-close 为 500,让接入方区别于 404/409")
  759. assert.NotContains(t, se.Message, "connection lost",
  760. "对外文案不能泄露原始 driver 错误(避免信息披露)")
  761. }