Merge branch 'dev'

This commit is contained in:
dongli 2025-08-11 21:24:48 -07:00
commit f7988ce1d7
100 changed files with 3954 additions and 63 deletions

View File

View File

View File

@ -1,4 +1,8 @@
from typing import Optional, Tuple
from typing import Optional, Tuple, List
from backend.services.permission.permission_service import PermissionService
from backend.services.permission.role_service import RoleService
from common.constants.region import UserRegion
from common.log.log_utils import log_entry_exit_async
from backend.business.signin_manager import SignInManager
from backend.models.user.constants import UserLoginAction
@ -13,10 +17,28 @@ class SignInHub:
@log_entry_exit_async
async def signin_with_email_and_code(
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
) -> Tuple[int, Optional[int], Optional[str], Optional[str]]:
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str], Optional[UserRegion], Optional[List[str]],
Optional[List[str]]]:
"""
Interacts with the business layer to handle the sign-in process with email and code.
Try to signin with email and code.
create a new user account, if the email address has never been used before.
Args:
email (str): email address
code (str): auth code to be verified
host (str): the host address by which the client access the frontend service
time_zone (Optional[str]): time zone of the frontend service
Returns:
[int, Optional[int], Optional[str], Optional[str]]:
- int: UserLoginAction
- Optional[int]: user role
- Optional[str]: user_id
- Optional[str]: flid
- Optional[str]: region
- Optional[str]: user role names
- Optional[str]: user permission keys
"""
return await self.signin_manager.signin_with_email_and_code(
email=email, code=code, host=host, time_zone=time_zone
@ -25,7 +47,7 @@ class SignInHub:
@log_entry_exit_async
async def signin_with_email_and_password(
self, email: str, password: str
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]:
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str], Optional[List[str]], Optional[List[str]]]:
"""Try to signin with email and password.
Args:
@ -38,6 +60,8 @@ class SignInHub:
- Optional[int]: user role
- Optional[str]: user_id
- Optional[str]: flid
- Optional[List[str]]: user role names
- Optional[List[str]]: user permission keys
"""
return await self.signin_manager.signin_with_email_and_password(
email=email, password=password

View File

@ -1,9 +1,11 @@
import random
from typing import Tuple, Optional
from typing import Tuple, Optional, List
from backend.services.auth.user_auth_service import UserAuthService
from common.constants.region import UserRegion
from common.utils.region import RegionHandler
from backend.models.user.constants import (
UserLoginAction,
NewUserMethod,
)
from backend.models.user.constants import UserLoginAction
@ -36,14 +38,15 @@ class SignInManager:
async def signin_with_email_and_code(
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
) -> Tuple[int, Optional[int], Optional[str], Optional[str]]:
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[UserRegion], Optional[str], Optional[List[str]], Optional[List[str]]]:
"""Try to signin with email and code.
create a new user account, if the email address has never been used before.
Args:
email (str): email address
code (str): auth code to be verified
host (str): the host address by which the client access the frontend service
host (str): the host address by which the client access the frontend service, for detecting UserRegion
time_zone (str, optional): timezone of the frontend service
Returns:
[int, Optional[int], Optional[str], Optional[str]]:
@ -51,6 +54,9 @@ class SignInManager:
- Optional[int]: user role
- Optional[str]: user_id
- Optional[str]: flid
- Optional[str]: region
- Optional[str]: user role names
- Optional[str]: user permission keys
"""
# check if the user account exist
user_id = await self.user_auth_service.get_user_id_by_email(email)
@ -67,7 +73,6 @@ class SignInManager:
method=NewUserMethod.EMAIL, region=preferred_region
)
)
user_id = str(user_account.id)
await self.user_management_service.initialize_new_user_data(
user_id=str(user_account.id),
@ -80,6 +85,9 @@ class SignInManager:
user_account = await self.user_management_service.get_account_by_id(
user_id=user_id
)
role_names, permission_keys = await self.user_management_service.get_role_and_permission_by_user_id(
user_id=user_id
)
if await self.user_auth_service.is_flid_reset_required(user_id):
return (
UserLoginAction.REVIEW_AND_REVISE_FLID,
@ -87,10 +95,11 @@ class SignInManager:
user_id,
email.split("@")[0],
preferred_region,
role_names,
permission_keys,
)
user_flid = await self.user_auth_service.get_user_flid(user_id)
if await self.user_auth_service.is_password_reset_required(user_id):
return (
UserLoginAction.NEW_USER_SET_PASSWORD,
@ -98,25 +107,29 @@ class SignInManager:
user_id,
user_flid,
preferred_region,
role_names,
permission_keys,
)
return (
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
user_account.user_role,
user_id,
user_flid,
preferred_region,
role_names,
permission_keys,
)
else:
await self.module_logger.log_warning(
warning="The auth code is invalid.",
properties={"email": email, "code": code},
)
return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None
# TODO refactor this to reduce None
return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None, None, None
async def signin_with_email_and_password(
self, email: str, password: str
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]:
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str], Optional[List[str]], Optional[List[str]]]:
# check if the user account exist
user_id = await self.user_auth_service.get_user_id_by_email(email)
@ -126,16 +139,19 @@ class SignInManager:
if is_new_user:
# cannot find the email address
return [UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None]
# TODO refactor this to reduce None
return (UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None, None)
else:
if await self.user_auth_service.is_password_reset_required(user_id):
# password hasn't been set before, save password for the user
return [
return (
UserLoginAction.NEW_USER_SET_PASSWORD,
None,
None,
None,
]
None,
None
)
else:
if await self.user_auth_service.verify_user_with_password(
user_id, password
@ -143,33 +159,40 @@ class SignInManager:
user_account = await self.user_management_service.get_account_by_id(
user_id=user_id
)
role_names, permission_keys = await self.user_management_service.get_role_and_permission_by_user_id(user_id)
if await self.user_auth_service.is_flid_reset_required(user_id):
return [
return (
UserLoginAction.REVIEW_AND_REVISE_FLID,
user_account.user_role,
user_id,
email.split("@")[0],
]
role_names,
permission_keys,
)
user_flid = await self.user_auth_service.get_user_flid(user_id)
# password verification passed
return [
return (
UserLoginAction.USER_SIGNED_IN,
user_account.user_role,
user_id,
user_flid,
]
role_names,
permission_keys
)
else:
# ask user to input password again.
# TODO: we need to limit times of user to input the wrong password
return [
# TODO refactor this to reduce None
return (
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
None,
None,
None,
]
None,
None
)
async def update_new_user_flid(
self, user_id: str, user_flid: str
@ -192,26 +215,26 @@ class SignInManager:
"code_depot_email": code_depot_email,
},
)
return [
return (
UserLoginAction.REVIEW_AND_REVISE_FLID,
"{}{}".format(user_flid, random.randint(100, 999)),
]
)
await self.user_auth_service.update_flid(user_id, user_flid)
if await self.user_auth_service.is_password_reset_required(user_id):
return [
return (
UserLoginAction.NEW_USER_SET_PASSWORD,
user_flid,
]
)
else:
return [
return (
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
user_flid,
]
)
else:
return [
return (
UserLoginAction.REVIEW_AND_REVISE_FLID,
"{}{}".format(user_flid, random.randint(100, 999)),
]
)
async def try_signin_with_email(self, email: str, host: str) -> UserLoginAction:
"""try signin through email, generate auth code and send to the email address

View File

@ -0,0 +1,97 @@
from typing import Optional, List, Tuple
from fastapi.exceptions import RequestValidationError
from backend.models.permission.models import PermissionDoc, RoleDoc
from beanie import PydanticObjectId
from datetime import datetime
class PermissionHandler:
def __init__(self):
pass
async def create_permission(self, permission_key: str, permission_name: str,
description: Optional[str] = None) -> Optional[PermissionDoc]:
"""Create a new permission document"""
if not permission_key or not permission_name:
raise RequestValidationError("permission_key and permission_name are required.")
# if exists.
if await PermissionDoc.find_one(
{str(PermissionDoc.permission_key): permission_key}) or await PermissionDoc.find_one(
{str(PermissionDoc.permission_name): permission_name}):
raise RequestValidationError("permission has already been created.")
doc = PermissionDoc(
permission_key=permission_key,
permission_name=permission_name,
description=description,
created_at=datetime.now(),
updated_at=datetime.now()
)
await doc.insert()
return doc
async def update_permission(self, permission_id: PydanticObjectId, permission_key: Optional[str] = None,
permission_name: Optional[str] = None, description: Optional[str] = None) -> Optional[
PermissionDoc]:
"""Update an existing permission document by id, ensuring permission_key is unique"""
if not permission_id or not permission_key or not permission_name:
raise RequestValidationError("permission_id, permission_key and permission_name is required.")
doc = await PermissionDoc.get(permission_id)
if not doc:
raise RequestValidationError("Permission not found.")
if doc.is_default:
raise RequestValidationError("Default permission cannot be updated.")
# Check for uniqueness (exclude self)
conflict = await PermissionDoc.find_one({
"$and": [
{"_id": {"$ne": permission_id}},
{"$or": [
{str(PermissionDoc.permission_key): permission_key},
{str(PermissionDoc.permission_name): permission_name}
]}
]
})
if conflict:
raise RequestValidationError("Permission name or permission key already exists.")
doc.permission_key = permission_key
doc.permission_name = permission_name
doc.description = description
doc.updated_at = datetime.now()
await doc.save()
return doc
async def query_permissions(
self,
permission_key: Optional[str] = None,
permission_name: Optional[str] = None,
skip: int = 0,
limit: int = 10
) -> Tuple[List[PermissionDoc], int]:
"""Query permissions with pagination and fuzzy search"""
query = {}
if permission_key:
query[str(PermissionDoc.permission_key)] = {"$regex": permission_key, "$options": "i"}
if permission_name:
query[str(PermissionDoc.permission_name)] = {"$regex": permission_name, "$options": "i"}
cursor = PermissionDoc.find(query)
total = await cursor.count()
docs = await cursor.skip(skip).limit(limit).to_list()
return docs, total
async def delete_permission(self, permission_id: PydanticObjectId) -> None:
"""Delete a permission document after checking if it is referenced by any role and is not default"""
if not permission_id:
raise RequestValidationError("permission_id is required.")
# Check if any role references this permission
role = await RoleDoc.find_one({"permission_ids": str(permission_id)})
if role:
raise RequestValidationError("Permission is referenced by a role and cannot be deleted.")
doc = await PermissionDoc.get(permission_id)
if not doc:
raise RequestValidationError("Permission not found.")
# Check if the permission is default
if doc.is_default:
raise RequestValidationError("Default permission cannot be deleted.")
await doc.delete()

View File

@ -0,0 +1,113 @@
from typing import Optional, List, Tuple
from fastapi.exceptions import RequestValidationError
from backend.models.permission.models import RoleDoc, PermissionDoc, UserRoleDoc
from beanie import PydanticObjectId
from datetime import datetime
class RoleHandler:
def __init__(self):
pass
async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> Optional[RoleDoc]:
"""Create a new role, ensuring role_key and role_name are unique and not empty"""
if not role_key or not role_name:
raise RequestValidationError("role_key and role_name are required.")
if await RoleDoc.find_one({str(RoleDoc.role_key): role_key}) or await RoleDoc.find_one(
{str(RoleDoc.role_name): role_name}):
raise RequestValidationError("role_key or role_name has already been created.")
doc = RoleDoc(
role_key=role_key,
role_name=role_name,
role_description=role_description,
permission_ids=[],
role_level=role_level,
created_at=datetime.now(),
updated_at=datetime.now()
)
await doc.insert()
return doc
async def update_role(self, role_id: PydanticObjectId, role_key: str, role_name: str,
role_description: Optional[str], role_level: int) -> Optional[
RoleDoc]:
"""Update an existing role, ensuring role_key and role_name are unique and not empty"""
if not role_id or not role_key or not role_name:
raise RequestValidationError("role_id, role_key and role_name are required.")
doc = await RoleDoc.get(role_id)
if not doc:
raise RequestValidationError("role not found.")
if doc.is_default:
raise RequestValidationError("Default role cannot be updated.")
# Check for uniqueness (exclude self)
conflict = await RoleDoc.find_one({
"$and": [
{"_id": {"$ne": role_id}},
{"$or": [
{str(RoleDoc.role_key): role_key},
{str(RoleDoc.role_name): role_name}
]}
]
})
if conflict:
raise RequestValidationError("role_key or role_name already exists.")
doc.role_key = role_key
doc.role_name = role_name
doc.role_description = role_description
doc.role_level = role_level
doc.updated_at = datetime.now()
await doc.save()
return doc
async def query_roles(self, role_key: Optional[str], role_name: Optional[str], skip: int = 0, limit: int = 10) -> \
Tuple[List[RoleDoc], int]:
"""Query roles with pagination and fuzzy search by role_key and role_name"""
query = {}
if role_key:
query[str(RoleDoc.role_key)] = {"$regex": role_key, "$options": "i"}
if role_name:
query[str(RoleDoc.role_name)] = {"$regex": role_name, "$options": "i"}
cursor = RoleDoc.find(query)
total = await cursor.count()
docs = await cursor.skip(skip).limit(limit).to_list()
return docs, total
async def assign_permissions_to_role(self, role_id: PydanticObjectId, permission_ids: List[str]) -> Optional[RoleDoc]:
"""Assign permissions to a role by updating the permission_ids field"""
if not role_id or not permission_ids:
raise RequestValidationError("role_id and permission_ids are required.")
doc = await RoleDoc.get(role_id)
if not doc:
raise RequestValidationError("Role not found.")
# Validate that all permission_ids exist in the permission collection
for permission_id in permission_ids:
permission_doc = await PermissionDoc.get(PydanticObjectId(permission_id))
if not permission_doc:
raise RequestValidationError(f"Permission with id {permission_id} not found.")
# Remove duplicates from permission_ids
unique_permission_ids = list(dict.fromkeys(permission_ids))
doc.permission_ids = unique_permission_ids
doc.updated_at = datetime.now()
await doc.save()
return doc
async def delete_role(self, role_id: PydanticObjectId) -> None:
"""Delete a role document after checking if it is referenced by any user and is not default"""
if not role_id:
raise RequestValidationError("role_id is required.")
# Check if any user references this role
user_role = await UserRoleDoc.find_one({"role_ids": str(role_id)})
if user_role:
raise RequestValidationError("Role is referenced by a user and cannot be deleted.")
doc = await RoleDoc.get(role_id)
if not doc:
raise RequestValidationError("Role not found.")
# Check if the role is default
if doc.is_default:
raise RequestValidationError("Default role cannot be deleted.")
await doc.delete()

View File

@ -0,0 +1,65 @@
from typing import Optional, List
from fastapi.exceptions import RequestValidationError
from backend.models.permission.models import RoleDoc, UserRoleDoc, PermissionDoc
from beanie import PydanticObjectId
class UserRoleHandler:
def __init__(self):
pass
async def assign_roles_to_user(self, user_id: str, role_ids: List[str]) -> Optional[UserRoleDoc]:
"""Assign roles to a user by updating or creating the UserRoleDoc"""
if not user_id or not role_ids:
raise RequestValidationError("user_id and role_ids are required.")
# Validate that all role_ids exist in the role collection
for role_id in role_ids:
role_doc = await RoleDoc.get(PydanticObjectId(role_id))
if not role_doc:
raise RequestValidationError(f"Role with id {role_id} not found.")
# Remove duplicates from role_ids
unique_role_ids = list(dict.fromkeys(role_ids))
# Check if UserRoleDoc already exists for this user
existing_user_role = await UserRoleDoc.find_one(UserRoleDoc.user_id == user_id)
if existing_user_role:
# Update existing UserRoleDoc
existing_user_role.role_ids = unique_role_ids
await existing_user_role.save()
return existing_user_role
else:
# Create new UserRoleDoc
user_role_doc = UserRoleDoc(
user_id=user_id,
role_ids=unique_role_ids
)
await user_role_doc.insert()
return user_role_doc
async def get_role_and_permission_by_user_id(self, user_id: str) -> tuple[list[str], list[str]]:
"""Get all role names and permission keys for a user by user_id"""
# Query user roles
user_role_doc = await UserRoleDoc.find_one(UserRoleDoc.user_id == user_id)
if not user_role_doc or not user_role_doc.role_ids:
# No roles assigned
return [], []
# Query all roles by role_ids
roles = await RoleDoc.find({"_id": {"$in": [PydanticObjectId(rid) for rid in user_role_doc.role_ids]}}).to_list()
role_names = [role.role_name for role in roles]
# Collect all permission_ids from all roles
all_permission_ids = []
for role in roles:
if role.permission_ids:
all_permission_ids.extend(role.permission_ids)
# Remove duplicates
unique_permission_ids = list(dict.fromkeys(all_permission_ids))
# Query all permissions by permission_ids
if unique_permission_ids:
permissions = await PermissionDoc.find({"_id": {"$in": [PydanticObjectId(pid) for pid in unique_permission_ids]}}).to_list()
permission_keys = [perm.permission_key for perm in permissions]
else:
permission_keys = []
return role_names, permission_keys

View File

@ -1,6 +1,8 @@
from .user import user_models
from .user_profile import profile_models
from .permission import permission_models
backend_models = []
backend_models.extend(user_models)
backend_models.extend(profile_models)
backend_models.extend(permission_models)

View File

@ -0,0 +1,3 @@
from .models import PermissionDoc, RoleDoc, UserRoleDoc
permission_models = [PermissionDoc, RoleDoc, UserRoleDoc]

View File

@ -1,4 +1,32 @@
from enum import IntEnum
from dataclasses import dataclass
from enum import IntEnum, Enum
@dataclass(frozen=True) # frozen=True
class DefaultRole:
role_name: str
role_key: str
role_description: str
role_level: int
# Default roles, which all tenants will have, cannot be modified.
class DefaultRoleEnum(Enum):
ADMIN = DefaultRole("Administrator", "admin", "Have all permissions", 0)
@dataclass(frozen=True) # frozen=True
class DefaultPermission:
permission_key: str
permission_name: str
permission_description: str
# Default permissions, which all tenants will have, cannot be modified.
class DefaultPermissionEnum(Enum):
CHANGE_ROLES = DefaultPermission("change:roles", "Change roles", "Add/Update/Delete roles")
CHANGE_PERMISSIONS = DefaultPermission("change:permissions", "Change permissions", "Add/Update/Remove permissions")
ASSIGN_ROLES = DefaultPermission("assign:roles", "Assign roles", "Assign roles to user")
class AdministrativeRole(IntEnum):

View File

@ -0,0 +1,52 @@
from beanie import Document
from datetime import datetime
from typing import Optional, List
class PermissionDoc(Document):
permission_name: str
permission_key: str
description: Optional[str] = None # Description of the permission, optional
created_at: datetime = datetime.now() # Creation timestamp, auto-generated
updated_at: datetime = datetime.now() # Last update timestamp, auto-updated
is_default: bool = False
class Settings:
# Default collections created by Freeleaps for tenant databases use '_' prefix
# to prevent naming conflicts with tenant-created collections
name = "_permission"
indexes = [
"permission_key"
]
class RoleDoc(Document):
role_key: str
role_name: str
role_description: Optional[str] = None
permission_ids: list[str]
role_level: int
created_at: datetime = datetime.now() # Creation timestamp, auto-generated
updated_at: datetime = datetime.now() # Last update timestamp, auto-updated
is_default: bool = False
class Settings:
# Default collections created by Freeleaps for tenant databases use '_' prefix
# to prevent naming conflicts with tenant-created collections
name = "_role"
indexes = [
"role_level"
]
class UserRoleDoc(Document):
"""User role doc"""
user_id: str
role_ids: Optional[List[str]]
class Settings:
# Default collections created by Freeleaps for tenant databases use '_' prefix
# to prevent naming conflicts with tenant-created collections
name = "_user_role"
indexes = [
"user_id"
]

View File

@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional, List
from beanie import Document

View File

@ -0,0 +1,36 @@
from typing import Optional, Dict, Any
from fastapi.exceptions import RequestValidationError
from backend.infra.permission.permission_handler import PermissionHandler
from backend.models.permission.models import PermissionDoc
from beanie import PydanticObjectId
class PermissionService:
def __init__(self):
self.permission_handler = PermissionHandler()
async def create_permission(self, permission_key: str, permission_name: str, description: Optional[str] = None) -> PermissionDoc:
"""Create a new permission document"""
return await self.permission_handler.create_permission(permission_key, permission_name, description)
async def update_permission(self, permission_id: str, permission_key: Optional[str] = None, permission_name: Optional[str] = None, description: Optional[str] = None) -> PermissionDoc:
"""Update an existing permission document by id"""
return await self.permission_handler.update_permission(PydanticObjectId(permission_id), permission_key, permission_name, description)
async def query_permissions(self, permission_key: Optional[str] = None, permission_name: Optional[str] = None, page: int = 1, page_size: int = 10) -> Dict[str, Any]:
"""Query permissions with pagination and fuzzy search"""
if page < 1 or page_size < 1:
raise RequestValidationError("page and page_size must be positive integers.")
skip = (page - 1) * page_size
docs, total = await self.permission_handler.query_permissions(permission_key, permission_name, skip, page_size)
return {
"items": [doc.dict() for doc in docs],
"total": total,
"page": page,
"page_size": page_size
}
async def delete_permission(self, permission_id: str) -> None:
"""Delete a permission document after checking if it is referenced by any role"""
return await self.permission_handler.delete_permission(PydanticObjectId(permission_id))

View File

@ -0,0 +1,44 @@
from typing import Optional, Dict, Any, List
from fastapi.exceptions import RequestValidationError
from backend.infra.permission.role_handler import RoleHandler
from backend.models.permission.models import RoleDoc
from beanie import PydanticObjectId
class RoleService:
def __init__(self):
self.role_handler = RoleHandler()
async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> RoleDoc:
"""Create a new role, ensuring role_key and role_name are unique and not empty"""
doc = await self.role_handler.create_role(role_key, role_name, role_description, role_level)
return doc
async def update_role(self, role_id: str, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> RoleDoc:
"""Update an existing role, ensuring role_key and role_name are unique and not empty"""
doc = await self.role_handler.update_role(PydanticObjectId(role_id), role_key, role_name, role_description, role_level)
return doc
async def query_roles(self, role_key: Optional[str], role_name: Optional[str], page: int = 1, page_size: int = 10) -> Dict[str, Any]:
"""Query roles with pagination and fuzzy search by role_key and role_name"""
if page < 1 or page_size < 1:
raise RequestValidationError("page and page_size must be positive integers.")
skip = (page - 1) * page_size
docs, total = await self.role_handler.query_roles(role_key, role_name, skip, page_size)
return {
"items": [doc.dict() for doc in docs],
"total": total,
"page": page,
"page_size": page_size
}
async def assign_permissions_to_role(self, role_id: str, permission_ids: List[str]) -> RoleDoc:
"""Assign permissions to a role by updating the permission_ids field"""
return await self.role_handler.assign_permissions_to_role(PydanticObjectId(role_id), permission_ids)
async def delete_role(self, role_id: str) -> None:
"""Delete a role document after checking if it is referenced by any user"""
return await self.role_handler.delete_role(PydanticObjectId(role_id))

View File

@ -1,5 +1,6 @@
from backend.models.permission.models import UserRoleDoc
from common.log.module_logger import ModuleLogger
from typing import Optional
from typing import Optional, List, Tuple
from backend.models.user.constants import (
NewUserMethod,
@ -16,6 +17,9 @@ from backend.infra.auth.user_auth_handler import (
from backend.infra.user_profile.user_profile_handler import (
UserProfileHandler,
)
from backend.infra.permission.user_role_handler import (
UserRoleHandler,
)
from common.log.log_utils import log_entry_exit_async
from common.constants.region import UserRegion
@ -24,6 +28,7 @@ class UserManagementService:
def __init__(self) -> None:
self.user_auth_handler = UserAuthHandler()
self.user_profile_handler = UserProfileHandler()
self.user_role_handler = UserRoleHandler()
self.module_logger = ModuleLogger(sender_id=UserManagementService)
@log_entry_exit_async
@ -97,3 +102,16 @@ class UserManagementService:
async def get_account_by_id(self, user_id: str) -> UserAccountDoc:
return await self.user_profile_handler.get_account_by_id(user_id)
async def assign_roles_to_user(self, user_id: str, role_ids: List[str]) -> UserRoleDoc:
"""Assign roles to a user by updating or creating the UserRoleDoc"""
return await self.user_role_handler.assign_roles_to_user(user_id, role_ids)
async def get_role_and_permission_by_user_id(self, user_id: str) -> Tuple[List[str], List[str]]:
"""Get user role names and permission keys by user id
Args:
user_id (str): user id
Returns:
Tuple[List[str], List[str]]: user role names and permission keys
"""
return await self.user_role_handler.get_role_and_permission_by_user_id(user_id)

View File

View File

@ -0,0 +1,2 @@
USER_ROLE_NAMES = "role_names"
USER_PERMISSIONS = "user_permissions"

View File

@ -1,11 +1,34 @@
from datetime import datetime, timedelta, timezone
import uuid
from typing import Dict
from typing import Dict, List
from jose import jwt, JWTError
from common.config.app_settings import app_settings
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from starlette.status import HTTP_401_UNAUTHORIZED
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from common.constants.jwt_constants import USER_ROLE_NAMES, USER_PERMISSIONS
class CurrentUser:
def __init__(self, user_id: str, user_role_names: List[str], user_permission_keys: List[str]):
self.user_id = user_id
self.user_role_names = user_role_names
self.user_permission_keys = user_permission_keys
def has_all_permissions(self, permissions: List[str]) -> bool:
"""Check if the user has all the specified permissions"""
if not permissions:
return True
return all(p in self.user_permission_keys for p in permissions)
def has_any_permissions(self, permissions: List[str]) -> bool:
"""Check if the user has at least one of the specified permissions"""
if not permissions:
return True
return any(p in self.user_permission_keys for p in permissions)
security = HTTPBearer()
class TokenManager:
@ -73,16 +96,35 @@ class TokenManager:
else:
raise ValueError("Invalid refresh token")
async def get_current_user(
self, token: str = Depends(OAuth2PasswordBearer(tokenUrl="token"))
) -> Dict:
async def get_current_user(self, credentials: HTTPAuthorizationCredentials = Depends(security)) -> CurrentUser:
"""
Extract and validate user information from the JWT token.
Returns the current user object for the given credentials.
"""
try:
payload = self.decode_token(token) # Decode JWT token
return payload
except ValueError:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED, detail="Invalid or expired token"
)
payload = self.decode_token(credentials.credentials)
user = payload.get("subject")
if not user or "id" not in user:
raise HTTPException(status_code=401, detail="Invalid authentication token")
return CurrentUser(user.get("id"), user.get(USER_ROLE_NAMES), user.get(USER_PERMISSIONS))
except JWTError:
raise HTTPException(status_code=401, detail="Invalid authentication token")
def has_all_permissions(self, permissions: List[str]):
"""Check if the user has all the specified permissions"""
def inner_dependency(current_user: CurrentUser = Depends(self.get_current_user)):
if not current_user.has_all_permissions(permissions):
raise HTTPException(status_code=403, detail="Not allowed")
return True
return inner_dependency
def has_any_permissions(self, permissions: List[str]):
"""Check if the user has at least one of the specified permissions"""
def inner_dependency(current_user: CurrentUser = Depends(self.get_current_user)):
if not current_user.has_any_permissions(permissions):
raise HTTPException(status_code=403, detail="Not allowed")
return True
return inner_dependency

View File

@ -0,0 +1,24 @@
APP_NAME=authentication
export SERVICE_API_ACCESS_HOST=0.0.0.0
export SERVICE_API_ACCESS_PORT=8004
export CONTAINER_APP_ROOT=/app
export LOG_BASE_PATH=$CONTAINER_APP_ROOT/log/$APP_NAME
export BACKEND_LOG_FILE_NAME=$APP_NAME
export APPLICATION_ACTIVITY_LOG=$APP_NAME-activity
export MONGODB_NAME=freeleaps2
export MONGODB_PORT=27017
export JWT_SECRET_KEY=ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0
GIT_REPO_ROOT=/mnt/freeleaps/freeleaps-service-hub
CODEBASE_ROOT=/mnt/freeleaps/freeleaps-service-hub/apps/authentication
SITE_DEPLOY_FOLDER=/mnt/freeleaps/freeleaps-service-hub/sites/authentication/deploy
#!/bin/bash
export VENV_DIR=venv_t
export VENV_ACTIVATE=venv_t/bin/activate
export DOCKER_HOME=/var/lib/docker
export DOCKER_APP_HOME=$DOCKER_HOME/app
export DOCKER_BACKEND_HOME=$DOCKER_APP_HOME/$APP_NAME
export DOCKER_BACKEND_LOG_HOME=$DOCKER_BACKEND_HOME/log
export MONGODB_URI=mongodb://localhost:27017/
export FREELEAPS_ENV=local
export LOG_BASE_PATH=${CODEBASE_ROOT}/log

View File

@ -13,4 +13,6 @@ httpx
pydantic-settings
python-jose
passlib[bcrypt]
prometheus-fastapi-instrumentator==7.0.2
prometheus-fastapi-instrumentator==7.0.2
pytest==8.4.1
pytest-asyncio==0.21.2

View File

View File

@ -0,0 +1,86 @@
# Permission API Test Report
## How to Run the Tests
**Run all permission API tests with coverage:**
```bash
pytest --cov=authentication --cov-report=term-missing tests/api_tests/permission/
```
---
## Test Results Summary
- **Total tests collected:** 26
- **All tests passed.**
- **Warnings:**
- Deprecation warnings from Pydantic/Beanie (upgrade recommended for future compatibility).
- Coverage warning: `Module authentication was never imported. (module-not-imported)`
---
## Test Case Explanations
### test_create_permission.py
- **test_create_permission_success**
Admin user can create a permission with valid data.
- **test_create_permission_fail_duplicate_key/name**
Creating a permission with duplicate key or name fails.
- **test_create_permission_fail_empty_key/name**
Creating a permission with empty key or name fails.
- **test_create_permission_success_empty_description**
Description is optional.
- **test_create_permission_fail_by_non_admin**
Non-admin user cannot create permissions.
- **test_create_permission_success_after_grant_admin**
After admin grants admin role to a temp user and the user re-logs in, the user can create permissions.
### test_delete_permission.py
- **test_delete_permission_success**
Admin user can delete a permission.
- **test_delete_permission_fail_not_found**
Deleting a non-existent permission fails.
- **test_delete_default_permission_fail**
Default permissions cannot be deleted.
- **test_delete_permission_fail_by_non_admin**
Non-admin user cannot delete permissions.
- **test_delete_permission_success_after_grant_admin**
After admin grants admin role to a temp user and the user re-logs in, the user can delete permissions.
### test_update_permission.py
- **test_update_permission_success**
Admin user can update a permission.
- **test_update_permission_fail_not_found**
Updating a non-existent permission fails.
- **test_update_permission_fail_duplicate_key/name**
Updating to a duplicate key or name fails.
- **test_update_permission_fail_empty_key/name**
Updating with empty key or name fails.
- **test_update_default_permission_fail**
Default permissions cannot be updated.
- **test_update_permission_fail_by_non_admin**
Non-admin user cannot update permissions.
- **test_update_permission_success_after_grant_admin**
After admin grants admin role to a temp user and the user re-logs in, the user can update permissions.
### test_query_permission.py
- **test_query_all_permissions**
Query all permissions, expect a list.
- **test_query_permissions_by_key/name**
Query permissions by key or name (fuzzy search).
- **test_query_permissions_pagination**
Query permissions with pagination.
---
## Summary
- These tests ensure that only admin users can manage permissions, and that permission can be delegated by granting the admin role to other users.
- Each test case is designed to verify both positive and negative scenarios, including permission escalation and proper error handling.
- **Coverage reporting is not working** due to import or execution issues—fix this for a complete report.
---

View File

@ -0,0 +1,21 @@
import pytest
from tests.base.authentication_web import AuthenticationWeb
@pytest.fixture(scope="session")
def authentication_web() -> AuthenticationWeb:
authentication_web = AuthenticationWeb()
authentication_web.login()
return authentication_web
@pytest.fixture(scope="session")
def authentication_web_of_temp_user1() -> AuthenticationWeb:
authentication_web = AuthenticationWeb()
user = authentication_web.create_temporary_user()
authentication_web.user_email = user["email"]
authentication_web.password = user["password"]
authentication_web.user_id = user["user_id"]
authentication_web.login()
return authentication_web

View File

@ -0,0 +1,143 @@
import pytest
import random
from tests.base.authentication_web import AuthenticationWeb
class TestCreatePermission:
@pytest.mark.asyncio
async def test_create_permission_success(self, authentication_web: AuthenticationWeb):
"""Test creating a permission successfully with valid and unique permission_key and permission_name."""
suffix = str(random.randint(10000, 99999))
perm_data = {
"permission_key": f"test_perm_key_success_{suffix}",
"permission_name": f"Test Permission Success {suffix}",
"description": "Permission for testing success"
}
response = await authentication_web.create_permission(perm_data)
assert response.status_code == 200
json = response.json()
assert json["permission_key"] == perm_data["permission_key"]
assert json["permission_name"] == perm_data["permission_name"]
assert json["description"] == perm_data["description"]
assert json["id"] is not None
assert json["created_at"] is not None
assert json["updated_at"] is not None
@pytest.mark.asyncio
async def test_create_permission_fail_duplicate_key(self, authentication_web: AuthenticationWeb):
"""Test creating a permission fails when permission_key is duplicated."""
suffix = str(random.randint(10000, 99999))
perm_data = {
"permission_key": f"test_perm_key_dup_{suffix}",
"permission_name": f"Test Permission DupKey {suffix}",
"description": "desc"
}
await authentication_web.create_permission(perm_data)
perm_data2 = {
"permission_key": f"test_perm_key_dup_{suffix}",
"permission_name": f"Test Permission DupKey2 {suffix}",
"description": "desc2"
}
response = await authentication_web.create_permission(perm_data2)
assert response.status_code == 422 or response.status_code == 400
@pytest.mark.asyncio
async def test_create_permission_fail_duplicate_name(self, authentication_web: AuthenticationWeb):
"""Test creating a permission fails when permission_name is duplicated."""
suffix = str(random.randint(10000, 99999))
perm_data = {
"permission_key": f"test_perm_key_dupname1_{suffix}",
"permission_name": f"Test Permission DupName {suffix}",
"description": "desc"
}
await authentication_web.create_permission(perm_data)
perm_data2 = {
"permission_key": f"test_perm_key_dupname2_{suffix}",
"permission_name": f"Test Permission DupName {suffix}",
"description": "desc2"
}
response = await authentication_web.create_permission(perm_data2)
assert response.status_code == 422 or response.status_code == 400
@pytest.mark.asyncio
async def test_create_permission_fail_empty_key(self, authentication_web: AuthenticationWeb):
"""Test creating a permission fails when permission_key is empty."""
suffix = str(random.randint(10000, 99999))
perm_data = {
"permission_key": "",
"permission_name": f"Test Permission EmptyKey {suffix}",
"description": "desc"
}
response = await authentication_web.create_permission(perm_data)
assert response.status_code == 422 or response.status_code == 400
@pytest.mark.asyncio
async def test_create_permission_fail_empty_name(self, authentication_web: AuthenticationWeb):
"""Test creating a permission fails when permission_name is empty."""
suffix = str(random.randint(10000, 99999))
perm_data = {
"permission_key": f"test_perm_key_emptyname_{suffix}",
"permission_name": "",
"description": "desc"
}
response = await authentication_web.create_permission(perm_data)
assert response.status_code == 422 or response.status_code == 400
@pytest.mark.asyncio
async def test_create_permission_success_empty_description(self, authentication_web: AuthenticationWeb):
"""Test creating a permission successfully when description is None (optional field)."""
suffix = str(random.randint(10000, 99999))
perm_data = {
"permission_key": f"test_perm_key_emptydesc_{suffix}",
"permission_name": f"Test Permission EmptyDesc {suffix}",
"description": None
}
response = await authentication_web.create_permission(perm_data)
assert response.status_code == 200
json = response.json()
assert json["permission_key"] == perm_data["permission_key"]
assert json["permission_name"] == perm_data["permission_name"]
assert json["description"] is None or json["description"] == ""
@pytest.mark.asyncio
async def test_create_permission_fail_by_non_admin(self, authentication_web_of_temp_user1: AuthenticationWeb):
"""Test creating a permission fails by non-admin user (no permission)."""
suffix = str(random.randint(10000, 99999))
perm_data = {
"permission_key": f"test_perm_key_nonadmin_{suffix}",
"permission_name": f"Test Permission NonAdmin {suffix}",
"description": "desc"
}
response = await authentication_web_of_temp_user1.create_permission(perm_data)
assert response.status_code == 403 or response.status_code == 401
@pytest.mark.asyncio
async def test_create_permission_success_after_grant_admin(self, authentication_web: AuthenticationWeb):
"""Test creating a permission succeeds after granting admin role to a new temporary user and re-login."""
# Create a new temp user
user = authentication_web.create_temporary_user()
temp_authentication_web = AuthenticationWeb(user_email=user["email"], password=user["password"])
temp_authentication_web.user_id = user["user_id"]
temp_authentication_web.login()
# Grant admin role to temp user
resp = await authentication_web.query_roles({"role_key": "admin"})
admin_role_id = resp.json()["items"][0]["id"]
await authentication_web.assign_roles_to_user({
"user_id": temp_authentication_web.user_id,
"role_ids": [admin_role_id]
})
# Re-login as temp user
temp_authentication_web.login()
# Try to create permission
suffix = str(random.randint(10000, 99999))
perm_data = {
"permission_key": f"test_perm_key_tempadmin_{suffix}",
"permission_name": f"Test Permission TempAdmin {suffix}",
"description": "desc"
}
response = await temp_authentication_web.create_permission(perm_data)
assert response.status_code == 200
if __name__ == '__main__':
pytest.main([__file__])

View File

@ -0,0 +1,85 @@
import pytest
import random
from backend.models.permission.constants import DefaultPermissionEnum
from tests.base.authentication_web import AuthenticationWeb
class TestDeletePermission:
@pytest.mark.asyncio
async def test_delete_permission_success(self, authentication_web: AuthenticationWeb):
"""Test deleting a permission successfully."""
suffix = str(random.randint(10000, 99999))
perm = await authentication_web.create_permission({
"permission_key": f"delperm_{suffix}",
"permission_name": f"delperm_{suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
resp = await authentication_web.delete_permission({"permission_id": perm_id})
assert resp.status_code == 200
assert resp.json()["success"] is True
@pytest.mark.asyncio
async def test_delete_permission_fail_not_found(self, authentication_web: AuthenticationWeb):
"""Test deleting a permission fails when permission_id does not exist."""
resp = await authentication_web.delete_permission({"permission_id": "000000000000000000000000"})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_delete_default_permission_fail(self, authentication_web: AuthenticationWeb):
"""Test deleting a default permission fails. Default permission cannot be deleted."""
# Query a default role
resp = await authentication_web.query_permissions(
params={"page": 1, "page_size": 2, "permission_key": DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key})
json = resp.json()
default_permission_id = json["items"][0]["id"]
resp = await authentication_web.delete_permission(perm_data={"permission_id": default_permission_id})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_delete_permission_fail_by_non_admin(self, authentication_web: AuthenticationWeb, authentication_web_of_temp_user1: AuthenticationWeb):
"""Test deleting a permission fails by non-admin user (no permission)."""
# Create a permission as admin
suffix = str(random.randint(10000, 99999))
perm = await authentication_web.create_permission({
"permission_key": f"delperm_nonadmin_{suffix}",
"permission_name": f"delperm_nonadmin_{suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
# Try to delete as temp user
resp = await authentication_web_of_temp_user1.delete_permission({"permission_id": perm_id})
assert resp.status_code == 403 or resp.status_code == 401
@pytest.mark.asyncio
async def test_delete_permission_success_after_grant_admin(self, authentication_web: AuthenticationWeb):
"""Test deleting a permission succeeds after granting admin role to a new temporary user and re-login."""
# Create a new temp user
user = authentication_web.create_temporary_user()
temp_authentication_web = AuthenticationWeb(user_email=user["email"], password=user["password"])
temp_authentication_web.user_id = user["user_id"]
temp_authentication_web.login()
# Create a permission as admin
suffix = str(random.randint(10000, 99999))
perm = await authentication_web.create_permission({
"permission_key": f"delperm_tempadmin_{suffix}",
"permission_name": f"delperm_tempadmin_{suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
# Grant admin role to temp user
resp = await authentication_web.query_roles({"role_key": "admin"})
admin_role_id = resp.json()["items"][0]["id"]
await authentication_web.assign_roles_to_user({
"user_id": temp_authentication_web.user_id,
"role_ids": [admin_role_id]
})
# Re-login as temp user
temp_authentication_web.login()
# Try to delete as temp user
resp = await temp_authentication_web.delete_permission({"permission_id": perm_id})
assert resp.status_code == 200
if __name__ == '__main__':
pytest.main([__file__])

View File

@ -0,0 +1,57 @@
import random
import pytest
from tests.base.authentication_web import AuthenticationWeb
class TestQueryPermission:
@pytest.mark.asyncio
async def test_query_all_permissions(self, authentication_web: AuthenticationWeb):
"""Test querying all permissions returns a list."""
resp = await authentication_web.query_permissions({})
assert resp.status_code == 200
json = resp.json()
assert "items" in json
assert "total" in json
@pytest.mark.asyncio
async def test_query_permissions_by_key(self, authentication_web: AuthenticationWeb):
"""Test querying permissions by permission_key with fuzzy search."""
suffix = str(random.randint(10000, 99999))
await authentication_web.create_permission({
"permission_key": f"querykey_{suffix}",
"permission_name": f"querykey_{suffix}",
"description": "desc"
})
resp = await authentication_web.query_permissions({"permission_key": f"querykey_{suffix}"})
assert resp.status_code == 200
json = resp.json()
assert any(f"querykey_{suffix}" in item["permission_key"] for item in json["items"])
@pytest.mark.asyncio
async def test_query_permissions_by_name(self, authentication_web: AuthenticationWeb):
"""Test querying permissions by permission_name with fuzzy search."""
suffix = str(random.randint(10000, 99999))
await authentication_web.create_permission({
"permission_key": f"queryname_{suffix}",
"permission_name": f"queryname_{suffix}",
"description": "desc"
})
resp = await authentication_web.query_permissions({"permission_name": f"queryname_{suffix}"})
assert resp.status_code == 200
json = resp.json()
assert any(f"queryname_{suffix}" in item["permission_name"] for item in json["items"])
@pytest.mark.asyncio
async def test_query_permissions_pagination(self, authentication_web: AuthenticationWeb):
"""Test querying permissions with pagination."""
resp = await authentication_web.query_permissions({"page": 1, "page_size": 2})
assert resp.status_code == 200
json = resp.json()
assert "items" in json
assert "total" in json
assert "page" in json
assert "page_size" in json
if __name__ == '__main__':
pytest.main([__file__])

View File

@ -0,0 +1,205 @@
import pytest
import random
from backend.models.permission.constants import DefaultPermissionEnum
from tests.base.authentication_web import AuthenticationWeb
class TestUpdatePermission:
@pytest.mark.asyncio
async def test_update_permission_success(self, authentication_web: AuthenticationWeb):
"""Test updating a permission successfully with valid and unique fields."""
suffix = str(random.randint(10000, 99999))
perm_data = {
"permission_key": f"update_perm_key_{suffix}",
"permission_name": f"Update Permission {suffix}",
"description": "desc"
}
create_resp = await authentication_web.create_permission(perm_data)
perm_id = create_resp.json()["id"]
update_data = {
"permission_id": perm_id,
"permission_key": f"update_perm_key_{suffix}_new",
"permission_name": f"Update Permission {suffix} New",
"description": "desc new"
}
resp = await authentication_web.update_permission(update_data)
assert resp.status_code == 200
json = resp.json()
assert json["permission_key"] == update_data["permission_key"]
assert json["permission_name"] == update_data["permission_name"]
assert json["description"] == update_data["description"]
@pytest.mark.asyncio
async def test_update_permission_fail_not_found(self, authentication_web: AuthenticationWeb):
"""Test updating a permission fails when permission_id does not exist."""
suffix = str(random.randint(10000, 99999))
update_data = {
"permission_id": "000000000000000000000000",
"permission_key": f"notfound_key_{suffix}",
"permission_name": f"NotFound Permission {suffix}",
"description": "desc"
}
resp = await authentication_web.update_permission(update_data)
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_permission_fail_duplicate_key(self, authentication_web: AuthenticationWeb):
"""Test updating a permission fails when permission_key is duplicated."""
suffix = str(random.randint(10000, 99999))
perm1 = await authentication_web.create_permission({
"permission_key": f"dupkey1_{suffix}",
"permission_name": f"dupkey1_{suffix}",
"description": "desc"
})
perm2 = await authentication_web.create_permission({
"permission_key": f"dupkey2_{suffix}",
"permission_name": f"dupkey2_{suffix}",
"description": "desc"
})
perm2_id = perm2.json()["id"]
update_data = {
"permission_id": perm2_id,
"permission_key": f"dupkey1_{suffix}",
"permission_name": f"dupkey2_{suffix}_new",
"description": "desc"
}
resp = await authentication_web.update_permission(update_data)
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_permission_fail_duplicate_name(self, authentication_web: AuthenticationWeb):
"""Test updating a permission fails when permission_name is duplicated."""
suffix = str(random.randint(10000, 99999))
perm1 = await authentication_web.create_permission({
"permission_key": f"dupname1_{suffix}",
"permission_name": f"dupname1_{suffix}",
"description": "desc"
})
perm2 = await authentication_web.create_permission({
"permission_key": f"dupname2_{suffix}",
"permission_name": f"dupname2_{suffix}",
"description": "desc"
})
perm2_id = perm2.json()["id"]
update_data = {
"permission_id": perm2_id,
"permission_key": f"dupname2_{suffix}_new",
"permission_name": f"dupname1_{suffix}",
"description": "desc"
}
resp = await authentication_web.update_permission(update_data)
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_permission_fail_empty_key(self, authentication_web: AuthenticationWeb):
"""Test updating a permission fails when permission_key is empty."""
suffix = str(random.randint(10000, 99999))
perm = await authentication_web.create_permission({
"permission_key": f"emptykey_{suffix}",
"permission_name": f"emptykey_{suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
update_data = {
"permission_id": perm_id,
"permission_key": "",
"permission_name": f"emptykey_{suffix}_new",
"description": "desc"
}
resp = await authentication_web.update_permission(update_data)
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_permission_fail_empty_name(self, authentication_web: AuthenticationWeb):
"""Test updating a permission fails when permission_name is empty."""
suffix = str(random.randint(10000, 99999))
perm = await authentication_web.create_permission({
"permission_key": f"emptyname_{suffix}",
"permission_name": f"emptyname_{suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
update_data = {
"permission_id": perm_id,
"permission_key": f"emptyname_{suffix}_new",
"permission_name": "",
"description": "desc"
}
resp = await authentication_web.update_permission(update_data)
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_default_permission_fail(self, authentication_web: AuthenticationWeb):
"""Test updating a default permission fails. Default permission cannot be updated."""
suffix = str(random.randint(10000, 99999))
# Query a default role
resp = await authentication_web.query_permissions(
params={"page": 1, "page_size": 2, "permission_key": DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key})
json = resp.json()
default_permission = json["items"][0]
resp = await authentication_web.update_permission(perm_data={
"permission_id": default_permission["id"],
"permission_key": f"{default_permission['permission_key']}_{suffix}_update",
"permission_name": f"{default_permission['permission_name']}_{suffix}_update",
"description": "desc",
})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_permission_fail_by_non_admin(self, authentication_web: AuthenticationWeb, authentication_web_of_temp_user1: AuthenticationWeb):
"""Test updating a permission fails by non-admin user (no permission)."""
# Create a permission as admin
suffix = str(random.randint(10000, 99999))
perm = await authentication_web.create_permission({
"permission_key": f"updateperm_nonadmin_{suffix}",
"permission_name": f"updateperm_nonadmin_{suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
update_data = {
"permission_id": perm_id,
"permission_key": f"updateperm_nonadmin_{suffix}_new",
"permission_name": f"updateperm_nonadmin_{suffix}_new",
"description": "desc new"
}
resp = await authentication_web_of_temp_user1.update_permission(update_data)
assert resp.status_code == 403 or resp.status_code == 401
@pytest.mark.asyncio
async def test_update_permission_success_after_grant_admin(self, authentication_web: AuthenticationWeb):
"""Test updating a permission succeeds after granting admin role to a new temporary user and re-login."""
# Create a new temp user
user = authentication_web.create_temporary_user()
temp_authentication_web = AuthenticationWeb(user_email=user["email"], password=user["password"])
temp_authentication_web.user_id = user["user_id"]
temp_authentication_web.login()
# Create a permission as admin
suffix = str(random.randint(10000, 99999))
perm = await authentication_web.create_permission({
"permission_key": f"updateperm_tempadmin_{suffix}",
"permission_name": f"updateperm_tempadmin_{suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
# Grant admin role to temp user
resp = await authentication_web.query_roles({"role_key": "admin"})
admin_role_id = resp.json()["items"][0]["id"]
await authentication_web.assign_roles_to_user({
"user_id": temp_authentication_web.user_id,
"role_ids": [admin_role_id]
})
# Re-login as temp user
temp_authentication_web.login()
# Try to update as temp user
update_data = {
"permission_id": perm_id,
"permission_key": f"updateperm_tempadmin_{suffix}_new",
"permission_name": f"updateperm_tempadmin_{suffix}_new",
"description": "desc new"
}
resp = await temp_authentication_web.update_permission(update_data)
assert resp.status_code == 200
if __name__ == '__main__':
pytest.main([__file__])

View File

@ -0,0 +1,99 @@
# Role API Test Report
## How to Run the Tests
**Run all role API tests:**
```bash
pytest --tb=short tests/api_tests/role/
```
---
## Test Results Summary
- **Total tests collected:** 33
- **All tests passed.**
- **Warnings:**
- Deprecation warnings from Pydantic/Beanie (upgrade recommended for future compatibility).
---
## Test Case Explanations
### test_assign_permissions.py
- **test_assign_permissions_success**
Assign multiple permissions to a role successfully.
- **test_assign_permissions_fail_role_not_found**
Assigning permissions to a non-existent role fails.
- **test_assign_permissions_fail_permission_not_found**
Assigning a non-existent permission to a role fails.
- **test_assign_permissions_fail_empty_permission_ids**
Assigning with an empty permission list fails.
- **test_assign_permissions_fail_empty_role_id**
Assigning with an empty role ID fails.
- **test_assign_permissions_remove_duplicates**
Assigning duplicate permission IDs results in de-duplication.
- **test_assign_permissions_to_default_role**
Assigning permissions to a default role (should succeed if not restricted).
### test_create_role.py
- **test_create_role_success**
Admin user can create a role with valid and unique data.
- **test_create_role_fail_duplicate_role_key/name**
Creating a role with duplicate key or name fails.
- **test_create_role_fail_empty_role_key/name**
Creating a role with empty key or name fails.
- **test_create_role_success_empty_description**
Description is optional.
- **test_create_role_fail_by_non_admin**
Non-admin user cannot create roles.
- **test_create_role_success_after_grant_admin**
After admin grants admin role to a temp user and the user re-logs in, the user can create roles.
### test_delete_role.py
- **test_delete_role_success**
Admin user can delete a role.
- **test_delete_role_fail_not_found**
Deleting a non-existent role fails.
- **test_delete_default_role_fail**
Default roles cannot be deleted.
- **test_delete_role_fail_by_non_admin**
Non-admin user cannot delete roles.
- **test_delete_role_success_after_grant_admin**
After admin grants admin role to a temp user and the user re-logs in, the user can delete roles.
### test_query_role.py
- **test_query_all_roles**
Query all roles, expect a list.
- **test_query_roles_by_key/name**
Query roles by key or name (fuzzy search).
- **test_query_roles_pagination**
Query roles with pagination.
### test_update_role.py
- **test_update_role_success**
Admin user can update a role with valid and unique data.
- **test_update_role_fail_not_found**
Updating a non-existent role fails.
- **test_update_role_fail_duplicate_key/name**
Updating to a duplicate key or name fails.
- **test_update_role_fail_empty_key/name**
Updating with empty key or name fails.
- **test_update_default_role_fail**
Default roles cannot be updated.
- **test_update_role_fail_by_non_admin**
Non-admin user cannot update roles.
- **test_update_role_success_after_grant_admin**
After admin grants admin role to a temp user and the user re-logs in, the user can update roles.
---
## Summary
- These tests ensure that only admin users can manage roles, and that permission can be delegated by granting the admin role to other users.
- Each test case is designed to verify both positive and negative scenarios, including permission escalation and proper error handling.
- **Coverage reporting is not included in this report.**
---
If you need a more detailed, markdown-formatted report with actual coverage numbers, please enable coverage and re-run the tests.

View File

@ -0,0 +1,21 @@
import pytest
from tests.base.authentication_web import AuthenticationWeb
@pytest.fixture(scope="session")
def authentication_web()->AuthenticationWeb:
authentication_web = AuthenticationWeb()
authentication_web.login()
return authentication_web
@pytest.fixture(scope="session")
def authentication_web_of_temp_user1() -> AuthenticationWeb:
authentication_web = AuthenticationWeb()
user = authentication_web.create_temporary_user()
authentication_web.user_email = user["email"]
authentication_web.password = user["password"]
authentication_web.user_id = user["user_id"]
authentication_web.login()
return authentication_web

View File

@ -0,0 +1,163 @@
import pytest
import random
from typing import List
from backend.models.permission.constants import DefaultRoleEnum, DefaultPermissionEnum
from tests.base.authentication_web import AuthenticationWeb
class TestAssignPermissionsToRole:
@pytest.mark.asyncio
async def test_assign_permissions_success(self, authentication_web: AuthenticationWeb):
"""Test assigning permissions to a role successfully."""
# Create a role
suffix = str(random.randint(10000, 99999))
role_resp = await authentication_web.create_role({
"role_key": f"assignperm_role_{suffix}",
"role_name": f"AssignPerm Role {suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role_resp.json()["id"]
# Create two permissions
perm1 = await authentication_web.create_permission({
"permission_key": f"assignperm_key1_{suffix}",
"permission_name": f"AssignPerm Permission1 {suffix}",
"description": "desc"
})
perm2 = await authentication_web.create_permission({
"permission_key": f"assignperm_key2_{suffix}",
"permission_name": f"AssignPerm Permission2 {suffix}",
"description": "desc"
})
perm_ids = [perm1.json()["id"], perm2.json()["id"]]
# Assign permissions
resp = await authentication_web.assign_permissions_to_role({
"role_id": role_id,
"permission_ids": perm_ids
})
assert resp.status_code == 200
json = resp.json()
assert set(json["permission_ids"]) == set(perm_ids)
@pytest.mark.asyncio
async def test_assign_permissions_fail_role_not_found(self, authentication_web: AuthenticationWeb):
"""Test assigning permissions fails when role_id does not exist."""
# Create a permission
suffix = str(random.randint(10000, 99999))
perm = await authentication_web.create_permission({
"permission_key": f"assignperm_key_nf_{suffix}",
"permission_name": f"AssignPerm PermissionNF {suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
resp = await authentication_web.assign_permissions_to_role({
"role_id": "000000000000000000000000",
"permission_ids": [perm_id]
})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_assign_permissions_fail_permission_not_found(self, authentication_web: AuthenticationWeb):
"""Test assigning permissions fails when a permission_id does not exist."""
# Create a role
suffix = str(random.randint(10000, 99999))
role_resp = await authentication_web.create_role({
"role_key": f"assignperm_role_nf_{suffix}",
"role_name": f"AssignPerm RoleNF {suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role_resp.json()["id"]
resp = await authentication_web.assign_permissions_to_role({
"role_id": role_id,
"permission_ids": ["000000000000000000000000"]
})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_assign_permissions_fail_empty_permission_ids(self, authentication_web: AuthenticationWeb):
"""Test assigning permissions fails when permission_ids is empty."""
# Create a role
suffix = str(random.randint(10000, 99999))
role_resp = await authentication_web.create_role({
"role_key": f"assignperm_role_empty_{suffix}",
"role_name": f"AssignPerm RoleEmpty {suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role_resp.json()["id"]
resp = await authentication_web.assign_permissions_to_role({
"role_id": role_id,
"permission_ids": []
})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_assign_permissions_fail_empty_role_id(self, authentication_web: AuthenticationWeb):
"""Test assigning permissions fails when role_id is empty."""
# Create a permission
suffix = str(random.randint(10000, 99999))
perm = await authentication_web.create_permission({
"permission_key": f"assignperm_key_emptyrole_{suffix}",
"permission_name": f"AssignPerm PermissionEmptyRole {suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
resp = await authentication_web.assign_permissions_to_role({
"role_id": "",
"permission_ids": [perm_id]
})
assert resp.status_code == 422 or resp.status_code == 400 or resp.status_code == 500
@pytest.mark.asyncio
async def test_assign_permissions_remove_duplicates(self, authentication_web: AuthenticationWeb):
"""Test assigning permissions with duplicate permission_ids removes duplicates."""
# Create a role
suffix = str(random.randint(10000, 99999))
role_resp = await authentication_web.create_role({
"role_key": f"assignperm_role_dup_{suffix}",
"role_name": f"AssignPerm RoleDup {suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role_resp.json()["id"]
# Create a permission
perm = await authentication_web.create_permission({
"permission_key": f"assignperm_key_dup_{suffix}",
"permission_name": f"AssignPerm PermissionDup {suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
# Assign duplicate permission_ids
resp = await authentication_web.assign_permissions_to_role({
"role_id": role_id,
"permission_ids": [perm_id, perm_id, perm_id]
})
assert resp.status_code == 200
json = resp.json()
assert json["permission_ids"] == [perm_id]
@pytest.mark.asyncio
async def test_assign_permissions_to_default_role(self, authentication_web: AuthenticationWeb):
"""Test assigning permissions to a default role (should succeed if not restricted)."""
# Query default admin role
resp = await authentication_web.query_roles({"role_key": DefaultRoleEnum.ADMIN.value.role_key})
json = resp.json()
default_role_id = json["items"][0]["id"]
# Create a permission
suffix = str(random.randint(10000, 99999))
perm = await authentication_web.create_permission({
"permission_key": f"assignperm_key_default_{suffix}",
"permission_name": f"AssignPerm PermissionDefault {suffix}",
"description": "desc"
})
perm_id = perm.json()["id"]
# Try to assign permission to default role
resp = await authentication_web.assign_permissions_to_role({
"role_id": default_role_id,
"permission_ids": [perm_id, *json["items"][0]["permission_ids"]]
})
assert resp.status_code in [200]
if __name__ == '__main__':
pytest.main([__file__])

View File

@ -0,0 +1,159 @@
import pytest
import random
from tests.base.authentication_web import AuthenticationWeb
class TestCreateRole:
@pytest.mark.asyncio
async def test_create_role_success(self, authentication_web: AuthenticationWeb):
"""Test creating a role successfully with valid and unique role_key and role_name."""
suffix = str(random.randint(10000, 99999))
role_data = {
"role_key": f"test_role_key_success_{suffix}",
"role_name": f"Test Role Success {suffix}",
"role_description": "Role for testing success",
"role_level": 1
}
response = await authentication_web.create_role(role_data)
assert response.status_code == 200
json = response.json()
assert json["role_key"] == role_data["role_key"]
assert json["role_name"] == role_data["role_name"]
assert json["role_description"] == role_data["role_description"]
assert json["role_level"] == role_data["role_level"]
assert json["id"] is not None
assert json["created_at"] is not None
assert json["updated_at"] is not None
@pytest.mark.asyncio
async def test_create_role_fail_duplicate_role_key(self, authentication_web: AuthenticationWeb):
"""Test creating a role fails when role_key is duplicated."""
suffix = str(random.randint(10000, 99999))
role_data = {
"role_key": f"test_role_key_dup_{suffix}",
"role_name": f"Test Role DupKey {suffix}",
"role_description": "desc",
"role_level": 1
}
await authentication_web.create_role(role_data)
role_data2 = {
"role_key": f"test_role_key_dup_{suffix}",
"role_name": f"Test Role DupKey2 {suffix}",
"role_description": "desc2",
"role_level": 2
}
response = await authentication_web.create_role(role_data2)
assert response.status_code == 422 or response.status_code == 400
@pytest.mark.asyncio
async def test_create_role_fail_duplicate_role_name(self, authentication_web: AuthenticationWeb):
"""Test creating a role fails when role_name is duplicated."""
suffix = str(random.randint(10000, 99999))
role_data = {
"role_key": f"test_role_key_dupname1_{suffix}",
"role_name": f"Test Role DupName {suffix}",
"role_description": "desc",
"role_level": 1
}
await authentication_web.create_role(role_data)
role_data2 = {
"role_key": f"test_role_key_dupname2_{suffix}",
"role_name": f"Test Role DupName {suffix}",
"role_description": "desc2",
"role_level": 2
}
response = await authentication_web.create_role(role_data2)
assert response.status_code == 422 or response.status_code == 400
@pytest.mark.asyncio
async def test_create_role_fail_empty_role_key(self, authentication_web: AuthenticationWeb):
"""Test creating a role fails when role_key is empty."""
suffix = str(random.randint(10000, 99999))
role_data = {
"role_key": "",
"role_name": f"Test Role EmptyKey {suffix}",
"role_description": "desc",
"role_level": 1
}
response = await authentication_web.create_role(role_data)
assert response.status_code == 422 or response.status_code == 400
@pytest.mark.asyncio
async def test_create_role_fail_empty_role_name(self, authentication_web: AuthenticationWeb):
"""Test creating a role fails when role_name is empty."""
suffix = str(random.randint(10000, 99999))
role_data = {
"role_key": f"test_role_key_emptyname_{suffix}",
"role_name": "",
"role_description": "desc",
"role_level": 1
}
response = await authentication_web.create_role(role_data)
assert response.status_code == 422 or response.status_code == 400
@pytest.mark.asyncio
async def test_create_role_success_empty_description(self, authentication_web: AuthenticationWeb):
"""Test creating a role successfully when role_description is None (optional field)."""
suffix = str(random.randint(10000, 99999))
role_data = {
"role_key": f"test_role_key_emptydesc_{suffix}",
"role_name": f"Test Role EmptyDesc {suffix}",
"role_description": None,
"role_level": 1
}
response = await authentication_web.create_role(role_data)
assert response.status_code == 200
json = response.json()
assert json["role_key"] == role_data["role_key"]
assert json["role_name"] == role_data["role_name"]
assert json["role_description"] is None or json["role_description"] == ""
assert json["role_level"] == role_data["role_level"]
@pytest.mark.asyncio
async def test_create_role_fail_by_non_admin(self, authentication_web_of_temp_user1: AuthenticationWeb):
"""Test creating a role fails by non-admin user (no permission)."""
suffix = str(random.randint(10000, 99999))
role_data = {
"role_key": f"test_role_key_nonadmin_{suffix}",
"role_name": f"Test Role NonAdmin {suffix}",
"role_description": "desc",
"role_level": 1
}
response = await authentication_web_of_temp_user1.create_role(role_data)
assert response.status_code == 403 or response.status_code == 401
@pytest.mark.asyncio
async def test_create_role_success_after_grant_admin(self, authentication_web: AuthenticationWeb):
"""Test creating a role succeeds after granting admin role to a temporary user and re-login."""
# Create a temp user
user = authentication_web.create_temporary_user()
temp_authentication_web = AuthenticationWeb(user_email=user["email"], password=user["password"])
temp_authentication_web.user_id = user["user_id"]
temp_authentication_web.login()
# Grant admin role to temp user
resp = await authentication_web.query_roles({"role_key": "admin"})
admin_role_id = resp.json()["items"][0]["id"]
response1 = await authentication_web.assign_roles_to_user({
"user_id": temp_authentication_web.user_id,
"role_ids": [admin_role_id]
})
# Re-login as temp user
temp_authentication_web.login()
# Try to create role
suffix = str(random.randint(10000, 99999))
role_data = {
"role_key": f"test_role_key_tempadmin_{suffix}",
"role_name": f"Test Role TempAdmin {suffix}",
"role_description": "desc",
"role_level": 1
}
response = await temp_authentication_web.create_role(role_data)
assert response.status_code == 200
if __name__ == '__main__':
pytest.main([__file__])

View File

@ -0,0 +1,91 @@
import pytest
import random
from backend.models.permission.constants import DefaultRole, DefaultRoleEnum
from tests.base.authentication_web import AuthenticationWeb
class TestDeleteRole:
@pytest.mark.asyncio
async def test_delete_role_success(self, authentication_web: AuthenticationWeb):
"""Test deleting a role successfully."""
suffix = str(random.randint(10000, 99999))
role = await authentication_web.create_role({
"role_key": f"delrole_{suffix}",
"role_name": f"delrole_{suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role.json()["id"]
resp = await authentication_web.delete_role(role_data={"role_id": role_id})
assert resp.status_code == 200
assert resp.json()["success"] is True
@pytest.mark.asyncio
async def test_delete_role_fail_not_found(self, authentication_web: AuthenticationWeb):
"""Test deleting a role fails when role_id does not exist."""
resp = await authentication_web.delete_role(role_data={"role_id": "000000000000000000000000"})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_delete_default_role_fail(self, authentication_web: AuthenticationWeb):
"""Test deleting a default role fails. Default role cannot be deleted."""
# Query a default role
resp = await authentication_web.query_roles(
params={"page": 1, "page_size": 2, "role_key": DefaultRoleEnum.ADMIN.value.role_key})
json = resp.json()
default_role_id = json["items"][0]["id"]
resp = await authentication_web.delete_role(role_data={"role_id": default_role_id})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_delete_role_fail_by_non_admin(self, authentication_web: AuthenticationWeb, authentication_web_of_temp_user1: AuthenticationWeb):
"""Test deleting a role fails by non-admin user (no permission)."""
# Create a role as admin
suffix = str(random.randint(10000, 99999))
role = await authentication_web.create_role({
"role_key": f"delrole_nonadmin_{suffix}",
"role_name": f"delrole_nonadmin_{suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role.json()["id"]
# Try to delete as temp user
resp = await authentication_web_of_temp_user1.delete_role(role_data={"role_id": role_id})
assert resp.status_code == 403 or resp.status_code == 401
@pytest.mark.asyncio
async def test_delete_role_success_after_grant_admin(self, authentication_web: AuthenticationWeb):
"""Test deleting a role succeeds after granting admin role to a temporary user and re-login."""
# Create a temp user
user = authentication_web.create_temporary_user()
temp_authentication_web = AuthenticationWeb(user_email=user["email"], password=user["password"])
temp_authentication_web.user_id = user["user_id"]
temp_authentication_web.login()
# Create a role as admin
suffix = str(random.randint(10000, 99999))
role = await authentication_web.create_role({
"role_key": f"delrole_tempadmin_{suffix}",
"role_name": f"delrole_tempadmin_{suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role.json()["id"]
# Grant admin role to temp user
resp = await authentication_web.query_roles({"role_key": DefaultRoleEnum.ADMIN.value.role_key})
admin_role_id = resp.json()["items"][0]["id"]
response1 = await authentication_web.assign_roles_to_user({
"user_id": temp_authentication_web.user_id,
"role_ids": [admin_role_id]
})
# Re-login as temp user
temp_authentication_web.login()
# Try to delete as temp user
resp = await temp_authentication_web.delete_role(role_data={"role_id": role_id})
assert resp.status_code == 200
if __name__ == '__main__':
pytest.main([__file__])

View File

@ -0,0 +1,58 @@
import random
import pytest
from tests.base.authentication_web import AuthenticationWeb
class TestQueryRole:
@pytest.mark.asyncio
async def test_query_all_roles(self, authentication_web: AuthenticationWeb):
"""Test querying all roles returns a list."""
resp = await authentication_web.query_roles(params={})
assert resp.status_code == 200
json = resp.json()
assert "items" in json
assert "total" in json
@pytest.mark.asyncio
async def test_query_roles_by_key(self, authentication_web: AuthenticationWeb):
"""Test querying roles by role_key with fuzzy search."""
suffix = str(random.randint(10000, 99999))
await authentication_web.create_role({
"role_key": f"querykey_{suffix}",
"role_name": f"querykey_{suffix}",
"role_description": "desc",
"role_level": 1
})
resp = await authentication_web.query_roles(params={"role_key": f"querykey_{suffix}"})
assert resp.status_code == 200
json = resp.json()
assert any(f"querykey_{suffix}" in item["role_key"] for item in json["items"])
@pytest.mark.asyncio
async def test_query_roles_by_name(self, authentication_web: AuthenticationWeb):
"""Test querying roles by role_name with fuzzy search."""
suffix = str(random.randint(10000, 99999))
await authentication_web.create_role({
"role_key": f"queryname_{suffix}",
"role_name": f"queryname_{suffix}",
"role_description": "desc",
"role_level": 1
})
resp = await authentication_web.query_roles(params={"role_name": f"queryname_{suffix}"})
assert resp.status_code == 200
json = resp.json()
assert any(f"queryname_{suffix}" in item["role_name"] for item in json["items"])
@pytest.mark.asyncio
async def test_query_roles_pagination(self, authentication_web: AuthenticationWeb):
"""Test querying roles with pagination."""
resp = await authentication_web.query_roles(params={"page": 1, "page_size": 2})
assert resp.status_code == 200
json = resp.json()
assert "items" in json
assert "total" in json
assert "page" in json
assert "page_size" in json

View File

@ -0,0 +1,233 @@
import pytest
import random
from backend.models.permission.constants import DefaultRoleEnum
from tests.base.authentication_web import AuthenticationWeb
class TestUpdateRole:
@pytest.mark.asyncio
async def test_update_role_success(self, authentication_web: AuthenticationWeb):
"""Test updating a role successfully with valid and unique fields."""
suffix = str(random.randint(10000, 99999))
# create firstly
role_data = {
"role_key": f"update_role_key_{suffix}",
"role_name": f"Update Role {suffix}",
"role_description": "desc",
"role_level": 1
}
create_resp = await authentication_web.create_role(role_data)
role_id = create_resp.json()["id"]
# update
update_data = {
"role_id": role_id,
"role_key": f"update_role_key_{suffix}_new",
"role_name": f"Update Role {suffix} New",
"role_description": "desc new",
"role_level": 2
}
resp = await authentication_web.update_role(role_data=update_data)
assert resp.status_code == 200
json = resp.json()
assert json["role_key"] == update_data["role_key"]
assert json["role_name"] == update_data["role_name"]
assert json["role_description"] == update_data["role_description"]
assert json["role_level"] == update_data["role_level"]
@pytest.mark.asyncio
async def test_update_role_fail_not_found(self, authentication_web: AuthenticationWeb):
"""Test updating a role fails when role_id does not exist."""
suffix = str(random.randint(10000, 99999))
update_data = {
"role_id": "000000000000000000000000", # 不存在的ObjectId
"role_key": f"notfound_key_{suffix}",
"role_name": f"NotFound Role {suffix}",
"role_description": "desc",
"role_level": 1
}
resp = await authentication_web.update_role(role_data=update_data)
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_role_fail_duplicate_key(self, authentication_web: AuthenticationWeb):
"""Test updating a role fails when role_key is duplicated."""
suffix = str(random.randint(10000, 99999))
# create two roles
role1 = await authentication_web.create_role({
"role_key": f"dupkey1_{suffix}",
"role_name": f"dupkey1_{suffix}",
"role_description": "desc",
"role_level": 1
})
role2 = await authentication_web.create_role({
"role_key": f"dupkey2_{suffix}",
"role_name": f"dupkey2_{suffix}",
"role_description": "desc",
"role_level": 1
})
role2_id = role2.json()["id"]
# modify role_key
update_data = {
"role_id": role2_id,
"role_key": f"dupkey1_{suffix}",
"role_name": f"dupkey2_{suffix}_new",
"role_description": "desc",
"role_level": 2
}
resp = await authentication_web.update_role(role_data=update_data)
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_role_fail_duplicate_name(self, authentication_web: AuthenticationWeb):
"""Test updating a role fails when role_name is duplicated."""
suffix = str(random.randint(10000, 99999))
# create two roles
role1 = await authentication_web.create_role({
"role_key": f"dupname1_{suffix}",
"role_name": f"dupname1_{suffix}",
"role_description": "desc",
"role_level": 1
})
role2 = await authentication_web.create_role({
"role_key": f"dupname2_{suffix}",
"role_name": f"dupname2_{suffix}",
"role_description": "desc",
"role_level": 1
})
role2_id = role2.json()["id"]
# modify role name
update_data = {
"role_id": role2_id,
"role_key": f"dupname2_{suffix}_new",
"role_name": f"dupname1_{suffix}",
"role_description": "desc",
"role_level": 2
}
resp = await authentication_web.update_role(role_data=update_data)
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_role_fail_empty_key(self, authentication_web: AuthenticationWeb):
"""Test updating a role fails when role_key is empty."""
suffix = str(random.randint(10000, 99999))
role = await authentication_web.create_role({
"role_key": f"emptykey_{suffix}",
"role_name": f"emptykey_{suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role.json()["id"]
update_data = {
"role_id": role_id,
"role_key": "",
"role_name": f"emptykey_{suffix}_new",
"role_description": "desc",
"role_level": 2
}
resp = await authentication_web.update_role(role_data=update_data)
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_role_fail_empty_name(self, authentication_web: AuthenticationWeb):
"""Test updating a role fails when role_name is empty."""
suffix = str(random.randint(10000, 99999))
role = await authentication_web.create_role({
"role_key": f"emptyname_{suffix}",
"role_name": f"emptyname_{suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role.json()["id"]
update_data = {
"role_id": role_id,
"role_key": f"emptyname_{suffix}_new",
"role_name": "",
"role_description": "desc",
"role_level": 2
}
resp = await authentication_web.update_role(role_data=update_data)
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_default_role_fail(self, authentication_web: AuthenticationWeb):
"""Test updating a default role fails. Default role cannot be updated."""
suffix = str(random.randint(10000, 99999))
# Query a default role
resp = await authentication_web.query_roles(
params={"page": 1, "page_size": 2, "role_key": DefaultRoleEnum.ADMIN.value.role_key})
json = resp.json()
default_role = json["items"][0]
resp = await authentication_web.update_role(role_data={
"role_id": default_role["id"],
"role_key": f"{default_role['role_key']}_{suffix}_update",
"role_name": f"{default_role['role_name']}_{suffix}_update",
"role_description": "desc",
"role_level": 2
})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_update_role_fail_by_non_admin(self, authentication_web: AuthenticationWeb, authentication_web_of_temp_user1: AuthenticationWeb):
"""Test updating a role fails by non-admin user (no permission)."""
# Create a role as admin
suffix = str(random.randint(10000, 99999))
role = await authentication_web.create_role({
"role_key": f"updaterole_nonadmin_{suffix}",
"role_name": f"updaterole_nonadmin_{suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role.json()["id"]
update_data = {
"role_id": role_id,
"role_key": f"updaterole_nonadmin_{suffix}_new",
"role_name": f"updaterole_nonadmin_{suffix}_new",
"role_description": "desc new",
"role_level": 2
}
resp = await authentication_web_of_temp_user1.update_role(role_data=update_data)
assert resp.status_code == 403 or resp.status_code == 401
@pytest.mark.asyncio
async def test_update_role_success_after_grant_admin(self, authentication_web: AuthenticationWeb):
"""Test updating a role succeeds after granting admin role to a temporary user and re-login."""
# Create a temp user
user = authentication_web.create_temporary_user()
temp_authentication_web = AuthenticationWeb(user_email=user["email"], password=user["password"])
temp_authentication_web.user_id = user["user_id"]
temp_authentication_web.login()
# Create a role as admin
suffix = str(random.randint(10000, 99999))
role = await authentication_web.create_role({
"role_key": f"updaterole_tempadmin_{suffix}",
"role_name": f"updaterole_tempadmin_{suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role.json()["id"]
# Grant admin role to temp user
resp = await authentication_web.query_roles({"role_key": "admin"})
admin_role_id = resp.json()["items"][0]["id"]
await authentication_web.assign_roles_to_user({
"user_id": temp_authentication_web.user_id,
"role_ids": [admin_role_id]
})
# Re-login as temp user
temp_authentication_web.login()
# Try to update as temp user
update_data = {
"role_id": role_id,
"role_key": f"updaterole_tempadmin_{suffix}_new",
"role_name": f"updaterole_tempadmin_{suffix}_new",
"role_description": "desc new",
"role_level": 2
}
resp = await temp_authentication_web.update_role(role_data=update_data)
assert resp.status_code == 200
if __name__ == '__main__':
pytest.main([__file__])

View File

@ -0,0 +1,37 @@
# Signin API Test Report
## How to Run the Tests
**Run all signin API tests:**
```bash
pytest --tb=short tests/api_tests/siginin/
```
---
## Test Results Summary
- **Total tests collected:** 1
- **All tests passed.**
- **Warnings:**
- Deprecation warning from Pydantic (upgrade recommended for future compatibility).
---
## Test Case Explanations
### test_signin_with_email_and_password.py
- **test_sign_in_with_email_and_password**
This test verifies the email and password sign-in flow:
- Calls the login API with valid credentials.
- Asserts that the response contains a valid access token, refresh token, expiration, identity, role names, and user permissions.
- Decodes the JWT access token and checks that the payload contains the expected subject fields (id, role_names, user_permissions).
---
## Summary
- This test ensures that the email/password sign-in API returns all required authentication and user information fields, and that the JWT token is correctly structured.
- If you need to add more signin scenarios, add new test cases to this directory and re-run the tests.
---

View File

@ -0,0 +1,2 @@
JWT_SECRET_KEY = "ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0"
JWT_ALGORITHM = "HS256"

View File

@ -0,0 +1,10 @@
import pytest
from tests.base.authentication_web import AuthenticationWeb
@pytest.fixture
def authentication_web() -> AuthenticationWeb:
authentication_web = AuthenticationWeb()
authentication_web.login()
return authentication_web

View File

@ -0,0 +1,30 @@
import jwt
import pytest
from tests.api_tests.siginin import config
from tests.base.authentication_web import AuthenticationWeb
class TestSignInWithEmailAndPassword:
@pytest.mark.asyncio
async def test_sign_in_with_email_and_password(self, authentication_web: AuthenticationWeb):
response = authentication_web.do_login()
assert response.status_code == 200
json = response.json()
assert json["access_token"] is not None, "access_token should not be None"
assert json["refresh_token"] is not None, "refresh_token should not be None"
assert json["expires_in"] is not None, "expires_in should not be None"
assert json["identity"] is not None, "identity should not be None"
assert json["role_names"] is not None, "role_names should not be None"
assert json["user_permissions"] is not None, "user_permissions should not be None"
payload = jwt.decode(json["access_token"], config.JWT_SECRET_KEY, algorithms=[config.JWT_ALGORITHM])
assert payload["subject"] is not None, "subject should not be None"
assert payload["subject"]["id"] is not None, "subject.id should not be None"
assert payload["subject"]["role_names"] is not None, "subject.role_names should not be None"
assert payload["subject"]["user_permissions"] is not None, "subject.user_permissions should not be None"
print(payload)
if __name__ == '__main__':
pytest.main([__file__])

View File

@ -0,0 +1,45 @@
# User API Test Report
## How to Run the Tests
**Run all user API tests:**
```bash
pytest --tb=short tests/api_tests/user/
```
---
## Test Results Summary
- **Total tests collected:** 6
- **All tests passed.**
- **Warnings:**
- Deprecation warnings from Pydantic/Beanie (upgrade recommended for future compatibility).
---
## Test Case Explanations
### test_assign_roles.py
- **test_assign_roles_success_by_admin**
Admin user can assign a role to a user successfully.
- **test_assign_roles_fail_by_non_admin**
Non-admin user cannot assign roles to other users (permission denied).
- **test_assign_roles_fail_role_not_found**
Assigning a non-existent role to a user fails.
- **test_assign_roles_fail_empty_role_ids**
Assigning with an empty role list fails.
- **test_assign_roles_fail_empty_user_id**
Assigning roles with an empty user ID fails.
- **test_assign_roles_remove_duplicates**
Assigning duplicate role IDs results in de-duplication; the user ends up with a single instance of the role.
---
## Summary
- These tests ensure that only admin users can assign roles to users, and that the system properly handles invalid input and duplicate assignments.
- Each test case is designed to verify both positive and negative scenarios, including permission checks and input validation.
- If you need to add more user management scenarios, add new test cases to this directory and re-run the tests.
---

View File

@ -0,0 +1,21 @@
import pytest
from tests.base.authentication_web import AuthenticationWeb
@pytest.fixture(scope="session")
def authentication_web() -> AuthenticationWeb:
authentication_web = AuthenticationWeb()
authentication_web.login()
return authentication_web
@pytest.fixture(scope="session")
def authentication_web_of_temp_user1() -> AuthenticationWeb:
authentication_web = AuthenticationWeb()
user = authentication_web.create_temporary_user()
authentication_web.user_email = user["email"]
authentication_web.password = user["password"]
authentication_web.user_id = user["user_id"]
authentication_web.login()
return authentication_web

View File

@ -0,0 +1,100 @@
import pytest
import random
from backend.models.permission.constants import DefaultRoleEnum
from tests.base.authentication_web import AuthenticationWeb
class TestAssignRolesToUser:
@pytest.mark.asyncio
async def test_assign_roles_success_by_admin(self, authentication_web: AuthenticationWeb):
"""Test assigning roles to a user successfully by admin user."""
# Create a temporary user
temp_user = authentication_web.create_temporary_user()
# Create a new role
suffix = str(random.randint(10000, 99999))
role_resp = await authentication_web.create_role({
"role_key": f"assignrole_role_{suffix}",
"role_name": f"AssignRole Role {suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role_resp.json()["id"]
# Assign role to user
resp = await authentication_web.assign_roles_to_user({
"user_id": temp_user["user_id"], "role_ids": [role_id]
})
assert resp.status_code == 200
json = resp.json()
assert json["user_id"] == temp_user["user_id"]
assert json["role_ids"] == [role_id]
@pytest.mark.asyncio
async def test_assign_roles_fail_by_non_admin(self, authentication_web_of_temp_user1: AuthenticationWeb):
"""Test assigning roles to a user fails by non-admin user (no permission)."""
# Create another temporary user
temp_user = authentication_web_of_temp_user1.create_temporary_user()
# Query default admin role
resp = await authentication_web_of_temp_user1.query_roles({"role_key": DefaultRoleEnum.ADMIN.value.role_key})
admin_role_id = resp.json()["items"][0]["id"]
# Try to assign admin role to another user
resp = await authentication_web_of_temp_user1.assign_roles_to_user({
"user_id": temp_user["user_id"], "role_ids": [admin_role_id]
})
assert resp.status_code == 403 or resp.status_code == 401
@pytest.mark.asyncio
async def test_assign_roles_fail_role_not_found(self, authentication_web: AuthenticationWeb):
"""Test assigning roles fails when role_id does not exist."""
# Create a temporary user
temp_user = authentication_web.create_temporary_user()
# Try to assign non-existent role
resp = await authentication_web.assign_roles_to_user({
"user_id": temp_user["user_id"], "role_ids": ["000000000000000000000000"]
})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_assign_roles_fail_empty_role_ids(self, authentication_web: AuthenticationWeb):
"""Test assigning roles fails when role_ids is empty."""
# Create a temporary user
temp_user = authentication_web.create_temporary_user()
resp = await authentication_web.assign_roles_to_user({
"user_id": temp_user["user_id"], "role_ids": []
})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_assign_roles_fail_empty_user_id(self, authentication_web: AuthenticationWeb):
"""Test assigning roles fails when user_id is empty."""
# Query default admin role
resp = await authentication_web.query_roles({"role_key": DefaultRoleEnum.ADMIN.value.role_key})
admin_role_id = resp.json()["items"][0]["id"]
resp = await authentication_web.assign_roles_to_user({
"user_id": "", "role_ids": [admin_role_id]
})
assert resp.status_code == 422 or resp.status_code == 400
@pytest.mark.asyncio
async def test_assign_roles_remove_duplicates(self, authentication_web: AuthenticationWeb):
"""Test assigning roles with duplicate role_ids removes duplicates."""
# Create a temporary user
temp_user = authentication_web.create_temporary_user()
# Create a new role
suffix = str(random.randint(10000, 99999))
role_resp = await authentication_web.create_role({
"role_key": f"assignrole_role_dup_{suffix}",
"role_name": f"AssignRole RoleDup {suffix}",
"role_description": "desc",
"role_level": 1
})
role_id = role_resp.json()["id"]
# Assign duplicate role_ids
resp = await authentication_web.assign_roles_to_user({
"user_id": temp_user["user_id"], "role_ids": [role_id, role_id, role_id]
})
assert resp.status_code == 200
json = resp.json()
assert json["role_ids"] == [role_id]
if __name__ == '__main__':
pytest.main([__file__])

View File

@ -0,0 +1,157 @@
import asyncio
import httpx
from typing import Optional
from tests.base.config import USER_EMAIL, USER_PASSWORD, BASE_URL
from tests.util.temporary_email import *
class AuthenticationWeb:
def __init__(self, user_email: str = USER_EMAIL, password: str = USER_PASSWORD, base_url: str = BASE_URL):
self.user_email = user_email
self.password = password
self.user_id = None
self.base_url = base_url
self.token: Optional[str] = None
def create_temporary_user(self) -> dict[str, str]:
"""Create a temporary user."""
# generate temporary user email
email = generate_email()
print("temporary user email:", email)
# call try-signin-with-email api
response1 = self.try_signin_with_email(params={"email": email, "host": self.base_url})
print("try_signin_with_email", response1.json())
# query auth code
auth_code = get_auth_code(email)
print("temporary user auth code:", auth_code)
response2 = self.signin_with_email_and_code(
params={"email": email, "code": auth_code, "host": self.base_url})
print("signin_with_email_and_code", response2.json())
access_token = response2.json()["access_token"]
response3 = self.update_new_user_flid(token=access_token, params={'flid': response2.json()['flid']})
print("update_new_user_flid", response3.json())
password = "Kdwy12#$"
# set password
response4 = self.update_user_password(token=access_token, params={
'password': password,
'password2': password
})
print("update_user_password", response4.json())
return {
"email": email,
"password": password,
"user_id": response2.json()["identity"]
}
def update_new_user_flid(self, params: dict, token: str = None):
"""Update the user's FLID."""
if token is None:
token = self.token
headers = {"Authorization": f"Bearer {token}"}
with httpx.Client(base_url=self.base_url) as client:
resp = client.request("POST", "/api/auth/signin/update-new-user-flid", headers=headers, json=params)
return resp
def update_user_password(self, params: dict, token: str = None):
"""Update the user's password."""
if token is None:
token = self.token
headers = {"Authorization": f"Bearer {token}"}
with httpx.Client(base_url=self.base_url) as client:
resp = client.request("POST", "/api/auth/signin/update-user-password", headers=headers, json=params)
return resp
def try_signin_with_email(self, params):
"""try signin with email."""
return self.request_sync("POST", "/api/auth/signin/try-signin-with-email", json=params)
def signin_with_email_and_code(self, params):
"""try signin with email and code."""
return self.request_sync("POST", "/api/auth/signin/signin-with-email-and-code", json=params)
def login(self):
"""Login and store JWT token"""
with httpx.Client(base_url=self.base_url) as client:
resp = self.do_login(self.user_email, self.password)
self.token = resp.json()["access_token"]
return resp
def do_login(self, user_email: str = USER_EMAIL, password: str = USER_PASSWORD):
"""Login and store JWT token"""
with httpx.Client(base_url=self.base_url) as client:
resp = client.post("/api/auth/signin/signin-with-email-and-password", json={
"email": user_email,
"password": password
})
return resp
def request_sync(self, method: str, url: str, **kwargs):
"""Send authenticated request"""
headers = kwargs.pop("headers", {})
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
with httpx.Client(base_url=self.base_url) as client:
resp = client.request(method, url, headers=headers, **kwargs)
return resp
async def request(self, method: str, url: str, **kwargs):
"""Send authenticated request"""
headers = kwargs.pop("headers", {})
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
async with httpx.AsyncClient(base_url=self.base_url) as client:
resp = await client.request(method, url, headers=headers, **kwargs)
return resp
async def create_role(self, role_data: dict):
"""Create a new role via API"""
return await self.request("POST", "/api/auth/role/create", json=role_data)
async def delete_role(self, role_data: dict):
"""Delete role via API"""
return await self.request("POST", "/api/auth/role/delete", json=role_data)
async def update_role(self, role_data: dict):
"""Update role via API"""
return await self.request("POST", "/api/auth/role/update", json=role_data)
async def query_roles(self, params: dict = None):
"""Query roles via API"""
if params is None:
params = {}
return await self.request("POST", "/api/auth/role/query", json=params)
async def create_permission(self, perm_data: dict):
"""Create a new permission via API"""
return await self.request("POST", "/api/auth/permission/create", json=perm_data)
async def delete_permission(self, perm_data: dict):
"""Delete a permission via API"""
return await self.request("POST", "/api/auth/permission/delete", json=perm_data)
async def update_permission(self, perm_data: dict):
"""Update a permission via API"""
return await self.request("POST", "/api/auth/permission/update", json=perm_data)
async def query_permissions(self, params: dict = None):
"""Query permissions via API"""
if params is None:
params = {}
return await self.request("POST", "/api/auth/permission/query", json=params)
async def assign_permissions_to_role(self, data: dict):
"""Assign permissions to a role via API"""
return await self.request("POST", "/api/auth/role/assign-permissions", json=data)
async def assign_roles_to_user(self, data: dict):
"""Assign roles to a user via API"""
return await self.request("POST", "/api/auth/user/assign-roles", json=data)
if __name__ == '__main__':
authentication = AuthenticationWeb()
user = authentication.create_temporary_user()
print(user)

