package member import ( "database/sql" "errors" "testing" "time" permModel "perms-system-server/internal/model/perm" productModel "perms-system-server/internal/model/product" memberModel "perms-system-server/internal/model/productmember" roleModel "perms-system-server/internal/model/role" userModel "perms-system-server/internal/model/user" "perms-system-server/internal/model/userperm" "perms-system-server/internal/model/userrole" "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" ) // TC-0226: 正常移除+级联(事务内) func TestRemoveMember_WithCascade(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() now := time.Now().Unix() uid := testutil.UniqueId() pRes, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{ Code: uid, Name: "test_prod", AppKey: uid, AppSecret: "s1", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pId, _ := pRes.LastInsertId() uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{ Username: uid, Password: testutil.HashPassword("pass"), Nickname: "nick", 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: uid, UserId: uId, MemberType: "MEMBER", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) mId, _ := mRes.LastInsertId() rRes, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{ ProductCode: uid, Name: uid, Status: 1, PermsLevel: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) rId, _ := rRes.LastInsertId() urRes, err := svcCtx.SysUserRoleModel.Insert(ctx, &userrole.SysUserRole{ UserId: uId, RoleId: rId, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) urId, _ := urRes.LastInsertId() pmRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{ ProductCode: uid, Name: "perm1", Code: uid + "_perm", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pmId, _ := pmRes.LastInsertId() upRes, err := svcCtx.SysUserPermModel.Insert(ctx, &userperm.SysUserPerm{ UserId: uId, PermId: pmId, Effect: "ALLOW", CreateTime: now, UpdateTime: now, }) require.NoError(t, err) upId, _ := upRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user_perm`", upId) testutil.CleanTable(ctx, conn, "`sys_user_role`", urId) testutil.CleanTable(ctx, conn, "`sys_perm`", pmId) testutil.CleanTable(ctx, conn, "`sys_role`", rId) testutil.CleanTable(ctx, conn, "`sys_product_member`", mId) testutil.CleanTable(ctx, conn, "`sys_user`", uId) testutil.CleanTable(ctx, conn, "`sys_product`", pId) }) logic := NewRemoveMemberLogic(ctx, svcCtx) err = logic.RemoveMember(&types.RemoveMemberReq{Id: mId}) require.NoError(t, err) _, err = svcCtx.SysProductMemberModel.FindOne(ctx, mId) assert.Error(t, err) roles, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, uId) require.NoError(t, err) assert.Empty(t, roles) allow, err := svcCtx.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(ctx, uId, "ALLOW", uid) require.NoError(t, err) assert.Empty(t, allow) deny, err := svcCtx.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(ctx, uId, "DENY", uid) require.NoError(t, err) assert.Empty(t, deny) } // TC-0228: 成员不存在 func TestRemoveMember_NotFound(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewRemoveMemberLogic(ctx, svcCtx) err := logic.RemoveMember(&types.RemoveMemberReq{Id: 999999999}) require.Error(t, err) ce, ok := err.(*response.CodeError) require.True(t, ok) assert.Equal(t, 404, ce.Code()) assert.Equal(t, "成员不存在", ce.Error()) } // TC-0227: 跨产品隔离 func TestRemoveMember_CrossProductIsolation(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() now := time.Now().Unix() uid1 := testutil.UniqueId() uid2 := testutil.UniqueId() p1Res, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{ Code: uid1, Name: "prod1", AppKey: uid1, AppSecret: "s1", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) p1Id, _ := p1Res.LastInsertId() p2Res, err := svcCtx.SysProductModel.Insert(ctx, &productModel.SysProduct{ Code: uid2, Name: "prod2", AppKey: uid2, AppSecret: "s2", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) p2Id, _ := p2Res.LastInsertId() uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{ Username: uid1, Password: testutil.HashPassword("pass"), Nickname: "nick", Avatar: sql.NullString{}, IsSuperAdmin: 2, MustChangePassword: 2, Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) uId, _ := uRes.LastInsertId() m1Res, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{ ProductCode: uid1, UserId: uId, MemberType: "MEMBER", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) m1Id, _ := m1Res.LastInsertId() m2Res, err := svcCtx.SysProductMemberModel.Insert(ctx, &memberModel.SysProductMember{ ProductCode: uid2, UserId: uId, MemberType: "MEMBER", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) m2Id, _ := m2Res.LastInsertId() r1Res, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{ ProductCode: uid1, Name: uid1, Status: 1, PermsLevel: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) r1Id, _ := r1Res.LastInsertId() r2Res, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{ ProductCode: uid2, Name: uid2, Status: 1, PermsLevel: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) r2Id, _ := r2Res.LastInsertId() ur1Res, err := svcCtx.SysUserRoleModel.Insert(ctx, &userrole.SysUserRole{ UserId: uId, RoleId: r1Id, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) ur1Id, _ := ur1Res.LastInsertId() ur2Res, err := svcCtx.SysUserRoleModel.Insert(ctx, &userrole.SysUserRole{ UserId: uId, RoleId: r2Id, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) ur2Id, _ := ur2Res.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user_role`", ur1Id, ur2Id) testutil.CleanTable(ctx, conn, "`sys_role`", r1Id, r2Id) testutil.CleanTable(ctx, conn, "`sys_product_member`", m1Id, m2Id) testutil.CleanTable(ctx, conn, "`sys_user`", uId) testutil.CleanTable(ctx, conn, "`sys_product`", p1Id, p2Id) }) logic := NewRemoveMemberLogic(ctx, svcCtx) err = logic.RemoveMember(&types.RemoveMemberReq{Id: m1Id}) require.NoError(t, err) _, err = svcCtx.SysProductMemberModel.FindOne(ctx, m1Id) assert.Error(t, err) m2, err := svcCtx.SysProductMemberModel.FindOne(ctx, m2Id) require.NoError(t, err) assert.Equal(t, uid2, m2.ProductCode) roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, uId) require.NoError(t, err) assert.Contains(t, roleIds, r2Id) assert.NotContains(t, roleIds, r1Id) } // strPtr / int64Ptr 是 后 UpdateMemberReq.MemberType / Status 指针化的 helper。 // 若 nil 表示不改该字段,两者都 nil 会被 Logic 400。 func strPtr(s string) *string { return &s } 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: 修复:不能移除产品最后一个 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-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) }