| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- package auth
- import (
- "errors"
- "math"
- "testing"
- "perms-system-server/internal/consts"
- "perms-system-server/internal/loaders"
- deptModel "perms-system-server/internal/model/dept"
- memberModel "perms-system-server/internal/model/productmember"
- userModel "perms-system-server/internal/model/user"
- "perms-system-server/internal/response"
- "perms-system-server/internal/testutil/ctxhelper"
- "perms-system-server/internal/testutil/mocks"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- "go.uber.org/mock/gomock"
- )
- // ---------------------------------------------------------------------------
- // 覆盖目标:审计 L-4 修复 —— checkPermLevel 在 DB 非 ErrNotFound 错误时必须 fail-close 返回 500,
- // 而不是被默默降级为"目标无角色 → 权限最低 → 放行"。
- // 该测试用 gomock 伪造 SysRoleModel.FindMinPermsLevelByUserIdAndProductCode 返回一个通用 DB 错误,
- // 验证 CheckManageAccess 的响应是 500 CodeError(非 403)。
- // ---------------------------------------------------------------------------
- // TC-0819: L-4 —— checkPermLevel 遇到非 ErrNotFound 的 DB 错误时必须 500。
- func TestCheckManageAccess_DBError_FailCloseWith500(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- const targetUserId = int64(42)
- const callerDeptId = int64(1)
- const targetDeptId = int64(2)
- const productCode = "test_product"
- // 让 checkDeptHierarchy 顺利放行:target 在 caller 子部门下(path 前缀 /1/)。
- mockUser := mocks.NewMockSysUserModel(ctrl)
- mockUser.EXPECT().FindOne(gomock.Any(), int64(targetUserId)).
- Return(&userModel.SysUser{Id: targetUserId, DeptId: targetDeptId}, nil).AnyTimes()
- mockDept := mocks.NewMockSysDeptModel(ctrl)
- mockDept.EXPECT().FindOne(gomock.Any(), targetDeptId).
- Return(&deptModel.SysDept{Id: targetDeptId, Path: "/1/2/"}, nil).AnyTimes()
- // 让 permsLevel 判定路径进入:"target 也是 MEMBER,同级 → 需要 DB 查 permsLevel"。
- mockPM := mocks.NewMockSysProductMemberModel(ctrl)
- mockPM.EXPECT().FindOneByProductCodeUserId(gomock.Any(), productCode, int64(targetUserId)).
- Return(&memberModel.SysProductMember{
- UserId: targetUserId, ProductCode: productCode,
- MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled,
- }, nil).AnyTimes()
- // 关键:SysRoleModel 返回非 ErrNotFound 的 DB 错误。
- dbErr := errors.New("driver: bad connection")
- mockRole := mocks.NewMockSysRoleModel(ctrl)
- mockRole.EXPECT().
- FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(targetUserId), productCode).
- Return(int64(0), dbErr).AnyTimes()
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
- User: mockUser,
- Dept: mockDept,
- Role: mockRole,
- ProductMember: mockPM,
- })
- ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
- UserId: 100,
- Username: "l4_member_caller",
- IsSuperAdmin: false,
- MemberType: consts.MemberTypeMember,
- Status: consts.StatusEnabled,
- ProductCode: productCode,
- DeptId: callerDeptId,
- DeptPath: "/1/",
- MinPermsLevel: 100,
- })
- err := CheckManageAccess(ctx, svcCtx, targetUserId, productCode)
- require.Error(t, err, "DB 错误时必须 fail-close")
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce), "必须是结构化 CodeError")
- assert.Equal(t, 500, ce.Code(),
- "L-4:DB 非 ErrNotFound 错误绝不能被伪装成'无角色'从而降级为 403/放行;必须是 500")
- assert.NotContains(t, ce.Error(), "无权管理",
- "错误消息不得看起来像权限判定成功后做出的业务决策(避免误导运维)")
- }
- // TC-0820: L-4 对照组 —— ErrNotFound 仍应被视作"无角色",即按最低权限处理(由 caller.MinPermsLevel 决定放行还是 403)。
- // 这里构造 caller 的 MinPermsLevel=MaxInt64(sentinel),target 无角色(ErrNotFound) →
- // caller.MinPermsLevel(=MaxInt64) >= targetLevel(=MaxInt64) → 返回 403。这个分支不是本次回归重点,
- // 只是用来证明 ErrNotFound 路径没有被修复误伤为 500。
- func TestCheckManageAccess_ErrNotFound_StillTreatedAsNoRole(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- const targetUserId = int64(43)
- const callerDeptId = int64(1)
- const targetDeptId = int64(2)
- const productCode = "test_product"
- mockUser := mocks.NewMockSysUserModel(ctrl)
- mockUser.EXPECT().FindOne(gomock.Any(), int64(targetUserId)).
- Return(&userModel.SysUser{Id: targetUserId, DeptId: targetDeptId}, nil).AnyTimes()
- mockDept := mocks.NewMockSysDeptModel(ctrl)
- mockDept.EXPECT().FindOne(gomock.Any(), targetDeptId).
- Return(&deptModel.SysDept{Id: targetDeptId, Path: "/1/2/"}, nil).AnyTimes()
- mockPM := mocks.NewMockSysProductMemberModel(ctrl)
- mockPM.EXPECT().FindOneByProductCodeUserId(gomock.Any(), productCode, int64(targetUserId)).
- Return(&memberModel.SysProductMember{
- UserId: targetUserId, ProductCode: productCode,
- MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled,
- }, nil).AnyTimes()
- mockRole := mocks.NewMockSysRoleModel(ctrl)
- mockRole.EXPECT().
- FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(targetUserId), productCode).
- Return(int64(0), sqlx.ErrNotFound).AnyTimes()
- // 审计 H-2:checkPermLevel 现在也会对 caller 做 fresh read。
- // 这里构造"caller 同样无角色 → callerNoRole=true → >= 比较由 callerNoRole 决定,结果仍 403"。
- mockRole.EXPECT().
- FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(101), productCode).
- Return(int64(0), sqlx.ErrNotFound).AnyTimes()
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
- User: mockUser, Dept: mockDept, Role: mockRole, ProductMember: mockPM,
- })
- ctx := ctxhelper.CustomCtx(&loaders.UserDetails{
- UserId: 101,
- Username: "l4_caller_no_role",
- IsSuperAdmin: false, MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled,
- ProductCode: productCode, DeptId: callerDeptId, DeptPath: "/1/",
- // sentinel:自己也没有任何角色。
- MinPermsLevel: math.MaxInt64,
- })
- err := CheckManageAccess(ctx, svcCtx, targetUserId, productCode)
- require.Error(t, err, "caller 与 target 都 sentinel → >= 比较应拦截")
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 403, ce.Code(),
- "ErrNotFound 正常降级为 sentinel;结果应是业务 403 而非基础设施 500")
- }
|