View File

@ -0,0 +1,5 @@
# user with admin role
USER_EMAIL = "XXXX"
USER_PASSWORD = "XXXX"
# authentication base url
BASE_URL = "http://localhost:8103"

View File

@ -0,0 +1,10 @@
import pytest
from tests.base.authentication_web import AuthenticationWeb
@pytest.fixture
def authentication_web() -> AuthenticationWeb:
authentication_web = AuthenticationWeb()
authentication_web.login()
return authentication_web

View File

@ -0,0 +1,28 @@
# Test Coverage Report (backend modules only)
---
## Coverage Table
| File Name | Statements | Missed | Coverage |
|------------------------------------------------------------------|------------|--------|----------|
| backend/infra/permission/permission_handler.py | 55 | 0 | 100% |
| backend/infra/permission/role_handler.py | 71 | 0 | 100% |
| backend/infra/permission/user_role_handler.py | 39 | 7 | 82% |
| backend/services/permission/permission_service.py | 20 | 0 | 100% |
| backend/services/permission/role_service.py | 24 | 0 | 100% |
| backend/services/user/user_management_service.py | 39 | 0 | 100% |
---
## Summary
This test report only includes the test coverage of functions related to role management.
See the integration tests:
- tests/api_tests/permission/README.md
- tests/api_tests/role/README.md
- tests/api_tests/user/README.md
## TODO
Add tests for the previous functions.

