userDetailsLoader_test.go 78 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162
  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. }
  1696. // TC-1205: NORMAL 部门冻结(DeptStatus=Disabled)后成员 Perms 为空 []。
  1697. // loadPerms 在新增的 DeptStatus 前置检查下,NORMAL 部门被禁用后成员重登应立即无权。
  1698. func TestLoadPerms_NormalDeptDisabled_NoPerms(t *testing.T) {
  1699. ctx := context.Background()
  1700. conn := testConn()
  1701. m := testModels()
  1702. loader := newTestLoader()
  1703. uid := uniqueId()
  1704. ts := now()
  1705. pcode := "p_" + uid
  1706. deptId := insertDept(ctx, t, m, &deptModel.SysDept{
  1707. ParentId: 0, Name: "normdept_dis_" + uid, Path: "/1/", Sort: 1,
  1708. DeptType: consts.DeptTypeNormal, Status: consts.StatusDisabled,
  1709. CreateTime: ts, UpdateTime: ts,
  1710. })
  1711. userId := insertUser(ctx, t, m, &userModel.SysUser{
  1712. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  1713. Email: uid + "@test.com", Phone: "13900000001", DeptId: deptId,
  1714. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  1715. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1716. })
  1717. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  1718. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  1719. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1720. })
  1721. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  1722. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeMember,
  1723. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1724. })
  1725. permCode := "perm_normdis:" + uid
  1726. permId := insertPerm(ctx, t, m, &permModel.SysPerm{
  1727. ProductCode: pcode, Name: "p_" + uid, Code: permCode,
  1728. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1729. })
  1730. t.Cleanup(func() {
  1731. loader.Del(ctx, userId, pcode)
  1732. cleanTable(ctx, conn, "`sys_perm`", permId)
  1733. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  1734. cleanTable(ctx, conn, "`sys_product`", pid)
  1735. cleanTable(ctx, conn, "`sys_user`", userId)
  1736. cleanTable(ctx, conn, "`sys_dept`", deptId)
  1737. })
  1738. loader.Del(ctx, userId, pcode)
  1739. ud, err := loader.Load(ctx, userId, pcode)
  1740. require.NoError(t, err)
  1741. require.NotNil(t, ud)
  1742. assert.Equal(t, consts.DeptTypeNormal, ud.DeptType)
  1743. assert.Equal(t, int64(consts.StatusDisabled), ud.DeptStatus)
  1744. assert.NotNil(t, ud.Perms,
  1745. "Perms 必须是非 nil 的空 slice([]string{}),而非 nil;下游 JSON 输出必须为 [] 而非 null")
  1746. assert.Empty(t, ud.Perms,
  1747. "NORMAL 部门冻结后,成员不应拥有任何权限;冻结部门的'会话吊销'需要 loadPerms 也配合清零才能闭环")
  1748. }
  1749. // TC-1206: loadPerms 出口 Perms 恒为非 nil 数组。
  1750. // 普通成员无任何角色和附加权限时,Perms 应为 []string{} 而非 nil。
  1751. // encoding/json 对 nil slice 输出 null,对 []string{} 输出 [];两种空表达不一致会给前端带来冗余 defensive check。
  1752. func TestLoadPerms_EmptyPerms_IsNotNilSlice(t *testing.T) {
  1753. ctx := context.Background()
  1754. conn := testConn()
  1755. m := testModels()
  1756. loader := newTestLoader()
  1757. uid := uniqueId()
  1758. ts := now()
  1759. pcode := "p_" + uid
  1760. userId := insertUser(ctx, t, m, &userModel.SysUser{
  1761. Username: uid, Password: hashPwd("pass123"), Nickname: "nick_" + uid,
  1762. Email: uid + "@test.com", Phone: "13900000002", DeptId: 0,
  1763. IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: consts.MustChangePasswordNo,
  1764. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1765. })
  1766. pid := insertProduct(ctx, t, m, &productModel.SysProduct{
  1767. Code: pcode, Name: "prod_" + uid, AppKey: "ak_" + uid, AppSecret: "as_" + uid,
  1768. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1769. })
  1770. memberId := insertMember(ctx, t, m, &memberModel.SysProductMember{
  1771. ProductCode: pcode, UserId: userId, MemberType: consts.MemberTypeMember,
  1772. Status: consts.StatusEnabled, CreateTime: ts, UpdateTime: ts,
  1773. })
  1774. t.Cleanup(func() {
  1775. loader.Del(ctx, userId, pcode)
  1776. cleanTable(ctx, conn, "`sys_product_member`", memberId)
  1777. cleanTable(ctx, conn, "`sys_product`", pid)
  1778. cleanTable(ctx, conn, "`sys_user`", userId)
  1779. })
  1780. loader.Del(ctx, userId, pcode)
  1781. ud, err := loader.Load(ctx, userId, pcode)
  1782. require.NoError(t, err)
  1783. require.NotNil(t, ud)
  1784. // 关键断言:Perms 必须为非 nil 的空 slice,不能是 nil。
  1785. assert.NotNil(t, ud.Perms,
  1786. "无权限成员的 Perms 必须是 []string{}(非 nil);"+
  1787. "Go encoding/json 对 nil 输出 null,对 [] 输出 [],两种'空'造成下游 defensive check 不一致")
  1788. // 验证 JSON 序列化确实输出 []。
  1789. type wrapper struct {
  1790. Perms []string `json:"perms"`
  1791. }
  1792. jsonBytes, marshalErr := json.Marshal(wrapper{Perms: ud.Perms})
  1793. require.NoError(t, marshalErr)
  1794. jsonStr := string(jsonBytes)
  1795. assert.Contains(t, jsonStr, `"perms":[]`,
  1796. "空 Perms 序列化必须为 [],不得为 null;实际 JSON: %s", jsonStr)
  1797. }
  1798. // TC-1207: loadMembership errors.Is 语义稳健性契约测试。
  1799. // productmember.ErrNotFound = sqlx.ErrNotFound;当前代码已改为 errors.Is,确保未来 model 层包装
  1800. // 后 ErrNotFound 仍能被识别,而不会把"用户非成员"退化为 ErrLoaderDegraded 503。
  1801. func TestLoadMembership_ErrNotFound_IsStableContract(t *testing.T) {
  1802. // productmember.ErrNotFound 应等于 sqlx.ErrNotFound。
  1803. require.True(t, errors.Is(memberModel.ErrNotFound, sqlx.ErrNotFound),
  1804. "productmember.ErrNotFound 必须是 sqlx.ErrNotFound 或其包装,"+
  1805. "否则 loadMembership 的 errors.Is 检查无法识别'用户非成员'场景")
  1806. // 包装一层后 errors.Is 仍应成立——防止未来 model 层引入 fmt.Errorf("%w", err) 时失配。
  1807. wrapped := fmt.Errorf("model wrap: %w", memberModel.ErrNotFound)
  1808. require.True(t, errors.Is(wrapped, sqlx.ErrNotFound),
  1809. "单层 fmt.Errorf 包装后 errors.Is 仍须成立;若失败说明 ErrNotFound 不是通过 %%w 传播的哨兵")
  1810. }