package auth import ( "context" "errors" "fmt" "math" "math/rand" "testing" "time" "perms-system-server/internal/consts" "perms-system-server/internal/loaders" deptModel "perms-system-server/internal/model/dept" "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" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zeromicro/go-zero/core/stores/sqlx" ) // ===================================================================== // RequireSuperAdmin // ===================================================================== // TC-0461: 超管通过 func TestRequireSuperAdmin_SuperAdmin(t *testing.T) { err := RequireSuperAdmin(ctxhelper.SuperAdminCtx()) assert.NoError(t, err) } // TC-0462: ADMIN → 403 "仅超级管理员" func TestRequireSuperAdmin_Admin(t *testing.T) { err := RequireSuperAdmin(ctxhelper.AdminCtx("p1")) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Contains(t, ce.Error(), "仅超级管理员") } // TC-0463: MEMBER → 403 func TestRequireSuperAdmin_Member(t *testing.T) { err := RequireSuperAdmin(ctxhelper.MemberCtx("p1")) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // TC-0464: 无 UserDetails → 401 "未登录" func TestRequireSuperAdmin_NoUserDetails(t *testing.T) { err := RequireSuperAdmin(context.Background()) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code()) assert.Contains(t, ce.Error(), "未登录") } // ===================================================================== // RequireProductAdmin // ===================================================================== // TC-0465: SuperAdmin → nil func TestRequireProductAdminFor_SuperAdmin(t *testing.T) { err := RequireProductAdminFor(ctxhelper.SuperAdminCtx(), "p1") assert.NoError(t, err) } // TC-0466: ADMIN → nil (same product) func TestRequireProductAdminFor_Admin(t *testing.T) { err := RequireProductAdminFor(ctxhelper.AdminCtx("p1"), "p1") assert.NoError(t, err) } // TC-0467: DEVELOPER → 403 func TestRequireProductAdminFor_Developer(t *testing.T) { err := RequireProductAdminFor(ctxhelper.DeveloperCtx("p1"), "p1") require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // TC-0468: MEMBER → 403 func TestRequireProductAdminFor_Member(t *testing.T) { err := RequireProductAdminFor(ctxhelper.MemberCtx("p1"), "p1") require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // TC-0469: 无 UserDetails → 401 func TestRequireProductAdminFor_NoUserDetails(t *testing.T) { err := RequireProductAdminFor(context.Background(), "p1") require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code()) assert.Contains(t, ce.Error(), "未登录") } // TC-0555: ADMIN 跨产品被拒绝 func TestRequireProductAdminFor_AdminCrossProduct(t *testing.T) { err := RequireProductAdminFor(ctxhelper.AdminCtx("p1"), "other_product") require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // ===================================================================== // CheckMemberTypeAssignment // ===================================================================== // TC-0470: 超管分配 ADMIN → nil func TestCheckMemberTypeAssignment_SuperAdminAssignsAdmin(t *testing.T) { err := CheckMemberTypeAssignment(ctxhelper.SuperAdminCtx(), consts.MemberTypeAdmin) assert.NoError(t, err) } // TC-0471: ADMIN 分配 DEVELOPER → nil func TestCheckMemberTypeAssignment_AdminAssignsDeveloper(t *testing.T) { err := CheckMemberTypeAssignment(ctxhelper.AdminCtx("p1"), consts.MemberTypeDeveloper) assert.NoError(t, err) } // TC-0472: ADMIN 分配 ADMIN(同级)→ 403 func TestCheckMemberTypeAssignment_AdminAssignsAdmin(t *testing.T) { err := CheckMemberTypeAssignment(ctxhelper.AdminCtx("p1"), consts.MemberTypeAdmin) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // TC-0473: DEVELOPER 分配 ADMIN(更高级)→ 403 func TestCheckMemberTypeAssignment_DeveloperAssignsAdmin(t *testing.T) { err := CheckMemberTypeAssignment(ctxhelper.DeveloperCtx("p1"), consts.MemberTypeAdmin) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // TC-0474: MEMBER 分配 MEMBER(同级)→ 403 func TestCheckMemberTypeAssignment_MemberAssignsMember(t *testing.T) { err := CheckMemberTypeAssignment(ctxhelper.MemberCtx("p1"), consts.MemberTypeMember) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // TC-0475: 无 UserDetails → 401 func TestCheckMemberTypeAssignment_NoUserDetails(t *testing.T) { err := CheckMemberTypeAssignment(context.Background(), consts.MemberTypeMember) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code()) assert.Contains(t, ce.Error(), "未登录") } // ===================================================================== // memberTypePriority (未导出,同包可测) // ===================================================================== // TC-0484: 所有成员类型返回正确优先级 func TestMemberTypePriority(t *testing.T) { tests := []struct { name string memberType string want int }{ {"SUPER_ADMIN=0", consts.MemberTypeSuperAdmin, 0}, {"ADMIN=1", consts.MemberTypeAdmin, 1}, {"DEVELOPER=2", consts.MemberTypeDeveloper, 2}, {"MEMBER=3", consts.MemberTypeMember, 3}, {"unknown=MaxInt32", "UNKNOWN_TYPE", math.MaxInt32}, {"empty=MaxInt32", "", math.MaxInt32}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := memberTypePriority(tt.memberType) assert.Equal(t, tt.want, got) }) } } // ===================================================================== // CheckManageAccess (集成测试,需要真实 DB) // ===================================================================== func newIntegrationSvcCtx() *svc.ServiceContext { return svc.NewServiceContext(testutil.GetTestConfig()) } func createTestUser(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext, conn sqlx.SqlConn, opts struct { username string deptId int64 isSuperAdmin int64 }) int64 { t.Helper() now := time.Now().Unix() res, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{ Username: opts.username, Password: testutil.HashPassword("test123"), Nickname: opts.username, Email: opts.username + "@test.com", Phone: "", Remark: "", DeptId: opts.deptId, IsSuperAdmin: opts.isSuperAdmin, MustChangePassword: consts.MustChangePasswordNo, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() return id } // TC-0476: SuperAdmin 跳过所有检查 func TestCheckManageAccess_SuperAdminBypasses(t *testing.T) { ctx := context.Background() svcCtx := newIntegrationSvcCtx() conn := testutil.GetTestSqlConn() targetId := createTestUser(t, ctx, svcCtx, conn, struct { username string deptId int64 isSuperAdmin int64 }{ username: fmt.Sprintf("target_sa_%d", rand.Intn(100000)), deptId: 0, isSuperAdmin: consts.IsSuperAdminNo, }) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", targetId) }) err := CheckManageAccess(ctxhelper.SuperAdminCtx(), svcCtx, targetId, "p1") assert.NoError(t, err) } // TC-0477: 操作自己豁免 func TestCheckManageAccess_SelfManagement(t *testing.T) { selfCtx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 100, Username: "self_user", IsSuperAdmin: false, MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled, ProductCode: "p1", DeptId: 1, DeptPath: "/1/", }) svcCtx := newIntegrationSvcCtx() err := CheckManageAccess(selfCtx, svcCtx, 100, "p1") assert.NoError(t, err) } // TC-0478: ADMIN 跳过部门层级检查 func TestCheckManageAccess_AdminSkipsDeptCheck(t *testing.T) { ctx := context.Background() svcCtx := newIntegrationSvcCtx() conn := testutil.GetTestSqlConn() now := time.Now().Unix() pc := fmt.Sprintf("tp_access_%d", rand.Intn(100000)) targetId := createTestUser(t, ctx, svcCtx, conn, struct { username string deptId int64 isSuperAdmin int64 }{ username: fmt.Sprintf("target_admin_%d", rand.Intn(100000)), deptId: 0, isSuperAdmin: consts.IsSuperAdminNo, }) pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmember.SysProductMember{ ProductCode: pc, UserId: targetId, MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pmId, _ := pmRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId) testutil.CleanTable(ctx, conn, "`sys_user`", targetId) }) adminCtx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 2, Username: "admin_test", IsSuperAdmin: false, MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled, ProductCode: pc, DeptId: 999, DeptPath: "/999/", MinPermsLevel: math.MaxInt64, }) err = CheckManageAccess(adminCtx, svcCtx, targetId, pc) assert.NoError(t, err) } // TC-0479: 无 UserDetails → 401 func TestCheckManageAccess_NoUserDetails(t *testing.T) { svcCtx := newIntegrationSvcCtx() err := CheckManageAccess(context.Background(), svcCtx, 1, "p1") require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code()) } // TC-0480: DEVELOPER 无部门归属 → 403 func TestCheckManageAccess_NoDept(t *testing.T) { svcCtx := newIntegrationSvcCtx() noDeptCtx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 3, Username: "dev_nodept", IsSuperAdmin: false, MemberType: consts.MemberTypeDeveloper, Status: consts.StatusEnabled, ProductCode: "p1", DeptId: 0, DeptPath: "", MinPermsLevel: math.MaxInt64, }) err := CheckManageAccess(noDeptCtx, svcCtx, 99999, "p1") require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Contains(t, ce.Error(), "未归属任何部门") } // TC-0481: DEVELOPER 操作不同部门的用户 → 403 func TestCheckManageAccess_CrossDeptForbidden(t *testing.T) { ctx := context.Background() svcCtx := newIntegrationSvcCtx() conn := testutil.GetTestSqlConn() now := time.Now().Unix() dept1Res, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{ ParentId: 0, Name: fmt.Sprintf("dept_a_%d", rand.Intn(100000)), Path: fmt.Sprintf("/%d/", rand.Intn(100000)), Sort: 1, DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) dept1Id, _ := dept1Res.LastInsertId() dept2Res, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{ ParentId: 0, Name: fmt.Sprintf("dept_b_%d", rand.Intn(100000)), Path: fmt.Sprintf("/%d/", rand.Intn(100000)), Sort: 1, DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) dept2Id, _ := dept2Res.LastInsertId() targetId := createTestUser(t, ctx, svcCtx, conn, struct { username string deptId int64 isSuperAdmin int64 }{ username: fmt.Sprintf("target_crossdept_%d", rand.Intn(100000)), deptId: dept2Id, isSuperAdmin: consts.IsSuperAdminNo, }) dept2, err := svcCtx.SysDeptModel.FindOne(ctx, dept2Id) require.NoError(t, err) dept1, err := svcCtx.SysDeptModel.FindOne(ctx, dept1Id) require.NoError(t, err) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", targetId) testutil.CleanTable(ctx, conn, "`sys_dept`", dept1Id, dept2Id) }) callerCtx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 99998, Username: "dev_crossdept", IsSuperAdmin: false, MemberType: consts.MemberTypeDeveloper, Status: consts.StatusEnabled, ProductCode: "p1", DeptId: dept1Id, DeptPath: dept1.Path, MinPermsLevel: math.MaxInt64, }) _ = dept2 err = CheckManageAccess(callerCtx, svcCtx, targetId, "p1") require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // TC-0483: caller.DeptPath为空时拒绝 func TestCheckManageAccess_EmptyDeptPath(t *testing.T) { svcCtx := newIntegrationSvcCtx() emptyPathCtx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 88888, Username: "dev_emptypath", IsSuperAdmin: false, MemberType: consts.MemberTypeDeveloper, Status: consts.StatusEnabled, ProductCode: "p1", DeptId: 1, DeptPath: "", MinPermsLevel: math.MaxInt64, }) err := CheckManageAccess(emptyPathCtx, svcCtx, 99999, "p1") require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Contains(t, ce.Error(), "部门信息异常") } // TC-0482: DEVELOPER 操作同部门的 MEMBER 且权限级别更高 → nil func TestCheckManageAccess_SameDeptLowerLevel(t *testing.T) { ctx := context.Background() svcCtx := newIntegrationSvcCtx() conn := testutil.GetTestSqlConn() now := time.Now().Unix() pc := fmt.Sprintf("tp_samedept_%d", rand.Intn(100000)) deptRes, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{ ParentId: 0, Name: fmt.Sprintf("dept_same_%d", rand.Intn(100000)), Path: "/", Sort: 1, DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) deptId, _ := deptRes.LastInsertId() deptObj, err := svcCtx.SysDeptModel.FindOne(ctx, deptId) require.NoError(t, err) targetId := createTestUser(t, ctx, svcCtx, conn, struct { username string deptId int64 isSuperAdmin int64 }{ username: fmt.Sprintf("target_samedept_%d", rand.Intn(100000)), deptId: deptId, isSuperAdmin: consts.IsSuperAdminNo, }) pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmember.SysProductMember{ ProductCode: pc, UserId: targetId, MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) pmId, _ := pmRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId) testutil.CleanTable(ctx, conn, "`sys_user`", targetId) testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) }) callerCtx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 99997, Username: "dev_samedept", IsSuperAdmin: false, MemberType: consts.MemberTypeDeveloper, Status: consts.StatusEnabled, ProductCode: pc, DeptId: deptId, DeptPath: deptObj.Path, MinPermsLevel: 1, }) err = CheckManageAccess(callerCtx, svcCtx, targetId, pc) assert.NoError(t, err) } // suppress unused import var _ = sqlx.ErrNotFound