View File

@ -0,0 +1,137 @@
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from fastapi.exceptions import RequestValidationError
from backend.infra.permission.permission_handler import PermissionHandler
from backend.models.permission.models import PermissionDoc, RoleDoc
from beanie import PydanticObjectId
@pytest.fixture(autouse=True)
def mock_db():
with patch('backend.infra.permission.permission_handler.PermissionDoc') as MockPermissionDoc, \
patch('backend.infra.permission.permission_handler.RoleDoc') as MockRoleDoc:
yield MockPermissionDoc, MockRoleDoc
@pytest.mark.asyncio
class TestPermissionHandler:
@pytest.fixture(autouse=True)
def setup(self, mock_db):
self.MockPermissionDoc, self.MockRoleDoc = mock_db
self.handler = PermissionHandler()
async def test_create_permission_success(self):
# Test creating a permission successfully
self.MockPermissionDoc.find_one = AsyncMock(side_effect=[None, None])
mock_doc = MagicMock(spec=PermissionDoc)
self.MockPermissionDoc.return_value = mock_doc
mock_doc.insert = AsyncMock()
result = await self.handler.create_permission('key', 'name', 'desc')
assert result == mock_doc
mock_doc.insert.assert_awaited_once()
async def test_create_permission_missing_key_or_name(self):
# Test missing permission_key or permission_name raises validation error
with pytest.raises(RequestValidationError):
await self.handler.create_permission('', 'name', 'desc')
with pytest.raises(RequestValidationError):
await self.handler.create_permission('key', '', 'desc')
async def test_create_permission_duplicate(self):
# Test duplicate permission_key or permission_name raises validation error
self.MockPermissionDoc.find_one = AsyncMock(side_effect=[MagicMock(), None])
with pytest.raises(RequestValidationError):
await self.handler.create_permission('key', 'name', 'desc')
self.MockPermissionDoc.find_one = AsyncMock(side_effect=[None, MagicMock()])
with pytest.raises(RequestValidationError):
await self.handler.create_permission('key', 'name', 'desc')
async def test_update_permission_success(self):
# Test updating a permission successfully
mock_doc = MagicMock(spec=PermissionDoc)
mock_doc.is_default = False
self.MockPermissionDoc.get = AsyncMock(return_value=mock_doc)
self.MockPermissionDoc.find_one = AsyncMock(return_value=None)
mock_doc.save = AsyncMock()
result = await self.handler.update_permission(PydanticObjectId('507f1f77bcf86cd799439011'), 'key', 'name', 'desc')
assert result == mock_doc
mock_doc.save.assert_awaited_once()
async def test_update_permission_missing_args(self):
# Test missing permission_id, permission_key or permission_name raises validation error
with pytest.raises(RequestValidationError):
await self.handler.update_permission(None, 'key', 'name', 'desc')
with pytest.raises(RequestValidationError):
await self.handler.update_permission(PydanticObjectId('507f1f77bcf86cd799439011'), '', 'name', 'desc')
with pytest.raises(RequestValidationError):
await self.handler.update_permission(PydanticObjectId('507f1f77bcf86cd799439011'), 'key', '', 'desc')
async def test_update_permission_not_found(self):
# Test updating a non-existent permission raises validation error
self.MockPermissionDoc.get = AsyncMock(return_value=None)
with pytest.raises(RequestValidationError):
await self.handler.update_permission(PydanticObjectId('507f1f77bcf86cd799439011'), 'key', 'name', 'desc')
async def test_update_permission_is_default(self):
# Test updating a default permission raises validation error
mock_doc = MagicMock(spec=PermissionDoc)
mock_doc.is_default = True
self.MockPermissionDoc.get = AsyncMock(return_value=mock_doc)
with pytest.raises(RequestValidationError):
await self.handler.update_permission(PydanticObjectId('507f1f77bcf86cd799439011'), 'key', 'name', 'desc')
async def test_update_permission_conflict(self):
# Test updating a permission with duplicate key or name raises validation error
mock_doc = MagicMock(spec=PermissionDoc)
mock_doc.is_default = False
self.MockPermissionDoc.get = AsyncMock(return_value=mock_doc)
self.MockPermissionDoc.find_one = AsyncMock(return_value=MagicMock())
with pytest.raises(RequestValidationError):
await self.handler.update_permission(PydanticObjectId('507f1f77bcf86cd799439011'), 'key', 'name', 'desc')
async def test_query_permissions_success(self):
# Test querying permissions returns docs and total
mock_cursor = MagicMock()
mock_cursor.count = AsyncMock(return_value=2)
mock_cursor.skip.return_value = mock_cursor
mock_cursor.limit.return_value = mock_cursor
mock_cursor.to_list = AsyncMock(return_value=['doc1', 'doc2'])
self.MockPermissionDoc.find.return_value = mock_cursor
docs, total = await self.handler.query_permissions('key', 'name', 0, 10)
assert docs == ['doc1', 'doc2']
assert total == 2
async def test_delete_permission_success(self):
# Test deleting a permission successfully
self.MockRoleDoc.find_one = AsyncMock(return_value=None)
mock_doc = MagicMock(spec=PermissionDoc)
mock_doc.is_default = False
self.MockPermissionDoc.get = AsyncMock(return_value=mock_doc)
mock_doc.delete = AsyncMock()
await self.handler.delete_permission(PydanticObjectId('507f1f77bcf86cd799439011'))
mock_doc.delete.assert_awaited_once()
async def test_delete_permission_missing_id(self):
# Test missing permission_id raises validation error
with pytest.raises(RequestValidationError):
await self.handler.delete_permission(None)
async def test_delete_permission_referenced_by_role(self):
# Test deleting a permission referenced by a role raises validation error
self.MockRoleDoc.find_one = AsyncMock(return_value=MagicMock())
with pytest.raises(RequestValidationError):
await self.handler.delete_permission(PydanticObjectId('507f1f77bcf86cd799439011'))
async def test_delete_permission_not_found(self):
# Test deleting a non-existent permission raises validation error
self.MockRoleDoc.find_one = AsyncMock(return_value=None)
self.MockPermissionDoc.get = AsyncMock(return_value=None)
with pytest.raises(RequestValidationError):
await self.handler.delete_permission(PydanticObjectId('507f1f77bcf86cd799439011'))
async def test_delete_permission_is_default(self):
# Test deleting a default permission raises validation error
self.MockRoleDoc.find_one = AsyncMock(return_value=None)
mock_doc = MagicMock(spec=PermissionDoc)
mock_doc.is_default = True
self.MockPermissionDoc.get = AsyncMock(return_value=mock_doc)
with pytest.raises(RequestValidationError):
await self.handler.delete_permission(PydanticObjectId('507f1f77bcf86cd799439011'))

