|
@@ -0,0 +1,170 @@
|
|
|
|
|
+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())
|
|
|
|
|
+}
|