auditFixes_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. package member
  2. import (
  3. "database/sql"
  4. "errors"
  5. "testing"
  6. "time"
  7. productModel "perms-system-server/internal/model/product"
  8. memberModel "perms-system-server/internal/model/productmember"
  9. userModel "perms-system-server/internal/model/user"
  10. "perms-system-server/internal/response"
  11. "perms-system-server/internal/svc"
  12. "perms-system-server/internal/testutil"
  13. "perms-system-server/internal/testutil/ctxhelper"
  14. "perms-system-server/internal/types"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/require"
  17. )
  18. // strPtr / int64Ptr 是 L-R11-1 后 UpdateMemberReq.MemberType / Status 指针化的 helper。
  19. // 若 nil 表示不改该字段,两者都 nil 会被 Logic 400。
  20. func strPtr(s string) *string { return &s }
  21. type seededProduct struct {
  22. code string
  23. pId int64
  24. uId int64
  25. mId int64
  26. admin int64 // 成员 id 当该成员为 ADMIN
  27. }
  28. // seedEnabledProductWithMember 创建 enabled product + user + product_member(memberType 指定)
  29. func seedEnabledProductWithMember(t *testing.T, svcCtx *svc.ServiceContext, memberType string) seededProduct {
  30. t.Helper()
  31. ctx := ctxhelper.SuperAdminCtx()
  32. now := time.Now().Unix()
  33. code := testutil.UniqueId()
  34. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  35. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  36. Status: 1, CreateTime: now, UpdateTime: now,
  37. })
  38. require.NoError(t, err)
  39. pId, _ := pRes.LastInsertId()
  40. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  41. Username: code, Password: testutil.HashPassword("pw"), Nickname: "n",
  42. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  43. Status: 1, CreateTime: now, UpdateTime: now,
  44. })
  45. require.NoError(t, err)
  46. uId, _ := uRes.LastInsertId()
  47. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  48. ProductCode: code, UserId: uId, MemberType: memberType,
  49. Status: 1, CreateTime: now, UpdateTime: now,
  50. })
  51. require.NoError(t, err)
  52. mId, _ := mRes.LastInsertId()
  53. return seededProduct{code: code, pId: pId, uId: uId, mId: mId, admin: mId}
  54. }
  55. func cleanupSeeded(t *testing.T, svcCtx *svc.ServiceContext, sp seededProduct) {
  56. t.Helper()
  57. ctx := ctxhelper.SuperAdminCtx()
  58. conn := testutil.GetTestSqlConn()
  59. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", sp.code)
  60. testutil.CleanTable(ctx, conn, "`sys_user`", sp.uId)
  61. testutil.CleanTable(ctx, conn, "`sys_product`", sp.pId)
  62. }
  63. // TC-0723: H-4 修复:不能移除产品最后一个 ADMIN
  64. func TestRemoveMember_LastAdminRejected(t *testing.T) {
  65. ctx := ctxhelper.SuperAdminCtx()
  66. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  67. sp := seededProduct{code: testutil.UniqueId()}
  68. now := time.Now().Unix()
  69. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  70. Code: sp.code, Name: "p_" + sp.code, AppKey: sp.code + "_k", AppSecret: "s",
  71. Status: 1, CreateTime: now, UpdateTime: now,
  72. })
  73. require.NoError(t, err)
  74. sp.pId, _ = pRes.LastInsertId()
  75. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  76. Username: sp.code, Password: testutil.HashPassword("pw"),
  77. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  78. Status: 1, CreateTime: now, UpdateTime: now,
  79. })
  80. require.NoError(t, err)
  81. sp.uId, _ = uRes.LastInsertId()
  82. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  83. ProductCode: sp.code, UserId: sp.uId, MemberType: "ADMIN",
  84. Status: 1, CreateTime: now, UpdateTime: now,
  85. })
  86. require.NoError(t, err)
  87. sp.mId, _ = mRes.LastInsertId()
  88. t.Cleanup(func() { cleanupSeeded(t, svcCtx, sp) })
  89. logic := NewRemoveMemberLogic(ctx, svcCtx)
  90. err = logic.RemoveMember(&types.RemoveMemberReq{Id: sp.mId})
  91. require.Error(t, err)
  92. var ce *response.CodeError
  93. require.True(t, errors.As(err, &ce))
  94. assert.Equal(t, 400, ce.Code())
  95. assert.Contains(t, ce.Error(), "最后一个管理员")
  96. m, ferr := svcCtx.SysProductMemberModel.FindOne(ctx, sp.mId)
  97. require.NoError(t, ferr, "ADMIN 必须仍然存在")
  98. assert.Equal(t, "ADMIN", m.MemberType)
  99. }
  100. // TC-0724: 存在 >=2 个 ADMIN 时可以移除其中一个
  101. func TestRemoveMember_AdminNotLast_Allowed(t *testing.T) {
  102. ctx := ctxhelper.SuperAdminCtx()
  103. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  104. conn := testutil.GetTestSqlConn()
  105. now := time.Now().Unix()
  106. code := testutil.UniqueId()
  107. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  108. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  109. Status: 1, CreateTime: now, UpdateTime: now,
  110. })
  111. require.NoError(t, err)
  112. pId, _ := pRes.LastInsertId()
  113. var uIds, mIds []int64
  114. for i := 0; i < 2; i++ {
  115. uid := testutil.UniqueId() + "_a"
  116. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  117. Username: uid, Password: testutil.HashPassword("pw"),
  118. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  119. Status: 1, CreateTime: now, UpdateTime: now,
  120. })
  121. require.NoError(t, err)
  122. uId, _ := uRes.LastInsertId()
  123. uIds = append(uIds, uId)
  124. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  125. ProductCode: code, UserId: uId, MemberType: "ADMIN",
  126. Status: 1, CreateTime: now, UpdateTime: now,
  127. })
  128. require.NoError(t, err)
  129. mId, _ := mRes.LastInsertId()
  130. mIds = append(mIds, mId)
  131. }
  132. t.Cleanup(func() {
  133. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  134. testutil.CleanTable(ctx, conn, "`sys_user`", uIds...)
  135. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  136. })
  137. err = NewRemoveMemberLogic(ctx, svcCtx).RemoveMember(&types.RemoveMemberReq{Id: mIds[0]})
  138. require.NoError(t, err)
  139. _, err = svcCtx.SysProductMemberModel.FindOne(ctx, mIds[0])
  140. require.Error(t, err)
  141. _, err = svcCtx.SysProductMemberModel.FindOne(ctx, mIds[1])
  142. require.NoError(t, err, "另一个 ADMIN 必须保留")
  143. }
  144. // TC-0725: H-4 修复:不能将最后一个 ADMIN 降级为 MEMBER
  145. func TestUpdateMember_DemoteLastAdminRejected(t *testing.T) {
  146. ctx := ctxhelper.SuperAdminCtx()
  147. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  148. sp := seedEnabledProductWithMember(t, svcCtx, "ADMIN")
  149. t.Cleanup(func() { cleanupSeeded(t, svcCtx, sp) })
  150. err := NewUpdateMemberLogic(ctx, svcCtx).UpdateMember(&types.UpdateMemberReq{
  151. Id: sp.mId, MemberType: strPtr("MEMBER"),
  152. })
  153. require.Error(t, err)
  154. var ce *response.CodeError
  155. require.True(t, errors.As(err, &ce))
  156. assert.Equal(t, 400, ce.Code())
  157. assert.Contains(t, ce.Error(), "最后一个管理员")
  158. m, err := svcCtx.SysProductMemberModel.FindOne(ctx, sp.mId)
  159. require.NoError(t, err)
  160. assert.Equal(t, "ADMIN", m.MemberType, "MemberType 不应被改动")
  161. }
  162. // TC-0726: H-4 修复:有多个 ADMIN 时可以降级其中一个
  163. func TestUpdateMember_DemoteAdmin_WhenMultiple_Allowed(t *testing.T) {
  164. ctx := ctxhelper.SuperAdminCtx()
  165. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  166. conn := testutil.GetTestSqlConn()
  167. now := time.Now().Unix()
  168. code := testutil.UniqueId()
  169. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  170. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  171. Status: 1, CreateTime: now, UpdateTime: now,
  172. })
  173. require.NoError(t, err)
  174. pId, _ := pRes.LastInsertId()
  175. var uIds, mIds []int64
  176. for i := 0; i < 2; i++ {
  177. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  178. Username: testutil.UniqueId(), Password: testutil.HashPassword("pw"),
  179. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  180. Status: 1, CreateTime: now, UpdateTime: now,
  181. })
  182. require.NoError(t, err)
  183. uId, _ := uRes.LastInsertId()
  184. uIds = append(uIds, uId)
  185. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  186. ProductCode: code, UserId: uId, MemberType: "ADMIN",
  187. Status: 1, CreateTime: now, UpdateTime: now,
  188. })
  189. require.NoError(t, err)
  190. mId, _ := mRes.LastInsertId()
  191. mIds = append(mIds, mId)
  192. }
  193. t.Cleanup(func() {
  194. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  195. testutil.CleanTable(ctx, conn, "`sys_user`", uIds...)
  196. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  197. })
  198. err = NewUpdateMemberLogic(ctx, svcCtx).UpdateMember(&types.UpdateMemberReq{
  199. Id: mIds[0], MemberType: strPtr("MEMBER"),
  200. })
  201. require.NoError(t, err)
  202. m, err := svcCtx.SysProductMemberModel.FindOne(ctx, mIds[0])
  203. require.NoError(t, err)
  204. assert.Equal(t, "MEMBER", m.MemberType)
  205. }
  206. // TC-0727: H-4 修复:禁用状态的 ADMIN 不计入 active admin 计数,导致剩余 0 个启用 ADMIN 时仍拒绝降级
  207. // 说明:CountActiveAdmins 只统计 status=1 的 ADMIN;即便 DB 里有 2 个 ADMIN,但仅 1 个启用,
  208. // 降级这个唯一启用的 ADMIN 仍应被拒绝。
  209. func TestUpdateMember_DemoteLastActiveAdmin_Rejected(t *testing.T) {
  210. ctx := ctxhelper.SuperAdminCtx()
  211. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  212. conn := testutil.GetTestSqlConn()
  213. now := time.Now().Unix()
  214. code := testutil.UniqueId()
  215. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  216. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  217. Status: 1, CreateTime: now, UpdateTime: now,
  218. })
  219. require.NoError(t, err)
  220. pId, _ := pRes.LastInsertId()
  221. var uIds, mIds []int64
  222. statuses := []int64{1, 2} // 一个启用,一个禁用
  223. for _, st := range statuses {
  224. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  225. Username: testutil.UniqueId(), Password: testutil.HashPassword("pw"),
  226. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  227. Status: 1, CreateTime: now, UpdateTime: now,
  228. })
  229. require.NoError(t, err)
  230. uId, _ := uRes.LastInsertId()
  231. uIds = append(uIds, uId)
  232. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  233. ProductCode: code, UserId: uId, MemberType: "ADMIN",
  234. Status: st, CreateTime: now, UpdateTime: now,
  235. })
  236. require.NoError(t, err)
  237. mId, _ := mRes.LastInsertId()
  238. mIds = append(mIds, mId)
  239. }
  240. t.Cleanup(func() {
  241. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  242. testutil.CleanTable(ctx, conn, "`sys_user`", uIds...)
  243. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  244. })
  245. // 启用中的那个 ADMIN (mIds[0]) 降级应被拒绝
  246. err = NewUpdateMemberLogic(ctx, svcCtx).UpdateMember(&types.UpdateMemberReq{
  247. Id: mIds[0], MemberType: strPtr("DEVELOPER"),
  248. })
  249. require.Error(t, err)
  250. var ce *response.CodeError
  251. require.True(t, errors.As(err, &ce))
  252. assert.Equal(t, 400, ce.Code())
  253. assert.Contains(t, ce.Error(), "最后一个管理员")
  254. }
  255. // TC-0728: 移除非 ADMIN 成员不受 last-admin 保护
  256. func TestRemoveMember_NonAdmin_Unaffected(t *testing.T) {
  257. ctx := ctxhelper.SuperAdminCtx()
  258. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  259. sp := seedEnabledProductWithMember(t, svcCtx, "MEMBER")
  260. t.Cleanup(func() { cleanupSeeded(t, svcCtx, sp) })
  261. err := NewRemoveMemberLogic(ctx, svcCtx).RemoveMember(&types.RemoveMemberReq{Id: sp.mId})
  262. require.NoError(t, err)
  263. }
  264. // TC-0950: H-3 修复 —— AddMember 必须显式拒绝把 SuperAdmin 作为普通产品成员加入。
  265. // 背景:loadMembership 会把 SuperAdmin 的 MemberType 固定为 SuperAdmin 让其实际权限不受影响,
  266. // 但若 sys_product_member 里仍落一条记录,会污染审计日志 / 权限推理工具,且给产品 ADMIN
  267. // "纳管了 superadmin" 的错觉。必须在 AddMember 入口就 403。
  268. func TestAddMember_SuperAdminTargetRejected(t *testing.T) {
  269. ctx := ctxhelper.SuperAdminCtx()
  270. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  271. conn := testutil.GetTestSqlConn()
  272. now := time.Now().Unix()
  273. code := testutil.UniqueId()
  274. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  275. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  276. Status: 1, CreateTime: now, UpdateTime: now,
  277. })
  278. require.NoError(t, err)
  279. pId, _ := pRes.LastInsertId()
  280. // target 是 SuperAdmin(IsSuperAdmin=1)
  281. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  282. Username: "h3_su_" + code, Password: testutil.HashPassword("pw"),
  283. Avatar: sql.NullString{}, IsSuperAdmin: 1, MustChangePassword: 2,
  284. Status: 1, CreateTime: now, UpdateTime: now,
  285. })
  286. require.NoError(t, err)
  287. uId, _ := uRes.LastInsertId()
  288. t.Cleanup(func() {
  289. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  290. testutil.CleanTable(ctx, conn, "`sys_user`", uId)
  291. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  292. })
  293. _, err = NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
  294. ProductCode: code, UserId: uId, MemberType: "MEMBER",
  295. })
  296. require.Error(t, err, "H-3:禁止把 SuperAdmin 加入具体产品为普通成员")
  297. var ce *response.CodeError
  298. require.True(t, errors.As(err, &ce))
  299. assert.Equal(t, 403, ce.Code())
  300. assert.Contains(t, ce.Error(), "超级管理员")
  301. // DB 侧必须没有落下 SuperAdmin 的成员记录(regression:确保 AddMember 未短路在插入之后)
  302. _, findErr := svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(ctx, code, uId)
  303. require.Error(t, findErr, "SuperAdmin 不得被落入 sys_product_member")
  304. }
  305. // TC-0729: L-5 修复:禁用产品不允许添加成员
  306. func TestAddMember_DisabledProductRejected(t *testing.T) {
  307. ctx := ctxhelper.SuperAdminCtx()
  308. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  309. conn := testutil.GetTestSqlConn()
  310. now := time.Now().Unix()
  311. code := testutil.UniqueId()
  312. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  313. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  314. Status: 2, CreateTime: now, UpdateTime: now, // 禁用
  315. })
  316. require.NoError(t, err)
  317. pId, _ := pRes.LastInsertId()
  318. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  319. Username: code, Password: testutil.HashPassword("pw"),
  320. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  321. Status: 1, CreateTime: now, UpdateTime: now,
  322. })
  323. require.NoError(t, err)
  324. uId, _ := uRes.LastInsertId()
  325. t.Cleanup(func() {
  326. testutil.CleanTable(ctx, conn, "`sys_user`", uId)
  327. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  328. })
  329. _, err = NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
  330. ProductCode: code, UserId: uId, MemberType: "MEMBER",
  331. })
  332. require.Error(t, err)
  333. var ce *response.CodeError
  334. require.True(t, errors.As(err, &ce))
  335. assert.Equal(t, 400, ce.Code())
  336. assert.Contains(t, ce.Error(), "禁用")
  337. }