auditFixes_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. type seededProduct struct {
  19. code string
  20. pId int64
  21. uId int64
  22. mId int64
  23. admin int64 // 成员 id 当该成员为 ADMIN
  24. }
  25. // seedEnabledProductWithMember 创建 enabled product + user + product_member(memberType 指定)
  26. func seedEnabledProductWithMember(t *testing.T, svcCtx *svc.ServiceContext, memberType string) seededProduct {
  27. t.Helper()
  28. ctx := ctxhelper.SuperAdminCtx()
  29. now := time.Now().Unix()
  30. code := testutil.UniqueId()
  31. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  32. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  33. Status: 1, CreateTime: now, UpdateTime: now,
  34. })
  35. require.NoError(t, err)
  36. pId, _ := pRes.LastInsertId()
  37. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  38. Username: code, Password: testutil.HashPassword("pw"), Nickname: "n",
  39. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  40. Status: 1, CreateTime: now, UpdateTime: now,
  41. })
  42. require.NoError(t, err)
  43. uId, _ := uRes.LastInsertId()
  44. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  45. ProductCode: code, UserId: uId, MemberType: memberType,
  46. Status: 1, CreateTime: now, UpdateTime: now,
  47. })
  48. require.NoError(t, err)
  49. mId, _ := mRes.LastInsertId()
  50. return seededProduct{code: code, pId: pId, uId: uId, mId: mId, admin: mId}
  51. }
  52. func cleanupSeeded(t *testing.T, svcCtx *svc.ServiceContext, sp seededProduct) {
  53. t.Helper()
  54. ctx := ctxhelper.SuperAdminCtx()
  55. conn := testutil.GetTestSqlConn()
  56. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", sp.code)
  57. testutil.CleanTable(ctx, conn, "`sys_user`", sp.uId)
  58. testutil.CleanTable(ctx, conn, "`sys_product`", sp.pId)
  59. }
  60. // TC-0723: H-4 修复:不能移除产品最后一个 ADMIN
  61. func TestRemoveMember_LastAdminRejected(t *testing.T) {
  62. ctx := ctxhelper.SuperAdminCtx()
  63. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  64. sp := seededProduct{code: testutil.UniqueId()}
  65. now := time.Now().Unix()
  66. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  67. Code: sp.code, Name: "p_" + sp.code, AppKey: sp.code + "_k", AppSecret: "s",
  68. Status: 1, CreateTime: now, UpdateTime: now,
  69. })
  70. require.NoError(t, err)
  71. sp.pId, _ = pRes.LastInsertId()
  72. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  73. Username: sp.code, Password: testutil.HashPassword("pw"),
  74. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  75. Status: 1, CreateTime: now, UpdateTime: now,
  76. })
  77. require.NoError(t, err)
  78. sp.uId, _ = uRes.LastInsertId()
  79. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  80. ProductCode: sp.code, UserId: sp.uId, MemberType: "ADMIN",
  81. Status: 1, CreateTime: now, UpdateTime: now,
  82. })
  83. require.NoError(t, err)
  84. sp.mId, _ = mRes.LastInsertId()
  85. t.Cleanup(func() { cleanupSeeded(t, svcCtx, sp) })
  86. logic := NewRemoveMemberLogic(ctx, svcCtx)
  87. err = logic.RemoveMember(&types.RemoveMemberReq{Id: sp.mId})
  88. require.Error(t, err)
  89. var ce *response.CodeError
  90. require.True(t, errors.As(err, &ce))
  91. assert.Equal(t, 400, ce.Code())
  92. assert.Contains(t, ce.Error(), "最后一个管理员")
  93. m, ferr := svcCtx.SysProductMemberModel.FindOne(ctx, sp.mId)
  94. require.NoError(t, ferr, "ADMIN 必须仍然存在")
  95. assert.Equal(t, "ADMIN", m.MemberType)
  96. }
  97. // TC-0724: 存在 >=2 个 ADMIN 时可以移除其中一个
  98. func TestRemoveMember_AdminNotLast_Allowed(t *testing.T) {
  99. ctx := ctxhelper.SuperAdminCtx()
  100. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  101. conn := testutil.GetTestSqlConn()
  102. now := time.Now().Unix()
  103. code := testutil.UniqueId()
  104. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  105. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  106. Status: 1, CreateTime: now, UpdateTime: now,
  107. })
  108. require.NoError(t, err)
  109. pId, _ := pRes.LastInsertId()
  110. var uIds, mIds []int64
  111. for i := 0; i < 2; i++ {
  112. uid := testutil.UniqueId() + "_a"
  113. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  114. Username: uid, Password: testutil.HashPassword("pw"),
  115. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  116. Status: 1, CreateTime: now, UpdateTime: now,
  117. })
  118. require.NoError(t, err)
  119. uId, _ := uRes.LastInsertId()
  120. uIds = append(uIds, uId)
  121. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  122. ProductCode: code, UserId: uId, MemberType: "ADMIN",
  123. Status: 1, CreateTime: now, UpdateTime: now,
  124. })
  125. require.NoError(t, err)
  126. mId, _ := mRes.LastInsertId()
  127. mIds = append(mIds, mId)
  128. }
  129. t.Cleanup(func() {
  130. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  131. testutil.CleanTable(ctx, conn, "`sys_user`", uIds...)
  132. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  133. })
  134. err = NewRemoveMemberLogic(ctx, svcCtx).RemoveMember(&types.RemoveMemberReq{Id: mIds[0]})
  135. require.NoError(t, err)
  136. _, err = svcCtx.SysProductMemberModel.FindOne(ctx, mIds[0])
  137. require.Error(t, err)
  138. _, err = svcCtx.SysProductMemberModel.FindOne(ctx, mIds[1])
  139. require.NoError(t, err, "另一个 ADMIN 必须保留")
  140. }
  141. // TC-0725: H-4 修复:不能将最后一个 ADMIN 降级为 MEMBER
  142. func TestUpdateMember_DemoteLastAdminRejected(t *testing.T) {
  143. ctx := ctxhelper.SuperAdminCtx()
  144. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  145. sp := seedEnabledProductWithMember(t, svcCtx, "ADMIN")
  146. t.Cleanup(func() { cleanupSeeded(t, svcCtx, sp) })
  147. err := NewUpdateMemberLogic(ctx, svcCtx).UpdateMember(&types.UpdateMemberReq{
  148. Id: sp.mId, MemberType: "MEMBER",
  149. })
  150. require.Error(t, err)
  151. var ce *response.CodeError
  152. require.True(t, errors.As(err, &ce))
  153. assert.Equal(t, 400, ce.Code())
  154. assert.Contains(t, ce.Error(), "最后一个管理员")
  155. m, err := svcCtx.SysProductMemberModel.FindOne(ctx, sp.mId)
  156. require.NoError(t, err)
  157. assert.Equal(t, "ADMIN", m.MemberType, "MemberType 不应被改动")
  158. }
  159. // TC-0726: H-4 修复:有多个 ADMIN 时可以降级其中一个
  160. func TestUpdateMember_DemoteAdmin_WhenMultiple_Allowed(t *testing.T) {
  161. ctx := ctxhelper.SuperAdminCtx()
  162. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  163. conn := testutil.GetTestSqlConn()
  164. now := time.Now().Unix()
  165. code := testutil.UniqueId()
  166. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  167. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  168. Status: 1, CreateTime: now, UpdateTime: now,
  169. })
  170. require.NoError(t, err)
  171. pId, _ := pRes.LastInsertId()
  172. var uIds, mIds []int64
  173. for i := 0; i < 2; i++ {
  174. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  175. Username: testutil.UniqueId(), Password: testutil.HashPassword("pw"),
  176. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  177. Status: 1, CreateTime: now, UpdateTime: now,
  178. })
  179. require.NoError(t, err)
  180. uId, _ := uRes.LastInsertId()
  181. uIds = append(uIds, uId)
  182. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  183. ProductCode: code, UserId: uId, MemberType: "ADMIN",
  184. Status: 1, CreateTime: now, UpdateTime: now,
  185. })
  186. require.NoError(t, err)
  187. mId, _ := mRes.LastInsertId()
  188. mIds = append(mIds, mId)
  189. }
  190. t.Cleanup(func() {
  191. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  192. testutil.CleanTable(ctx, conn, "`sys_user`", uIds...)
  193. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  194. })
  195. err = NewUpdateMemberLogic(ctx, svcCtx).UpdateMember(&types.UpdateMemberReq{
  196. Id: mIds[0], MemberType: "MEMBER",
  197. })
  198. require.NoError(t, err)
  199. m, err := svcCtx.SysProductMemberModel.FindOne(ctx, mIds[0])
  200. require.NoError(t, err)
  201. assert.Equal(t, "MEMBER", m.MemberType)
  202. }
  203. // TC-0727: H-4 修复:禁用状态的 ADMIN 不计入 active admin 计数,导致剩余 0 个启用 ADMIN 时仍拒绝降级
  204. // 说明:CountActiveAdmins 只统计 status=1 的 ADMIN;即便 DB 里有 2 个 ADMIN,但仅 1 个启用,
  205. // 降级这个唯一启用的 ADMIN 仍应被拒绝。
  206. func TestUpdateMember_DemoteLastActiveAdmin_Rejected(t *testing.T) {
  207. ctx := ctxhelper.SuperAdminCtx()
  208. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  209. conn := testutil.GetTestSqlConn()
  210. now := time.Now().Unix()
  211. code := testutil.UniqueId()
  212. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  213. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  214. Status: 1, CreateTime: now, UpdateTime: now,
  215. })
  216. require.NoError(t, err)
  217. pId, _ := pRes.LastInsertId()
  218. var uIds, mIds []int64
  219. statuses := []int64{1, 2} // 一个启用,一个禁用
  220. for _, st := range statuses {
  221. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  222. Username: testutil.UniqueId(), Password: testutil.HashPassword("pw"),
  223. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  224. Status: 1, CreateTime: now, UpdateTime: now,
  225. })
  226. require.NoError(t, err)
  227. uId, _ := uRes.LastInsertId()
  228. uIds = append(uIds, uId)
  229. mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
  230. ProductCode: code, UserId: uId, MemberType: "ADMIN",
  231. Status: st, CreateTime: now, UpdateTime: now,
  232. })
  233. require.NoError(t, err)
  234. mId, _ := mRes.LastInsertId()
  235. mIds = append(mIds, mId)
  236. }
  237. t.Cleanup(func() {
  238. testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
  239. testutil.CleanTable(ctx, conn, "`sys_user`", uIds...)
  240. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  241. })
  242. // 启用中的那个 ADMIN (mIds[0]) 降级应被拒绝
  243. err = NewUpdateMemberLogic(ctx, svcCtx).UpdateMember(&types.UpdateMemberReq{
  244. Id: mIds[0], MemberType: "DEVELOPER",
  245. })
  246. require.Error(t, err)
  247. var ce *response.CodeError
  248. require.True(t, errors.As(err, &ce))
  249. assert.Equal(t, 400, ce.Code())
  250. assert.Contains(t, ce.Error(), "最后一个管理员")
  251. }
  252. // TC-0728: 移除非 ADMIN 成员不受 last-admin 保护
  253. func TestRemoveMember_NonAdmin_Unaffected(t *testing.T) {
  254. ctx := ctxhelper.SuperAdminCtx()
  255. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  256. sp := seedEnabledProductWithMember(t, svcCtx, "MEMBER")
  257. t.Cleanup(func() { cleanupSeeded(t, svcCtx, sp) })
  258. err := NewRemoveMemberLogic(ctx, svcCtx).RemoveMember(&types.RemoveMemberReq{Id: sp.mId})
  259. require.NoError(t, err)
  260. }
  261. // TC-0729: L-5 修复:禁用产品不允许添加成员
  262. func TestAddMember_DisabledProductRejected(t *testing.T) {
  263. ctx := ctxhelper.SuperAdminCtx()
  264. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  265. conn := testutil.GetTestSqlConn()
  266. now := time.Now().Unix()
  267. code := testutil.UniqueId()
  268. pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
  269. Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
  270. Status: 2, CreateTime: now, UpdateTime: now, // 禁用
  271. })
  272. require.NoError(t, err)
  273. pId, _ := pRes.LastInsertId()
  274. uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  275. Username: code, Password: testutil.HashPassword("pw"),
  276. Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
  277. Status: 1, CreateTime: now, UpdateTime: now,
  278. })
  279. require.NoError(t, err)
  280. uId, _ := uRes.LastInsertId()
  281. t.Cleanup(func() {
  282. testutil.CleanTable(ctx, conn, "`sys_user`", uId)
  283. testutil.CleanTable(ctx, conn, "`sys_product`", pId)
  284. })
  285. _, err = NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
  286. ProductCode: code, UserId: uId, MemberType: "MEMBER",
  287. })
  288. require.Error(t, err)
  289. var ce *response.CodeError
  290. require.True(t, errors.As(err, &ce))
  291. assert.Equal(t, 400, ce.Code())
  292. assert.Contains(t, ce.Error(), "禁用")
  293. }