Files
backend/internal/service/role_service_test.go
lafay eb931bf1c6
All checks were successful
Build Backend / build (push) Successful in 2m19s
Build Backend / build-docker (push) Successful in 46s
feat(auth): implement session-based token management with revocation support
Add Session model, SessionService, and SessionRepository to track user login
sessions and enable token revocation on auth-critical events.

- Introduce explicit TokenType (access/refresh) in JWT claims to prevent
  refresh token misuse via access token endpoints
- Add SessionID field to JWT claims, enabling stateless JWT validation
  against revoked sessions
- Replace legacy Auth middleware with RequireAuth/OptionalAuth pipeline
  that validates token type, account status, and session validity
- Implement session revocation on password change, reset, user ban/inactive,
  and explicit logout
- Add Principal cache with active invalidation for banned/role-changed users
- Fix IDOR vulnerability: GetMessagesByCursor now validates currentUserID
  is conversation participant via GetParticipantStrict
- Add group member visibility checks: announcements, group info, member list
  now require group membership
- Simplify Casbin policy: remove g grouping, use r.sub == p.sub matcher
  with globMatch; user_roles table is single source of truth for roles
- Add migration logic to clean legacy casbin g rules and migrate old p rules
  from path-style to admin/<domain> resource naming
2026-07-05 18:28:08 +08:00