View File

@ -0,0 +1,169 @@
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from fastapi.exceptions import RequestValidationError
from backend.infra.permission.role_handler import RoleHandler
from backend.models.permission.models import RoleDoc, PermissionDoc, UserRoleDoc
from beanie import PydanticObjectId
@pytest.fixture(autouse=True)
def mock_db():
with patch('backend.infra.permission.role_handler.RoleDoc') as MockRoleDoc, \
patch('backend.infra.permission.role_handler.PermissionDoc') as MockPermissionDoc, \
patch('backend.infra.permission.role_handler.UserRoleDoc') as MockUserRoleDoc:
yield MockRoleDoc, MockPermissionDoc, MockUserRoleDoc
@pytest.mark.asyncio
class TestRoleHandler:
@pytest.fixture(autouse=True)
def setup(self, mock_db):
self.MockRoleDoc, self.MockPermissionDoc, self.MockUserRoleDoc = mock_db
self.handler = RoleHandler()
async def test_create_role_success(self):
# Test creating a role successfully
self.MockRoleDoc.find_one = AsyncMock(side_effect=[None, None])
mock_doc = MagicMock(spec=RoleDoc)
self.MockRoleDoc.return_value = mock_doc
mock_doc.insert = AsyncMock()
result = await self.handler.create_role('key', 'name', 'desc', 1)
assert result == mock_doc
mock_doc.insert.assert_awaited_once()
async def test_create_role_missing_key_or_name(self):
# Test missing role_key or role_name raises validation error
with pytest.raises(RequestValidationError):
await self.handler.create_role('', 'name', 'desc', 1)
with pytest.raises(RequestValidationError):
await self.handler.create_role('key', '', 'desc', 1)
async def test_create_role_duplicate(self):
# Test duplicate role_key or role_name raises validation error
self.MockRoleDoc.find_one = AsyncMock(side_effect=[MagicMock(), None])
with pytest.raises(RequestValidationError):
await self.handler.create_role('key', 'name', 'desc', 1)
self.MockRoleDoc.find_one = AsyncMock(side_effect=[None, MagicMock()])
with pytest.raises(RequestValidationError):
await self.handler.create_role('key', 'name', 'desc', 1)
async def test_update_role_success(self):
# Test updating a role successfully
mock_doc = MagicMock(spec=RoleDoc)
mock_doc.is_default = False
self.MockRoleDoc.get = AsyncMock(return_value=mock_doc)
self.MockRoleDoc.find_one = AsyncMock(return_value=None)
mock_doc.save = AsyncMock()
result = await self.handler.update_role(PydanticObjectId('507f1f77bcf86cd799439011'), 'key', 'name', 'desc', 1)
assert result == mock_doc
mock_doc.save.assert_awaited_once()
async def test_update_role_missing_args(self):
# Test missing role_id, role_key or role_name raises validation error
with pytest.raises(RequestValidationError):
await self.handler.update_role(None, 'key', 'name', 'desc', 1)
with pytest.raises(RequestValidationError):
await self.handler.update_role(PydanticObjectId('507f1f77bcf86cd799439011'), '', 'name', 'desc', 1)
with pytest.raises(RequestValidationError):
await self.handler.update_role(PydanticObjectId('507f1f77bcf86cd799439011'), 'key', '', 'desc', 1)
async def test_update_role_not_found(self):
# Test updating a non-existent role raises validation error
self.MockRoleDoc.get = AsyncMock(return_value=None)
with pytest.raises(RequestValidationError):
await self.handler.update_role(PydanticObjectId('507f1f77bcf86cd799439011'), 'key', 'name', 'desc', 1)
async def test_update_role_is_default(self):
# Test updating a default role raises validation error
mock_doc = MagicMock(spec=RoleDoc)
mock_doc.is_default = True
self.MockRoleDoc.get = AsyncMock(return_value=mock_doc)
with pytest.raises(RequestValidationError):
await self.handler.update_role(PydanticObjectId('507f1f77bcf86cd799439011'), 'key', 'name', 'desc', 1)
async def test_update_role_conflict(self):
# Test updating a role with duplicate key or name raises validation error
mock_doc = MagicMock(spec=RoleDoc)
mock_doc.is_default = False
self.MockRoleDoc.get = AsyncMock(return_value=mock_doc)
self.MockRoleDoc.find_one = AsyncMock(return_value=MagicMock())
with pytest.raises(RequestValidationError):
await self.handler.update_role(PydanticObjectId('507f1f77bcf86cd799439011'), 'key', 'name', 'desc', 1)
async def test_query_roles_success(self):
# Test querying roles returns docs and total
mock_cursor = MagicMock()
mock_cursor.count = AsyncMock(return_value=2)
mock_cursor.skip.return_value = mock_cursor
mock_cursor.limit.return_value = mock_cursor
mock_cursor.to_list = AsyncMock(return_value=['doc1', 'doc2'])
self.MockRoleDoc.find.return_value = mock_cursor
docs, total = await self.handler.query_roles('key', 'name', 0, 10)
assert docs == ['doc1', 'doc2']
assert total == 2
async def test_assign_permissions_to_role_success(self):
# Test assigning permissions to a role successfully
mock_doc = MagicMock(spec=RoleDoc)
self.MockRoleDoc.get = AsyncMock(return_value=mock_doc)
self.MockPermissionDoc.get = AsyncMock(return_value=MagicMock())
mock_doc.save = AsyncMock()
result = await self.handler.assign_permissions_to_role(PydanticObjectId('507f1f77bcf86cd799439011'), ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439011'])
assert result == mock_doc
mock_doc.save.assert_awaited_once()
async def test_assign_permissions_to_role_missing_args(self):
# Test missing role_id or permission_ids raises validation error
with pytest.raises(RequestValidationError):
await self.handler.assign_permissions_to_role(None, ['pid1'])
with pytest.raises(RequestValidationError):
await self.handler.assign_permissions_to_role(PydanticObjectId('507f1f77bcf86cd799439011'), None)
async def test_assign_permissions_to_role_role_not_found(self):
# Test assigning permissions to a non-existent role raises validation error
self.MockRoleDoc.get = AsyncMock(return_value=None)
with pytest.raises(RequestValidationError):
await self.handler.assign_permissions_to_role(PydanticObjectId('507f1f77bcf86cd799439011'), ['507f1f77bcf86cd799439011'])
async def test_assign_permissions_to_role_permission_not_found(self):
# Test assigning a non-existent permission raises validation error
mock_doc = MagicMock(spec=RoleDoc)
self.MockRoleDoc.get = AsyncMock(return_value=mock_doc)
self.MockPermissionDoc.get = AsyncMock(side_effect=[None, MagicMock()])
with pytest.raises(RequestValidationError):
await self.handler.assign_permissions_to_role(PydanticObjectId('507f1f77bcf86cd799439011'), ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439011'])
async def test_delete_role_success(self):
# Test deleting a role successfully
self.MockUserRoleDoc.find_one = AsyncMock(return_value=None)
mock_doc = MagicMock(spec=RoleDoc)
mock_doc.is_default = False
self.MockRoleDoc.get = AsyncMock(return_value=mock_doc)
mock_doc.delete = AsyncMock()
await self.handler.delete_role(PydanticObjectId('507f1f77bcf86cd799439011'))
mock_doc.delete.assert_awaited_once()
async def test_delete_role_missing_id(self):
# Test missing role_id raises validation error
with pytest.raises(RequestValidationError):
await self.handler.delete_role(None)
async def test_delete_role_referenced_by_user(self):
# Test deleting a role referenced by a user raises validation error
self.MockUserRoleDoc.find_one = AsyncMock(return_value=MagicMock())
with pytest.raises(RequestValidationError):
await self.handler.delete_role(PydanticObjectId('507f1f77bcf86cd799439011'))
async def test_delete_role_not_found(self):
# Test deleting a non-existent role raises validation error
self.MockUserRoleDoc.find_one = AsyncMock(return_value=None)
self.MockRoleDoc.get = AsyncMock(return_value=None)
with pytest.raises(RequestValidationError):
await self.handler.delete_role(PydanticObjectId('507f1f77bcf86cd799439011'))
async def test_delete_role_is_default(self):
# Test deleting a default role raises validation error
self.MockUserRoleDoc.find_one = AsyncMock(return_value=None)
mock_doc = MagicMock(spec=RoleDoc)
mock_doc.is_default = True
self.MockRoleDoc.get = AsyncMock(return_value=mock_doc)
with pytest.raises(RequestValidationError):
await self.handler.delete_role(PydanticObjectId('507f1f77bcf86cd799439011'))

View File

@ -0,0 +1,43 @@
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from backend.infra.permission.user_role_handler import UserRoleHandler
from backend.models.permission.models import RoleDoc, UserRoleDoc, PermissionDoc
@pytest.fixture(autouse=True)
def mock_db():
with patch('backend.infra.permission.user_role_handler.RoleDoc') as MockRoleDoc, \
patch('backend.infra.permission.user_role_handler.UserRoleDoc') as MockUserRoleDoc, \
patch('backend.infra.permission.user_role_handler.PermissionDoc') as MockPermissionDoc:
yield MockRoleDoc, MockUserRoleDoc, MockPermissionDoc
@pytest.mark.asyncio
class TestUserRoleHandler:
@pytest.fixture(autouse=True)
def setup(self, mock_db):
self.MockRoleDoc, self.MockUserRoleDoc, self.MockPermissionDoc = mock_db
self.handler = UserRoleHandler()
async def test_assign_roles_to_user_success(self):
# Test assigning roles to a user when no UserRoleDoc exists (create new)
self.MockRoleDoc.get = AsyncMock(return_value=MagicMock())
self.MockUserRoleDoc.find_one = AsyncMock(return_value=None)
mock_user_role = MagicMock(spec=UserRoleDoc)
self.MockUserRoleDoc.return_value = mock_user_role
mock_user_role.insert = AsyncMock()
result = await self.handler.assign_roles_to_user('507f1f77bcf86cd799439011', ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012'])
assert result == mock_user_role
mock_user_role.insert.assert_awaited_once()
async def test_get_role_and_permission_by_user_id_with_roles_and_permissions(self):
# Test getting roles and permissions when user has roles and permissions
self.MockUserRoleDoc.find_one = AsyncMock(return_value=MagicMock(role_ids=['507f1f77bcf86cd799439010', '507f1f77bcf86cd799439017']))
mock_role1 = MagicMock(role_name='role1', permission_ids=['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012'])
mock_role2 = MagicMock(role_name='role2', permission_ids=['507f1f77bcf86cd799439014', '507f1f77bcf86cd799439013'])
self.MockRoleDoc.find.return_value.to_list = AsyncMock(return_value=[mock_role1, mock_role2])
mock_perm1 = MagicMock(permission_key='perm1')
mock_perm2 = MagicMock(permission_key='perm2')
mock_perm3 = MagicMock(permission_key='perm3')
self.MockPermissionDoc.find.return_value.to_list = AsyncMock(return_value=[mock_perm1, mock_perm2, mock_perm3])
result = await self.handler.get_role_and_permission_by_user_id('uid')
assert set(result[0]) == {'role1', 'role2'}
assert set(result[1]) == {'perm1', 'perm2', 'perm3'}

View File

@ -0,0 +1,152 @@
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from fastapi.exceptions import RequestValidationError
from backend.services.permission.permission_service import PermissionService
from backend.models.permission.models import PermissionDoc
import datetime
@pytest.fixture
def mock_permission_handler():
# Fixture to patch PermissionHandler for isolation and mocking
with patch('backend.services.permission.permission_service.PermissionHandler') as MockHandler:
yield MockHandler
@pytest.mark.asyncio
class TestPermissionService:
@pytest.fixture(autouse=True)
def setup(self, mock_permission_handler):
# Automatically set up a PermissionService with a mocked handler for each test
self.mock_handler = mock_permission_handler.return_value
self.service = PermissionService()
self.service.permission_handler = self.mock_handler
async def test_create_permission_success(self):
# Test creating a permission successfully returns the expected PermissionDoc
mock_doc = MagicMock(spec=PermissionDoc)
self.mock_handler.create_permission = AsyncMock(return_value=mock_doc)
result = await self.service.create_permission('key', 'name', 'desc')
assert result == mock_doc
self.mock_handler.create_permission.assert_awaited_once_with('key', 'name', 'desc')
async def test_create_permission_fail(self):
# Test that an exception is raised if the handler fails to create a permission
self.mock_handler.create_permission = AsyncMock(side_effect=RequestValidationError('error'))
with pytest.raises(RequestValidationError):
await self.service.create_permission('key', 'name', 'desc')
async def test_create_permission_with_none_description(self):
# Test creating a permission with None as description
mock_doc = MagicMock(spec=PermissionDoc)
self.mock_handler.create_permission = AsyncMock(return_value=mock_doc)
result = await self.service.create_permission('key', 'name', None)
assert result == mock_doc
self.mock_handler.create_permission.assert_awaited_once_with('key', 'name', None)
async def test_create_permission_with_empty_description(self):
# Test creating a permission with empty string as description
mock_doc = MagicMock(spec=PermissionDoc)
self.mock_handler.create_permission = AsyncMock(return_value=mock_doc)
result = await self.service.create_permission('key', 'name', '')
assert result == mock_doc
self.mock_handler.create_permission.assert_awaited_once_with('key', 'name', '')
async def test_create_permission_handler_unexpected_exception(self):
# Test handler raises unexpected exception during creation
self.mock_handler.create_permission = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.create_permission('key', 'name', 'desc')
async def test_update_permission_success(self):
# Test updating a permission successfully returns the expected PermissionDoc
mock_doc = MagicMock(spec=PermissionDoc)
self.mock_handler.update_permission = AsyncMock(return_value=mock_doc)
result = await self.service.update_permission('507f1f77bcf86cd799439011', 'key', 'name', 'desc')
assert result == mock_doc
self.mock_handler.update_permission.assert_awaited_once()
async def test_update_permission_fail(self):
# Test that an exception is raised if the handler fails to update a permission
self.mock_handler.update_permission = AsyncMock(side_effect=RequestValidationError('error'))
with pytest.raises(RequestValidationError):
await self.service.update_permission('507f1f77bcf86cd799439011', 'key', 'name', 'desc')
async def test_update_permission_with_invalid_id(self):
# Test updating a permission with invalid id (empty string)
self.mock_handler.update_permission = AsyncMock(side_effect=RequestValidationError('invalid id'))
with pytest.raises(RequestValidationError):
await self.service.update_permission('507f1f77bcf86cd799439011', 'key', 'name', 'desc')
async def test_update_permission_handler_unexpected_exception(self):
# Test handler raises unexpected exception during update
self.mock_handler.update_permission = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.update_permission('507f1f77bcf86cd799439011', 'key', 'name', 'desc')
async def test_update_permission_partial_args(self):
# Test updating a permission with only key provided
mock_doc = MagicMock(spec=PermissionDoc)
self.mock_handler.update_permission = AsyncMock(return_value=mock_doc)
result = await self.service.update_permission('507f1f77bcf86cd799439011', 'key', None, None)
assert result == mock_doc
self.mock_handler.update_permission.assert_awaited_once()
async def test_query_permissions_success(self):
# Test querying permissions returns a paginated result with correct items and meta
mock_doc = MagicMock(spec=PermissionDoc)
mock_doc.dict.return_value = {'permission_key': 'key', 'permission_name': 'name'}
self.mock_handler.query_permissions = AsyncMock(return_value=([mock_doc], 1))
result = await self.service.query_permissions('key', 'name', 1, 10)
assert result['items'] == [{'permission_key': 'key', 'permission_name': 'name'}]
assert result['total'] == 1
assert result['page'] == 1
assert result['page_size'] == 10
self.mock_handler.query_permissions.assert_awaited_once_with('key', 'name', 0, 10)
async def test_query_permissions_handler_exception(self):
# Test handler raises exception during query
self.mock_handler.query_permissions = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.query_permissions('key', 'name', 1, 10)
async def test_query_permissions_empty_result(self):
# Test query returns empty list
self.mock_handler.query_permissions = AsyncMock(return_value=([], 0))
result = await self.service.query_permissions('key', 'name', 1, 10)
assert result['items'] == []
assert result['total'] == 0
assert result['page'] == 1
assert result['page_size'] == 10
async def test_query_permissions_with_none_and_special_chars(self):
# Test query with None and special characters
self.mock_handler.query_permissions = AsyncMock(return_value=([], 0))
result = await self.service.query_permissions(None, None, 1, 10)
assert result['items'] == []
result2 = await self.service.query_permissions('!@#$', '', 1, 10)
assert result2['items'] == []
@pytest.mark.parametrize('page,page_size', [(0, 10), (1, 0), (0, 0), (-1, 10), (1, -1)])
async def test_query_permissions_invalid_page(self, page, page_size):
# Test that invalid page or page_size raises a validation error
with pytest.raises(RequestValidationError):
await self.service.query_permissions('key', 'name', page, page_size)
async def test_delete_permission_success(self):
# Test deleting a permission successfully returns None
self.mock_handler.delete_permission = AsyncMock(return_value=None)
result = await self.service.delete_permission('507f1f77bcf86cd799439011')
assert result is None
self.mock_handler.delete_permission.assert_awaited_once()
async def test_delete_permission_fail(self):
# Test that an exception is raised if the handler fails to delete a permission
self.mock_handler.delete_permission = AsyncMock(side_effect=RequestValidationError('error'))
with pytest.raises(RequestValidationError):
await self.service.delete_permission('507f1f77bcf86cd799439011')
async def test_delete_permission_handler_unexpected_exception(self):
# Test handler raises unexpected exception during delete
self.mock_handler.delete_permission = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.delete_permission('507f1f77bcf86cd799439011')

View File

@ -0,0 +1,172 @@
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from fastapi.exceptions import RequestValidationError
from backend.services.permission.role_service import RoleService
from backend.models.permission.models import RoleDoc
@pytest.fixture
def mock_role_handler():
# Fixture to patch RoleHandler for isolation and mocking
with patch('backend.services.permission.role_service.RoleHandler') as MockHandler:
yield MockHandler
@pytest.mark.asyncio
class TestRoleService:
@pytest.fixture(autouse=True)
def setup(self, mock_role_handler):
# Automatically set up a RoleService with a mocked handler for each test
self.mock_handler = mock_role_handler.return_value
self.service = RoleService()
self.service.role_handler = self.mock_handler
async def test_create_role_success(self):
# Test creating a role successfully returns the expected RoleDoc
mock_doc = MagicMock(spec=RoleDoc)
self.mock_handler.create_role = AsyncMock(return_value=mock_doc)
result = await self.service.create_role('key', 'name', 'desc', 1)
assert result == mock_doc
self.mock_handler.create_role.assert_awaited_once_with('key', 'name', 'desc', 1)
async def test_create_role_fail(self):
# Test that an exception is raised if the handler fails to create a role
self.mock_handler.create_role = AsyncMock(side_effect=RequestValidationError('error'))
with pytest.raises(RequestValidationError):
await self.service.create_role('key', 'name', 'desc', 1)
async def test_create_role_with_none_description(self):
# Test creating a role with None as description
mock_doc = MagicMock(spec=RoleDoc)
self.mock_handler.create_role = AsyncMock(return_value=mock_doc)
result = await self.service.create_role('key', 'name', None, 1)
assert result == mock_doc
self.mock_handler.create_role.assert_awaited_once_with('key', 'name', None, 1)
async def test_create_role_with_empty_description(self):
# Test creating a role with empty string as description
mock_doc = MagicMock(spec=RoleDoc)
self.mock_handler.create_role = AsyncMock(return_value=mock_doc)
result = await self.service.create_role('key', 'name', '', 1)
assert result == mock_doc
self.mock_handler.create_role.assert_awaited_once_with('key', 'name', '', 1)
async def test_create_role_handler_unexpected_exception(self):
# Test handler raises unexpected exception during creation
self.mock_handler.create_role = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.create_role('key', 'name', 'desc', 1)
async def test_update_role_success(self):
# Test updating a role successfully returns the expected RoleDoc
mock_doc = MagicMock(spec=RoleDoc)
self.mock_handler.update_role = AsyncMock(return_value=mock_doc)
result = await self.service.update_role('507f1f77bcf86cd799439011', 'key', 'name', 'desc', 1)
assert result == mock_doc
self.mock_handler.update_role.assert_awaited_once()
async def test_update_role_fail(self):
# Test that an exception is raised if the handler fails to update a role
self.mock_handler.update_role = AsyncMock(side_effect=RequestValidationError('error'))
with pytest.raises(RequestValidationError):
await self.service.update_role('507f1f77bcf86cd799439011', 'key', 'name', 'desc', 1)
async def test_update_role_handler_unexpected_exception(self):
# Test handler raises unexpected exception during update
self.mock_handler.update_role = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.update_role('507f1f77bcf86cd799439011', 'key', 'name', 'desc', 1)
async def test_update_role_partial_args(self):
# Test updating a role with only key provided
mock_doc = MagicMock(spec=RoleDoc)
self.mock_handler.update_role = AsyncMock(return_value=mock_doc)
result = await self.service.update_role('507f1f77bcf86cd799439011', 'key', None, None, 1)
assert result == mock_doc
self.mock_handler.update_role.assert_awaited_once()
async def test_query_roles_success(self):
# Test querying roles returns a paginated result with correct items and meta
mock_doc = MagicMock(spec=RoleDoc)
mock_doc.dict.return_value = {'role_key': 'key', 'role_name': 'name'}
self.mock_handler.query_roles = AsyncMock(return_value=([mock_doc], 1))
result = await self.service.query_roles('key', 'name', 1, 10)
assert result['items'] == [{'role_key': 'key', 'role_name': 'name'}]
assert result['total'] == 1
assert result['page'] == 1
assert result['page_size'] == 10
self.mock_handler.query_roles.assert_awaited_once_with('key', 'name', 0, 10)
async def test_query_roles_handler_exception(self):
# Test handler raises exception during query
self.mock_handler.query_roles = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.query_roles('key', 'name', 1, 10)
async def test_query_roles_empty_result(self):
# Test query returns empty list
self.mock_handler.query_roles = AsyncMock(return_value=([], 0))
result = await self.service.query_roles('key', 'name', 1, 10)
assert result['items'] == []
assert result['total'] == 0
assert result['page'] == 1
assert result['page_size'] == 10
async def test_query_roles_with_none_and_special_chars(self):
# Test query with None and special characters
self.mock_handler.query_roles = AsyncMock(return_value=([], 0))
result = await self.service.query_roles(None, None, 1, 10)
assert result['items'] == []
result2 = await self.service.query_roles('!@#$', '', 1, 10)
assert result2['items'] == []
@pytest.mark.parametrize('page,page_size', [(0, 10), (1, 0), (0, 0), (-1, 10), (1, -1)])
async def test_query_roles_invalid_page(self, page, page_size):
# Test that invalid page or page_size raises a validation error
with pytest.raises(RequestValidationError):
await self.service.query_roles('key', 'name', page, page_size)
async def test_assign_permissions_to_role_success(self):
# Test assigning permissions to a role returns the updated RoleDoc
mock_doc = MagicMock(spec=RoleDoc)
self.mock_handler.assign_permissions_to_role = AsyncMock(return_value=mock_doc)
result = await self.service.assign_permissions_to_role('507f1f77bcf86cd799439011', ['pid1', 'pid2'])
assert result == mock_doc
self.mock_handler.assign_permissions_to_role.assert_awaited_once()
async def test_assign_permissions_to_role_fail(self):
# Test that an exception is raised if the handler fails to assign permissions
self.mock_handler.assign_permissions_to_role = AsyncMock(side_effect=RequestValidationError('error'))
with pytest.raises(RequestValidationError):
await self.service.assign_permissions_to_role('507f1f77bcf86cd799439011', ['pid1', 'pid2'])
async def test_assign_permissions_to_role_empty_list(self):
# Test assigning permissions to a role with empty permission_ids list
mock_doc = MagicMock(spec=RoleDoc)
self.mock_handler.assign_permissions_to_role = AsyncMock(return_value=mock_doc)
result = await self.service.assign_permissions_to_role('507f1f77bcf86cd799439011', [])
assert result == mock_doc
self.mock_handler.assign_permissions_to_role.assert_awaited_once()
async def test_assign_permissions_to_role_handler_unexpected_exception(self):
# Test handler raises unexpected exception during assign
self.mock_handler.assign_permissions_to_role = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.assign_permissions_to_role('507f1f77bcf86cd799439011', ['pid1'])
async def test_delete_role_success(self):
# Test deleting a role successfully returns None
self.mock_handler.delete_role = AsyncMock(return_value=None)
result = await self.service.delete_role('507f1f77bcf86cd799439011')
assert result is None
self.mock_handler.delete_role.assert_awaited_once()
async def test_delete_role_fail(self):
# Test that an exception is raised if the handler fails to delete a role
self.mock_handler.delete_role = AsyncMock(side_effect=RequestValidationError('error'))
with pytest.raises(RequestValidationError):
await self.service.delete_role('507f1f77bcf86cd799439011')
async def test_delete_role_handler_unexpected_exception(self):
# Test handler raises unexpected exception during delete
self.mock_handler.delete_role = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.delete_role('507f1f77bcf86cd799439011')

View File

@ -0,0 +1,172 @@
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from backend.services.user.user_management_service import UserManagementService
from backend.models.user.models import UserAccountDoc
from backend.models.permission.models import UserRoleDoc
from backend.models.user.constants import NewUserMethod
from common.constants.region import UserRegion
@pytest.fixture
def mock_handlers():
with patch('backend.services.user.user_management_service.UserProfileHandler') as MockProfileHandler, \
patch('backend.services.user.user_management_service.UserAuthHandler') as MockAuthHandler, \
patch('backend.services.user.user_management_service.UserRoleHandler') as MockRoleHandler:
yield MockProfileHandler, MockAuthHandler, MockRoleHandler
@pytest.mark.asyncio
class TestUserManagementService:
@pytest.fixture(autouse=True)
def setup(self, mock_handlers):
# Automatically set up a UserManagementService with mocked handlers for each test
MockProfileHandler, MockAuthHandler, MockRoleHandler = mock_handlers
self.mock_profile_handler = MockProfileHandler.return_value
self.mock_auth_handler = MockAuthHandler.return_value
self.mock_role_handler = MockRoleHandler.return_value
self.service = UserManagementService()
self.service.user_profile_handler = self.mock_profile_handler
self.service.user_auth_handler = self.mock_auth_handler
self.service.user_role_handler = self.mock_role_handler
async def test_create_new_user_account_email(self):
# Test creating a new user account with EMAIL method
mock_account = MagicMock(spec=UserAccountDoc)
self.mock_profile_handler.create_new_user_account = AsyncMock(return_value=mock_account)
result = await self.service.create_new_user_account(NewUserMethod.EMAIL, UserRegion.ZH_CN)
assert result == mock_account
self.mock_profile_handler.create_new_user_account.assert_awaited_once()
async def test_create_new_user_account_mobile(self):
# Test creating a new user account with MOBILE method
mock_account = MagicMock(spec=UserAccountDoc)
self.mock_profile_handler.create_new_user_account = AsyncMock(return_value=mock_account)
result = await self.service.create_new_user_account(NewUserMethod.MOBILE, UserRegion.ZH_CN)
assert result == mock_account
self.mock_profile_handler.create_new_user_account.assert_awaited_once()
async def test_create_new_user_account_handler_exception(self):
# Test handler exception is propagated when creating a new user account
self.mock_profile_handler.create_new_user_account = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.create_new_user_account(NewUserMethod.EMAIL, UserRegion.ZH_CN)
async def test_initialize_new_user_data_email(self):
# Test initializing new user data with EMAIL method
self.mock_profile_handler.create_basic_profile = AsyncMock()
self.mock_auth_handler.save_email_auth_method = AsyncMock()
self.mock_profile_handler.create_provider_profile = AsyncMock()
result = await self.service.initialize_new_user_data('uid', NewUserMethod.EMAIL, email_address='a@b.com')
assert result is True
self.mock_profile_handler.create_basic_profile.assert_awaited_once()
self.mock_auth_handler.save_email_auth_method.assert_awaited_once()
self.mock_profile_handler.create_provider_profile.assert_awaited_once()
async def test_initialize_new_user_data_mobile(self):
# Test initializing new user data with MOBILE method
self.mock_profile_handler.create_basic_profile = AsyncMock()
self.mock_profile_handler.create_provider_profile = AsyncMock()
result = await self.service.initialize_new_user_data('uid', NewUserMethod.MOBILE, mobile_number='123456')
assert result is True
self.mock_profile_handler.create_basic_profile.assert_awaited_once()
self.mock_profile_handler.create_provider_profile.assert_awaited_once()
async def test_initialize_new_user_data_other(self):
# Test initializing new user data with unsupported method returns False
result = await self.service.initialize_new_user_data('uid', 'OTHER')
assert result is False
async def test_initialize_new_user_data_email_handler_exception(self):
# Test exception in create_basic_profile is propagated
self.mock_profile_handler.create_basic_profile = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.initialize_new_user_data('uid', NewUserMethod.EMAIL, email_address='a@b.com')
async def test_initialize_new_user_data_mobile_handler_exception(self):
# Test exception in create_basic_profile for MOBILE is propagated
self.mock_profile_handler.create_basic_profile = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.initialize_new_user_data('uid', NewUserMethod.MOBILE, mobile_number='123456')
async def test_initialize_new_user_data_email_missing_email(self):
# Test initializing new user data with EMAIL method but missing email_address
self.mock_profile_handler.create_basic_profile = AsyncMock()
self.mock_auth_handler.save_email_auth_method = AsyncMock()
self.mock_profile_handler.create_provider_profile = AsyncMock()
# Should still call with None, but may not be valid in real logic
result = await self.service.initialize_new_user_data('uid', NewUserMethod.EMAIL)
assert result is True
self.mock_profile_handler.create_basic_profile.assert_awaited_once()
self.mock_auth_handler.save_email_auth_method.assert_awaited_once()
self.mock_profile_handler.create_provider_profile.assert_awaited_once()
async def test_initialize_new_user_data_mobile_missing_mobile(self):
# Test initializing new user data with MOBILE method but missing mobile_number
self.mock_profile_handler.create_basic_profile = AsyncMock()
self.mock_profile_handler.create_provider_profile = AsyncMock()
result = await self.service.initialize_new_user_data('uid', NewUserMethod.MOBILE)
assert result is True
self.mock_profile_handler.create_basic_profile.assert_awaited_once()
self.mock_profile_handler.create_provider_profile.assert_awaited_once()
async def test_initialize_new_user_data_provider_profile_exception(self):
# Test exception in create_provider_profile is propagated
self.mock_profile_handler.create_basic_profile = AsyncMock()
self.mock_auth_handler.save_email_auth_method = AsyncMock()
self.mock_profile_handler.create_provider_profile = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.initialize_new_user_data('uid', NewUserMethod.EMAIL, email_address='a@b.com')
async def test_get_account_by_id(self):
# Test getting account by user id
mock_account = MagicMock(spec=UserAccountDoc)
self.mock_profile_handler.get_account_by_id = AsyncMock(return_value=mock_account)
result = await self.service.get_account_by_id('uid')
assert result == mock_account
self.mock_profile_handler.get_account_by_id.assert_awaited_once_with('uid')
async def test_get_account_by_id_none(self):
# Test getting account by user id returns None if not found
self.mock_profile_handler.get_account_by_id = AsyncMock(return_value=None)
result = await self.service.get_account_by_id('uid')
assert result is None
self.mock_profile_handler.get_account_by_id.assert_awaited_once_with('uid')
async def test_get_account_by_id_exception(self):
# Test exception in get_account_by_id is propagated
self.mock_profile_handler.get_account_by_id = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.get_account_by_id('uid')
async def test_assign_roles_to_user(self):
# Test assigning roles to user
mock_user_role = MagicMock(spec=UserRoleDoc)
self.mock_role_handler.assign_roles_to_user = AsyncMock(return_value=mock_user_role)
result = await self.service.assign_roles_to_user('uid', ['rid1', 'rid2'])
assert result == mock_user_role
self.mock_role_handler.assign_roles_to_user.assert_awaited_once_with('uid', ['rid1', 'rid2'])
async def test_assign_roles_to_user_handler_exception(self):
# Test exception in assign_roles_to_user is propagated
self.mock_role_handler.assign_roles_to_user = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.assign_roles_to_user('uid', ['rid1', 'rid2'])
async def test_get_role_and_permission_by_user_id(self):
# Test getting role and permission by user id
self.mock_role_handler.get_role_and_permission_by_user_id = AsyncMock(return_value=(['role1'], ['perm1']))
result = await self.service.get_role_and_permission_by_user_id('uid')
assert result == (['role1'], ['perm1'])
self.mock_role_handler.get_role_and_permission_by_user_id.assert_awaited_once_with('uid')
async def test_get_role_and_permission_by_user_id_empty(self):
# Test getting role and permission by user id returns empty lists if no roles/permissions
self.mock_role_handler.get_role_and_permission_by_user_id = AsyncMock(return_value=([], []))
result = await self.service.get_role_and_permission_by_user_id('uid')
assert result == ([], [])
self.mock_role_handler.get_role_and_permission_by_user_id.assert_awaited_once_with('uid')
async def test_get_role_and_permission_by_user_id_exception(self):
# Test exception in get_role_and_permission_by_user_id is propagated
self.mock_role_handler.get_role_and_permission_by_user_id = AsyncMock(side_effect=Exception('db error'))
with pytest.raises(Exception):
await self.service.get_role_and_permission_by_user_id('uid')

View File

@ -0,0 +1,76 @@
import re
from time import sleep
import requests
from faker import Faker
TEMPORARY_EMAIL_DOMAIN = "https://api.mail.cx/api/v1"
def generate_email() -> str:
fake = Faker('en_US')
while True:
name = fake.name().replace(' ', '_')
if len(name) <= 10:
break
return f"{name}@nqmo.com"
def get_auth_email_token() -> str:
url = TEMPORARY_EMAIL_DOMAIN + "/auth/authorize_token"
headers = {
'accept': 'application/json',
'Authorization': 'Bearer undefined',
}
response = requests.post(url, headers=headers)
return str(response.json())
def get_mail_id(address, token):
url = TEMPORARY_EMAIL_DOMAIN + f"/mailbox/{address}"
headers = {
'accept': 'application/json',
'Authorization': f'Bearer {token}',
}
response = requests.get(url, headers=headers)
body = response.json()
return body[0]['id'] if len(body) and len(body[0]['id']) > 0 else None
def get_auth_code(email):
# get token
token = get_auth_email_token()
print(f"token: {token}")
# Waiting for verification code email
id_ = None
for _ in range(30):
id_ = get_mail_id(email, token)
if id_ is not None:
break
sleep(1)
if id_ is None:
raise Exception(f"Could not get auth code for {email}")
# get code
url = TEMPORARY_EMAIL_DOMAIN + f'/mailbox/{email}/{id_}'
headers = {
'accept': 'application/json',
'Authorization': f'Bearer {token}',
}
response = requests.get(url, headers=headers)
print(response.json())
# Regular matching captcha, here the regular expression matching captcha is changed to its own
captcha = re.search(r'The auth code is:\s+(\d+)', response.json()['body']['html'])
if captcha:
print("code:", captcha.group(1))
else:
print("Unable to find verification code")
return captcha.group(1)
if __name__ == '__main__':
email = generate_email()
code = get_auth_code(email)
print(code)

View File

View File

@ -11,6 +11,7 @@ from webapi.providers import metrics
# from webapi.providers import scheduler
from webapi.providers import exception_handler
from webapi.providers import permission_initialize
from .freeleaps_app import FreeleapsApp
from common.config.app_settings import app_settings
@ -23,6 +24,7 @@ def create_app() -> FastAPI:
register(app, exception_handler)
register(app, database)
register(app, router)
register(app, permission_initialize)
# register(app, scheduler)
register(app, common)

View File

@ -1,7 +1,6 @@
from webapi.bootstrap.application import create_app
from webapi.config.site_settings import site_settings
from fastapi.responses import RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from typing import Any

View File

@ -1,3 +1,4 @@
from bson.errors import InvalidId
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from starlette.requests import Request
@ -26,6 +27,12 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
content={"error": str(exc)},
)
async def validation_error_exception_handler(request: Request, exc: InvalidId):
return JSONResponse(
status_code=HTTP_400_BAD_REQUEST,
content={"error": str(exc)},
)
async def exception_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
@ -36,4 +43,5 @@ async def exception_handler(request: Request, exc: Exception):
def register(app: FastAPI):
app.add_exception_handler(HTTPException, custom_http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(InvalidId, exception_handler)
app.add_exception_handler(Exception, exception_handler)

View File

@ -0,0 +1,42 @@
import logging
from backend.models.permission import PermissionDoc, RoleDoc
from backend.models.permission.constants import DefaultPermissionEnum, DefaultRoleEnum
def register(app):
# Configure logging for pymongo
logging.getLogger("init_admin_permission").setLevel(logging.INFO) # Suppress DEBUG logs
@app.on_event("startup")
async def init_admin_permission():
# Initialize permissions if not exist
default_permission_ids = []
for default_permission in \
[DefaultPermissionEnum.CHANGE_PERMISSIONS,
DefaultPermissionEnum.CHANGE_ROLES,
DefaultPermissionEnum.ASSIGN_ROLES]:
if not await PermissionDoc.find_one(
{str(PermissionDoc.permission_key): default_permission.value.permission_key}):
doc = await PermissionDoc(
permission_key=default_permission.value.permission_key,
permission_name=default_permission.value.permission_name,
description=default_permission.value.permission_description,
is_default=True,
).insert()
default_permission_ids.append(str(doc.id))
logging.info(f"default permissions initialized {default_permission_ids}")
# Initialize roles if not exist
default_role_ids = []
for default_role in [DefaultRoleEnum.ADMIN]:
if not await RoleDoc.find_one({str(RoleDoc.role_key): default_role.value.role_key}):
doc = await RoleDoc(
role_key=default_role.value.role_key,
role_name=default_role.value.role_name,
role_description=default_role.value.role_description,
permission_ids=default_permission_ids,
role_level=default_role.value.role_level,
is_default=True,
).insert()
default_role_ids.append(str(doc.id))
logging.info(f"default roles initialized {default_role_ids}")

View File

@ -2,9 +2,15 @@ from fastapi import APIRouter
from .signin import router as signin_router
from .tokens import router as token_router
from .auth import router as auth_router
from .permission import router as permission_router
from .role import router as role_router
from .user import router as user_router
api_router = APIRouter(prefix="/auth")
api_router.include_router(signin_router, tags=["user"])
api_router.include_router(signin_router, tags=["signin"])
api_router.include_router(token_router, tags=["token"])
api_router.include_router(auth_router, tags=["auth"])
api_router.include_router(permission_router, tags=["permission"])
api_router.include_router(role_router, tags=["role"])
api_router.include_router(user_router, tags=["user"])
websocket_router = APIRouter()

View File

@ -1,6 +1,6 @@
from backend.application.signin_hub import SignInHub
from pydantic import BaseModel
from common.token.token_manager import TokenManager
from common.token.token_manager import TokenManager, CurrentUser
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi import APIRouter, Depends, HTTPException
@ -28,14 +28,14 @@ class RequestIn(BaseModel):
)
async def send_email_code(
item: RequestIn,
current_user: dict = Depends(token_manager.get_current_user),
current_user: CurrentUser = Depends(token_manager.get_current_user),
):
user_id = current_user.get("id")
user_id = current_user.user_id
if not user_id:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
)
result = await SignInHub(user_id).send_email_code(item.sender_id, item.email)
result = await SignInHub().send_email_code(item.sender_id, item.email)
return JSONResponse(content=jsonable_encoder(result))

View File

@ -1,6 +1,6 @@
from backend.application.signin_hub import SignInHub
from pydantic import BaseModel
from common.token.token_manager import TokenManager
from common.token.token_manager import TokenManager, CurrentUser
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi import APIRouter, Depends, HTTPException
@ -27,14 +27,14 @@ class RequestIn(BaseModel):
)
async def send_email_code(
item: RequestIn,
current_user: dict = Depends(token_manager.get_current_user),
current_user: CurrentUser = Depends(token_manager.get_current_user),
):
user_id = current_user.get("id")
user_id = current_user.user_id
if not user_id:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
)
result = await SignInHub(user_id).send_mobile_code(item.email)
result = await SignInHub().send_mobile_code(item.email)
return JSONResponse(content=jsonable_encoder(result))

