| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- package member
- import (
- "database/sql"
- "errors"
- "sync"
- "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"
- )
- // TC-0213: 正常添加
- func TestAddMember_Normal(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("pass123"), Nickname: "nick",
- Avatar: sql.NullString{}, IsSuperAdmin: 2, 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", uid)
- testutil.CleanTable(ctx, conn, "`sys_user`", uId)
- testutil.CleanTable(ctx, conn, "`sys_product`", pId)
- })
- logic := NewAddMemberLogic(ctx, svcCtx)
- resp, err := logic.AddMember(&types.AddMemberReq{
- ProductCode: uid,
- UserId: uId,
- MemberType: "MEMBER",
- })
- require.NoError(t, err)
- assert.True(t, resp.Id > 0)
- member, err := svcCtx.SysProductMemberModel.FindOne(ctx, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, uid, member.ProductCode)
- assert.Equal(t, uId, member.UserId)
- assert.Equal(t, "MEMBER", member.MemberType)
- assert.Equal(t, int64(1), member.Status)
- }
- // TC-0214: 产品不存在
- func TestAddMember_ProductNotFound(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewAddMemberLogic(ctx, svcCtx)
- _, err := logic.AddMember(&types.AddMemberReq{
- ProductCode: "nonexistent_product_xyz",
- UserId: 1,
- MemberType: "MEMBER",
- })
- 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-0215: 用户不存在
- func TestAddMember_UserNotFound(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()
- t.Cleanup(func() {
- testutil.CleanTable(ctx, conn, "`sys_product`", pId)
- })
- logic := NewAddMemberLogic(ctx, svcCtx)
- _, err = logic.AddMember(&types.AddMemberReq{
- ProductCode: uid,
- UserId: 999999999,
- MemberType: "MEMBER",
- })
- 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-0216: 已是成员
- func TestAddMember_AlreadyMember(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("pass123"), 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()
- t.Cleanup(func() {
- testutil.CleanTable(ctx, conn, "`sys_product_member`", mId)
- testutil.CleanTable(ctx, conn, "`sys_user`", uId)
- testutil.CleanTable(ctx, conn, "`sys_product`", pId)
- })
- logic := NewAddMemberLogic(ctx, svcCtx)
- _, err = logic.AddMember(&types.AddMemberReq{
- ProductCode: uid,
- UserId: uId,
- MemberType: "ADMIN",
- })
- require.Error(t, err)
- ce, ok := err.(*response.CodeError)
- require.True(t, ok)
- assert.Equal(t, 409, ce.Code())
- assert.Equal(t, "该用户已是该产品成员", ce.Error())
- }
- // TC-0218: 无效MemberType
- func TestAddMember_InvalidMemberType(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("pass123"), Nickname: "nick",
- 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)
- })
- logic := NewAddMemberLogic(ctx, svcCtx)
- _, err = logic.AddMember(&types.AddMemberReq{
- ProductCode: uid,
- UserId: uId,
- MemberType: "INVALID",
- })
- require.Error(t, err)
- ce, ok := err.(*response.CodeError)
- require.True(t, ok)
- assert.Equal(t, 400, ce.Code())
- assert.Equal(t, "无效的成员类型", ce.Error())
- }
- // TC-0217: 并发添加
- func TestAddMember_ConcurrentSameUserProduct(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: "concurrent_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("pass123"), Nickname: "nick",
- Avatar: sql.NullString{}, IsSuperAdmin: 2, 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", uid)
- testutil.CleanTable(ctx, conn, "`sys_user`", uId)
- testutil.CleanTable(ctx, conn, "`sys_product`", pId)
- })
- var wg sync.WaitGroup
- results := make(chan error, 2)
- for i := 0; i < 2; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- logic := NewAddMemberLogic(ctx, svcCtx)
- _, err := logic.AddMember(&types.AddMemberReq{
- ProductCode: uid,
- UserId: uId,
- MemberType: "MEMBER",
- })
- results <- err
- }()
- }
- wg.Wait()
- close(results)
- var errs []error
- for e := range results {
- errs = append(errs, e)
- }
- require.Len(t, errs, 2)
- successCount := 0
- failCount := 0
- for _, e := range errs {
- if e == nil {
- successCount++
- } else {
- failCount++
- }
- }
- assert.Equal(t, 1, successCount, "exactly one goroutine should succeed")
- assert.Equal(t, 1, failCount, "exactly one goroutine should fail (409 or DB duplicate)")
- }
- // TC-0950: 修复 —— 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, "禁止把 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: 修复:禁用产品不允许添加成员
- 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(), "禁用")
- }
- // TC-1107: 非 ADMIN caller + 不存在的 productCode —— 必须 403(不是 404),
- // L-R13-1:`RequireProductAdminFor` 先行于 `SysProductModel.FindOneByCode`,消除通过
- // "产品不存在 vs 权限不足"的响应码差分枚举产品 code 的 oracle。
- func TestAddMember_L_R13_1_ProductEnumerationBlocked(t *testing.T) {
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- // caller 是另一个产品的 ADMIN,对目标产品没有权限。
- callerCtx := ctxhelper.AdminCtx("some_other_product")
- _, err := NewAddMemberLogic(callerCtx, svcCtx).AddMember(&types.AddMemberReq{
- ProductCode: "definitely_does_not_exist_" + testutil.UniqueId(),
- UserId: 999999999,
- MemberType: "MEMBER",
- })
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 403, ce.Code(),
- "L-R13-1:productCode 不存在的差异必须被权限闸吞掉,不得 404 泄漏产品存在性")
- // 并发安全:DB 未因这条请求留下任何 sys_product_member 行。
- var count int64
- _ = conn.QueryRowCtx(callerCtx, &count,
- "SELECT COUNT(*) FROM `sys_product_member` WHERE `userId` = ?", int64(999999999))
- assert.Equal(t, int64(0), count)
- }
- // TC-1108: 非 ADMIN caller + 非法 MemberType —— 必须 403 而不是 400(权限先于字面校验),
- // 防御通过 400 "无效的成员类型" 和 404 "产品不存在" 的差分探测 productCode 是否在线。
- func TestAddMember_L_R13_1_InvalidMemberTypeBeforeAuth(t *testing.T) {
- ctx := ctxhelper.MemberCtx("test_product")
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- _, err := NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
- ProductCode: "test_product",
- UserId: 1,
- MemberType: "INVALID",
- })
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 403, ce.Code(),
- "L-R13-1:MEMBER 无 ADMIN 权限必须先于 MemberType 字面校验 403,不得返 400")
- }
- // TC-1109: 超管 + 非法 MemberType:权限通过后仍必须命中 400 字面校验,
- // 回归 L-R13-1 改动没有把合法路径的 400 语义也吃掉。
- func TestAddMember_L_R13_1_SuperAdminStillGets400ForInvalidType(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()
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product`", pId) })
- _, err = NewAddMemberLogic(ctx, svcCtx).AddMember(&types.AddMemberReq{
- ProductCode: uid,
- UserId: 999999999,
- MemberType: "INVALID",
- })
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 400, ce.Code(),
- "超管权限通过后必须继续走字面 400 检查,不得因 L-R13-1 改动被吞掉")
- assert.Equal(t, "无效的成员类型", ce.Error())
- }
|