| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- package auth
- import (
- "context"
- "errors"
- "testing"
- "perms-system-server/internal/consts"
- "perms-system-server/internal/loaders"
- "perms-system-server/internal/response"
- "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"
- )
- // ---------------------------------------------------------------------------
- // 覆盖目标:审计 M-R10-3 —— LoadCallerAssignableLevel 在一次请求内对同一 caller 只做
- // 一次 DB 读;CheckRoleLevelAgainst 不再访问 DB,给 BindRoles 这种"批量覆盖"的接口把
- // N 次 loadFreshMinPermsLevel 合并为 1 次。
- //
- // 核心断言口径:
- // 1. SuperAdmin / ADMIN / DEVELOPER 等全权调用者不打 DB(HasFullPerms=true 短路);
- // 2. MEMBER caller 打 1 次 FindMinPermsLevelByUserIdAndProductCode;
- // 3. caller.ErrNotFound → NoRole=true(不打翻 500);
- // 4. caller 其他 DB 错误 → fail-close 500(保持与 loadFreshMinPermsLevel 一致的口径,
- // 避免降级为"无角色 = 最低级"放行)。
- // 5. CheckRoleLevelAgainst 是纯函数,不访问 svcCtx。
- // ---------------------------------------------------------------------------
- // TC-1017: M-R10-3 —— SuperAdmin / ADMIN / DEVELOPER 走 HasFullPerms 短路,不触碰 DB。
- func TestLoadCallerAssignableLevel_FullPermsShortCircuit_NoDB(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- mockRole := mocks.NewMockSysRoleModel(ctrl)
- // 关键:没有 EXPECT.FindMinPermsLevelByUserIdAndProductCode —— 一旦被调用会 fail。
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
- cases := []struct {
- name string
- caller *loaders.UserDetails
- }{
- {
- name: "SuperAdmin",
- caller: &loaders.UserDetails{UserId: 1, IsSuperAdmin: true, ProductCode: "p"},
- },
- {
- name: "ADMIN",
- caller: &loaders.UserDetails{UserId: 2, MemberType: consts.MemberTypeAdmin, ProductCode: "p"},
- },
- {
- name: "DEVELOPER",
- caller: &loaders.UserDetails{UserId: 3, MemberType: consts.MemberTypeDeveloper, ProductCode: "p"},
- },
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- snap, err := LoadCallerAssignableLevel(context.Background(), svcCtx, c.caller)
- require.NoError(t, err)
- assert.True(t, snap.HasFullPerms, "全权调用者必须落 HasFullPerms 分支")
- assert.False(t, snap.NoRole)
- })
- }
- }
- // TC-1018: M-R10-3 —— MEMBER caller 仅打 1 次 FindMinPermsLevelByUserIdAndProductCode;
- // 循环内对 N 个角色走 CheckRoleLevelAgainst 不再打 DB。
- func TestLoadCallerAssignableLevel_Member_ReadsDBOnce_ThenConstantTime(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- const callerId = int64(1001)
- const productCode = "pc_m_r10_3"
- mockRole := mocks.NewMockSysRoleModel(ctrl)
- // 关键断言:Times(1) 保证 N 个角色场景不会退化为 N 次 DB 读。
- mockRole.EXPECT().
- FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode).
- Return(int64(100), nil).
- Times(1)
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
- caller := &loaders.UserDetails{
- UserId: callerId,
- MemberType: consts.MemberTypeMember,
- ProductCode: productCode,
- }
- snap, err := LoadCallerAssignableLevel(context.Background(), svcCtx, caller)
- require.NoError(t, err)
- assert.False(t, snap.HasFullPerms)
- assert.False(t, snap.NoRole)
- assert.Equal(t, int64(100), snap.Level)
- // 模拟 BindRoles 批量覆盖的循环:5 个角色,全部走 CheckRoleLevelAgainst 的纯比较,
- // 任何一个角色额外打 DB 都会命中 gomock 的 "unexpected call" 断言。
- roleLevels := []int64{200, 150, 300, 120, 999}
- for _, rl := range roleLevels {
- if err := CheckRoleLevelAgainst(snap, rl); err != nil {
- t.Fatalf("role level %d should be assignable against caller level %d: %v", rl, snap.Level, err)
- }
- }
- // 同级与更高级一律拒绝(与 GuardRoleLevelAssignable 对称):
- for _, rl := range []int64{100, 50, 1} {
- err := CheckRoleLevelAgainst(snap, rl)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr), "同级或更高级必须返回 CodeError")
- assert.Equal(t, 403, codeErr.Code())
- }
- }
- // TC-1019: M-R10-3 —— caller 在该产品下无角色 → NoRole=true,不回滚 500。
- func TestLoadCallerAssignableLevel_Member_ErrNotFound_MapsToNoRole(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- mockRole := mocks.NewMockSysRoleModel(ctrl)
- mockRole.EXPECT().
- FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(42), "pc").
- Return(int64(0), sqlx.ErrNotFound).
- Times(1)
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
- caller := &loaders.UserDetails{
- UserId: 42,
- MemberType: consts.MemberTypeMember,
- ProductCode: "pc",
- }
- snap, err := LoadCallerAssignableLevel(context.Background(), svcCtx, caller)
- require.NoError(t, err, "ErrNotFound 必须被归一为 NoRole=true,不得外泄为 500")
- assert.False(t, snap.HasFullPerms)
- assert.True(t, snap.NoRole)
- // 验证 NoRole 的 caller 连最低级角色也无法分配(与修复前保持的业务语义一致)。
- err = CheckRoleLevelAgainst(snap, 999)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 403, codeErr.Code())
- assert.Contains(t, codeErr.Error(), "没有可分配的角色等级")
- }
- // TC-1020: M-R10-3 —— caller 其他 DB 错误必须 fail-close 500,不得降级为 NoRole 放行。
- // 保证修复没有把"DB 抖动"悄悄压成"无角色 → 最低级 → 403"这种语义欺骗。
- func TestLoadCallerAssignableLevel_Member_DBError_FailClose500(t *testing.T) {
- ctrl := gomock.NewController(t)
- t.Cleanup(ctrl.Finish)
- dbErr := errors.New("driver: bad connection")
- mockRole := mocks.NewMockSysRoleModel(ctrl)
- mockRole.EXPECT().
- FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(7), "pc").
- Return(int64(0), dbErr).
- Times(1)
- svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
- caller := &loaders.UserDetails{
- UserId: 7,
- MemberType: consts.MemberTypeMember,
- ProductCode: "pc",
- }
- _, err := LoadCallerAssignableLevel(context.Background(), svcCtx, caller)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr), "DB 抖动必须 fail-close 为 CodeError 而非 nil")
- assert.Equal(t, 500, codeErr.Code())
- }
|