View File

@ -0,0 +1,13 @@
from fastapi import APIRouter
from .create_permission import router as cp_router
from .query_permission import router as qp_router
from .update_permission import router as up_router
from .delete_permission import router as delp_router
router = APIRouter()
router.include_router(cp_router, prefix="/permission", tags=["permission"])
router.include_router(qp_router, prefix="/permission", tags=["permission"])
router.include_router(up_router, prefix="/permission", tags=["permission"])
router.include_router(delp_router, prefix="/permission", tags=["permission"])

View File

@ -0,0 +1,44 @@
from datetime import datetime
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import Optional
from backend.models.permission.constants import DefaultPermissionEnum
from backend.services.permission.permission_service import PermissionService
from common.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager()
permission_service = PermissionService()
class CreatePermissionRequest(BaseModel):
permission_key: str
permission_name: str
description: Optional[str] = None
class PermissionResponse(BaseModel):
id: str
permission_key: str
permission_name: str
description: Optional[str] = None
created_at: datetime
updated_at: datetime
@router.post(
"/create",
response_model=PermissionResponse,
operation_id="create-permission",
summary="Create Permission",
description="Create a new permission."
)
async def create_permission(
req: CreatePermissionRequest,
_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key]))
) -> PermissionResponse:
doc = await permission_service.create_permission(req.permission_key, req.permission_name, req.description)
return PermissionResponse(**doc.dict())