222 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"testing"
apperrors "with_you/internal/errors"
"with_you/internal/model"
"github.com/google/uuid"
"gorm.io/gorm"
)
// fakeRoleRepo 实现 repository.RoleRepository 接口,用于测试。
type fakeRoleRepo struct {
roles map[string]bool // userID -> role set不支持多角色测试足够
addErr error
getRoleErr error
rolesForUser []string
getRoleByName *model.Role
}
func (f *fakeRoleRepo) GetUserRoles(ctx context.Context, userID string) ([]string, error) {
return f.rolesForUser, nil
}
func (f *fakeRoleRepo) AddRoleForUser(ctx context.Context, userID, role string) error {
if f.addErr != nil {
return f.addErr
}
if f.roles == nil {
f.roles = make(map[string]bool)
}
f.roles[userID+":"+role] = true
return nil
}
func (f *fakeRoleRepo) DeleteRoleForUser(ctx context.Context, userID, role string) error {
if f.roles == nil {
return nil
}
delete(f.roles, userID+":"+role)
return nil
}
func (f *fakeRoleRepo) DeleteAllRolesForUser(ctx context.Context, userID string) error {
return nil
}
func (f *fakeRoleRepo) HasRole(ctx context.Context, userID, role string) (bool, error) {
if f.roles == nil {
return false, nil
}
return f.roles[userID+":"+role], nil
}
func (f *fakeRoleRepo) GetRole(ctx context.Context, name string) (*model.Role, error) {
if f.getRoleErr != nil {
return nil, f.getRoleErr
}
if f.getRoleByName != nil && f.getRoleByName.Name == name {
return f.getRoleByName, nil
}
return &model.Role{Name: name}, nil
}
func (f *fakeRoleRepo) GetAllRoles(ctx context.Context) ([]model.Role, error) {
return nil, nil
}
func (f *fakeRoleRepo) GetRoleUserCount(ctx context.Context, role string) (int64, error) {
return 0, nil
}
// fakeCasbinForRole 测试 CasbinService 的最小实现,覆盖 AssignRole 所需方法。
type fakeCasbinForRole struct {
roles map[string][]string // userID -> roles
hasRoleMap map[string]bool // "userID:role" -> bool
addErr error
}
func (f *fakeCasbinForRole) Enforce(ctx context.Context, sub, obj, act string) (bool, error) {
return false, nil
}
func (f *fakeCasbinForRole) EnforceForUser(ctx context.Context, userID, obj, act string) (bool, error) {
return false, nil
}
func (f *fakeCasbinForRole) AddRoleForUser(ctx context.Context, userID, role string) error {
if f.addErr != nil {
return f.addErr
}
if f.roles == nil {
f.roles = make(map[string][]string)
}
f.roles[userID] = append(f.roles[userID], role)
return nil
}
func (f *fakeCasbinForRole) DeleteRoleForUser(ctx context.Context, userID, role string) error {
return nil
}
func (f *fakeCasbinForRole) GetRolesForUser(ctx context.Context, userID string) ([]string, error) {
return f.roles[userID], nil
}
func (f *fakeCasbinForRole) HasRoleForUser(ctx context.Context, userID, role string) (bool, error) {
if f.hasRoleMap == nil {
return false, nil
}
return f.hasRoleMap[userID+":"+role], nil
}
func (f *fakeCasbinForRole) AddPolicy(ctx context.Context, role, resource, action string) error {
return nil
}
func (f *fakeCasbinForRole) RemovePolicy(ctx context.Context, role, resource, action string) error {
return nil
}
func (f *fakeCasbinForRole) LoadPolicy(ctx context.Context) error { return nil }
func (f *fakeCasbinForRole) ClearCache(ctx context.Context) error { return nil }
func (f *fakeCasbinForRole) GetPermissionsForRole(ctx context.Context, role string) ([][]string, error) {
return nil, nil
}
func (f *fakeCasbinForRole) UpdatePermissionsForRole(ctx context.Context, role string, perms [][]string) error {
return nil
}
func (f *fakeCasbinForRole) GetAllPermissions(ctx context.Context) [][]string { return nil }
// TestAssignRole_NonSuperAdminAssigningSuperAdmin 非 super_admin 分配 super_admin → 拒绝。
func TestAssignRole_NonSuperAdminAssigningSuperAdmin(t *testing.T) {
roleRepo := &fakeRoleRepo{
getRoleByName: &model.Role{Name: model.RoleSuperAdmin},
}
casbinSvc := &fakeCasbinForRole{
roles: map[string][]string{
"operator": {model.RoleAdmin}, // operator 是普通 admin不是 super_admin
},
}
svc := NewRoleService(roleRepo, casbinSvc)
err := svc.AssignRole(context.Background(), "operator", "target", model.RoleSuperAdmin)
if !errors.Is(err, apperrors.ErrCannotModifySuperAdmin) {
t.Errorf("expected ErrCannotModifySuperAdmin, got %v", err)
}
}
// TestAssignRole_SuperAdminAssigningSuperAdmin super_admin 分配 super_admin → 成功。
func TestAssignRole_SuperAdminAssigningSuperAdmin(t *testing.T) {
roleRepo := &fakeRoleRepo{
getRoleByName: &model.Role{Name: model.RoleSuperAdmin},
}
casbinSvc := &fakeCasbinForRole{
roles: map[string][]string{
"operator": {model.RoleSuperAdmin}, // operator 是 super_admin
},
}
svc := NewRoleService(roleRepo, casbinSvc)
err := svc.AssignRole(context.Background(), "operator", "target", model.RoleSuperAdmin)
if err != nil {
t.Errorf("super_admin should be able to assign super_admin, got %v", err)
}
}
// TestAssignRole_SelfModification 不能改自己的角色。
func TestAssignRole_SelfModification(t *testing.T) {
roleRepo := &fakeRoleRepo{}
casbinSvc := &fakeCasbinForRole{}
svc := NewRoleService(roleRepo, casbinSvc)
err := svc.AssignRole(context.Background(), "u1", "u1", model.RoleAdmin)
if !errors.Is(err, apperrors.ErrCannotModifyOwnRole) {
t.Errorf("expected ErrCannotModifyOwnRole, got %v", err)
}
}
// TestRemoveRole_SuperAdminRole 不能移除 super_admin 角色。
func TestRemoveRole_SuperAdminRole(t *testing.T) {
roleRepo := &fakeRoleRepo{
roles: map[string]bool{"target:" + model.RoleSuperAdmin: true},
}
casbinSvc := &fakeCasbinForRole{
hasRoleMap: map[string]bool{"target:" + model.RoleSuperAdmin: true},
}
svc := NewRoleService(roleRepo, casbinSvc)
err := svc.RemoveRole(context.Background(), "operator", "target", model.RoleSuperAdmin)
if !errors.Is(err, apperrors.ErrCannotModifySuperAdmin) {
t.Errorf("expected ErrCannotModifySuperAdmin, got %v", err)
}
}
// TestAssignRole_RoleNotFound 角色不存在 → ErrRoleNotFound。
func TestAssignRole_RoleNotFound(t *testing.T) {
roleRepo := &fakeRoleRepo{
getRoleErr: gorm.ErrRecordNotFound,
}
casbinSvc := &fakeCasbinForRole{
roles: map[string][]string{
"operator": {model.RoleSuperAdmin},
},
}
svc := NewRoleService(roleRepo, casbinSvc)
err := svc.AssignRole(context.Background(), "operator", "target", model.RoleAdmin)
if !errors.Is(err, apperrors.ErrRoleNotFound) {
t.Errorf("expected ErrRoleNotFound, got %v", err)
}
}
// 确保编译期 import 使用。
var _ = uuid.New