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() 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") }