View File

@ -0,0 +1,33 @@
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from backend.models.permission.constants import DefaultPermissionEnum
from backend.services.permission.permission_service import PermissionService
from common.token.token_manager import TokenManager
token_manager = TokenManager()
router = APIRouter()
permission_service = PermissionService()
class DeletePermissionRequest(BaseModel):
permission_id: str
class DeletePermissionResponse(BaseModel):
success: bool
@router.post(
"/delete",
response_model=DeletePermissionResponse,
operation_id="delete-permission",
summary="Delete Permission",
description="Delete a permission after checking if it is referenced by any role."
)
async def delete_permission(
req: DeletePermissionRequest,
_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key]))
) -> DeletePermissionResponse:
await permission_service.delete_permission(req.permission_id)
return DeletePermissionResponse(success=True)

View File

@ -0,0 +1,50 @@
from datetime import datetime
from fastapi import APIRouter
from pydantic import BaseModel
from typing import Optional, List
from backend.services.permission.permission_service import PermissionService
from common.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager()
permission_service = PermissionService()
class QueryPermissionRequest(BaseModel):
permission_key: Optional[str] = None
permission_name: Optional[str] = None
page: int = 1
page_size: int = 10
class PermissionResponse(BaseModel):
id: str
permission_key: str
permission_name: str
description: Optional[str] = None
created_at: datetime
updated_at: datetime
class QueryPermissionResponse(BaseModel):
items: List[PermissionResponse]
total: int
page: int
page_size: int
@router.post(
"/query",
response_model=QueryPermissionResponse,
operation_id="query-permission",
summary="Query Permissions (paginated)",
description="Query permissions with pagination and fuzzy search. Only Admin role allowed."
)
async def query_permissions(
req: QueryPermissionRequest,
) -> QueryPermissionResponse:
result = await permission_service.query_permissions(req.permission_key, req.permission_name, req.page, req.page_size)
items = [PermissionResponse(**item) for item in result["items"]]
return QueryPermissionResponse(
items=items,
total=result["total"],
page=result["page"],
page_size=result["page_size"]
)

