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