| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- package member
- import (
- "database/sql"
- "errors"
- "testing"
- "time"
- productModel "perms-system-server/internal/model/product"
- memberModel "perms-system-server/internal/model/productmember"
- userModel "perms-system-server/internal/model/user"
- "perms-system-server/internal/response"
- "perms-system-server/internal/svc"
- "perms-system-server/internal/testutil"
- "perms-system-server/internal/testutil/ctxhelper"
- "perms-system-server/internal/types"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- type seededProduct struct {
- code string
- pId int64
- uId int64
- mId int64
- admin int64 // 成员 id 当该成员为 ADMIN
- }
- // seedEnabledProductWithMember 创建 enabled product + user + product_member(memberType 指定)
- func seedEnabledProductWithMember(t *testing.T, svcCtx *svc.ServiceContext, memberType string) seededProduct {
- t.Helper()
- ctx := ctxhelper.SuperAdminCtx()
- now := time.Now().Unix()
- code := testutil.UniqueId()
- pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
- Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- pId, _ := pRes.LastInsertId()
- uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
- Username: code, Password: testutil.HashPassword("pw"), Nickname: "n",
- Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- uId, _ := uRes.LastInsertId()
- mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
- ProductCode: code, UserId: uId, MemberType: memberType,
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- mId, _ := mRes.LastInsertId()
- return seededProduct{code: code, pId: pId, uId: uId, mId: mId, admin: mId}
- }
- func cleanupSeeded(t *testing.T, svcCtx *svc.ServiceContext, sp seededProduct) {
- t.Helper()
- ctx := ctxhelper.SuperAdminCtx()
- conn := testutil.GetTestSqlConn()
- testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", sp.code)
- testutil.CleanTable(ctx, conn, "`sys_user`", sp.uId)
- testutil.CleanTable(ctx, conn, "`sys_product`", sp.pId)
- }
- // TC-0723: H-4 修复:不能移除产品最后一个 ADMIN
- func TestRemoveMember_LastAdminRejected(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- sp := seededProduct{code: testutil.UniqueId()}
- now := time.Now().Unix()
- pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
- Code: sp.code, Name: "p_" + sp.code, AppKey: sp.code + "_k", AppSecret: "s",
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- sp.pId, _ = pRes.LastInsertId()
- uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
- Username: sp.code, Password: testutil.HashPassword("pw"),
- Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- sp.uId, _ = uRes.LastInsertId()
- mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
- ProductCode: sp.code, UserId: sp.uId, MemberType: "ADMIN",
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- sp.mId, _ = mRes.LastInsertId()
- t.Cleanup(func() { cleanupSeeded(t, svcCtx, sp) })
- logic := NewRemoveMemberLogic(ctx, svcCtx)
- err = logic.RemoveMember(&types.RemoveMemberReq{Id: sp.mId})
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 400, ce.Code())
- assert.Contains(t, ce.Error(), "最后一个管理员")
- m, ferr := svcCtx.SysProductMemberModel.FindOne(ctx, sp.mId)
- require.NoError(t, ferr, "ADMIN 必须仍然存在")
- assert.Equal(t, "ADMIN", m.MemberType)
- }
- // TC-0724: 存在 >=2 个 ADMIN 时可以移除其中一个
- func TestRemoveMember_AdminNotLast_Allowed(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- now := time.Now().Unix()
- code := testutil.UniqueId()
- pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
- Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- pId, _ := pRes.LastInsertId()
- var uIds, mIds []int64
- for i := 0; i < 2; i++ {
- uid := testutil.UniqueId() + "_a"
- uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
- Username: uid, Password: testutil.HashPassword("pw"),
- Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- uId, _ := uRes.LastInsertId()
- uIds = append(uIds, uId)
- mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
- ProductCode: code, UserId: uId, MemberType: "ADMIN",
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- mId, _ := mRes.LastInsertId()
- mIds = append(mIds, mId)
- }
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
- testutil.CleanTable(ctx, conn, "`sys_user`", uIds...)
- testutil.CleanTable(ctx, conn, "`sys_product`", pId)
- })
- err = NewRemoveMemberLogic(ctx, svcCtx).RemoveMember(&types.RemoveMemberReq{Id: mIds[0]})
- require.NoError(t, err)
- _, err = svcCtx.SysProductMemberModel.FindOne(ctx, mIds[0])
- require.Error(t, err)
- _, err = svcCtx.SysProductMemberModel.FindOne(ctx, mIds[1])
- require.NoError(t, err, "另一个 ADMIN 必须保留")
- }
- // TC-0725: H-4 修复:不能将最后一个 ADMIN 降级为 MEMBER
- func TestUpdateMember_DemoteLastAdminRejected(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- sp := seedEnabledProductWithMember(t, svcCtx, "ADMIN")
- t.Cleanup(func() { cleanupSeeded(t, svcCtx, sp) })
- err := NewUpdateMemberLogic(ctx, svcCtx).UpdateMember(&types.UpdateMemberReq{
- Id: sp.mId, MemberType: "MEMBER",
- })
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 400, ce.Code())
- assert.Contains(t, ce.Error(), "最后一个管理员")
- m, err := svcCtx.SysProductMemberModel.FindOne(ctx, sp.mId)
- require.NoError(t, err)
- assert.Equal(t, "ADMIN", m.MemberType, "MemberType 不应被改动")
- }
- // TC-0726: H-4 修复:有多个 ADMIN 时可以降级其中一个
- func TestUpdateMember_DemoteAdmin_WhenMultiple_Allowed(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- now := time.Now().Unix()
- code := testutil.UniqueId()
- pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
- Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- pId, _ := pRes.LastInsertId()
- var uIds, mIds []int64
- for i := 0; i < 2; i++ {
- uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
- Username: testutil.UniqueId(), Password: testutil.HashPassword("pw"),
- Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- uId, _ := uRes.LastInsertId()
- uIds = append(uIds, uId)
- mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
- ProductCode: code, UserId: uId, MemberType: "ADMIN",
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- mId, _ := mRes.LastInsertId()
- mIds = append(mIds, mId)
- }
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
- testutil.CleanTable(ctx, conn, "`sys_user`", uIds...)
- testutil.CleanTable(ctx, conn, "`sys_product`", pId)
- })
- err = NewUpdateMemberLogic(ctx, svcCtx).UpdateMember(&types.UpdateMemberReq{
- Id: mIds[0], MemberType: "MEMBER",
- })
- require.NoError(t, err)
- m, err := svcCtx.SysProductMemberModel.FindOne(ctx, mIds[0])
- require.NoError(t, err)
- assert.Equal(t, "MEMBER", m.MemberType)
- }
- // TC-0727: H-4 修复:禁用状态的 ADMIN 不计入 active admin 计数,导致剩余 0 个启用 ADMIN 时仍拒绝降级
- // 说明:CountActiveAdmins 只统计 status=1 的 ADMIN;即便 DB 里有 2 个 ADMIN,但仅 1 个启用,
- // 降级这个唯一启用的 ADMIN 仍应被拒绝。
- func TestUpdateMember_DemoteLastActiveAdmin_Rejected(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- now := time.Now().Unix()
- code := testutil.UniqueId()
- pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
- Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- pId, _ := pRes.LastInsertId()
- var uIds, mIds []int64
- statuses := []int64{1, 2} // 一个启用,一个禁用
- for _, st := range statuses {
- uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
- Username: testutil.UniqueId(), Password: testutil.HashPassword("pw"),
- Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- uId, _ := uRes.LastInsertId()
- uIds = append(uIds, uId)
- mRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{
- ProductCode: code, UserId: uId, MemberType: "ADMIN",
- Status: st, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- mId, _ := mRes.LastInsertId()
- mIds = append(mIds, mId)
- }
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
- testutil.CleanTable(ctx, conn, "`sys_user`", uIds...)
- testutil.CleanTable(ctx, conn, "`sys_product`", pId)
- })
- // 启用中的那个 ADMIN (mIds[0]) 降级应被拒绝
- err = NewUpdateMemberLogic(ctx, svcCtx).UpdateMember(&types.UpdateMemberReq{
- Id: mIds[0], MemberType: "DEVELOPER",
- })
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 400, ce.Code())
- assert.Contains(t, ce.Error(), "最后一个管理员")
- }
- // TC-0728: 移除非 ADMIN 成员不受 last-admin 保护
- func TestRemoveMember_NonAdmin_Unaffected(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- sp := seedEnabledProductWithMember(t, svcCtx, "MEMBER")
- t.Cleanup(func() { cleanupSeeded(t, svcCtx, sp) })
- err := NewRemoveMemberLogic(ctx, svcCtx).RemoveMember(&types.RemoveMemberReq{Id: sp.mId})
- require.NoError(t, err)
- }
- // TC-0950: H-3 修复 —— AddMember 必须显式拒绝把 SuperAdmin 作为普通产品成员加入。
- // 背景:loadMembership 会把 SuperAdmin 的 MemberType 固定为 SuperAdmin 让其实际权限不受影响,
- // 但若 sys_product_member 里仍落一条记录,会污染审计日志 / 权限推理工具,且给产品 ADMIN
- // "纳管了 superadmin" 的错觉。必须在 AddMember 入口就 403。
- func TestAddMember_SuperAdminTargetRejected(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- now := time.Now().Unix()
- code := testutil.UniqueId()
- pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
- Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- pId, _ := pRes.LastInsertId()
- // target 是 SuperAdmin(IsSuperAdmin=1)
- uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
- Username: "h3_su_" + code, Password: testutil.HashPassword("pw"),
- Avatar: sql.NullString{}, IsSuperAdmin: 1, MustChangePassword: 2,
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- uId, _ := uRes.LastInsertId()
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_product_member`", "productCode", code)
- testutil.CleanTable(ctx, conn, "`sys_user`", uId)
- testutil.CleanTable(ctx, conn, "`sys_product`", pId)
- })
- _, err = NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
- ProductCode: code, UserId: uId, MemberType: "MEMBER",
- })
- require.Error(t, err, "H-3:禁止把 SuperAdmin 加入具体产品为普通成员")
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 403, ce.Code())
- assert.Contains(t, ce.Error(), "超级管理员")
- // DB 侧必须没有落下 SuperAdmin 的成员记录(regression:确保 AddMember 未短路在插入之后)
- _, findErr := svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(ctx, code, uId)
- require.Error(t, findErr, "SuperAdmin 不得被落入 sys_product_member")
- }
- // TC-0729: L-5 修复:禁用产品不允许添加成员
- func TestAddMember_DisabledProductRejected(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- now := time.Now().Unix()
- code := testutil.UniqueId()
- pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{
- Code: code, Name: "p_" + code, AppKey: code + "_k", AppSecret: "s",
- Status: 2, CreateTime: now, UpdateTime: now, // 禁用
- })
- require.NoError(t, err)
- pId, _ := pRes.LastInsertId()
- uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
- Username: code, Password: testutil.HashPassword("pw"),
- Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2,
- Status: 1, CreateTime: now, UpdateTime: now,
- })
- require.NoError(t, err)
- uId, _ := uRes.LastInsertId()
- t.Cleanup(func() {
- testutil.CleanTable(ctx, conn, "`sys_user`", uId)
- testutil.CleanTable(ctx, conn, "`sys_product`", pId)
- })
- _, err = NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
- ProductCode: code, UserId: uId, MemberType: "MEMBER",
- })
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 400, ce.Code())
- assert.Contains(t, ce.Error(), "禁用")
- }
|