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 common.log.log_utils import log_entry_exit_async
from backend.business.signin_manager import SignInManager from backend.business.signin_manager import SignInManager
from backend.models.user.constants import UserLoginAction from backend.models.user.constants import UserLoginAction
@ -13,10 +17,28 @@ class SignInHub:
@log_entry_exit_async @log_entry_exit_async
async def signin_with_email_and_code( async def signin_with_email_and_code(
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC" 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[str], Optional[UserRegion], Optional[List[str]],
Optional[List[str]]]:
""" """
Interacts with the business layer to handle the sign-in process with email and code. 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( return await self.signin_manager.signin_with_email_and_code(
email=email, code=code, host=host, time_zone=time_zone email=email, code=code, host=host, time_zone=time_zone
@ -25,7 +47,7 @@ class SignInHub:
@log_entry_exit_async @log_entry_exit_async
async def signin_with_email_and_password( async def signin_with_email_and_password(
self, email: str, password: str 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. """Try to signin with email and password.
Args: Args:
@ -38,6 +60,8 @@ class SignInHub:
- Optional[int]: user role - Optional[int]: user role
- Optional[str]: user_id - Optional[str]: user_id
- Optional[str]: flid - 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( return await self.signin_manager.signin_with_email_and_password(
email=email, password=password email=email, password=password

View File

@ -1,9 +1,11 @@
import random import random
from typing import Tuple, Optional from typing import Tuple, Optional, List
from backend.services.auth.user_auth_service import UserAuthService from backend.services.auth.user_auth_service import UserAuthService
from common.constants.region import UserRegion
from common.utils.region import RegionHandler from common.utils.region import RegionHandler
from backend.models.user.constants import ( from backend.models.user.constants import (
UserLoginAction,
NewUserMethod, NewUserMethod,
) )
from backend.models.user.constants import UserLoginAction from backend.models.user.constants import UserLoginAction
@ -36,14 +38,15 @@ class SignInManager:
async def signin_with_email_and_code( async def signin_with_email_and_code(
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC" 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. """Try to signin with email and code.
create a new user account, if the email address has never been used before. create a new user account, if the email address has never been used before.
Args: Args:
email (str): email address email (str): email address
code (str): auth code to be verified 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: Returns:
[int, Optional[int], Optional[str], Optional[str]]: [int, Optional[int], Optional[str], Optional[str]]:
@ -51,6 +54,9 @@ class SignInManager:
- Optional[int]: user role - Optional[int]: user role
- Optional[str]: user_id - Optional[str]: user_id
- Optional[str]: flid - Optional[str]: flid
- Optional[str]: region
- Optional[str]: user role names
- Optional[str]: user permission keys
""" """
# check if the user account exist # check if the user account exist
user_id = await self.user_auth_service.get_user_id_by_email(email) 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 method=NewUserMethod.EMAIL, region=preferred_region
) )
) )
user_id = str(user_account.id) user_id = str(user_account.id)
await self.user_management_service.initialize_new_user_data( await self.user_management_service.initialize_new_user_data(
user_id=str(user_account.id), user_id=str(user_account.id),
@ -80,6 +85,9 @@ class SignInManager:
user_account = await self.user_management_service.get_account_by_id( user_account = await self.user_management_service.get_account_by_id(
user_id=user_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): if await self.user_auth_service.is_flid_reset_required(user_id):
return ( return (
UserLoginAction.REVIEW_AND_REVISE_FLID, UserLoginAction.REVIEW_AND_REVISE_FLID,
@ -87,10 +95,11 @@ class SignInManager:
user_id, user_id,
email.split("@")[0], email.split("@")[0],
preferred_region, preferred_region,
role_names,
permission_keys,
) )
user_flid = await self.user_auth_service.get_user_flid(user_id) user_flid = await self.user_auth_service.get_user_flid(user_id)
if await self.user_auth_service.is_password_reset_required(user_id): if await self.user_auth_service.is_password_reset_required(user_id):
return ( return (
UserLoginAction.NEW_USER_SET_PASSWORD, UserLoginAction.NEW_USER_SET_PASSWORD,
@ -98,25 +107,29 @@ class SignInManager:
user_id, user_id,
user_flid, user_flid,
preferred_region, preferred_region,
role_names,
permission_keys,
) )
return ( return (
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED, UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
user_account.user_role, user_account.user_role,
user_id, user_id,
user_flid, user_flid,
preferred_region, preferred_region,
role_names,
permission_keys,
) )
else: else:
await self.module_logger.log_warning( await self.module_logger.log_warning(
warning="The auth code is invalid.", warning="The auth code is invalid.",
properties={"email": email, "code": code}, 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( async def signin_with_email_and_password(
self, email: str, password: str 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 # check if the user account exist
user_id = await self.user_auth_service.get_user_id_by_email(email) user_id = await self.user_auth_service.get_user_id_by_email(email)
@ -126,16 +139,19 @@ class SignInManager:
if is_new_user: if is_new_user:
# cannot find the email address # 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: else:
if await self.user_auth_service.is_password_reset_required(user_id): if await self.user_auth_service.is_password_reset_required(user_id):
# password hasn't been set before, save password for the user # password hasn't been set before, save password for the user
return [ return (
UserLoginAction.NEW_USER_SET_PASSWORD, UserLoginAction.NEW_USER_SET_PASSWORD,
None, None,
None, None,
None, None,
] None,
None
)
else: else:
if await self.user_auth_service.verify_user_with_password( if await self.user_auth_service.verify_user_with_password(
user_id, password user_id, password
@ -143,33 +159,40 @@ class SignInManager:
user_account = await self.user_management_service.get_account_by_id( user_account = await self.user_management_service.get_account_by_id(
user_id=user_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): if await self.user_auth_service.is_flid_reset_required(user_id):
return [ return (
UserLoginAction.REVIEW_AND_REVISE_FLID, UserLoginAction.REVIEW_AND_REVISE_FLID,
user_account.user_role, user_account.user_role,
user_id, user_id,
email.split("@")[0], email.split("@")[0],
] role_names,
permission_keys,
)
user_flid = await self.user_auth_service.get_user_flid(user_id) user_flid = await self.user_auth_service.get_user_flid(user_id)
# password verification passed # password verification passed
return [ return (
UserLoginAction.USER_SIGNED_IN, UserLoginAction.USER_SIGNED_IN,
user_account.user_role, user_account.user_role,
user_id, user_id,
user_flid, user_flid,
] role_names,
permission_keys
)
else: else:
# ask user to input password again. # ask user to input password again.
# TODO: we need to limit times of user to input the wrong password # 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, UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
None, None,
None, None,
None, None,
] None,
None
)
async def update_new_user_flid( async def update_new_user_flid(
self, user_id: str, user_flid: str self, user_id: str, user_flid: str
@ -192,26 +215,26 @@ class SignInManager:
"code_depot_email": code_depot_email, "code_depot_email": code_depot_email,
}, },
) )
return [ return (
UserLoginAction.REVIEW_AND_REVISE_FLID, UserLoginAction.REVIEW_AND_REVISE_FLID,
"{}{}".format(user_flid, random.randint(100, 999)), "{}{}".format(user_flid, random.randint(100, 999)),
] )
await self.user_auth_service.update_flid(user_id, user_flid) await self.user_auth_service.update_flid(user_id, user_flid)
if await self.user_auth_service.is_password_reset_required(user_id): if await self.user_auth_service.is_password_reset_required(user_id):
return [ return (
UserLoginAction.NEW_USER_SET_PASSWORD, UserLoginAction.NEW_USER_SET_PASSWORD,
user_flid, user_flid,
] )
else: else:
return [ return (
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED, UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
user_flid, user_flid,
] )
else: else:
return [ return (
UserLoginAction.REVIEW_AND_REVISE_FLID, UserLoginAction.REVIEW_AND_REVISE_FLID,
"{}{}".format(user_flid, random.randint(100, 999)), "{}{}".format(user_flid, random.randint(100, 999)),
] )
async def try_signin_with_email(self, email: str, host: str) -> UserLoginAction: 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 """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 import user_models
from .user_profile import profile_models from .user_profile import profile_models
from .permission import permission_models
backend_models = [] backend_models = []
backend_models.extend(user_models) backend_models.extend(user_models)
backend_models.extend(profile_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): 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 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 common.log.module_logger import ModuleLogger
from typing import Optional from typing import Optional, List, Tuple
from backend.models.user.constants import ( from backend.models.user.constants import (
NewUserMethod, NewUserMethod,
@ -16,6 +17,9 @@ from backend.infra.auth.user_auth_handler import (
from backend.infra.user_profile.user_profile_handler import ( from backend.infra.user_profile.user_profile_handler import (
UserProfileHandler, UserProfileHandler,
) )
from backend.infra.permission.user_role_handler import (
UserRoleHandler,
)
from common.log.log_utils import log_entry_exit_async from common.log.log_utils import log_entry_exit_async
from common.constants.region import UserRegion from common.constants.region import UserRegion
@ -24,6 +28,7 @@ class UserManagementService:
def __init__(self) -> None: def __init__(self) -> None:
self.user_auth_handler = UserAuthHandler() self.user_auth_handler = UserAuthHandler()
self.user_profile_handler = UserProfileHandler() self.user_profile_handler = UserProfileHandler()
self.user_role_handler = UserRoleHandler()
self.module_logger = ModuleLogger(sender_id=UserManagementService) self.module_logger = ModuleLogger(sender_id=UserManagementService)
@log_entry_exit_async @log_entry_exit_async
@ -97,3 +102,16 @@ class UserManagementService:
async def get_account_by_id(self, user_id: str) -> UserAccountDoc: async def get_account_by_id(self, user_id: str) -> UserAccountDoc:
return await self.user_profile_handler.get_account_by_id(user_id) 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 from datetime import datetime, timedelta, timezone
import uuid import uuid
from typing import Dict from typing import Dict, List
from jose import jwt, JWTError from jose import jwt, JWTError
from common.config.app_settings import app_settings from common.config.app_settings import app_settings
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from starlette.status import HTTP_401_UNAUTHORIZED
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: class TokenManager:
@ -73,16 +96,35 @@ class TokenManager:
else: else:
raise ValueError("Invalid refresh token") raise ValueError("Invalid refresh token")
async def get_current_user( async def get_current_user(self, credentials: HTTPAuthorizationCredentials = Depends(security)) -> CurrentUser:
self, token: str = Depends(OAuth2PasswordBearer(tokenUrl="token"))
) -> Dict:
""" """
Extract and validate user information from the JWT token. Returns the current user object for the given credentials.
""" """
try: try:
payload = self.decode_token(token) # Decode JWT token payload = self.decode_token(credentials.credentials)
return payload user = payload.get("subject")
except ValueError: if not user or "id" not in user:
raise HTTPException( raise HTTPException(status_code=401, detail="Invalid authentication token")
status_code=HTTP_401_UNAUTHORIZED, detail="Invalid or expired 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

@ -14,3 +14,5 @@ pydantic-settings
python-jose python-jose
passlib[bcrypt] 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 scheduler
from webapi.providers import exception_handler from webapi.providers import exception_handler
from webapi.providers import permission_initialize
from .freeleaps_app import FreeleapsApp from .freeleaps_app import FreeleapsApp
from common.config.app_settings import app_settings from common.config.app_settings import app_settings
@ -23,6 +24,7 @@ def create_app() -> FastAPI:
register(app, exception_handler) register(app, exception_handler)
register(app, database) register(app, database)
register(app, router) register(app, router)
register(app, permission_initialize)
# register(app, scheduler) # register(app, scheduler)
register(app, common) register(app, common)

View File

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

View File

@ -1,3 +1,4 @@
from bson.errors import InvalidId
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from starlette.requests import Request from starlette.requests import Request
@ -26,6 +27,12 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
content={"error": str(exc)}, 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): async def exception_handler(request: Request, exc: Exception):
return JSONResponse( return JSONResponse(
status_code=HTTP_500_INTERNAL_SERVER_ERROR, status_code=HTTP_500_INTERNAL_SERVER_ERROR,
@ -36,4 +43,5 @@ async def exception_handler(request: Request, exc: Exception):
def register(app: FastAPI): def register(app: FastAPI):
app.add_exception_handler(HTTPException, custom_http_exception_handler) app.add_exception_handler(HTTPException, custom_http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler) app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(InvalidId, exception_handler)
app.add_exception_handler(Exception, 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 .signin import router as signin_router
from .tokens import router as token_router from .tokens import router as token_router
from .auth import router as auth_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 = 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(token_router, tags=["token"])
api_router.include_router(auth_router, tags=["auth"]) 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() websocket_router = APIRouter()

View File

@ -1,6 +1,6 @@
from backend.application.signin_hub import SignInHub from backend.application.signin_hub import SignInHub
from pydantic import BaseModel 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.encoders import jsonable_encoder
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
@ -28,14 +28,14 @@ class RequestIn(BaseModel):
) )
async def send_email_code( async def send_email_code(
item: RequestIn, 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: if not user_id:
raise HTTPException( raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials" 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)) return JSONResponse(content=jsonable_encoder(result))

View File

@ -1,6 +1,6 @@
from backend.application.signin_hub import SignInHub from backend.application.signin_hub import SignInHub
from pydantic import BaseModel 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.encoders import jsonable_encoder
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
@ -27,14 +27,14 @@ class RequestIn(BaseModel):
) )
async def send_email_code( async def send_email_code(
item: RequestIn, 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: if not user_id:
raise HTTPException( raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials" 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)) 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 backend.application.signin_hub import SignInHub
from pydantic import BaseModel from pydantic import BaseModel
from fastapi import APIRouter 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 import APIRouter, Depends
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@ -28,9 +28,9 @@ class RequestIn(BaseModel):
) )
async def sign_out( async def sign_out(
item: RequestIn, 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: if not user_id:
raise HTTPException( raise HTTPException(

View File

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

View File

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