View File

@ -0,0 +1,46 @@
from datetime import datetime
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import Optional
from backend.models.permission.constants import DefaultPermissionEnum
from backend.services.permission.permission_service import PermissionService
from common.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager()
permission_service = PermissionService()
class UpdatePermissionRequest(BaseModel):
permission_id: str
permission_key: str
permission_name: str
description: Optional[str] = None
class PermissionResponse(BaseModel):
id: str
permission_key: str
permission_name: str
description: Optional[str] = None
created_at: datetime
updated_at: datetime
@router.post(
"/update",
response_model=PermissionResponse,
operation_id="update-permission",
summary="Update Permission",
description="Update an existing permission by id. Only Admin role allowed."
)
async def update_permission(
req: UpdatePermissionRequest,
_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key]))
) -> PermissionResponse:
doc = await permission_service.update_permission(req.permission_id, req.permission_key, req.permission_name,
req.description)
return PermissionResponse(**doc.dict())

View File

@ -0,0 +1,14 @@
from fastapi import APIRouter
from .create_role import router as create_role_router
from .update_role import router as update_role_router
from .query_role import router as query_role_router
from .assign_permissions import router as assign_permissions_router
from .delete_role import router as delete_role_router
router = APIRouter()
router.include_router(create_role_router, prefix="/role", tags=["role"])
router.include_router(update_role_router, prefix="/role", tags=["role"])
router.include_router(query_role_router, prefix="/role", tags=["role"])
router.include_router(assign_permissions_router, prefix="/role", tags=["role"])
router.include_router(delete_role_router, prefix="/role", tags=["role"])

