userDetailsLoader_test.go 72 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025
  1. package loaders
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/require"
  10. "github.com/zeromicro/go-zero/core/stores/cache"
  11. "github.com/zeromicro/go-zero/core/stores/redis"
  12. "github.com/zeromicro/go-zero/core/stores/sqlx"
  13. "golang.org/x/crypto/bcrypt"
  14. "math"
  15. "math/rand"
  16. "perms-system-server/internal/consts"
  17. "perms-system-server/internal/model"
  18. deptModel "perms-system-server/internal/model/dept"
  19. permModel "perms-system-server/internal/model/perm"
  20. productModel "perms-system-server/internal/model/product"
  21. memberModel "perms-system-server/internal/model/productmember"
  22. roleModel "perms-system-server/internal/model/role"
  23. rolePermModel "perms-system-server/internal/model/roleperm"
  24. userModel "perms-system-server/internal/model/user"
  25. userPermModel "perms-system-server/internal/model/userperm"
  26. userRoleModel "perms-system-server/internal/model/userrole"
  27. "sort"
  28. "strings"
  29. "sync"
  30. "sync/atomic"
  31. "testing"
  32. "time"
  33. )
  34. var testCacheConf = cache.CacheConf{
  35. {
  36. RedisConf: redis.RedisConf{Host: "127.0.0.1:6379", Pass: "NsDmWyM@312", Type: "node"},
  37. Weight: 100,
  38. },
  39. }
  40. var testKeyPrefix = "test_perms"
  41. var testDataSource = "root:NsDmWyM@312@tcp(127.0.0.1:3306)/perms_system?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai"
  42. func testConn() sqlx.SqlConn { return sqlx.NewMysql(testDataSource) }
  43. func testRedis() *redis.Redis { return redis.MustNewRedis(testCacheConf[0].RedisConf) }
  44. func testModels() *model.Models {
  45. conn := testConn()
  46. return model.NewModels(conn, testCacheConf, testKeyPrefix)
  47. }
  48. func uniqueId() string {
  49. return fmt.Sprintf("t_%d_%d", time.Now().UnixNano(), rand.Intn(100000))
  50. }
  51. func hashPwd(p string) string {
  52. h, _ := bcrypt.GenerateFromPassword([]byte(p), bcrypt.MinCost)
  53. return string(h)
  54. }
  55. func cleanTable(ctx context.Context, conn sqlx.SqlConn, table string, ids ...int64) {
  56. for _, id := range ids {
  57. conn.ExecCtx(ctx, fmt.Sprintf("DELETE FROM %s WHERE `id` = ?", table), id)
  58. }
  59. }
  60. func cleanTableByField(ctx context.Context, conn sqlx.SqlConn, table, field string, value interface{}) {
  61. conn.ExecCtx(ctx, fmt.Sprintf("DELETE FROM %s WHERE `%s` = ?", table, field), value)
  62. }
  63. func newTestLoader() *UserDetailsLoader {
  64. rds := testRedis()
  65. m := testModels()
  66. return NewUserDetailsLoader(rds, testKeyPrefix, m)
  67. }
  68. func now() int64 { return time.Now().Unix() }
  69. // --------------- helpers: insert test data ---------------
  70. func insertUser(ctx context.Context, t *testing.T, m *model.Models, u *userModel.SysUser) int64 {
  71. t.Helper()
  72. res, err := m.SysUserModel.Insert(ctx, u)
  73. require.NoError(t, err)
  74. id, _ := res.LastInsertId()
  75. return id
  76. }
  77. func insertDept(ctx context.Context, t *testing.T, m *model.Models, d *deptModel.SysDept) int64 {
  78. t.Helper()
  79. res, err := m.SysDeptModel.Insert(ctx, d)
  80. require.NoError(t, err)
  81. id, _ := res.LastInsertId()
  82. return id
  83. }
  84. func insertProduct(ctx context.Context, t *testing.T, m *model.Models, p *productModel.SysProduct) int64 {
  85. t.Helper()
  86. res, err := m.SysProductModel.Insert(ctx, p)
  87. require.NoError(t, err)
  88. id, _ := res.LastInsertId()
  89. return id
  90. }
  91. func insertMember(ctx context.Context, t *testing.T, m *model.Models, mb *memberModel.SysProductMember) int64 {
  92. t.Helper()
  93. res, err := m.SysProductMemberModel.Insert(ctx, mb)
  94. require.NoError(t, err)
  95. id, _ := res.LastInsertId()
  96. return id
  97. }
  98. func insertRole(ctx context.Context, t *testing.T, m *model.Models, r *roleModel.SysRole) int64 {
  99. t.Helper()
  100. res, err := m.SysRoleModel.Insert(ctx, r)
  101. require.NoError(t, err)
  102. id, _ := res.LastInsertId()
  103. return id
  104. }
  105. func insertPerm(ctx context.Context, t *testing.T, m *model.Models, p *permModel.SysPerm) int64 {
  106. t.Helper()
  107. res, err := m.SysPermModel.Insert(ctx, p)
  108. require.NoError(t, err)
  109. id, _ := res.LastInsertId()
  110. return id
  111. }
  112. func insertUserRole(ctx context.Context, t *testing.T, m *model.Models, ur *userRoleModel.SysUserRole) int64 {
  113. t.Helper()
  114. res, err := m.SysUserRoleModel.Insert(ctx, ur)
  115. require.NoError(t, err)
  116. id, _ := res.LastInsertId()
  117. return id
  118. }
  119. func insertRolePerm(ctx context.Context, t *testing.T, m *model.Models, rp *rolePermModel.SysRolePerm) int64 {
  120. t.Helper()
  121. res, err := m.SysRolePermModel.Insert(ctx, rp)
  122. require.NoError(t, err)
  123. id, _ := res.LastInsertId()
  124. return id
  125. }
  126. func insertUserPerm(ctx context.Context, t *testing.T, m *model.Models, up *userPermModel.SysUserPerm) int64 {
  127. t.Helper()
  128. res, err := m.SysUserPermModel.Insert(ctx, up)
  129. require.NoError(t, err)
  130. id, _ := res.LastInsertId()
  131. return id
  132. }
  133. // --------------- TC-0506: Load-DB加载(缓存miss) ---------------
  134. func TestLoad_DBMiss(t *testing.T) {
  135. ctx := context.Background()
  136. conn := testConn()
  137. m := testModels()
  138. loader := newTestLoader()
  139. uid := uniqueId()
  140. ts := now()
  141. pcode := "p_" + uid
  142. deptId := insertDept(ctx, t, m, &deptModel.SysDept{
  143. ParentId: 0, Name: "dept_" + uid, Path: "/1/", Sort: 1,
  144. DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled,
  145. CreateTime: ts, UpdateTime: ts,
  146. })
  147. userId := insertUser(ctx, t, m, &userModel.SysUser{
  148. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  149. Avatar: sql.NullString{}, Email: uid + "@test.com", Phone: "13800000001",
  150. Remark: "remark", DeptId: deptId, IsSuperAdmin: consts.IsSuperAdminNo,
  151. MustChangePassword: consts.MustChangePasswordNo, Status: consts.StatusEnabled,
  152. CreateTime: ts, UpdateTime: ts,
  153. })
  154. productId := insertProduct(ctx, t, m, &productModel.SysProduct{
  155. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  156. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  157. })
  158. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  159. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeMember,
  160. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  161. })
  162. roleId := insertRole(ctx, t, m, &roleModel.SysRole{
  163. ProductCode: pcode, Name: "role_" + uid, Remark: "test",
  164. Status: consts.StatusEnabled, PermsLevel: 10, CreateTime: ts, UpdateTime: ts,
  165. })
  166. permId := insertPerm(ctx, t, m, &permModel.SysPerm{
  167. ProductCode: pcode, Name: "perm_" + uid, Code: "perm:" + uid,
  168. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  169. })
  170. urId := insertUserRole(ctx, t, m, &userRoleModel.SysUserRole{
  171. UserId: userId, RoleId: roleId, CreateTime: ts, UpdateTime: ts,
  172. })
  173. rpId := insertRolePerm(ctx, t, m, &rolePermModel.SysRolePerm{
  174. RoleId: roleId, PermId: permId, CreateTime: ts, UpdateTime: ts,
  175. })
  176. t.Cleanup(func() {
  177. loader.Del(ctx, userId, pcode)
  178. cleanTable(ctx, conn, "`sys_role_perm`", rpId)
  179. cleanTable(ctx, conn, "`sys_user_role`", urId)
  180. cleanTable(ctx, conn, "`sys_perm`", permId)
  181. cleanTable(ctx, conn, "`sys_role`", roleId)
  182. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  183. cleanTable(ctx, conn, "`sys_product`", productId)
  184. cleanTable(ctx, conn, "`sys_user`", userId)
  185. cleanTable(ctx, conn, "`sys_dept`", deptId)
  186. })
  187. // clear any leftover cache
  188. loader.Del(ctx, userId, pcode)
  189. ud, _ := loader.Load(ctx, userId, pcode)
  190. require.NotNil(t, ud)
  191. assert.Equal(t, userId, ud.UserId)
  192. assert.Equal(t, uid, ud.Username)
  193. assert.Equal(t, "nick_"+uid, ud.Nickname)
  194. assert.Equal(t, uid+"@test.com", ud.Email)
  195. assert.Equal(t, int64(consts.StatusEnabled), ud.Status)
  196. assert.Equal(t, deptId, ud.DeptId)
  197. assert.Equal(t, "dept_"+uid, ud.DeptName)
  198. assert.Equal(t, pcode, ud.ProductCode)
  199. assert.Equal(t, "prod_"+uid, ud.ProductName)
  200. assert.Equal(t, consts.MemberTypeMember, ud.MemberType)
  201. assert.Len(t, ud.Roles, 1)
  202. assert.Equal(t, roleId, ud.Roles[0].Id)
  203. assert.Equal(t, int64(10), ud.MinPermsLevel)
  204. assert.Contains(t, ud.Perms, "perm:"+uid)
  205. }
  206. // --------------- TC-0507: Load-缓存命中 ---------------
  207. func TestLoad_CacheHit(t *testing.T) {
  208. ctx := context.Background()
  209. conn := testConn()
  210. m := testModels()
  211. loader := newTestLoader()
  212. uid := uniqueId()
  213. ts := now()
  214. pcode := "p_" + uid
  215. userId := insertUser(ctx, t, m, &userModel.SysUser{
  216. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  217. Email: uid + "@test.com", Phone: "13800000002", DeptId: 0,
  218. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  219. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  220. })
  221. productId := insertProduct(ctx, t, m, &productModel.SysProduct{
  222. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  223. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  224. })
  225. t.Cleanup(func() {
  226. loader.Del(ctx, userId, pcode)
  227. cleanTable(ctx, conn, "`sys_product`", productId)
  228. cleanTable(ctx, conn, "`sys_user`", userId)
  229. })
  230. loader.Del(ctx, userId, pcode)
  231. ud1, _ := loader.Load(ctx, userId, pcode)
  232. require.NotNil(t, ud1)
  233. ud2, _ := loader.Load(ctx, userId, pcode)
  234. require.NotNil(t, ud2)
  235. assert.Equal(t, ud1.UserId, ud2.UserId)
  236. assert.Equal(t, ud1.Username, ud2.Username)
  237. assert.Equal(t, ud1.ProductName, ud2.ProductName)
  238. }
  239. // --------------- TC-0508: Load-用户不存在 ---------------
  240. func TestLoad_UserNotExist(t *testing.T) {
  241. ctx := context.Background()
  242. loader := newTestLoader()
  243. nonExistId := int64(999999999)
  244. loader.Del(ctx, nonExistId, "nonexist_product")
  245. ud, _ := loader.Load(ctx, nonExistId, "nonexist_product")
  246. require.NotNil(t, ud)
  247. assert.Equal(t, int64(0), ud.Status)
  248. assert.Empty(t, ud.Username)
  249. assert.Empty(t, ud.Perms)
  250. assert.Empty(t, ud.Roles)
  251. loader.Del(ctx, nonExistId, "nonexist_product")
  252. }
  253. // --------------- TC-0509: Load-productCode为空 ---------------
  254. func TestLoad_EmptyProductCode(t *testing.T) {
  255. ctx := context.Background()
  256. conn := testConn()
  257. m := testModels()
  258. loader := newTestLoader()
  259. uid := uniqueId()
  260. ts := now()
  261. userId := insertUser(ctx, t, m, &userModel.SysUser{
  262. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  263. Email: uid + "@test.com", Phone: "13800000003", DeptId: 0,
  264. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  265. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  266. })
  267. t.Cleanup(func() {
  268. loader.Del(ctx, userId, "")
  269. cleanTable(ctx, conn, "`sys_user`", userId)
  270. })
  271. loader.Del(ctx, userId, "")
  272. ud, _ := loader.Load(ctx, userId, "")
  273. require.NotNil(t, ud)
  274. assert.Equal(t, uid, ud.Username)
  275. assert.Equal(t, int64(consts.StatusEnabled), ud.Status)
  276. assert.Empty(t, ud.ProductCode)
  277. assert.Empty(t, ud.ProductName)
  278. assert.Empty(t, ud.MemberType)
  279. assert.Empty(t, ud.Roles)
  280. assert.Empty(t, ud.Perms)
  281. }
  282. // --------------- TC-0510: Del删除指定缓存 ---------------
  283. func TestDel(t *testing.T) {
  284. ctx := context.Background()
  285. conn := testConn()
  286. m := testModels()
  287. loader := newTestLoader()
  288. uid := uniqueId()
  289. ts := now()
  290. pcode := "p_" + uid
  291. userId := insertUser(ctx, t, m, &userModel.SysUser{
  292. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  293. Email: uid + "@test.com", Phone: "13800000004", DeptId: 0,
  294. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  295. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  296. })
  297. productId := insertProduct(ctx, t, m, &productModel.SysProduct{
  298. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  299. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  300. })
  301. t.Cleanup(func() {
  302. loader.Del(ctx, userId, pcode)
  303. cleanTable(ctx, conn, "`sys_product`", productId)
  304. cleanTable(ctx, conn, "`sys_user`", userId)
  305. })
  306. loader.Del(ctx, userId, pcode)
  307. ud1, _ := loader.Load(ctx, userId, pcode)
  308. require.NotNil(t, ud1)
  309. assert.Equal(t, uid, ud1.Username)
  310. loader.Del(ctx, userId, pcode)
  311. ud2, _ := loader.Load(ctx, userId, pcode)
  312. require.NotNil(t, ud2)
  313. assert.Equal(t, uid, ud2.Username)
  314. }
  315. // --------------- TC-0511: Clean清除用户所有产品缓存 ---------------
  316. func TestClean(t *testing.T) {
  317. ctx := context.Background()
  318. conn := testConn()
  319. m := testModels()
  320. loader := newTestLoader()
  321. uid := uniqueId()
  322. ts := now()
  323. pcode1 := "p1_" + uid
  324. pcode2 := "p2_" + uid
  325. userId := insertUser(ctx, t, m, &userModel.SysUser{
  326. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  327. Email: uid + "@test.com", Phone: "13800000005", DeptId: 0,
  328. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  329. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  330. })
  331. pid1 := insertProduct(ctx, t, m, &productModel.SysProduct{
  332. Code: pcode1, Name: "prod1_" + uid, AppKey: "ak1_" + uid, AppSecret: "as1_" + uid,
  333. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  334. })
  335. pid2 := insertProduct(ctx, t, m, &productModel.SysProduct{
  336. Code: pcode2, Name: "prod2_" + uid, AppKey: "ak2_" + uid, AppSecret: "as2_" + uid,
  337. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  338. })
  339. t.Cleanup(func() {
  340. loader.Del(ctx, userId, pcode1)
  341. loader.Del(ctx, userId, pcode2)
  342. cleanTable(ctx, conn, "`sys_product`", pid1, pid2)
  343. cleanTable(ctx, conn, "`sys_user`", userId)
  344. })
  345. loader.Del(ctx, userId, pcode1)
  346. loader.Del(ctx, userId, pcode2)
  347. ud1, _ := loader.Load(ctx, userId, pcode1)
  348. ud2, _ := loader.Load(ctx, userId, pcode2)
  349. require.NotNil(t, ud1)
  350. require.NotNil(t, ud2)
  351. rds := testRedis()
  352. key1 := loader.cacheKey(userId, pcode1)
  353. key2 := loader.cacheKey(userId, pcode2)
  354. v1, _ := rds.GetCtx(ctx, key1)
  355. v2, _ := rds.GetCtx(ctx, key2)
  356. assert.NotEmpty(t, v1)
  357. assert.NotEmpty(t, v2)
  358. loader.Clean(ctx, userId)
  359. v1After, _ := rds.GetCtx(ctx, key1)
  360. v2After, _ := rds.GetCtx(ctx, key2)
  361. assert.Empty(t, v1After)
  362. assert.Empty(t, v2After)
  363. }
  364. // --------------- TC-0512: CleanByProduct清除产品所有用户 ---------------
  365. func TestCleanByProduct(t *testing.T) {
  366. ctx := context.Background()
  367. conn := testConn()
  368. m := testModels()
  369. loader := newTestLoader()
  370. uid1 := uniqueId()
  371. uid2 := uniqueId()
  372. ts := now()
  373. pcode := "p_" + uid1
  374. userId1 := insertUser(ctx, t, m, &userModel.SysUser{
  375. Username: uid1, Password: hashPwd("pass123"), Nickname: "nick_" + uid1,
  376. Email: uid1 + "@test.com", Phone: "13800000006", DeptId: 0,
  377. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  378. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  379. })
  380. userId2 := insertUser(ctx, t, m, &userModel.SysUser{
  381. Username: uid2, Password: hashPwd("pass123"), Nickname: "nick_" + uid2,
  382. Email: uid2 + "@test.com", Phone: "13800000007", DeptId: 0,
  383. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  384. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  385. })
  386. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  387. Code: pcode, Name: "prod_" + uid1, AppKey: "ak_" + uid1, AppSecret: "as_" + uid1,
  388. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  389. })
  390. t.Cleanup(func() {
  391. loader.Del(ctx, userId1, pcode)
  392. loader.Del(ctx, userId2, pcode)
  393. cleanTable(ctx, conn, "`sys_product`", pid)
  394. cleanTable(ctx, conn, "`sys_user`", userId1, userId2)
  395. })
  396. loader.Del(ctx, userId1, pcode)
  397. loader.Del(ctx, userId2, pcode)
  398. _, _ = loader.Load(ctx, userId1, pcode)
  399. _, _ = loader.Load(ctx, userId2, pcode)
  400. rds := testRedis()
  401. k1 := loader.cacheKey(userId1, pcode)
  402. k2 := loader.cacheKey(userId2, pcode)
  403. v1, _ := rds.GetCtx(ctx, k1)
  404. v2, _ := rds.GetCtx(ctx, k2)
  405. assert.NotEmpty(t, v1)
  406. assert.NotEmpty(t, v2)
  407. loader.CleanByProduct(ctx, pcode)
  408. v1After, _ := rds.GetCtx(ctx, k1)
  409. v2After, _ := rds.GetCtx(ctx, k2)
  410. assert.Empty(t, v1After)
  411. assert.Empty(t, v2After)
  412. }
  413. // --------------- TC-0513: BatchDel批量删除 ---------------
  414. func TestBatchDel(t *testing.T) {
  415. ctx := context.Background()
  416. conn := testConn()
  417. m := testModels()
  418. loader := newTestLoader()
  419. uid1 := uniqueId()
  420. uid2 := uniqueId()
  421. ts := now()
  422. pcode := "p_" + uid1
  423. userId1 := insertUser(ctx, t, m, &userModel.SysUser{
  424. Username: uid1, Password: hashPwd("pass123"), Nickname: "nick_" + uid1,
  425. Email: uid1 + "@test.com", Phone: "13800000008", DeptId: 0,
  426. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  427. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  428. })
  429. userId2 := insertUser(ctx, t, m, &userModel.SysUser{
  430. Username: uid2, Password: hashPwd("pass123"), Nickname: "nick_" + uid2,
  431. Email: uid2 + "@test.com", Phone: "13800000009", DeptId: 0,
  432. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  433. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  434. })
  435. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  436. Code: pcode, Name: "prod_" + uid1, AppKey: "ak_" + uid1, AppSecret: "as_" + uid1,
  437. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  438. })
  439. t.Cleanup(func() {
  440. loader.Del(ctx, userId1, pcode)
  441. loader.Del(ctx, userId2, pcode)
  442. cleanTable(ctx, conn, "`sys_product`", pid)
  443. cleanTable(ctx, conn, "`sys_user`", userId1, userId2)
  444. })
  445. loader.Del(ctx, userId1, pcode)
  446. loader.Del(ctx, userId2, pcode)
  447. _, _ = loader.Load(ctx, userId1, pcode)
  448. _, _ = loader.Load(ctx, userId2, pcode)
  449. rds := testRedis()
  450. k1 := loader.cacheKey(userId1, pcode)
  451. k2 := loader.cacheKey(userId2, pcode)
  452. v1, _ := rds.GetCtx(ctx, k1)
  453. v2, _ := rds.GetCtx(ctx, k2)
  454. assert.NotEmpty(t, v1)
  455. assert.NotEmpty(t, v2)
  456. loader.BatchDel(ctx, []int64{userId1, userId2}, pcode)
  457. v1After, _ := rds.GetCtx(ctx, k1)
  458. v2After, _ := rds.GetCtx(ctx, k2)
  459. assert.Empty(t, v1After)
  460. assert.Empty(t, v2After)
  461. }
  462. // --------------- TC-0514: BatchDel空数组 ---------------
  463. func TestBatchDel_EmptySlice(t *testing.T) {
  464. ctx := context.Background()
  465. loader := newTestLoader()
  466. loader.BatchDel(ctx, []int64{}, "some_code")
  467. }
  468. // --------------- TC-0515: loadPerms-超管拥有全部权限 ---------------
  469. func TestLoadPerms_SuperAdmin(t *testing.T) {
  470. ctx := context.Background()
  471. conn := testConn()
  472. m := testModels()
  473. loader := newTestLoader()
  474. uid := uniqueId()
  475. ts := now()
  476. pcode := "p_" + uid
  477. userId := insertUser(ctx, t, m, &userModel.SysUser{
  478. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  479. Email: uid + "@test.com", Phone: "13800000010", DeptId: 0,
  480. IsSuperAdmin: consts.IsSuperAdminYes, MustChangePassword: consts.MustChangePasswordNo,
  481. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  482. })
  483. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  484. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  485. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  486. })
  487. permCode1 := "perm1:" + uid
  488. permCode2 := "perm2:" + uid
  489. permId1 := insertPerm(ctx, t, m, &permModel.SysPerm{
  490. ProductCode: pcode, Name: "p1_" + uid, Code: permCode1,
  491. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  492. })
  493. permId2 := insertPerm(ctx, t, m, &permModel.SysPerm{
  494. ProductCode: pcode, Name: "p2_" + uid, Code: permCode2,
  495. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  496. })
  497. t.Cleanup(func() {
  498. loader.Del(ctx, userId, pcode)
  499. cleanTable(ctx, conn, "`sys_perm`", permId1, permId2)
  500. cleanTable(ctx, conn, "`sys_product`", pid)
  501. cleanTable(ctx, conn, "`sys_user`", userId)
  502. })
  503. loader.Del(ctx, userId, pcode)
  504. ud, _ := loader.Load(ctx, userId, pcode)
  505. require.NotNil(t, ud)
  506. assert.True(t, ud.IsSuperAdmin)
  507. assert.Equal(t, consts.MemberTypeSuperAdmin, ud.MemberType)
  508. sort.Strings(ud.Perms)
  509. expected := []string{permCode1, permCode2}
  510. sort.Strings(expected)
  511. assert.Equal(t, expected, ud.Perms)
  512. }
  513. // --------------- TC-0516: loadPerms-ADMIN成员拥有全部权限 ---------------
  514. func TestLoadPerms_AdminMember(t *testing.T) {
  515. ctx := context.Background()
  516. conn := testConn()
  517. m := testModels()
  518. loader := newTestLoader()
  519. uid := uniqueId()
  520. ts := now()
  521. pcode := "p_" + uid
  522. userId := insertUser(ctx, t, m, &userModel.SysUser{
  523. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  524. Email: uid + "@test.com", Phone: "13800000011", DeptId: 0,
  525. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  526. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  527. })
  528. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  529. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  530. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  531. })
  532. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  533. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeAdmin,
  534. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  535. })
  536. permCode := "perm:" + uid
  537. permId := insertPerm(ctx, t, m, &permModel.SysPerm{
  538. ProductCode: pcode, Name: "p_" + uid, Code: permCode,
  539. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  540. })
  541. t.Cleanup(func() {
  542. loader.Del(ctx, userId, pcode)
  543. cleanTable(ctx, conn, "`sys_perm`", permId)
  544. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  545. cleanTable(ctx, conn, "`sys_product`", pid)
  546. cleanTable(ctx, conn, "`sys_user`", userId)
  547. })
  548. loader.Del(ctx, userId, pcode)
  549. ud, _ := loader.Load(ctx, userId, pcode)
  550. require.NotNil(t, ud)
  551. assert.Equal(t, consts.MemberTypeAdmin, ud.MemberType)
  552. assert.Contains(t, ud.Perms, permCode)
  553. }
  554. // --------------- TC-0517: loadPerms-DEVELOPER成员拥有全部权限 ---------------
  555. func TestLoadPerms_DeveloperMember(t *testing.T) {
  556. ctx := context.Background()
  557. conn := testConn()
  558. m := testModels()
  559. loader := newTestLoader()
  560. uid := uniqueId()
  561. ts := now()
  562. pcode := "p_" + uid
  563. userId := insertUser(ctx, t, m, &userModel.SysUser{
  564. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  565. Email: uid + "@test.com", Phone: "13800000012", DeptId: 0,
  566. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  567. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  568. })
  569. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  570. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  571. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  572. })
  573. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  574. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeDeveloper,
  575. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  576. })
  577. permCode := "perm:" + uid
  578. permId := insertPerm(ctx, t, m, &permModel.SysPerm{
  579. ProductCode: pcode, Name: "p_" + uid, Code: permCode,
  580. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  581. })
  582. t.Cleanup(func() {
  583. loader.Del(ctx, userId, pcode)
  584. cleanTable(ctx, conn, "`sys_perm`", permId)
  585. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  586. cleanTable(ctx, conn, "`sys_product`", pid)
  587. cleanTable(ctx, conn, "`sys_user`", userId)
  588. })
  589. loader.Del(ctx, userId, pcode)
  590. ud, _ := loader.Load(ctx, userId, pcode)
  591. require.NotNil(t, ud)
  592. assert.Equal(t, consts.MemberTypeDeveloper, ud.MemberType)
  593. assert.Contains(t, ud.Perms, permCode)
  594. }
  595. // --------------- TC-0518: loadPerms-DEV部门成员拥有全部权限 ---------------
  596. func TestLoadPerms_DevDept(t *testing.T) {
  597. ctx := context.Background()
  598. conn := testConn()
  599. m := testModels()
  600. loader := newTestLoader()
  601. uid := uniqueId()
  602. ts := now()
  603. pcode := "p_" + uid
  604. deptId := insertDept(ctx, t, m, &deptModel.SysDept{
  605. ParentId: 0, Name: "devdept_" + uid, Path: "/1/", Sort: 1,
  606. DeptType: consts.DeptTypeDev, Status: consts.StatusEnabled,
  607. CreateTime: ts, UpdateTime: ts,
  608. })
  609. userId := insertUser(ctx, t, m, &userModel.SysUser{
  610. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  611. Email: uid + "@test.com", Phone: "13800000013", DeptId: deptId,
  612. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  613. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  614. })
  615. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  616. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  617. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  618. })
  619. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  620. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeMember,
  621. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  622. })
  623. permCode := "perm:" + uid
  624. permId := insertPerm(ctx, t, m, &permModel.SysPerm{
  625. ProductCode: pcode, Name: "p_" + uid, Code: permCode,
  626. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  627. })
  628. t.Cleanup(func() {
  629. loader.Del(ctx, userId, pcode)
  630. cleanTable(ctx, conn, "`sys_perm`", permId)
  631. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  632. cleanTable(ctx, conn, "`sys_product`", pid)
  633. cleanTable(ctx, conn, "`sys_user`", userId)
  634. cleanTable(ctx, conn, "`sys_dept`", deptId)
  635. })
  636. loader.Del(ctx, userId, pcode)
  637. ud, _ := loader.Load(ctx, userId, pcode)
  638. require.NotNil(t, ud)
  639. assert.Equal(t, consts.DeptTypeDev, ud.DeptType)
  640. assert.Contains(t, ud.Perms, permCode)
  641. }
  642. // --------------- TC-0519: MEMBER角色权限+ALLOW-DENY ---------------
  643. func TestLoadPerms_MemberRolePermWithAllowDeny(t *testing.T) {
  644. ctx := context.Background()
  645. conn := testConn()
  646. m := testModels()
  647. loader := newTestLoader()
  648. uid := uniqueId()
  649. ts := now()
  650. pcode := "p_" + uid
  651. userId := insertUser(ctx, t, m, &userModel.SysUser{
  652. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  653. Email: uid + "@test.com", Phone: "13800000014", DeptId: 0,
  654. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  655. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  656. })
  657. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  658. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  659. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  660. })
  661. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  662. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeMember,
  663. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  664. })
  665. roleId := insertRole(ctx, t, m, &roleModel.SysRole{
  666. ProductCode: pcode, Name: "role_" + uid, Remark: "test",
  667. Status: consts.StatusEnabled, PermsLevel: 10, CreateTime: ts, UpdateTime: ts,
  668. })
  669. urId := insertUserRole(ctx, t, m, &userRoleModel.SysUserRole{
  670. UserId: userId, RoleId: roleId, CreateTime: ts, UpdateTime: ts,
  671. })
  672. // role perm: permA, permB
  673. permIdA := insertPerm(ctx, t, m, &permModel.SysPerm{
  674. ProductCode: pcode, Name: "permA_" + uid, Code: "permA:" + uid,
  675. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  676. })
  677. permIdB := insertPerm(ctx, t, m, &permModel.SysPerm{
  678. ProductCode: pcode, Name: "permB_" + uid, Code: "permB:" + uid,
  679. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  680. })
  681. // user ALLOW perm: permC
  682. permIdC := insertPerm(ctx, t, m, &permModel.SysPerm{
  683. ProductCode: pcode, Name: "permC_" + uid, Code: "permC:" + uid,
  684. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  685. })
  686. // user DENY perm: permB (should remove permB from result)
  687. rpIdA := insertRolePerm(ctx, t, m, &rolePermModel.SysRolePerm{
  688. RoleId: roleId, PermId: permIdA, CreateTime: ts, UpdateTime: ts,
  689. })
  690. rpIdB := insertRolePerm(ctx, t, m, &rolePermModel.SysRolePerm{
  691. RoleId: roleId, PermId: permIdB, CreateTime: ts, UpdateTime: ts,
  692. })
  693. upAllow := insertUserPerm(ctx, t, m, &userPermModel.SysUserPerm{
  694. UserId: userId, PermId: permIdC, Effect: consts.PermEffectAllow,
  695. CreateTime: ts, UpdateTime: ts,
  696. })
  697. upDeny := insertUserPerm(ctx, t, m, &userPermModel.SysUserPerm{
  698. UserId: userId, PermId: permIdB, Effect: consts.PermEffectDeny,
  699. CreateTime: ts, UpdateTime: ts,
  700. })
  701. t.Cleanup(func() {
  702. loader.Del(ctx, userId, pcode)
  703. cleanTable(ctx, conn, "`sys_user_perm`", upAllow, upDeny)
  704. cleanTable(ctx, conn, "`sys_role_perm`", rpIdA, rpIdB)
  705. cleanTable(ctx, conn, "`sys_perm`", permIdA, permIdB, permIdC)
  706. cleanTable(ctx, conn, "`sys_user_role`", urId)
  707. cleanTable(ctx, conn, "`sys_role`", roleId)
  708. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  709. cleanTable(ctx, conn, "`sys_product`", pid)
  710. cleanTable(ctx, conn, "`sys_user`", userId)
  711. })
  712. loader.Del(ctx, userId, pcode)
  713. ud, _ := loader.Load(ctx, userId, pcode)
  714. require.NotNil(t, ud)
  715. // permA (from role) + permC (from ALLOW) should be present
  716. // permB (denied) should NOT be present
  717. assert.Contains(t, ud.Perms, "permA:"+uid)
  718. assert.Contains(t, ud.Perms, "permC:"+uid)
  719. assert.NotContains(t, ud.Perms, "permB:"+uid)
  720. }
  721. // --------------- TC-0522: loadRoles-多角色取最小permsLevel ---------------
  722. func TestLoadRoles_MinPermsLevel(t *testing.T) {
  723. ctx := context.Background()
  724. conn := testConn()
  725. m := testModels()
  726. loader := newTestLoader()
  727. uid := uniqueId()
  728. ts := now()
  729. pcode := "p_" + uid
  730. userId := insertUser(ctx, t, m, &userModel.SysUser{
  731. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  732. Email: uid + "@test.com", Phone: "13800000015", DeptId: 0,
  733. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  734. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  735. })
  736. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  737. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  738. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  739. })
  740. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  741. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeMember,
  742. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  743. })
  744. roleId1 := insertRole(ctx, t, m, &roleModel.SysRole{
  745. ProductCode: pcode, Name: "roleH_" + uid, Remark: "high",
  746. Status: consts.StatusEnabled, PermsLevel: 10, CreateTime: ts, UpdateTime: ts,
  747. })
  748. roleId2 := insertRole(ctx, t, m, &roleModel.SysRole{
  749. ProductCode: pcode, Name: "roleL_" + uid, Remark: "low",
  750. Status: consts.StatusEnabled, PermsLevel: 5, CreateTime: ts, UpdateTime: ts,
  751. })
  752. urId1 := insertUserRole(ctx, t, m, &userRoleModel.SysUserRole{
  753. UserId: userId, RoleId: roleId1, CreateTime: ts, UpdateTime: ts,
  754. })
  755. urId2 := insertUserRole(ctx, t, m, &userRoleModel.SysUserRole{
  756. UserId: userId, RoleId: roleId2, CreateTime: ts, UpdateTime: ts,
  757. })
  758. t.Cleanup(func() {
  759. loader.Del(ctx, userId, pcode)
  760. cleanTable(ctx, conn, "`sys_user_role`", urId1, urId2)
  761. cleanTable(ctx, conn, "`sys_role`", roleId1, roleId2)
  762. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  763. cleanTable(ctx, conn, "`sys_product`", pid)
  764. cleanTable(ctx, conn, "`sys_user`", userId)
  765. })
  766. loader.Del(ctx, userId, pcode)
  767. ud, _ := loader.Load(ctx, userId, pcode)
  768. require.NotNil(t, ud)
  769. assert.Len(t, ud.Roles, 2)
  770. assert.Equal(t, int64(5), ud.MinPermsLevel)
  771. }
  772. // --------------- TC-0523: loadRoles-无角色 ---------------
  773. func TestLoadRoles_NoRoles(t *testing.T) {
  774. ctx := context.Background()
  775. conn := testConn()
  776. m := testModels()
  777. loader := newTestLoader()
  778. uid := uniqueId()
  779. ts := now()
  780. pcode := "p_" + uid
  781. userId := insertUser(ctx, t, m, &userModel.SysUser{
  782. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  783. Email: uid + "@test.com", Phone: "13800000016", DeptId: 0,
  784. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  785. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  786. })
  787. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  788. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  789. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  790. })
  791. t.Cleanup(func() {
  792. loader.Del(ctx, userId, pcode)
  793. cleanTable(ctx, conn, "`sys_product`", pid)
  794. cleanTable(ctx, conn, "`sys_user`", userId)
  795. })
  796. loader.Del(ctx, userId, pcode)
  797. ud, _ := loader.Load(ctx, userId, pcode)
  798. require.NotNil(t, ud)
  799. assert.Equal(t, int64(math.MaxInt64), ud.MinPermsLevel)
  800. }
  801. // --------------- TC-0524: loadRoles-角色跨产品过滤 ---------------
  802. func TestLoadRoles_CrossProductFilter(t *testing.T) {
  803. ctx := context.Background()
  804. conn := testConn()
  805. m := testModels()
  806. loader := newTestLoader()
  807. uid := uniqueId()
  808. ts := now()
  809. pcodeA := "pA_" + uid
  810. pcodeB := "pB_" + uid
  811. userId := insertUser(ctx, t, m, &userModel.SysUser{
  812. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  813. Email: uid + "@test.com", Phone: "13800000017", DeptId: 0,
  814. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  815. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  816. })
  817. pidA := insertProduct(ctx, t, m, &productModel.SysProduct{
  818. Code: pcodeA, Name: "prodA_" + uid, AppKey: "akA_" + uid, AppSecret: "asA_" + uid,
  819. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  820. })
  821. pidB := insertProduct(ctx, t, m, &productModel.SysProduct{
  822. Code: pcodeB, Name: "prodB_" + uid, AppKey: "akB_" + uid, AppSecret: "asB_" + uid,
  823. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  824. })
  825. memA := insertMember(ctx, t, m, &memberModel.SysProductMember{
  826. ProductCode: pcodeA, UserId: userId, MemberType: consts.MemberTypeMember,
  827. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  828. })
  829. roleA := insertRole(ctx, t, m, &roleModel.SysRole{
  830. ProductCode: pcodeA, Name: "roleA_" + uid, Remark: "A",
  831. Status: consts.StatusEnabled, PermsLevel: 10, CreateTime: ts, UpdateTime: ts,
  832. })
  833. roleB := insertRole(ctx, t, m, &roleModel.SysRole{
  834. ProductCode: pcodeB, Name: "roleB_" + uid, Remark: "B",
  835. Status: consts.StatusEnabled, PermsLevel: 20, CreateTime: ts, UpdateTime: ts,
  836. })
  837. urA := insertUserRole(ctx, t, m, &userRoleModel.SysUserRole{
  838. UserId: userId, RoleId: roleA, CreateTime: ts, UpdateTime: ts,
  839. })
  840. urB := insertUserRole(ctx, t, m, &userRoleModel.SysUserRole{
  841. UserId: userId, RoleId: roleB, CreateTime: ts, UpdateTime: ts,
  842. })
  843. t.Cleanup(func() {
  844. loader.Del(ctx, userId, pcodeA)
  845. loader.Del(ctx, userId, pcodeB)
  846. cleanTable(ctx, conn, "`sys_user_role`", urA, urB)
  847. cleanTable(ctx, conn, "`sys_role`", roleA, roleB)
  848. cleanTable(ctx, conn, "`sys_product_member`", memA)
  849. cleanTable(ctx, conn, "`sys_product`", pidA, pidB)
  850. cleanTable(ctx, conn, "`sys_user`", userId)
  851. })
  852. loader.Del(ctx, userId, pcodeA)
  853. ud, _ := loader.Load(ctx, userId, pcodeA)
  854. require.NotNil(t, ud)
  855. assert.Len(t, ud.Roles, 1)
  856. assert.Equal(t, roleA, ud.Roles[0].Id)
  857. assert.Equal(t, int64(10), ud.MinPermsLevel)
  858. }
  859. // --------------- TC-0525: loadRoles-禁用角色不计入 ---------------
  860. func TestLoadRoles_DisabledRoleExcluded(t *testing.T) {
  861. ctx := context.Background()
  862. conn := testConn()
  863. m := testModels()
  864. loader := newTestLoader()
  865. uid := uniqueId()
  866. ts := now()
  867. pcode := "p_" + uid
  868. userId := insertUser(ctx, t, m, &userModel.SysUser{
  869. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  870. Email: uid + "@test.com", Phone: "13800000018", DeptId: 0,
  871. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  872. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  873. })
  874. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  875. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  876. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  877. })
  878. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  879. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeMember,
  880. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  881. })
  882. enabledRole := insertRole(ctx, t, m, &roleModel.SysRole{
  883. ProductCode: pcode, Name: "rEnabled_" + uid, Remark: "enabled",
  884. Status: consts.StatusEnabled, PermsLevel: 5, CreateTime: ts, UpdateTime: ts,
  885. })
  886. disabledRole := insertRole(ctx, t, m, &roleModel.SysRole{
  887. ProductCode: pcode, Name: "rDisabled_" + uid, Remark: "disabled",
  888. Status: consts.StatusDisabled, PermsLevel: 1, CreateTime: ts, UpdateTime: ts,
  889. })
  890. ur1 := insertUserRole(ctx, t, m, &userRoleModel.SysUserRole{
  891. UserId: userId, RoleId: enabledRole, CreateTime: ts, UpdateTime: ts,
  892. })
  893. ur2 := insertUserRole(ctx, t, m, &userRoleModel.SysUserRole{
  894. UserId: userId, RoleId: disabledRole, CreateTime: ts, UpdateTime: ts,
  895. })
  896. t.Cleanup(func() {
  897. loader.Del(ctx, userId, pcode)
  898. cleanTable(ctx, conn, "`sys_user_role`", ur1, ur2)
  899. cleanTable(ctx, conn, "`sys_role`", enabledRole, disabledRole)
  900. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  901. cleanTable(ctx, conn, "`sys_product`", pid)
  902. cleanTable(ctx, conn, "`sys_user`", userId)
  903. })
  904. loader.Del(ctx, userId, pcode)
  905. ud, _ := loader.Load(ctx, userId, pcode)
  906. require.NotNil(t, ud)
  907. assert.Len(t, ud.Roles, 1)
  908. assert.Equal(t, enabledRole, ud.Roles[0].Id)
  909. assert.Equal(t, int64(5), ud.MinPermsLevel)
  910. }
  911. // --------------- TC-0526: loadMembership-超管自动SUPER_ADMIN ---------------
  912. func TestLoadMembership_SuperAdminAuto(t *testing.T) {
  913. ctx := context.Background()
  914. conn := testConn()
  915. m := testModels()
  916. loader := newTestLoader()
  917. uid := uniqueId()
  918. ts := now()
  919. pcode := "p_" + uid
  920. userId := insertUser(ctx, t, m, &userModel.SysUser{
  921. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  922. Email: uid + "@test.com", Phone: "13800000019", DeptId: 0,
  923. IsSuperAdmin: consts.IsSuperAdminYes, MustChangePassword: consts.MustChangePasswordNo,
  924. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  925. })
  926. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  927. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  928. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  929. })
  930. t.Cleanup(func() {
  931. loader.Del(ctx, userId, pcode)
  932. cleanTable(ctx, conn, "`sys_product`", pid)
  933. cleanTable(ctx, conn, "`sys_user`", userId)
  934. })
  935. loader.Del(ctx, userId, pcode)
  936. ud, _ := loader.Load(ctx, userId, pcode)
  937. require.NotNil(t, ud)
  938. assert.True(t, ud.IsSuperAdmin)
  939. assert.Equal(t, consts.MemberTypeSuperAdmin, ud.MemberType)
  940. }
  941. // --------------- TC-0527: loadMembership-非成员MemberType为空 ---------------
  942. func TestLoadMembership_NonMemberEmpty(t *testing.T) {
  943. ctx := context.Background()
  944. conn := testConn()
  945. m := testModels()
  946. loader := newTestLoader()
  947. uid := uniqueId()
  948. ts := now()
  949. pcode := "p_" + uid
  950. userId := insertUser(ctx, t, m, &userModel.SysUser{
  951. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  952. Email: uid + "@test.com", Phone: "13800000020", DeptId: 0,
  953. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  954. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  955. })
  956. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  957. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  958. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  959. })
  960. t.Cleanup(func() {
  961. loader.Del(ctx, userId, pcode)
  962. cleanTable(ctx, conn, "`sys_product`", pid)
  963. cleanTable(ctx, conn, "`sys_user`", userId)
  964. })
  965. loader.Del(ctx, userId, pcode)
  966. ud, _ := loader.Load(ctx, userId, pcode)
  967. require.NotNil(t, ud)
  968. assert.False(t, ud.IsSuperAdmin)
  969. assert.Empty(t, ud.MemberType)
  970. }
  971. // --------------- TC-0520: loadPerms-用户ALLOW权限不跨产品泄漏(修复验证) ---------------
  972. func TestLoadPerms_CrossProductPermIsolation(t *testing.T) {
  973. ctx := context.Background()
  974. conn := testConn()
  975. m := testModels()
  976. loader := newTestLoader()
  977. uid := uniqueId()
  978. ts := now()
  979. pcodeA := "pA_" + uid
  980. pcodeB := "pB_" + uid
  981. userId := insertUser(ctx, t, m, &userModel.SysUser{
  982. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  983. Email: uid + "@test.com", Phone: "13800000030", DeptId: 0,
  984. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  985. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  986. })
  987. pidA := insertProduct(ctx, t, m, &productModel.SysProduct{
  988. Code: pcodeA, Name: "prodA_" + uid, AppKey: "akA_" + uid, AppSecret: "asA_" + uid,
  989. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  990. })
  991. pidB := insertProduct(ctx, t, m, &productModel.SysProduct{
  992. Code: pcodeB, Name: "prodB_" + uid, AppKey: "akB_" + uid, AppSecret: "asB_" + uid,
  993. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  994. })
  995. memA := insertMember(ctx, t, m, &memberModel.SysProductMember{
  996. ProductCode: pcodeA, UserId: userId, MemberType: consts.MemberTypeMember,
  997. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  998. })
  999. memB := insertMember(ctx, t, m, &memberModel.SysProductMember{
  1000. ProductCode: pcodeB, UserId: userId, MemberType: consts.MemberTypeMember,
  1001. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1002. })
  1003. permA := insertPerm(ctx, t, m, &permModel.SysPerm{
  1004. ProductCode: pcodeA, Name: "permA_" + uid, Code: "permA:" + uid,
  1005. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1006. })
  1007. permB := insertPerm(ctx, t, m, &permModel.SysPerm{
  1008. ProductCode: pcodeB, Name: "permB_" + uid, Code: "permB:" + uid,
  1009. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1010. })
  1011. upA := insertUserPerm(ctx, t, m, &userPermModel.SysUserPerm{
  1012. UserId: userId, PermId: permA, Effect: consts.PermEffectAllow,
  1013. CreateTime: ts, UpdateTime: ts,
  1014. })
  1015. upB := insertUserPerm(ctx, t, m, &userPermModel.SysUserPerm{
  1016. UserId: userId, PermId: permB, Effect: consts.PermEffectAllow,
  1017. CreateTime: ts, UpdateTime: ts,
  1018. })
  1019. t.Cleanup(func() {
  1020. loader.Del(ctx, userId, pcodeA)
  1021. loader.Del(ctx, userId, pcodeB)
  1022. cleanTable(ctx, conn, "`sys_user_perm`", upA, upB)
  1023. cleanTable(ctx, conn, "`sys_perm`", permA, permB)
  1024. cleanTable(ctx, conn, "`sys_product_member`", memA, memB)
  1025. cleanTable(ctx, conn, "`sys_product`", pidA, pidB)
  1026. cleanTable(ctx, conn, "`sys_user`", userId)
  1027. })
  1028. loader.Del(ctx, userId, pcodeA)
  1029. udA, _ := loader.Load(ctx, userId, pcodeA)
  1030. require.NotNil(t, udA)
  1031. assert.Contains(t, udA.Perms, "permA:"+uid, "产品A应包含自身权限")
  1032. assert.NotContains(t, udA.Perms, "permB:"+uid, "产品A不应包含产品B的权限")
  1033. loader.Del(ctx, userId, pcodeB)
  1034. udB, _ := loader.Load(ctx, userId, pcodeB)
  1035. require.NotNil(t, udB)
  1036. assert.Contains(t, udB.Perms, "permB:"+uid, "产品B应包含自身权限")
  1037. assert.NotContains(t, udB.Perms, "permA:"+uid, "产品B不应包含产品A的权限")
  1038. }
  1039. // --------------- TC-0528: loadMembership-禁用成员MemberType为空(修复验证) ---------------
  1040. func TestLoadMembership_DisabledMemberEmpty(t *testing.T) {
  1041. ctx := context.Background()
  1042. conn := testConn()
  1043. m := testModels()
  1044. loader := newTestLoader()
  1045. uid := uniqueId()
  1046. ts := now()
  1047. pcode := "p_" + uid
  1048. userId := insertUser(ctx, t, m, &userModel.SysUser{
  1049. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  1050. Email: uid + "@test.com", Phone: "13800000031", DeptId: 0,
  1051. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  1052. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1053. })
  1054. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  1055. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  1056. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1057. })
  1058. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  1059. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeMember,
  1060. Status: consts.StatusDisabled, CreateTime: ts, UpdateTime: ts,
  1061. })
  1062. t.Cleanup(func() {
  1063. loader.Del(ctx, userId, pcode)
  1064. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  1065. cleanTable(ctx, conn, "`sys_product`", pid)
  1066. cleanTable(ctx, conn, "`sys_user`", userId)
  1067. })
  1068. loader.Del(ctx, userId, pcode)
  1069. ud, _ := loader.Load(ctx, userId, pcode)
  1070. require.NotNil(t, ud)
  1071. assert.Empty(t, ud.MemberType, "禁用成员的MemberType应为空")
  1072. }
  1073. // --------------- TC-0521: loadPerms-DEV部门禁用后不再拥有全部权限(修复验证) ---------------
  1074. func TestLoadPerms_DisabledDevDeptNoFullPerms(t *testing.T) {
  1075. ctx := context.Background()
  1076. conn := testConn()
  1077. m := testModels()
  1078. loader := newTestLoader()
  1079. uid := uniqueId()
  1080. ts := now()
  1081. pcode := "p_" + uid
  1082. deptId := insertDept(ctx, t, m, &deptModel.SysDept{
  1083. ParentId: 0, Name: "devdept_disabled_" + uid, Path: "/1/", Sort: 1,
  1084. DeptType: consts.DeptTypeDev, Status: consts.StatusDisabled,
  1085. CreateTime: ts, UpdateTime: ts,
  1086. })
  1087. userId := insertUser(ctx, t, m, &userModel.SysUser{
  1088. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  1089. Email: uid + "@test.com", Phone: "13800000032", DeptId: deptId,
  1090. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  1091. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1092. })
  1093. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  1094. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  1095. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1096. })
  1097. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  1098. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeMember,
  1099. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1100. })
  1101. permCode := "perm_devtest:" + uid
  1102. permId := insertPerm(ctx, t, m, &permModel.SysPerm{
  1103. ProductCode: pcode, Name: "p_" + uid, Code: permCode,
  1104. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1105. })
  1106. t.Cleanup(func() {
  1107. loader.Del(ctx, userId, pcode)
  1108. cleanTable(ctx, conn, "`sys_perm`", permId)
  1109. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  1110. cleanTable(ctx, conn, "`sys_product`", pid)
  1111. cleanTable(ctx, conn, "`sys_user`", userId)
  1112. cleanTable(ctx, conn, "`sys_dept`", deptId)
  1113. })
  1114. loader.Del(ctx, userId, pcode)
  1115. ud, _ := loader.Load(ctx, userId, pcode)
  1116. require.NotNil(t, ud)
  1117. assert.Equal(t, consts.DeptTypeDev, ud.DeptType)
  1118. assert.Equal(t, int64(consts.StatusDisabled), ud.DeptStatus)
  1119. assert.Empty(t, ud.Perms, "禁用的DEV部门成员不应拥有全部权限")
  1120. }
  1121. // ---------------------------------------------------------------------------
  1122. // audit 回归:DEV 部门用户即使 dept.status=Enabled,
  1123. // 一旦产品成员被禁用 (MemberType 清空),也不得继续获得全量权限。
  1124. // ---------------------------------------------------------------------------
  1125. // TC-0704: DEV 部门 + 产品成员已禁用 → 不应获得全量权限
  1126. func TestLoadPerms_DevDept_DisabledMember_NoFullPerms(t *testing.T) {
  1127. ctx := context.Background()
  1128. conn := testConn()
  1129. m := testModels()
  1130. loader := newTestLoader()
  1131. uid := uniqueId()
  1132. ts := now()
  1133. pcode := "p_" + uid
  1134. // DEV 部门本身启用
  1135. deptId := insertDept(ctx, t, m, &deptModel.SysDept{
  1136. ParentId: 0, Name: "devdept_h3_" + uid, Path: "/1/", Sort: 1,
  1137. DeptType: consts.DeptTypeDev, Status: consts.StatusEnabled,
  1138. CreateTime: ts, UpdateTime: ts,
  1139. })
  1140. userId := insertUser(ctx, t, m, &userModel.SysUser{
  1141. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  1142. Email: uid + "@test.com", Phone: "13800099901", DeptId: deptId,
  1143. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  1144. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1145. })
  1146. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  1147. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  1148. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1149. })
  1150. // 关键:产品成员被禁用 (Status=2)
  1151. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  1152. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeMember,
  1153. Status: consts.StatusDisabled, CreateTime: ts, UpdateTime: ts,
  1154. })
  1155. permCode := "perm_h3:" + uid
  1156. permId := insertPerm(ctx, t, m, &permModel.SysPerm{
  1157. ProductCode: pcode, Name: "p_" + uid, Code: permCode,
  1158. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1159. })
  1160. t.Cleanup(func() {
  1161. loader.Del(ctx, userId, pcode)
  1162. cleanTable(ctx, conn, "`sys_perm`", permId)
  1163. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  1164. cleanTable(ctx, conn, "`sys_product`", pid)
  1165. cleanTable(ctx, conn, "`sys_user`", userId)
  1166. cleanTable(ctx, conn, "`sys_dept`", deptId)
  1167. })
  1168. loader.Del(ctx, userId, pcode)
  1169. ud, _ := loader.Load(ctx, userId, pcode)
  1170. require.NotNil(t, ud)
  1171. // 部门信息正常载入
  1172. assert.Equal(t, consts.DeptTypeDev, ud.DeptType)
  1173. assert.Equal(t, int64(consts.StatusEnabled), ud.DeptStatus)
  1174. // 关键:禁用的产品成员,MemberType 被清空
  1175. assert.Equal(t, "", ud.MemberType, "禁用产品成员的 MemberType 应被清空")
  1176. // 关键:DEV 部门 + MemberType='' → 修复后不再命中全量权限分支
  1177. assert.Empty(t, ud.Perms,
  1178. "产品成员被禁用的 DEV 部门用户不应再被授予全量权限")
  1179. }
  1180. // ---------------------------------------------------------------------------
  1181. // audit 回归:当用户不存在时,Load 不应缓存零值 UserDetails
  1182. // ---------------------------------------------------------------------------
  1183. // TC-0705: Load 不存在用户时应返回 nil 且不在 Redis 中留下空缓存
  1184. func TestLoad_NonExistentUser_NotCached(t *testing.T) {
  1185. ctx := context.Background()
  1186. loader := newTestLoader()
  1187. nonExistentUserId := int64(999999999)
  1188. pcode := "p_" + uniqueId()
  1189. // 预先确保缓存中没有该 key
  1190. loader.Del(ctx, nonExistentUserId, pcode)
  1191. ud, _ := loader.Load(ctx, nonExistentUserId, pcode)
  1192. // 按当前实现,Load 返回的是 ud(可能是 nil 或零值的 UserDetails),调用方通过 ud.Username == "" 判定不存在。
  1193. // 的关键断言:不论返回什么,Redis 里必须没有缓存的 key(即下次 Load 依然走 DB)
  1194. // 通过再读一次 Redis 判定:间接用 loader.Del 的 key 规则读取
  1195. // 这里简化为:第二次 Load 依然必须从 DB 查询(不能命中缓存)
  1196. // 验证方式:调用 Del 不报错 + 再次 Load 也应得到空 Username
  1197. if ud != nil {
  1198. assert.Empty(t, ud.Username, "不存在用户返回的 ud 必须是空 Username")
  1199. }
  1200. ud2, _ := loader.Load(ctx, nonExistentUserId, pcode)
  1201. if ud2 != nil {
  1202. assert.Empty(t, ud2.Username)
  1203. }
  1204. }
  1205. func TestCleanByUserIds_WipesAllUserProductKeysAndIndexes(t *testing.T) {
  1206. rds := testRedis()
  1207. loader := newTestLoader()
  1208. ctx := context.Background()
  1209. type cell struct {
  1210. uid int64
  1211. pc string
  1212. }
  1213. cells := []cell{
  1214. {1000001, "pcX"}, {1000001, "pcY"},
  1215. {1000002, "pcX"}, {1000002, "pcY"},
  1216. {1000003, "pcX"}, {1000003, "pcY"},
  1217. }
  1218. // 预埋缓存:每个 cell 写一条 value 到 cacheKey,并 SADD 到 user / product 索引。
  1219. cacheKeys := make([]string, 0, len(cells))
  1220. for _, c := range cells {
  1221. ck := loader.cacheKey(c.uid, c.pc)
  1222. require.NoError(t, rds.SetCtx(ctx, ck, "dummy"))
  1223. _, _ = rds.SaddCtx(ctx, loader.userIndexKey(c.uid), ck)
  1224. _, _ = rds.SaddCtx(ctx, loader.productIndexKey(c.pc), ck)
  1225. cacheKeys = append(cacheKeys, ck)
  1226. }
  1227. // 调用 CleanByUserIds 触发 SUNION + 批 DEL。
  1228. loader.CleanByUserIds(ctx, []int64{1000001, 1000002, 1000003})
  1229. // 6 条 ud: key 必须全消失。
  1230. for _, ck := range cacheKeys {
  1231. exist, err := rds.ExistsCtx(ctx, ck)
  1232. require.NoError(t, err)
  1233. assert.False(t, exist, "cacheKey %s 必须被清理", ck)
  1234. }
  1235. // 3 条 user 索引 key 必须也被清掉(否则会漏缓存)。
  1236. for _, uid := range []int64{1000001, 1000002, 1000003} {
  1237. exist, err := rds.ExistsCtx(ctx, loader.userIndexKey(uid))
  1238. require.NoError(t, err)
  1239. assert.False(t, exist,
  1240. "user 索引集合必须被 DEL,否则下次 Clean 会复活假指针")
  1241. }
  1242. // 清理 product 索引残留(修复 SLA 不负责 product 索引,其残留 key 已在 user 索引里一并清掉
  1243. // 的那一组;但为了测试幂等性,手动 cleanup)。
  1244. t.Cleanup(func() {
  1245. _, _ = rds.DelCtx(ctx, loader.productIndexKey("pcX"), loader.productIndexKey("pcY"))
  1246. })
  1247. }
  1248. // TC-0847: 空 ids 切片必须直接返回,不打 Redis。
  1249. // 如果源码退化成把空 SUNION 交给 Redis,会收到 "SUNION wrong number of arguments" 错误;
  1250. // 我们通过断言 Redis 未产生任何错误以及函数未 panic 来验证。
  1251. func TestCleanByUserIds_EmptyIds_NoOp(t *testing.T) {
  1252. loader := newTestLoader()
  1253. // 只要不 panic、返回即可;如果源码 foundation 有 wrong-args 会 logx.Errorf 输出,
  1254. // 这里做最小断言:调用返回控制权。
  1255. loader.CleanByUserIds(context.Background(), nil)
  1256. loader.CleanByUserIds(context.Background(), []int64{})
  1257. // 若走到了 SUNION 分支,Redis 会在 wrong-args 下被 logx 记 Errorf,
  1258. // 业务回调仍然返回,此时不应 panic;通过到达本行说明 OK。
  1259. }
  1260. func TestUserDetailsLoader_MN2_BatchDelClearsUserAndProductIndexes(t *testing.T) {
  1261. ctx := context.Background()
  1262. conn := testConn()
  1263. m := testModels()
  1264. loader := newTestLoader()
  1265. rds := testRedis()
  1266. ts := now()
  1267. pcode := "mn2_" + uniqueId()
  1268. // 插入两个用户 + 一个真实产品,确保 Load 走到 5 分钟正缓存分支并注册索引
  1269. uid1 := uniqueId()
  1270. uid2 := uniqueId()
  1271. userId1 := insertUser(ctx, t, m, &userModel.SysUser{
  1272. Username: uid1, Password: hashPwd("pass123"), Nickname: "nick_" + uid1,
  1273. Email: uid1 + "@t.com", Phone: "13800000008", DeptId: 0,
  1274. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  1275. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1276. })
  1277. userId2 := insertUser(ctx, t, m, &userModel.SysUser{
  1278. Username: uid2, Password: hashPwd("pass123"), Nickname: "nick_" + uid2,
  1279. Email: uid2 + "@t.com", Phone: "13800000009", DeptId: 0,
  1280. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  1281. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1282. })
  1283. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  1284. Code: pcode, Name: "p_" + pcode, AppKey: "ak_" + pcode, AppSecret: "as_" + pcode,
  1285. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1286. })
  1287. t.Cleanup(func() {
  1288. loader.Del(ctx, userId1, pcode)
  1289. loader.Del(ctx, userId2, pcode)
  1290. cleanTable(ctx, conn, "`sys_product`", pid)
  1291. cleanTable(ctx, conn, "`sys_user`", userId1, userId2)
  1292. })
  1293. // 把缓存一次预热,让 userIndex/productIndex 被 registerCacheKey 真实写入
  1294. _, err := loader.Load(ctx, userId1, pcode)
  1295. require.NoError(t, err)
  1296. _, err = loader.Load(ctx, userId2, pcode)
  1297. require.NoError(t, err)
  1298. k1 := loader.cacheKey(userId1, pcode)
  1299. k2 := loader.cacheKey(userId2, pcode)
  1300. pIdx := loader.productIndexKey(pcode)
  1301. u1Idx := loader.userIndexKey(userId1)
  1302. u2Idx := loader.userIndexKey(userId2)
  1303. // 预检:主 key 写入、productIndex / userIndex 存在对应元素
  1304. for _, k := range []string{k1, k2} {
  1305. val, gerr := rds.GetCtx(ctx, k)
  1306. require.NoError(t, gerr)
  1307. require.NotEmpty(t, val, "主 cacheKey 必须被写入才有意义")
  1308. }
  1309. has, _ := rds.SismemberCtx(ctx, pIdx, k1)
  1310. require.True(t, has, "productIndex 必须含 k1")
  1311. has, _ = rds.SismemberCtx(ctx, pIdx, k2)
  1312. require.True(t, has, "productIndex 必须含 k2")
  1313. has, _ = rds.SismemberCtx(ctx, u1Idx, k1)
  1314. require.True(t, has, "userIndex(u1) 必须含 k1")
  1315. has, _ = rds.SismemberCtx(ctx, u2Idx, k2)
  1316. require.True(t, has, "userIndex(u2) 必须含 k2")
  1317. // 触发被测路径:BatchDel(pipelined SREM)
  1318. loader.BatchDel(ctx, []int64{userId1, userId2}, pcode)
  1319. // 主 key 被清空(原 TC-0513 已保障)
  1320. for _, k := range []string{k1, k2} {
  1321. val, _ := rds.GetCtx(ctx, k)
  1322. assert.Empty(t, val, "BatchDel 必须删除主 cacheKey")
  1323. }
  1324. // userIndex / productIndex 中的对应 cacheKey 必须被 SREM 清除(本 TC 核心断言)
  1325. has, _ = rds.SismemberCtx(ctx, u1Idx, k1)
  1326. assert.False(t, has, "BatchDel 必须把 k1 从 userIndex(u1) SREM 出去")
  1327. has, _ = rds.SismemberCtx(ctx, u2Idx, k2)
  1328. assert.False(t, has, "BatchDel 必须把 k2 从 userIndex(u2) SREM 出去")
  1329. has, _ = rds.SismemberCtx(ctx, pIdx, k1)
  1330. assert.False(t, has, "BatchDel 必须把 k1 从 productIndex SREM 出去")
  1331. has, _ = rds.SismemberCtx(ctx, pIdx, k2)
  1332. assert.False(t, has, "BatchDel 必须把 k2 从 productIndex SREM 出去")
  1333. }
  1334. // TC-1014: productCode 为空时 BatchDel 仅 SREM userIndex,不得 panic 或误访问 productIndex。
  1335. // 目前业务侧 BatchDel 的所有调用都传了 productCode;但 pipeline 分支必须对空串 fail-safe,
  1336. // 防止未来调用方误传时 pipeline 里塞空 key 把 Redis 侧写脏。
  1337. func TestUserDetailsLoader_MN2_BatchDelEmptyProductCodeDoesNotPanic(t *testing.T) {
  1338. ctx := context.Background()
  1339. loader := newTestLoader()
  1340. // 即便 uid 不存在,pipelined SREM 对不存在的集合是 no-op,不应报错/panic
  1341. require.NotPanics(t, func() {
  1342. loader.BatchDel(ctx, []int64{9999999991, 9999999992}, "")
  1343. })
  1344. }
  1345. func TestUserDetailsLoader_Load_NotExist_ReturnsUdWithNilErr(t *testing.T) {
  1346. ctx := context.Background()
  1347. loader := newTestLoader()
  1348. nonExistId := int64(900_100_000 + time.Now().UnixNano()%100_000)
  1349. productCode := "pc_nxud_" + uniqueId()
  1350. t.Cleanup(func() { loader.Del(ctx, nonExistId, productCode) })
  1351. ud, err := loader.Load(ctx, nonExistId, productCode)
  1352. require.NoError(t, err,
  1353. "用户不存在必须走 (ud,nil) 语义;否则中间件会把 DB 抖动同化成 401 强制下线引发雪崩")
  1354. require.NotNil(t, ud)
  1355. assert.Equal(t, nonExistId, ud.UserId)
  1356. assert.Equal(t, productCode, ud.ProductCode)
  1357. assert.Empty(t, ud.Username, "Username 必须为空以便调用方判定为 404 用户")
  1358. }
  1359. // TC-0914: 并发时序:CreateUser 成功但 Load 已经走到"写负缓存哨兵"分支之前,
  1360. // 再次 FindOne 复核必须把"刚创建的用户"识别出来,跳过哨兵写入,避免新用户被投毒。
  1361. //
  1362. // 本测试构造的时序:先 Insert 一个真实用户(这步 Insert 会 DEL 用户主键缓存),
  1363. // 再立即 Load 该 userId+productCode。 的 freshCheck 必须让"这个第一 Load"拿到用户数据,
  1364. // 而不是把 ud:<id>:<pc> 写为 _NOT_FOUND_。
  1365. func TestUserDetailsLoader_Load_L6_CreateUserThenLoadDoesNotWriteSentinel(t *testing.T) {
  1366. ctx := context.Background()
  1367. loader := newTestLoader()
  1368. conn := testConn()
  1369. m := testModels()
  1370. ts := now()
  1371. uid := uniqueId()
  1372. productCode := "pc_l6_" + uid
  1373. userId := insertUser(ctx, t, m, &userModel.SysUser{
  1374. Username: uid, Password: hashPwd("pw"), Nickname: "l6",
  1375. Avatar: sql.NullString{}, IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  1376. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1377. })
  1378. // 修复后,Load 要求 productCode 对应的产品真实存在才能进入正缓存分支;否则
  1379. // loadProduct 失败会被提升为 ErrLoaderDegraded。 的主题是"新用户写入后首次 Load
  1380. // 不得被自身写的负缓存哨兵投毒",与"产品不存在"正交,因此这里补一条真实产品。
  1381. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  1382. Code: productCode, Name: "l6_prod", AppKey: "ak", AppSecret: "as",
  1383. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1384. })
  1385. t.Cleanup(func() {
  1386. loader.Del(ctx, userId, productCode)
  1387. cleanTable(ctx, conn, "`sys_user`", userId)
  1388. cleanTable(ctx, conn, "`sys_product`", pid)
  1389. })
  1390. loader.Del(ctx, userId, productCode)
  1391. ud, err := loader.Load(ctx, userId, productCode)
  1392. require.NoError(t, err)
  1393. require.NotNil(t, ud)
  1394. assert.Equal(t, uid, ud.Username, "Load 必须识别出这是真实用户而不是写哨兵")
  1395. // 关键断言:Redis key 里的值绝不能是哨兵。
  1396. val, err := loader.rds.GetCtx(ctx, loader.cacheKey(userId, productCode))
  1397. require.NoError(t, err)
  1398. assert.NotEqual(t, negativeCacheMarker, val,
  1399. "新创建的用户首次 Load 不得被写入负缓存哨兵,否则 10s 内所有请求都会被判为'已删除'")
  1400. }
  1401. // TC-0915 (重写 · ): partial load 失败必须返回 ErrLoaderDegraded(而非 (ud,nil) 半成品),
  1402. // 让调用方统一把它映射为 503 / codes.Unavailable;同时 5 分钟正缓存绝不能被写入。
  1403. //
  1404. // 历史契约:loadOk=false 时 Load 返回 (ud, nil),ud 是 Username 非空但 DeptPath=""/Perms=nil 的
  1405. // 半成品,然后 jwtauth / refreshToken / GetUserPerms 等调用方因 MemberType=="" 或
  1406. // ProductStatus!=Enabled 错把它当成"产品已被禁用 / 无权限" 返 403,一次 DB 抖动全站静默 403。
  1407. // 新契约():loadOk=false → (nil, ErrLoaderDegraded);调用方 err!=nil 分支自然映射
  1408. // 503 / codes.Unavailable,SOC 侧能明确观测到基础设施故障。
  1409. func TestUserDetailsLoader_Load_MN1_PartialLoadReturnsErrDegradedAndSkipsCache(t *testing.T) {
  1410. ctx := context.Background()
  1411. loader := newTestLoader()
  1412. conn := testConn()
  1413. m := testModels()
  1414. ts := now()
  1415. uid := uniqueId()
  1416. productCode := "pc_mn1_" + uid
  1417. // 用一个极大的 DeptId 指向不存在的部门,让 loadDept 报 ErrNotFound → loadFromDB loadOk=false。
  1418. phantomDeptId := int64(999_000_000_000)
  1419. userId := insertUser(ctx, t, m, &userModel.SysUser{
  1420. Username: uid, Password: hashPwd("pw"), Nickname: "mn1",
  1421. Avatar: sql.NullString{}, DeptId: phantomDeptId,
  1422. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  1423. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1424. })
  1425. // 给产品落一条真实数据,让 loadProduct 本身成功,单独锁定"dept 子步骤失败"这个变量。
  1426. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  1427. Code: productCode, Name: "mn1_prod", AppKey: "ak", AppSecret: "as",
  1428. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1429. })
  1430. t.Cleanup(func() {
  1431. loader.Del(ctx, userId, productCode)
  1432. cleanTable(ctx, conn, "`sys_user`", userId)
  1433. cleanTable(ctx, conn, "`sys_product`", pid)
  1434. })
  1435. loader.Del(ctx, userId, productCode)
  1436. ud, err := loader.Load(ctx, userId, productCode)
  1437. // 新契约:partial load 必须向上冒 ErrLoaderDegraded;ud 必须为 nil,避免调用方误用半成品。
  1438. require.ErrorIs(t, err, ErrLoaderDegraded,
  1439. "partial load 必须返回 ErrLoaderDegraded,而不是把半成品 ud 静默当成业务拒绝")
  1440. assert.Nil(t, ud, "err 非 nil 时 ud 必须为 nil,杜绝上层误用半成品字段")
  1441. // 断言 1:Redis 里没有 5 分钟正缓存,主 key 要么完全未写,要么仅留空串。
  1442. val, err := loader.rds.GetCtx(ctx, loader.cacheKey(userId, productCode))
  1443. require.NoError(t, err)
  1444. if val != "" {
  1445. assert.NotContains(t, val, "\"username\":\""+uid+"\"",
  1446. "partial-load 不得把半残 UD 写进 5 分钟正缓存")
  1447. }
  1448. }
  1449. // TC-0917 (新增 · ): ErrLoaderDegraded 必须是可用 errors.Is 断言的独立 sentinel,
  1450. // 供调用方在 HTTP 中间件 / gRPC 拦截器里做到"统一映射 503"而不需要字符串匹配。
  1451. func TestUserDetailsLoader_ErrLoaderDegraded_IsStableSentinel(t *testing.T) {
  1452. require.NotNil(t, ErrLoaderDegraded, "必须导出 sentinel 便于调用方识别")
  1453. // 再次发生的派生错误仍应 errors.Is 成立(防御"被包一层后调用方失配")。
  1454. wrapped := errors.New("extra: " + ErrLoaderDegraded.Error())
  1455. assert.False(t, errors.Is(wrapped, ErrLoaderDegraded),
  1456. "新 error 与 sentinel 不应共享身份;如需传染请显式 fmt.Errorf(\"%%w\", ErrLoaderDegraded)")
  1457. assert.True(t, errors.Is(ErrLoaderDegraded, ErrLoaderDegraded),
  1458. "自身 Is 必须为 true(sanity check)")
  1459. }
  1460. // TC-0916: deny 查询失败时 fail-close 保底()。通过写一个完全无 perm 的普通 MEMBER,
  1461. // 再通过 productCode 设为 disabled 让 loadPerms 走 ProductStatus != Enabled 提前返回;再切回
  1462. // Enabled 状态,确保 perm 分支被正常 reach 到,覆盖 "allowIds 查询路径正常结束" 的成功契约。
  1463. // 这里的反面(fail-close)契约已经由上面 TC-0915 的 "dept 失败不写缓存" 验证;单独断言 deny 失败
  1464. // 路径需要 mock 数据库错误,属于下一轮覆盖。
  1465. func TestUserDetailsLoader_Load_H1_EnabledProductMemberPermsNonNil(t *testing.T) {
  1466. ctx := context.Background()
  1467. loader := newTestLoader()
  1468. conn := testConn()
  1469. m := testModels()
  1470. ts := now()
  1471. uid := uniqueId()
  1472. productCode := "pc_h1_" + uid
  1473. userId := insertUser(ctx, t, m, &userModel.SysUser{
  1474. Username: uid, Password: hashPwd("pw"), Nickname: "h1",
  1475. Avatar: sql.NullString{}, DeptId: 0,
  1476. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  1477. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1478. })
  1479. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  1480. Code: productCode, Name: "h1_prod", AppKey: "ak", AppSecret: "as",
  1481. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1482. })
  1483. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  1484. ProductCode: productCode, UserId: userId, MemberType: consts.MemberTypeMember,
  1485. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1486. })
  1487. _ = memberId
  1488. t.Cleanup(func() {
  1489. loader.Del(ctx, userId, productCode)
  1490. cleanTable(ctx, conn, "`sys_user`", userId)
  1491. cleanTable(ctx, conn, "`sys_product`", pid)
  1492. cleanTableByField(ctx, conn, "`sys_product_member`", "productCode", productCode)
  1493. })
  1494. loader.Del(ctx, userId, productCode)
  1495. ud, err := loader.Load(ctx, userId, productCode)
  1496. require.NoError(t, err)
  1497. require.NotNil(t, ud)
  1498. // 这里不强制 Perms 非 nil —— 用户没有任何角色 / allow,Perms 为空 slice 或 nil 都合理;
  1499. // 重点是 Load 不返回 error、不被 deny 查询(null 结果)污染。
  1500. assert.Equal(t, uid, ud.Username)
  1501. assert.Equal(t, productCode, ud.ProductCode)
  1502. // 再次 Load 必须命中正缓存:GET 出的 value 一定是合法 JSON 且能反序列化回同样的 UD。
  1503. val, err := loader.rds.GetCtx(ctx, loader.cacheKey(userId, productCode))
  1504. require.NoError(t, err)
  1505. require.NotEmpty(t, val, "正常路径必须落正缓存")
  1506. if strings.HasPrefix(val, "{") {
  1507. var cached UserDetails
  1508. require.NoError(t, json.Unmarshal([]byte(val), &cached))
  1509. assert.Equal(t, uid, cached.Username)
  1510. }
  1511. }
  1512. func TestUserDetailsLoader_NegativeCache_HitsOnSecondCall(t *testing.T) {
  1513. ctx := context.Background()
  1514. loader := newTestLoader()
  1515. // 随便选一个几乎肯定不存在的 id(避免与真实测试数据冲突)。
  1516. nonExistId := int64(900_000_000 + time.Now().UnixNano()%100_000)
  1517. productCode := "pc_neg_" + uniqueId()
  1518. // 确保无残留缓存。
  1519. loader.Del(ctx, nonExistId, productCode)
  1520. // 第 1 次 Load:预期回写负缓存哨兵。
  1521. // 后 Load 的返回契约从 *UserDetails 扩展为 (*UserDetails, error);
  1522. // 不存在用户走的是 (ud, nil) 语义 (ud.Username == ""),而不是 (nil, err)。
  1523. ud1, err := loader.Load(ctx, nonExistId, productCode)
  1524. require.NoError(t, err, "用户不存在应走 (ud,nil) 语义而不是 (nil,err)")
  1525. require.NotNil(t, ud1)
  1526. assert.Empty(t, ud1.Username, "不存在的用户 Load 后 Username 必须为空")
  1527. // 直接读 Redis,验证哨兵值真的写进去了。
  1528. key := loader.cacheKey(nonExistId, productCode)
  1529. val, err := loader.rds.GetCtx(ctx, key)
  1530. require.NoError(t, err)
  1531. assert.Equal(t, negativeCacheMarker, val,
  1532. "不存在的用户必须写入负缓存哨兵 %q,以便后续命中直接返回空 UserDetails", negativeCacheMarker)
  1533. // 第 2 次 Load:必须命中哨兵分支;哨兵应当返回空 UserDetails(Username 依然为空),
  1534. // 且不得再做 DB 查询(这里没有 mock DB counter,但结果的契约仍然成立)。
  1535. ud2, err := loader.Load(ctx, nonExistId, productCode)
  1536. require.NoError(t, err)
  1537. require.NotNil(t, ud2)
  1538. assert.Empty(t, ud2.Username)
  1539. assert.Equal(t, nonExistId, ud2.UserId)
  1540. assert.Equal(t, productCode, ud2.ProductCode)
  1541. // TTL 必须 > 0 且 <= negativeCacheTTL,说明负缓存是短 TTL,不会长期遮蔽刚刚被重建的用户。
  1542. ttl, err := loader.rds.TtlCtx(ctx, key)
  1543. require.NoError(t, err)
  1544. assert.Greater(t, ttl, 0, "负缓存必须是带 TTL 的短窗口")
  1545. assert.LessOrEqual(t, ttl, negativeCacheTTL,
  1546. "负缓存 TTL 不得超过 %ds,避免误伤刚 createUser 的合法用户", negativeCacheTTL)
  1547. t.Cleanup(func() { loader.Del(ctx, nonExistId, productCode) })
  1548. }
  1549. // TC-0822: 负缓存必须"不挂到 userIndex/productIndex 集合里",
  1550. // 否则 CleanByProduct / Clean 在 DEL 其它真实 key 的同时会顺带 DEL 哨兵,带来短暂"放穿"。
  1551. // 该测试验证:写入负缓存之后,userIndex/productIndex 集合为空。
  1552. func TestUserDetailsLoader_NegativeCache_NotIndexed(t *testing.T) {
  1553. ctx := context.Background()
  1554. loader := newTestLoader()
  1555. nonExistId := int64(900_000_123 + time.Now().UnixNano()%10_000)
  1556. productCode := "pc_idx_" + uniqueId()
  1557. loader.Del(ctx, nonExistId, productCode)
  1558. _, _ = loader.Load(ctx, nonExistId, productCode)
  1559. uidx, err := loader.rds.SmembersCtx(ctx, loader.userIndexKey(nonExistId))
  1560. require.NoError(t, err)
  1561. assert.Empty(t, uidx,
  1562. "负缓存不得注册到 user index,否则 Clean(userId) 会把哨兵一起抹掉导致立刻再次击穿 DB")
  1563. pidx, err := loader.rds.SmembersCtx(ctx, loader.productIndexKey(productCode))
  1564. require.NoError(t, err)
  1565. assert.Empty(t, pidx,
  1566. "负缓存同样不得进入 product index")
  1567. t.Cleanup(func() { loader.Del(ctx, nonExistId, productCode) })
  1568. }
  1569. // TC-0823: 多并发同一 nonExistId 只穿透 DB 一次(singleflight + 负缓存联动)。
  1570. // 使用 singleflight 组 + 负缓存的组合应保证:N 个并发 Load 对同一个不存在用户在第一次完成后,
  1571. // 后续都走哨兵命中;即便 singleflight 窗口内共享同一 DB 查询,对 DB 的压力也至多 1 次。
  1572. // 这里我们无法直接计数 DB 调用(没有 DB mock 接入 loader),因此用对 key 的最终 GET 值来验证
  1573. // 最终状态是哨兵,并且 Load 耗时稳定(不会因每次都查 DB 出现显著抖动)。
  1574. func TestUserDetailsLoader_NegativeCache_ConcurrentLoadsStabilize(t *testing.T) {
  1575. ctx := context.Background()
  1576. loader := newTestLoader()
  1577. nonExistId := int64(900_000_456 + time.Now().UnixNano()%10_000)
  1578. productCode := "pc_conc_" + uniqueId()
  1579. loader.Del(ctx, nonExistId, productCode)
  1580. const N = 32
  1581. var done int32
  1582. ch := make(chan struct{})
  1583. for i := 0; i < N; i++ {
  1584. go func() {
  1585. defer func() {
  1586. if atomic.AddInt32(&done, 1) == N {
  1587. close(ch)
  1588. }
  1589. }()
  1590. _, _ = loader.Load(ctx, nonExistId, productCode)
  1591. }()
  1592. }
  1593. select {
  1594. case <-ch:
  1595. case <-time.After(5 * time.Second):
  1596. t.Fatal("并发 Load 未在 5s 内收敛,singleflight/负缓存可能失效")
  1597. }
  1598. val, err := loader.rds.GetCtx(ctx, loader.cacheKey(nonExistId, productCode))
  1599. require.NoError(t, err)
  1600. assert.Equal(t, negativeCacheMarker, val)
  1601. t.Cleanup(func() { loader.Del(ctx, nonExistId, productCode) })
  1602. }
  1603. type countingUserModel struct {
  1604. userModel.SysUserModel
  1605. findOneHits int64
  1606. }
  1607. func (c *countingUserModel) FindOne(ctx context.Context, id int64) (*userModel.SysUser, error) {
  1608. atomic.AddInt64(&c.findOneHits, 1)
  1609. return c.SysUserModel.FindOne(ctx, id)
  1610. }
  1611. // TC-0792: 延伸 —— UserDetailsLoader 必须用 singleflight 合并同一 key 的并发 Load,
  1612. // 保证缓存 miss 时 DB 只被打一次, 防止冷启动/缓存击穿。
  1613. // 实现方式: 用 countingUserModel 拦截 SysUserModel.FindOne, 断言 N 个并发 Load
  1614. // 触发的 FindOne 次数远少于 N (严格来说, 在我们控制的并发时序下必须恰好 1 次)。
  1615. // 为避免 "第一个 goroutine 太快, 写完缓存后其他 goroutine 走 cache 路径也只是少调用"
  1616. // 这种"假阳性平局", 本用例刻意先 Del 缓存 + 用 WaitGroup barrier 同时释放所有 goroutine,
  1617. // 把所有 goroutine 都塞进 singleflight.Do 的同一 key flight 里。
  1618. func TestLoader_Load_SingleflightCollapsesConcurrentCalls(t *testing.T) {
  1619. ctx := context.Background()
  1620. rds := testRedis()
  1621. realModels := testModels()
  1622. counting := &countingUserModel{SysUserModel: realModels.SysUserModel}
  1623. // 替换 models 里的 SysUserModel 为计数包装; 其他模型保持真实以便 loader 的产品/成员/部门/角色/权限流转能跑通
  1624. wrappedModels := *realModels
  1625. wrappedModels.SysUserModel = counting
  1626. loader := NewUserDetailsLoader(rds, testKeyPrefix, &wrappedModels)
  1627. u := &userModel.SysUser{
  1628. Username: "ld_sf_" + uniqueId(), Password: hashPwd("x"), Nickname: "sf",
  1629. Avatar: sql.NullString{}, IsSuperAdmin: consts.IsSuperAdminNo,
  1630. MustChangePassword: consts.MustChangePasswordNo, Status: consts.StatusEnabled,
  1631. CreateTime: now(), UpdateTime: now(),
  1632. }
  1633. userId := insertUser(ctx, t, realModels, u)
  1634. t.Cleanup(func() { cleanTable(ctx, testConn(), "sys_user", userId) })
  1635. // 确保缓存为空
  1636. loader.Del(ctx, userId, "")
  1637. loader.Clean(ctx, userId)
  1638. const workers = 50
  1639. var (
  1640. wg sync.WaitGroup
  1641. start = make(chan struct{})
  1642. ptrs = make([]*UserDetails, workers)
  1643. )
  1644. for i := 0; i < workers; i++ {
  1645. wg.Add(1)
  1646. go func(idx int) {
  1647. defer wg.Done()
  1648. <-start
  1649. ud, _ := loader.Load(ctx, userId, "")
  1650. ptrs[idx] = ud
  1651. }(i)
  1652. }
  1653. close(start)
  1654. wg.Wait()
  1655. // 每个 goroutine 都应拿到完整的用户数据
  1656. for i, p := range ptrs {
  1657. require.NotNil(t, p, "worker %d 返回 nil", i)
  1658. assert.Equal(t, u.Username, p.Username, "worker %d 读到的 Username 错乱", i)
  1659. }
  1660. hits := atomic.LoadInt64(&counting.findOneHits)
  1661. assert.LessOrEqual(t, hits, int64(workers/5),
  1662. "singleflight 必须把 DB 命中压到极少次 (远低于 workers=%d); 实际 FindOne 被调 %d 次", workers, hits)
  1663. assert.Greater(t, hits, int64(0), "至少要有一次 DB 命中 (否则说明缓存未被真正清空)")
  1664. }
  1665. // TC-0793: 延伸 —— 第二波 Load 必须命中缓存, FindOne 不再增加。
  1666. // 这是对 TC-0762 的成对断言: singleflight 合并仅作用于"同一飞行中的并发",
  1667. // 而一旦首次加载完成并写入 Redis, 后续读取应进入 cache fast-path 而非再次走 DB。
  1668. func TestLoader_Load_SecondRoundHitsCache(t *testing.T) {
  1669. ctx := context.Background()
  1670. rds := testRedis()
  1671. realModels := testModels()
  1672. counting := &countingUserModel{SysUserModel: realModels.SysUserModel}
  1673. wrappedModels := *realModels
  1674. wrappedModels.SysUserModel = counting
  1675. loader := NewUserDetailsLoader(rds, testKeyPrefix, &wrappedModels)
  1676. u := &userModel.SysUser{
  1677. Username: "ld_sf2_" + uniqueId(), Password: hashPwd("x"), Nickname: "sf2",
  1678. Avatar: sql.NullString{}, IsSuperAdmin: consts.IsSuperAdminNo,
  1679. MustChangePassword: consts.MustChangePasswordNo, Status: consts.StatusEnabled,
  1680. CreateTime: now(), UpdateTime: now(),
  1681. }
  1682. userId := insertUser(ctx, t, realModels, u)
  1683. t.Cleanup(func() { cleanTable(ctx, testConn(), "sys_user", userId) })
  1684. loader.Del(ctx, userId, "")
  1685. loader.Clean(ctx, userId)
  1686. _, _ = loader.Load(ctx, userId, "")
  1687. firstHits := atomic.LoadInt64(&counting.findOneHits)
  1688. require.Equal(t, int64(1), firstHits, "首次 Load 应命中 DB 一次")
  1689. for i := 0; i < 20; i++ {
  1690. _, _ = loader.Load(ctx, userId, "")
  1691. }
  1692. secondRoundHits := atomic.LoadInt64(&counting.findOneHits) - firstHits
  1693. assert.Equal(t, int64(0), secondRoundHits,
  1694. "后续 Load 必须命中 Redis 缓存; 若持续打到 DB, 说明 cache 写入失败或 TTL 异常")
  1695. }