Merge branch 'dev'
This commit is contained in:
commit
f7988ce1d7
0
apps/authentication/__init__.py
Normal file
0
apps/authentication/__init__.py
Normal file
0
apps/authentication/backend/__init__.py
Normal file
0
apps/authentication/backend/__init__.py
Normal file
0
apps/authentication/backend/annotation/__init__.py
Normal file
0
apps/authentication/backend/annotation/__init__.py
Normal file
@ -1,4 +1,8 @@
|
||||
from typing import Optional, Tuple
|
||||
from typing import Optional, Tuple, List
|
||||
|
||||
from backend.services.permission.permission_service import PermissionService
|
||||
from backend.services.permission.role_service import RoleService
|
||||
from common.constants.region import UserRegion
|
||||
from common.log.log_utils import log_entry_exit_async
|
||||
from backend.business.signin_manager import SignInManager
|
||||
from backend.models.user.constants import UserLoginAction
|
||||
@ -13,10 +17,28 @@ class SignInHub:
|
||||
|
||||
@log_entry_exit_async
|
||||
async def signin_with_email_and_code(
|
||||
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
||||
) -> Tuple[int, Optional[int], Optional[str], Optional[str]]:
|
||||
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
||||
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str], Optional[UserRegion], Optional[List[str]],
|
||||
Optional[List[str]]]:
|
||||
"""
|
||||
Interacts with the business layer to handle the sign-in process with email and code.
|
||||
Try to signin with email and code.
|
||||
create a new user account, if the email address has never been used before.
|
||||
|
||||
Args:
|
||||
email (str): email address
|
||||
code (str): auth code to be verified
|
||||
host (str): the host address by which the client access the frontend service
|
||||
time_zone (Optional[str]): time zone of the frontend service
|
||||
Returns:
|
||||
[int, Optional[int], Optional[str], Optional[str]]:
|
||||
- int: UserLoginAction
|
||||
- Optional[int]: user role
|
||||
- Optional[str]: user_id
|
||||
- Optional[str]: flid
|
||||
- Optional[str]: region
|
||||
- Optional[str]: user role names
|
||||
- Optional[str]: user permission keys
|
||||
"""
|
||||
return await self.signin_manager.signin_with_email_and_code(
|
||||
email=email, code=code, host=host, time_zone=time_zone
|
||||
@ -25,7 +47,7 @@ class SignInHub:
|
||||
@log_entry_exit_async
|
||||
async def signin_with_email_and_password(
|
||||
self, email: str, password: str
|
||||
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]:
|
||||
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str], Optional[List[str]], Optional[List[str]]]:
|
||||
"""Try to signin with email and password.
|
||||
|
||||
Args:
|
||||
@ -38,6 +60,8 @@ class SignInHub:
|
||||
- Optional[int]: user role
|
||||
- Optional[str]: user_id
|
||||
- Optional[str]: flid
|
||||
- Optional[List[str]]: user role names
|
||||
- Optional[List[str]]: user permission keys
|
||||
"""
|
||||
return await self.signin_manager.signin_with_email_and_password(
|
||||
email=email, password=password
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import random
|
||||
from typing import Tuple, Optional
|
||||
from typing import Tuple, Optional, List
|
||||
|
||||
|
||||
from backend.services.auth.user_auth_service import UserAuthService
|
||||
from common.constants.region import UserRegion
|
||||
from common.utils.region import RegionHandler
|
||||
from backend.models.user.constants import (
|
||||
UserLoginAction,
|
||||
NewUserMethod,
|
||||
)
|
||||
from backend.models.user.constants import UserLoginAction
|
||||
@ -36,14 +38,15 @@ class SignInManager:
|
||||
|
||||
async def signin_with_email_and_code(
|
||||
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
||||
) -> Tuple[int, Optional[int], Optional[str], Optional[str]]:
|
||||
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[UserRegion], Optional[str], Optional[List[str]], Optional[List[str]]]:
|
||||
"""Try to signin with email and code.
|
||||
create a new user account, if the email address has never been used before.
|
||||
|
||||
Args:
|
||||
email (str): email address
|
||||
code (str): auth code to be verified
|
||||
host (str): the host address by which the client access the frontend service
|
||||
host (str): the host address by which the client access the frontend service, for detecting UserRegion
|
||||
time_zone (str, optional): timezone of the frontend service
|
||||
|
||||
Returns:
|
||||
[int, Optional[int], Optional[str], Optional[str]]:
|
||||
@ -51,6 +54,9 @@ class SignInManager:
|
||||
- Optional[int]: user role
|
||||
- Optional[str]: user_id
|
||||
- Optional[str]: flid
|
||||
- Optional[str]: region
|
||||
- Optional[str]: user role names
|
||||
- Optional[str]: user permission keys
|
||||
"""
|
||||
# check if the user account exist
|
||||
user_id = await self.user_auth_service.get_user_id_by_email(email)
|
||||
@ -67,7 +73,6 @@ class SignInManager:
|
||||
method=NewUserMethod.EMAIL, region=preferred_region
|
||||
)
|
||||
)
|
||||
|
||||
user_id = str(user_account.id)
|
||||
await self.user_management_service.initialize_new_user_data(
|
||||
user_id=str(user_account.id),
|
||||
@ -80,6 +85,9 @@ class SignInManager:
|
||||
user_account = await self.user_management_service.get_account_by_id(
|
||||
user_id=user_id
|
||||
)
|
||||
role_names, permission_keys = await self.user_management_service.get_role_and_permission_by_user_id(
|
||||
user_id=user_id
|
||||
)
|
||||
if await self.user_auth_service.is_flid_reset_required(user_id):
|
||||
return (
|
||||
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
||||
@ -87,10 +95,11 @@ class SignInManager:
|
||||
user_id,
|
||||
email.split("@")[0],
|
||||
preferred_region,
|
||||
role_names,
|
||||
permission_keys,
|
||||
)
|
||||
|
||||
user_flid = await self.user_auth_service.get_user_flid(user_id)
|
||||
|
||||
if await self.user_auth_service.is_password_reset_required(user_id):
|
||||
return (
|
||||
UserLoginAction.NEW_USER_SET_PASSWORD,
|
||||
@ -98,25 +107,29 @@ class SignInManager:
|
||||
user_id,
|
||||
user_flid,
|
||||
preferred_region,
|
||||
role_names,
|
||||
permission_keys,
|
||||
)
|
||||
|
||||
return (
|
||||
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
user_flid,
|
||||
preferred_region,
|
||||
role_names,
|
||||
permission_keys,
|
||||
)
|
||||
else:
|
||||
await self.module_logger.log_warning(
|
||||
warning="The auth code is invalid.",
|
||||
properties={"email": email, "code": code},
|
||||
)
|
||||
return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None
|
||||
# TODO refactor this to reduce None
|
||||
return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None, None, None
|
||||
|
||||
async def signin_with_email_and_password(
|
||||
self, email: str, password: str
|
||||
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]:
|
||||
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str], Optional[List[str]], Optional[List[str]]]:
|
||||
|
||||
# check if the user account exist
|
||||
user_id = await self.user_auth_service.get_user_id_by_email(email)
|
||||
@ -126,16 +139,19 @@ class SignInManager:
|
||||
|
||||
if is_new_user:
|
||||
# cannot find the email address
|
||||
return [UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None]
|
||||
# TODO refactor this to reduce None
|
||||
return (UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None, None)
|
||||
else:
|
||||
if await self.user_auth_service.is_password_reset_required(user_id):
|
||||
# password hasn't been set before, save password for the user
|
||||
return [
|
||||
return (
|
||||
UserLoginAction.NEW_USER_SET_PASSWORD,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
None,
|
||||
None
|
||||
)
|
||||
else:
|
||||
if await self.user_auth_service.verify_user_with_password(
|
||||
user_id, password
|
||||
@ -143,33 +159,40 @@ class SignInManager:
|
||||
user_account = await self.user_management_service.get_account_by_id(
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
role_names, permission_keys = await self.user_management_service.get_role_and_permission_by_user_id(user_id)
|
||||
if await self.user_auth_service.is_flid_reset_required(user_id):
|
||||
return [
|
||||
return (
|
||||
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
email.split("@")[0],
|
||||
]
|
||||
role_names,
|
||||
permission_keys,
|
||||
)
|
||||
|
||||
user_flid = await self.user_auth_service.get_user_flid(user_id)
|
||||
|
||||
# password verification passed
|
||||
return [
|
||||
return (
|
||||
UserLoginAction.USER_SIGNED_IN,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
user_flid,
|
||||
]
|
||||
role_names,
|
||||
permission_keys
|
||||
)
|
||||
else:
|
||||
# ask user to input password again.
|
||||
# TODO: we need to limit times of user to input the wrong password
|
||||
return [
|
||||
# TODO refactor this to reduce None
|
||||
return (
|
||||
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
None,
|
||||
None
|
||||
)
|
||||
|
||||
async def update_new_user_flid(
|
||||
self, user_id: str, user_flid: str
|
||||
@ -192,26 +215,26 @@ class SignInManager:
|
||||
"code_depot_email": code_depot_email,
|
||||
},
|
||||
)
|
||||
return [
|
||||
return (
|
||||
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
||||
"{}{}".format(user_flid, random.randint(100, 999)),
|
||||
]
|
||||
)
|
||||
await self.user_auth_service.update_flid(user_id, user_flid)
|
||||
if await self.user_auth_service.is_password_reset_required(user_id):
|
||||
return [
|
||||
return (
|
||||
UserLoginAction.NEW_USER_SET_PASSWORD,
|
||||
user_flid,
|
||||
]
|
||||
)
|
||||
else:
|
||||
return [
|
||||
return (
|
||||
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
|
||||
user_flid,
|
||||
]
|
||||
)
|
||||
else:
|
||||
return [
|
||||
return (
|
||||
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
||||
"{}{}".format(user_flid, random.randint(100, 999)),
|
||||
]
|
||||
)
|
||||
|
||||
async def try_signin_with_email(self, email: str, host: str) -> UserLoginAction:
|
||||
"""try signin through email, generate auth code and send to the email address
|
||||
|
||||
0
apps/authentication/backend/infra/__init__.py
Normal file
0
apps/authentication/backend/infra/__init__.py
Normal 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()
|
||||
113
apps/authentication/backend/infra/permission/role_handler.py
Normal file
113
apps/authentication/backend/infra/permission/role_handler.py
Normal 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()
|
||||
@ -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
|
||||
@ -1,6 +1,8 @@
|
||||
from .user import user_models
|
||||
from .user_profile import profile_models
|
||||
from .permission import permission_models
|
||||
|
||||
backend_models = []
|
||||
backend_models.extend(user_models)
|
||||
backend_models.extend(profile_models)
|
||||
backend_models.extend(permission_models)
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
from .models import PermissionDoc, RoleDoc, UserRoleDoc
|
||||
|
||||
permission_models = [PermissionDoc, RoleDoc, UserRoleDoc]
|
||||
@ -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):
|
||||
|
||||
52
apps/authentication/backend/models/permission/models.py
Normal file
52
apps/authentication/backend/models/permission/models.py
Normal 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"
|
||||
]
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from beanie import Document
|
||||
|
||||
|
||||
@ -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))
|
||||
@ -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))
|
||||
@ -1,5 +1,6 @@
|
||||
from backend.models.permission.models import UserRoleDoc
|
||||
from common.log.module_logger import ModuleLogger
|
||||
from typing import Optional
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
from backend.models.user.constants import (
|
||||
NewUserMethod,
|
||||
@ -16,6 +17,9 @@ from backend.infra.auth.user_auth_handler import (
|
||||
from backend.infra.user_profile.user_profile_handler import (
|
||||
UserProfileHandler,
|
||||
)
|
||||
from backend.infra.permission.user_role_handler import (
|
||||
UserRoleHandler,
|
||||
)
|
||||
from common.log.log_utils import log_entry_exit_async
|
||||
from common.constants.region import UserRegion
|
||||
|
||||
@ -24,6 +28,7 @@ class UserManagementService:
|
||||
def __init__(self) -> None:
|
||||
self.user_auth_handler = UserAuthHandler()
|
||||
self.user_profile_handler = UserProfileHandler()
|
||||
self.user_role_handler = UserRoleHandler()
|
||||
self.module_logger = ModuleLogger(sender_id=UserManagementService)
|
||||
|
||||
@log_entry_exit_async
|
||||
@ -97,3 +102,16 @@ class UserManagementService:
|
||||
|
||||
async def get_account_by_id(self, user_id: str) -> UserAccountDoc:
|
||||
return await self.user_profile_handler.get_account_by_id(user_id)
|
||||
|
||||
async def assign_roles_to_user(self, user_id: str, role_ids: List[str]) -> UserRoleDoc:
|
||||
"""Assign roles to a user by updating or creating the UserRoleDoc"""
|
||||
return await self.user_role_handler.assign_roles_to_user(user_id, role_ids)
|
||||
|
||||
async def get_role_and_permission_by_user_id(self, user_id: str) -> Tuple[List[str], List[str]]:
|
||||
"""Get user role names and permission keys by user id
|
||||
Args:
|
||||
user_id (str): user id
|
||||
Returns:
|
||||
Tuple[List[str], List[str]]: user role names and permission keys
|
||||
"""
|
||||
return await self.user_role_handler.get_role_and_permission_by_user_id(user_id)
|
||||
|
||||
0
apps/authentication/common/__init__.py
Normal file
0
apps/authentication/common/__init__.py
Normal file
0
apps/authentication/common/config/__init__.py
Normal file
0
apps/authentication/common/config/__init__.py
Normal file
2
apps/authentication/common/constants/jwt_constants.py
Normal file
2
apps/authentication/common/constants/jwt_constants.py
Normal file
@ -0,0 +1,2 @@
|
||||
USER_ROLE_NAMES = "role_names"
|
||||
USER_PERMISSIONS = "user_permissions"
|
||||
@ -1,11 +1,34 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import uuid
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
from jose import jwt, JWTError
|
||||
from common.config.app_settings import app_settings
|
||||
from fastapi import Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from common.constants.jwt_constants import USER_ROLE_NAMES, USER_PERMISSIONS
|
||||
|
||||
|
||||
class CurrentUser:
|
||||
def __init__(self, user_id: str, user_role_names: List[str], user_permission_keys: List[str]):
|
||||
self.user_id = user_id
|
||||
self.user_role_names = user_role_names
|
||||
self.user_permission_keys = user_permission_keys
|
||||
|
||||
def has_all_permissions(self, permissions: List[str]) -> bool:
|
||||
"""Check if the user has all the specified permissions"""
|
||||
if not permissions:
|
||||
return True
|
||||
return all(p in self.user_permission_keys for p in permissions)
|
||||
|
||||
def has_any_permissions(self, permissions: List[str]) -> bool:
|
||||
"""Check if the user has at least one of the specified permissions"""
|
||||
if not permissions:
|
||||
return True
|
||||
return any(p in self.user_permission_keys for p in permissions)
|
||||
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
class TokenManager:
|
||||
@ -73,16 +96,35 @@ class TokenManager:
|
||||
else:
|
||||
raise ValueError("Invalid refresh token")
|
||||
|
||||
async def get_current_user(
|
||||
self, token: str = Depends(OAuth2PasswordBearer(tokenUrl="token"))
|
||||
) -> Dict:
|
||||
async def get_current_user(self, credentials: HTTPAuthorizationCredentials = Depends(security)) -> CurrentUser:
|
||||
"""
|
||||
Extract and validate user information from the JWT token.
|
||||
Returns the current user object for the given credentials.
|
||||
"""
|
||||
try:
|
||||
payload = self.decode_token(token) # Decode JWT token
|
||||
return payload
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED, detail="Invalid or expired token"
|
||||
)
|
||||
payload = self.decode_token(credentials.credentials)
|
||||
user = payload.get("subject")
|
||||
if not user or "id" not in user:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
||||
return CurrentUser(user.get("id"), user.get(USER_ROLE_NAMES), user.get(USER_PERMISSIONS))
|
||||
except JWTError:
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
||||
|
||||
def has_all_permissions(self, permissions: List[str]):
|
||||
"""Check if the user has all the specified permissions"""
|
||||
|
||||
def inner_dependency(current_user: CurrentUser = Depends(self.get_current_user)):
|
||||
if not current_user.has_all_permissions(permissions):
|
||||
raise HTTPException(status_code=403, detail="Not allowed")
|
||||
return True
|
||||
|
||||
return inner_dependency
|
||||
|
||||
def has_any_permissions(self, permissions: List[str]):
|
||||
"""Check if the user has at least one of the specified permissions"""
|
||||
|
||||
def inner_dependency(current_user: CurrentUser = Depends(self.get_current_user)):
|
||||
if not current_user.has_any_permissions(permissions):
|
||||
raise HTTPException(status_code=403, detail="Not allowed")
|
||||
return True
|
||||
|
||||
return inner_dependency
|
||||
|
||||
24
apps/authentication/local.env
Normal file
24
apps/authentication/local.env
Normal 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
|
||||
|
||||
@ -13,4 +13,6 @@ httpx
|
||||
pydantic-settings
|
||||
python-jose
|
||||
passlib[bcrypt]
|
||||
prometheus-fastapi-instrumentator==7.0.2
|
||||
prometheus-fastapi-instrumentator==7.0.2
|
||||
pytest==8.4.1
|
||||
pytest-asyncio==0.21.2
|
||||
0
apps/authentication/tests/__init__.py
Normal file
0
apps/authentication/tests/__init__.py
Normal file
0
apps/authentication/tests/api_tests/__init__.py
Normal file
0
apps/authentication/tests/api_tests/__init__.py
Normal file
86
apps/authentication/tests/api_tests/permission/README.md
Normal file
86
apps/authentication/tests/api_tests/permission/README.md
Normal 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.
|
||||
|
||||
---
|
||||
21
apps/authentication/tests/api_tests/permission/conftest.py
Normal file
21
apps/authentication/tests/api_tests/permission/conftest.py
Normal 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
|
||||
@ -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__])
|
||||
@ -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__])
|
||||
@ -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__])
|
||||
@ -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__])
|
||||
99
apps/authentication/tests/api_tests/role/README.md
Normal file
99
apps/authentication/tests/api_tests/role/README.md
Normal 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.
|
||||
21
apps/authentication/tests/api_tests/role/conftest.py
Normal file
21
apps/authentication/tests/api_tests/role/conftest.py
Normal 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
|
||||
@ -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__])
|
||||
159
apps/authentication/tests/api_tests/role/test_create_role.py
Normal file
159
apps/authentication/tests/api_tests/role/test_create_role.py
Normal 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__])
|
||||
91
apps/authentication/tests/api_tests/role/test_delete_role.py
Normal file
91
apps/authentication/tests/api_tests/role/test_delete_role.py
Normal 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__])
|
||||
58
apps/authentication/tests/api_tests/role/test_query_role.py
Normal file
58
apps/authentication/tests/api_tests/role/test_query_role.py
Normal 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
|
||||
233
apps/authentication/tests/api_tests/role/test_update_role.py
Normal file
233
apps/authentication/tests/api_tests/role/test_update_role.py
Normal 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__])
|
||||
37
apps/authentication/tests/api_tests/siginin/README.md
Normal file
37
apps/authentication/tests/api_tests/siginin/README.md
Normal 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.
|
||||
|
||||
---
|
||||
2
apps/authentication/tests/api_tests/siginin/config.py
Normal file
2
apps/authentication/tests/api_tests/siginin/config.py
Normal file
@ -0,0 +1,2 @@
|
||||
JWT_SECRET_KEY = "ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0"
|
||||
JWT_ALGORITHM = "HS256"
|
||||
10
apps/authentication/tests/api_tests/siginin/conftest.py
Normal file
10
apps/authentication/tests/api_tests/siginin/conftest.py
Normal 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
|
||||
@ -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__])
|
||||
45
apps/authentication/tests/api_tests/user/README.md
Normal file
45
apps/authentication/tests/api_tests/user/README.md
Normal 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.
|
||||
|
||||
---
|
||||
21
apps/authentication/tests/api_tests/user/conftest.py
Normal file
21
apps/authentication/tests/api_tests/user/conftest.py
Normal 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
|
||||
100
apps/authentication/tests/api_tests/user/test_assign_roles.py
Normal file
100
apps/authentication/tests/api_tests/user/test_assign_roles.py
Normal 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__])
|
||||
0
apps/authentication/tests/base/__init__.py
Normal file
0
apps/authentication/tests/base/__init__.py
Normal file
157
apps/authentication/tests/base/authentication_web.py
Normal file
157
apps/authentication/tests/base/authentication_web.py
Normal 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)
|
||||
5
apps/authentication/tests/base/config.py
Normal file
5
apps/authentication/tests/base/config.py
Normal file
@ -0,0 +1,5 @@
|
||||
# user with admin role
|
||||
USER_EMAIL = "XXXX"
|
||||
USER_PASSWORD = "XXXX"
|
||||
# authentication base url
|
||||
BASE_URL = "http://localhost:8103"
|
||||
10
apps/authentication/tests/conftest.py
Normal file
10
apps/authentication/tests/conftest.py
Normal 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
|
||||
28
apps/authentication/tests/role_manage_coverage_report.md
Normal file
28
apps/authentication/tests/role_manage_coverage_report.md
Normal 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.
|
||||
0
apps/authentication/tests/unit_tests/__init__.py
Normal file
0
apps/authentication/tests/unit_tests/__init__.py
Normal 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'))
|
||||
@ -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'))
|
||||
@ -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'}
|
||||
@ -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')
|
||||
@ -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')
|
||||
@ -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')
|
||||
0
apps/authentication/tests/util/__init__.py
Normal file
0
apps/authentication/tests/util/__init__.py
Normal file
76
apps/authentication/tests/util/temporary_email.py
Normal file
76
apps/authentication/tests/util/temporary_email.py
Normal 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)
|
||||
0
apps/authentication/webapi/__init__.py
Normal file
0
apps/authentication/webapi/__init__.py
Normal file
@ -11,6 +11,7 @@ from webapi.providers import metrics
|
||||
|
||||
# from webapi.providers import scheduler
|
||||
from webapi.providers import exception_handler
|
||||
from webapi.providers import permission_initialize
|
||||
from .freeleaps_app import FreeleapsApp
|
||||
from common.config.app_settings import app_settings
|
||||
|
||||
@ -23,6 +24,7 @@ def create_app() -> FastAPI:
|
||||
register(app, exception_handler)
|
||||
register(app, database)
|
||||
register(app, router)
|
||||
register(app, permission_initialize)
|
||||
# register(app, scheduler)
|
||||
register(app, common)
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from webapi.bootstrap.application import create_app
|
||||
from webapi.config.site_settings import site_settings
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
from typing import Any
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from bson.errors import InvalidId
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.requests import Request
|
||||
@ -26,6 +27,12 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
||||
content={"error": str(exc)},
|
||||
)
|
||||
|
||||
async def validation_error_exception_handler(request: Request, exc: InvalidId):
|
||||
return JSONResponse(
|
||||
status_code=HTTP_400_BAD_REQUEST,
|
||||
content={"error": str(exc)},
|
||||
)
|
||||
|
||||
async def exception_handler(request: Request, exc: Exception):
|
||||
return JSONResponse(
|
||||
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@ -36,4 +43,5 @@ async def exception_handler(request: Request, exc: Exception):
|
||||
def register(app: FastAPI):
|
||||
app.add_exception_handler(HTTPException, custom_http_exception_handler)
|
||||
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
||||
app.add_exception_handler(InvalidId, exception_handler)
|
||||
app.add_exception_handler(Exception, exception_handler)
|
||||
|
||||
@ -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}")
|
||||
@ -2,9 +2,15 @@ from fastapi import APIRouter
|
||||
from .signin import router as signin_router
|
||||
from .tokens import router as token_router
|
||||
from .auth import router as auth_router
|
||||
from .permission import router as permission_router
|
||||
from .role import router as role_router
|
||||
from .user import router as user_router
|
||||
|
||||
api_router = APIRouter(prefix="/auth")
|
||||
api_router.include_router(signin_router, tags=["user"])
|
||||
api_router.include_router(signin_router, tags=["signin"])
|
||||
api_router.include_router(token_router, tags=["token"])
|
||||
api_router.include_router(auth_router, tags=["auth"])
|
||||
api_router.include_router(permission_router, tags=["permission"])
|
||||
api_router.include_router(role_router, tags=["role"])
|
||||
api_router.include_router(user_router, tags=["user"])
|
||||
websocket_router = APIRouter()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from backend.application.signin_hub import SignInHub
|
||||
from pydantic import BaseModel
|
||||
from common.token.token_manager import TokenManager
|
||||
from common.token.token_manager import TokenManager, CurrentUser
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
@ -28,14 +28,14 @@ class RequestIn(BaseModel):
|
||||
)
|
||||
async def send_email_code(
|
||||
item: RequestIn,
|
||||
current_user: dict = Depends(token_manager.get_current_user),
|
||||
current_user: CurrentUser = Depends(token_manager.get_current_user),
|
||||
):
|
||||
user_id = current_user.get("id")
|
||||
user_id = current_user.user_id
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
|
||||
)
|
||||
|
||||
result = await SignInHub(user_id).send_email_code(item.sender_id, item.email)
|
||||
result = await SignInHub().send_email_code(item.sender_id, item.email)
|
||||
return JSONResponse(content=jsonable_encoder(result))
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from backend.application.signin_hub import SignInHub
|
||||
from pydantic import BaseModel
|
||||
from common.token.token_manager import TokenManager
|
||||
from common.token.token_manager import TokenManager, CurrentUser
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
@ -27,14 +27,14 @@ class RequestIn(BaseModel):
|
||||
)
|
||||
async def send_email_code(
|
||||
item: RequestIn,
|
||||
current_user: dict = Depends(token_manager.get_current_user),
|
||||
current_user: CurrentUser = Depends(token_manager.get_current_user),
|
||||
):
|
||||
user_id = current_user.get("id")
|
||||
user_id = current_user.user_id
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
|
||||
)
|
||||
|
||||
result = await SignInHub(user_id).send_mobile_code(item.email)
|
||||
result = await SignInHub().send_mobile_code(item.email)
|
||||
return JSONResponse(content=jsonable_encoder(result))
|
||||
|
||||
13
apps/authentication/webapi/routes/permission/__init__.py
Normal file
13
apps/authentication/webapi/routes/permission/__init__.py
Normal 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"])
|
||||
@ -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())
|
||||
@ -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)
|
||||
@ -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"]
|
||||
)
|
||||
@ -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())
|
||||
14
apps/authentication/webapi/routes/role/__init__.py
Normal file
14
apps/authentication/webapi/routes/role/__init__.py
Normal 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"])
|
||||
41
apps/authentication/webapi/routes/role/assign_permissions.py
Normal file
41
apps/authentication/webapi/routes/role/assign_permissions.py
Normal 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())
|
||||
46
apps/authentication/webapi/routes/role/create_role.py
Normal file
46
apps/authentication/webapi/routes/role/create_role.py
Normal 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())
|
||||
33
apps/authentication/webapi/routes/role/delete_role.py
Normal file
33
apps/authentication/webapi/routes/role/delete_role.py
Normal 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)
|
||||
52
apps/authentication/webapi/routes/role/query_role.py
Normal file
52
apps/authentication/webapi/routes/role/query_role.py
Normal 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"]
|
||||
)
|
||||
47
apps/authentication/webapi/routes/role/update_role.py
Normal file
47
apps/authentication/webapi/routes/role/update_role.py
Normal 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())
|
||||
@ -1,7 +1,7 @@
|
||||
from backend.application.signin_hub import SignInHub
|
||||
from pydantic import BaseModel
|
||||
from fastapi import APIRouter
|
||||
from common.token.token_manager import TokenManager
|
||||
from common.token.token_manager import TokenManager, CurrentUser
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
@ -28,9 +28,9 @@ class RequestIn(BaseModel):
|
||||
)
|
||||
async def sign_out(
|
||||
item: RequestIn,
|
||||
current_user: dict = Depends(token_manager.get_current_user),
|
||||
current_user: CurrentUser = Depends(token_manager.get_current_user),
|
||||
):
|
||||
user_id = current_user.get("id")
|
||||
user_id = current_user.user_id
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
|
||||
@ -8,6 +8,7 @@ from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.application.signin_hub import SignInHub
|
||||
from common.constants.jwt_constants import USER_ROLE_NAMES, USER_PERMISSIONS
|
||||
from common.token.token_manager import TokenManager
|
||||
|
||||
|
||||
@ -58,6 +59,8 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
|
||||
identity,
|
||||
flid,
|
||||
preferred_region,
|
||||
user_role_names,
|
||||
user_permission_keys
|
||||
) = await SignInHub().signin_with_email_and_code(
|
||||
item.email, item.code, item.host, item.time_zone
|
||||
)
|
||||
@ -67,7 +70,7 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
|
||||
)
|
||||
|
||||
if signed_in and identity and adminstrative_role:
|
||||
subject = {"id": identity, "role": adminstrative_role}
|
||||
subject = {"id": identity, "role": adminstrative_role, USER_ROLE_NAMES: user_role_names, USER_PERMISSIONS: user_permission_keys}
|
||||
access_token = token_manager.create_access_token(subject=subject)
|
||||
refresh_token = token_manager.create_refresh_token(subject=subject)
|
||||
expires_in = datetime.now(timezone.utc) + timedelta(
|
||||
@ -85,6 +88,8 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
|
||||
"identity": identity,
|
||||
"expires_in": expires_in,
|
||||
"role": adminstrative_role,
|
||||
USER_ROLE_NAMES: user_role_names,
|
||||
USER_PERMISSIONS: user_permission_keys,
|
||||
"flid": flid,
|
||||
"preferred_region": preferred_region,
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ from fastapi import Depends, HTTPException
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
|
||||
from backend.application.signin_hub import SignInHub
|
||||
from common.constants.jwt_constants import USER_ROLE_NAMES, USER_PERMISSIONS
|
||||
from common.token.token_manager import TokenManager
|
||||
|
||||
router = APIRouter()
|
||||
@ -57,6 +58,8 @@ async def signin_with_email_and_password(
|
||||
adminstrative_role,
|
||||
identity,
|
||||
flid,
|
||||
user_role_names,
|
||||
user_permission_keys
|
||||
) = await SignInHub().signin_with_email_and_password(item.email, item.password)
|
||||
|
||||
logging.debug(
|
||||
@ -64,7 +67,7 @@ async def signin_with_email_and_password(
|
||||
)
|
||||
|
||||
if signed_in and adminstrative_role and identity:
|
||||
subject = {"id": identity, "role": adminstrative_role}
|
||||
subject = {"id": identity, "role": adminstrative_role, USER_ROLE_NAMES: user_role_names, USER_PERMISSIONS: user_permission_keys}
|
||||
access_token = token_manager.create_access_token(subject=subject)
|
||||
refresh_token = token_manager.create_refresh_token(subject=subject)
|
||||
expires_in = datetime.now(timezone.utc) + timedelta(
|
||||
@ -82,6 +85,8 @@ async def signin_with_email_and_password(
|
||||
"identity": identity,
|
||||
"expires_in": expires_in,
|
||||
"role": adminstrative_role,
|
||||
USER_ROLE_NAMES: user_role_names,
|
||||
USER_PERMISSIONS: user_permission_keys,
|
||||
"flid": flid,
|
||||
}
|
||||
return JSONResponse(content=jsonable_encoder(result))
|
||||
|
||||
6
apps/authentication/webapi/routes/user/__init__.py
Normal file
6
apps/authentication/webapi/routes/user/__init__.py
Normal 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"])
|
||||
37
apps/authentication/webapi/routes/user/assign_roles.py
Normal file
37
apps/authentication/webapi/routes/user/assign_roles.py
Normal 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())
|
||||
Loading…
Reference in New Issue
Block a user