View File

@ -0,0 +1,41 @@
from datetime import datetime
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import List
from backend.models.permission.constants import DefaultPermissionEnum
from backend.services.permission.role_service import RoleService
from common.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager()
role_service = RoleService()
class AssignPermissionsRequest(BaseModel):
role_id: str
permission_ids: List[str]
class RoleResponse(BaseModel):
id: str
role_key: str
role_name: str
role_description: str
permission_ids: List[str]
role_level: int
created_at: datetime
updated_at: datetime
@router.post(
"/assign-permissions",
response_model=RoleResponse,
operation_id="assign-permissions-to-role",
summary="Assign Permissions to Role",
description="Assign permissions to a role by updating the permission_ids field."
)
async def assign_permissions_to_role(
req: AssignPermissionsRequest,
_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key]))
) -> RoleResponse:
doc = await role_service.assign_permissions_to_role(req.role_id, req.permission_ids)
return RoleResponse(**doc.dict())

View File

@ -0,0 +1,46 @@
from datetime import datetime
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import Optional, List
from backend.models.permission.constants import DefaultPermissionEnum
from backend.services.permission.role_service import RoleService
from common.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager()
role_service = RoleService()
class CreateRoleRequest(BaseModel):
role_key: str
role_name: str
role_description: Optional[str] = None
role_level: int
class RoleResponse(BaseModel):
id: str
role_key: str
role_name: str
role_description: Optional[str] = None
permission_ids: List[str]
role_level: int
created_at: datetime
updated_at: datetime
@router.post(
"/create",
response_model=RoleResponse,
operation_id="create-role",
summary="Create Role",
description="Create a new role."
)
async def create_role(
req: CreateRoleRequest,
_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key]))
) -> RoleResponse:
doc = await role_service.create_role(req.role_key, req.role_name, req.role_description, req.role_level)
return RoleResponse(**doc.dict())

View File

@ -0,0 +1,33 @@
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from backend.models.permission.constants import DefaultPermissionEnum
from backend.services.permission.role_service import RoleService
from common.token.token_manager import TokenManager
token_manager = TokenManager()
router = APIRouter()
role_service = RoleService()
class DeleteRoleRequest(BaseModel):
role_id: str
class DeleteRoleResponse(BaseModel):
success: bool
@router.post(
"/delete",
response_model=DeleteRoleResponse,
operation_id="delete-role",
summary="Delete Role",
description="Delete a role after checking if it is referenced by any user."
)
async def delete_role(
req: DeleteRoleRequest,
_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key]))
) -> DeleteRoleResponse:
await role_service.delete_role(req.role_id)
return DeleteRoleResponse(success=True)

View File

@ -0,0 +1,52 @@
from datetime import datetime
from fastapi import APIRouter
from pydantic import BaseModel
from typing import Optional, List
from backend.services.permission.role_service import RoleService
from common.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager()
role_service = RoleService()
class QueryRoleRequest(BaseModel):
role_key: Optional[str] = None
role_name: Optional[str] = None
page: int = 1
page_size: int = 10
class RoleResponse(BaseModel):
id: str
role_key: str
role_name: str
role_description: Optional[str] = None
permission_ids: List[str]
role_level: int
created_at: datetime
updated_at: datetime
class QueryRoleResponse(BaseModel):
items: List[RoleResponse]
total: int
page: int
page_size: int
@router.post(
"/query",
response_model=QueryRoleResponse,
operation_id="query-role",
summary="Query Roles (paginated)",
description="Query roles with pagination and fuzzy search. Only Admin role allowed."
)
async def query_roles(
req: QueryRoleRequest,
) -> QueryRoleResponse:
result = await role_service.query_roles(req.role_key, req.role_name, req.page, req.page_size)
items = [RoleResponse(**item) for item in result["items"]]
return QueryRoleResponse(
items=items,
total=result["total"],
page=result["page"],
page_size=result["page_size"]
)

View File

@ -0,0 +1,47 @@
from datetime import datetime
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import Optional, List
from backend.models.permission.constants import DefaultPermissionEnum
from backend.services.permission.role_service import RoleService
from common.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager()
role_service = RoleService()
class UpdateRoleRequest(BaseModel):
role_id: str
role_key: str
role_name: str
role_description: Optional[str] = None
role_level: int
class RoleResponse(BaseModel):
id: str
role_key: str
role_name: str
role_description: Optional[str] = None
permission_ids: List[str]
role_level: int
created_at: datetime
updated_at: datetime
@router.post(
"/update",
response_model=RoleResponse,
operation_id="update-role",
summary="Update Role",
description="Update an existing role by id. Only Admin role allowed."
)
async def update_role(
req: UpdateRoleRequest,
_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key]))
) -> RoleResponse:
doc = await role_service.update_role(req.role_id, req.role_key, req.role_name, req.role_description, req.role_level)
return RoleResponse(**doc.dict())

View File

@ -1,7 +1,7 @@
from backend.application.signin_hub import SignInHub
from pydantic import BaseModel
from fastapi import APIRouter
from common.token.token_manager import TokenManager
from common.token.token_manager import TokenManager, CurrentUser
from fastapi import APIRouter, Depends
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
@ -28,9 +28,9 @@ class RequestIn(BaseModel):
)
async def sign_out(
item: RequestIn,
current_user: dict = Depends(token_manager.get_current_user),
current_user: CurrentUser = Depends(token_manager.get_current_user),
):
user_id = current_user.get("id")
user_id = current_user.user_id
if not user_id:
raise HTTPException(

View File

@ -8,6 +8,7 @@ from fastapi.responses import JSONResponse
from pydantic import BaseModel
from backend.application.signin_hub import SignInHub
from common.constants.jwt_constants import USER_ROLE_NAMES, USER_PERMISSIONS
from common.token.token_manager import TokenManager
@ -58,6 +59,8 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
identity,
flid,
preferred_region,
user_role_names,
user_permission_keys
) = await SignInHub().signin_with_email_and_code(
item.email, item.code, item.host, item.time_zone
)
@ -67,7 +70,7 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
)
if signed_in and identity and adminstrative_role:
subject = {"id": identity, "role": adminstrative_role}
subject = {"id": identity, "role": adminstrative_role, USER_ROLE_NAMES: user_role_names, USER_PERMISSIONS: user_permission_keys}
access_token = token_manager.create_access_token(subject=subject)
refresh_token = token_manager.create_refresh_token(subject=subject)
expires_in = datetime.now(timezone.utc) + timedelta(
@ -85,6 +88,8 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
"identity": identity,
"expires_in": expires_in,
"role": adminstrative_role,
USER_ROLE_NAMES: user_role_names,
USER_PERMISSIONS: user_permission_keys,
"flid": flid,
"preferred_region": preferred_region,
}

View File

@ -10,6 +10,7 @@ from fastapi import Depends, HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from backend.application.signin_hub import SignInHub
from common.constants.jwt_constants import USER_ROLE_NAMES, USER_PERMISSIONS
from common.token.token_manager import TokenManager
router = APIRouter()
@ -57,6 +58,8 @@ async def signin_with_email_and_password(
adminstrative_role,
identity,
flid,
user_role_names,
user_permission_keys
) = await SignInHub().signin_with_email_and_password(item.email, item.password)
logging.debug(
@ -64,7 +67,7 @@ async def signin_with_email_and_password(
)
if signed_in and adminstrative_role and identity:
subject = {"id": identity, "role": adminstrative_role}
subject = {"id": identity, "role": adminstrative_role, USER_ROLE_NAMES: user_role_names, USER_PERMISSIONS: user_permission_keys}
access_token = token_manager.create_access_token(subject=subject)
refresh_token = token_manager.create_refresh_token(subject=subject)
expires_in = datetime.now(timezone.utc) + timedelta(
@ -82,6 +85,8 @@ async def signin_with_email_and_password(
"identity": identity,
"expires_in": expires_in,
"role": adminstrative_role,
USER_ROLE_NAMES: user_role_names,
USER_PERMISSIONS: user_permission_keys,
"flid": flid,
}
return JSONResponse(content=jsonable_encoder(result))

View File

@ -0,0 +1,6 @@
from fastapi import APIRouter
from .assign_roles import router as assign_role_router
router = APIRouter()
router.include_router(assign_role_router, prefix="/user", tags=["user"])

View File

@ -0,0 +1,37 @@
from fastapi import APIRouter
from fastapi.params import Depends
from pydantic import BaseModel
from typing import List, Optional
from backend.models.permission.constants import DefaultPermissionEnum
from backend.services.user.user_management_service import UserManagementService
from common.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager()
user_management_service = UserManagementService()
class AssignRolesRequest(BaseModel):
user_id: str
role_ids: List[str]
class UserRoleResponse(BaseModel):
user_id: str
role_ids: Optional[List[str]]
@router.post(
"/assign-roles",
response_model=UserRoleResponse,
operation_id="assign-roles-to-user",
summary="Assign Roles to User",
description="Assign roles to a user by updating or creating the UserRoleDoc."
)
async def assign_roles_to_user(
req: AssignRolesRequest,
_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.ASSIGN_ROLES.value.permission_key])),
) -> UserRoleResponse:
doc = await user_management_service.assign_roles_to_user(req.user_id, req.role_ids)
return UserRoleResponse(**doc.dict())