package rbac import ( "context" "testing" "github.com/stretchr/testify/require" "portal-mini-go/internal/model/admin" "portal-mini-go/internal/pkg/errors" adminrepo "portal-mini-go/internal/repository/admin" ) // ---- fakes ---- type fakeCache struct{ m map[uint64]memPair } type memPair struct{ roles, perms []string } func newFakeCache() *fakeCache { return &fakeCache{m: map[uint64]memPair{}} } func (f *fakeCache) Get(_ context.Context, id uint64) ([]string, []string, bool) { p, ok := f.m[id] return p.roles, p.perms, ok } func (f *fakeCache) Set(_ context.Context, id uint64, roles, perms []string) { f.m[id] = memPair{roles: roles, perms: perms} } func (f *fakeCache) Invalidate(_ context.Context, id uint64) { delete(f.m, id) } func (f *fakeCache) InvalidateAll(_ context.Context) { f.m = map[uint64]memPair{} } type fakeUserRepo struct { roles map[uint64][]string perms map[uint64][]string count int64 created *admin.User setRoles map[uint64][]uint64 } func (r *fakeUserRepo) GetByUsername(context.Context, string) (*admin.User, error) { return nil, errors.ErrNotFound } func (r *fakeUserRepo) GetByID(context.Context, uint64) (*admin.User, error) { return nil, errors.ErrNotFound } func (r *fakeUserRepo) Create(_ context.Context, u *admin.User) error { r.created = u; return nil } func (r *fakeUserRepo) Count(context.Context) (int64, error) { return r.count, nil } func (r *fakeUserRepo) List(context.Context, adminrepo.UserListFilter) ([]admin.User, int64, error) { return nil, 0, nil } func (r *fakeUserRepo) Update(context.Context, uint64, map[string]interface{}) error { return nil } func (r *fakeUserRepo) Delete(context.Context, uint64) error { return nil } func (r *fakeUserRepo) SetRoles(_ context.Context, id uint64, roleIDs []uint64) error { if r.setRoles == nil { r.setRoles = map[uint64][]uint64{} } r.setRoles[id] = roleIDs return nil } func (r *fakeUserRepo) RoleCodesOf(_ context.Context, id uint64) ([]string, error) { return r.roles[id], nil } func (r *fakeUserRepo) PermissionCodesOf(_ context.Context, id uint64) ([]string, error) { return r.perms[id], nil } type fakeRoleRepo struct { byCode map[string]*admin.Role created *admin.Role } func newFakeRoleRepo() *fakeRoleRepo { return &fakeRoleRepo{byCode: map[string]*admin.Role{}} } func (r *fakeRoleRepo) GetByID(context.Context, uint64) (*admin.Role, error) { return nil, errors.ErrNotFound } func (r *fakeRoleRepo) GetByCode(_ context.Context, code string) (*admin.Role, error) { if m, ok := r.byCode[code]; ok { return m, nil } return nil, errors.ErrNotFound } func (r *fakeRoleRepo) Create(_ context.Context, m *admin.Role) error { r.created = m r.byCode[m.Code] = m return nil } func (r *fakeRoleRepo) List(context.Context, adminrepo.RoleListFilter) ([]admin.Role, int64, error) { return nil, 0, nil } func (r *fakeRoleRepo) Update(context.Context, uint64, map[string]interface{}) error { return nil } func (r *fakeRoleRepo) Delete(context.Context, uint64) error { return nil } func (r *fakeRoleRepo) SetPermissions(context.Context, uint64, []uint64) error { return nil } func (r *fakeRoleRepo) PermissionCodesOf(context.Context, uint64) ([]string, error) { return nil, nil } type fakePermRepo struct { menus []admin.Permission codes []string } func (r *fakePermRepo) GetByID(context.Context, uint64) (*admin.Permission, error) { return nil, errors.ErrNotFound } func (r *fakePermRepo) Create(context.Context, *admin.Permission) error { return nil } func (r *fakePermRepo) List(context.Context, adminrepo.PermListFilter) ([]admin.Permission, int64, error) { return nil, 0, nil } func (r *fakePermRepo) Update(context.Context, uint64, map[string]interface{}) error { return nil } func (r *fakePermRepo) Delete(context.Context, uint64) error { return nil } func (r *fakePermRepo) AllMenuPermissions(context.Context) ([]admin.Permission, error) { return r.menus, nil } func (r *fakePermRepo) AllCodes(context.Context) ([]string, error) { return r.codes, nil } // ---- tests ---- func TestProvider_Load_Normal(t *testing.T) { ur := &fakeUserRepo{ roles: map[uint64][]string{1: {"editor"}}, perms: map[uint64][]string{1: {"user:view"}}, } pr := &fakePermRepo{codes: []string{"user:view", "user:manage", "mini:view"}} p := NewProvider(ur, newFakeRoleRepo(), pr, newFakeCache()) roles, perms, err := p.Load(context.Background(), 1) require.NoError(t, err) require.Equal(t, []string{"editor"}, roles) require.Equal(t, []string{"user:view"}, perms) } func TestProvider_Load_SuperAdmin(t *testing.T) { ur := &fakeUserRepo{ roles: map[uint64][]string{1: {admin.SuperAdminRoleCode}}, perms: map[uint64][]string{1: {}}, } pr := &fakePermRepo{codes: []string{"user:view", "user:manage"}} p := NewProvider(ur, newFakeRoleRepo(), pr, newFakeCache()) _, perms, err := p.Load(context.Background(), 1) require.NoError(t, err) // 超级管理员短路:权限集为全部权限 code(忽略聚合结果)。 require.ElementsMatch(t, []string{"user:view", "user:manage"}, perms) } func TestProvider_MenuTree_Normal(t *testing.T) { menus := []admin.Permission{ {ID: 1, Code: "dash", Type: admin.PermTypeMenu, ParentID: 0, Sort: 1}, {ID: 2, Code: "user", Type: admin.PermTypeMenu, ParentID: 1, Sort: 1}, {ID: 3, Code: "user:view", Type: admin.PermTypeMenu, ParentID: 2, Sort: 1}, {ID: 4, Code: "sys", Type: admin.PermTypeMenu, ParentID: 0, Sort: 2}, // 无权限且无后代 } ur := &fakeUserRepo{ roles: map[uint64][]string{1: {"editor"}}, perms: map[uint64][]string{1: {"user:view"}}, } pr := &fakePermRepo{menus: menus, codes: nil} p := NewProvider(ur, newFakeRoleRepo(), pr, newFakeCache()) tree, err := p.MenuTree(context.Background(), 1) require.NoError(t, err) // 仅保留 dash -> user -> user:view;sys 被过滤。 require.Len(t, tree, 1) require.Equal(t, uint64(1), tree[0].ID) require.Len(t, tree[0].Children, 1) require.Equal(t, uint64(2), tree[0].Children[0].ID) require.Len(t, tree[0].Children[0].Children, 1) require.Equal(t, uint64(3), tree[0].Children[0].Children[0].ID) } func TestProvider_MenuTree_SuperAdmin(t *testing.T) { menus := []admin.Permission{ {ID: 1, Code: "dash", Type: admin.PermTypeMenu, ParentID: 0}, {ID: 4, Code: "sys", Type: admin.PermTypeMenu, ParentID: 0}, } ur := &fakeUserRepo{roles: map[uint64][]string{1: {admin.SuperAdminRoleCode}}} pr := &fakePermRepo{menus: menus} p := NewProvider(ur, newFakeRoleRepo(), pr, newFakeCache()) tree, err := p.MenuTree(context.Background(), 1) require.NoError(t, err) require.Len(t, tree, 2) // 超级管理员返回完整树 } func TestProvider_Seed(t *testing.T) { ur := &fakeUserRepo{count: 0} rr := newFakeRoleRepo() pr := &fakePermRepo{codes: nil} p := NewProvider(ur, rr, pr, newFakeCache()) require.NoError(t, p.Seed(context.Background(), "admin", "Admin@123456")) require.NotNil(t, rr.created) require.Equal(t, admin.SuperAdminRoleCode, rr.created.Code) require.NotNil(t, ur.created) require.Equal(t, "admin", ur.created.Username) require.NotEmpty(t, ur.created.PasswordHash) require.Equal(t, []uint64{rr.created.ID}, ur.setRoles[ur.created.ID]) // 已有管理员时不再创建。 ur2 := &fakeUserRepo{count: 3} p2 := NewProvider(ur2, newFakeRoleRepo(), pr, newFakeCache()) require.NoError(t, p2.Seed(context.Background(), "admin", "x")) require.Nil(t, ur2.created) }