[OND211-2329]: Updated team API's and added create group tests.
This commit is contained in:
parent
5af04fdf45
commit
3e7d57b208
5 changed files with 870 additions and 19 deletions
|
|
@ -17,7 +17,7 @@
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
|
|
@ -81,7 +81,13 @@ def encrypt_password(password: str) -> str:
|
||||||
return base64.b64encode(encrypted_password).decode()
|
return base64.b64encode(encrypted_password).decode()
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register() -> Optional[str]:
|
||||||
|
"""Register the test user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Authorization token if registration succeeded and user was logged in,
|
||||||
|
None if user already exists
|
||||||
|
"""
|
||||||
url: str = HOST_ADDRESS + f"/{VERSION}/user/register"
|
url: str = HOST_ADDRESS + f"/{VERSION}/user/register"
|
||||||
name: str = "qa"
|
name: str = "qa"
|
||||||
# Encrypt the plain password "123" before sending
|
# Encrypt the plain password "123" before sending
|
||||||
|
|
@ -89,9 +95,21 @@ def register():
|
||||||
encrypted_password: str = encrypt_password(plain_password)
|
encrypted_password: str = encrypt_password(plain_password)
|
||||||
register_data = {"email": EMAIL, "nickname": name, "password": encrypted_password}
|
register_data = {"email": EMAIL, "nickname": name, "password": encrypted_password}
|
||||||
res: requests.Response = requests.post(url=url, json=register_data)
|
res: requests.Response = requests.post(url=url, json=register_data)
|
||||||
res: Dict[str, Any] = res.json()
|
res_json: Dict[str, Any] = res.json()
|
||||||
if res.get("code") != 0 and "has already registered" not in res.get("message"):
|
if res_json.get("code") != 0 and "has already registered" not in res_json.get("message"):
|
||||||
raise Exception(res.get("message"))
|
print(f"Registration failed with code {res_json.get('code')}: {res_json.get('message')}")
|
||||||
|
raise Exception(res_json.get("message"))
|
||||||
|
elif res_json.get("code") == 0:
|
||||||
|
print(f"Registration successful for {EMAIL}")
|
||||||
|
# Registration endpoint logs user in and returns auth token
|
||||||
|
auth_token: str = res.headers.get("Authorization", "")
|
||||||
|
if auth_token:
|
||||||
|
print(f"Received auth token from registration")
|
||||||
|
return auth_token
|
||||||
|
else:
|
||||||
|
print(f"Warning: No auth token in registration response")
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def login():
|
def login():
|
||||||
|
|
@ -108,6 +126,102 @@ def login():
|
||||||
return auth
|
return auth
|
||||||
|
|
||||||
|
|
||||||
|
def delete_user_from_db(email: str) -> bool:
|
||||||
|
"""Delete a user directly from the database using SQL.
|
||||||
|
|
||||||
|
This is a helper function for cleanup when a user exists with wrong password.
|
||||||
|
Uses direct SQL to avoid import conflicts with test helper modules.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
email: Email of the user to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
project_root = os.path.abspath(os.path.join(current_dir, "..", ".."))
|
||||||
|
|
||||||
|
# Create a temporary Python script to hard delete the user
|
||||||
|
delete_script = f"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '{project_root}')
|
||||||
|
|
||||||
|
# Remove test directories from path to avoid conflicts
|
||||||
|
test_paths = [p for p in sys.path if 'test/testcases' in p or 'testcases' in p]
|
||||||
|
for p in test_paths:
|
||||||
|
if p in sys.path:
|
||||||
|
sys.path.remove(p)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from api.db.db_models import DB, User, Tenant, UserTenant, File
|
||||||
|
|
||||||
|
users = list(User.select().where(User.email == '{email}'))
|
||||||
|
if users:
|
||||||
|
with DB.atomic():
|
||||||
|
for user in users:
|
||||||
|
user_id = user.id
|
||||||
|
# Hard delete related records and user
|
||||||
|
try:
|
||||||
|
# Delete user-tenant relationships
|
||||||
|
UserTenant.delete().where(UserTenant.user_id == user_id).execute()
|
||||||
|
UserTenant.delete().where(UserTenant.tenant_id == user_id).execute()
|
||||||
|
|
||||||
|
# Delete files owned by user
|
||||||
|
File.delete().where(File.created_by == user_id).execute()
|
||||||
|
File.delete().where(File.tenant_id == user_id).execute()
|
||||||
|
|
||||||
|
# Delete tenant
|
||||||
|
Tenant.delete().where(Tenant.id == user_id).execute()
|
||||||
|
|
||||||
|
# Finally delete user
|
||||||
|
User.delete().where(User.id == user_id).execute()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning during cleanup: {{e}}")
|
||||||
|
print(f"DELETED_USER:{email}")
|
||||||
|
else:
|
||||||
|
print(f"USER_NOT_FOUND:{email}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR:{{e}}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Run the delete script in a subprocess to avoid import conflicts
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-c", delete_script],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
output = result.stdout + result.stderr
|
||||||
|
|
||||||
|
if "DELETED_USER:" in output:
|
||||||
|
print(f"Successfully deleted user {email} from database")
|
||||||
|
return True
|
||||||
|
elif "USER_NOT_FOUND:" in output:
|
||||||
|
print(f"User {email} not found in database")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f"Failed to delete user from database")
|
||||||
|
if output:
|
||||||
|
print(f"Output: {output}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print(f"Timeout while trying to delete user from database")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to delete user from database: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def login_as_user(email: str, password: str) -> RAGFlowWebApiAuth:
|
def login_as_user(email: str, password: str) -> RAGFlowWebApiAuth:
|
||||||
"""Login as a user and return authentication object.
|
"""Login as a user and return authentication object.
|
||||||
|
|
||||||
|
|
@ -140,21 +254,88 @@ def login_as_user(email: str, password: str) -> RAGFlowWebApiAuth:
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def auth():
|
def auth():
|
||||||
try:
|
"""Session fixture to authenticate test user.
|
||||||
register()
|
|
||||||
except Exception as e:
|
This fixture tries to login with the test user. If login fails because
|
||||||
print(e)
|
the user doesn't exist, it registers the user first. If the user exists
|
||||||
|
with a different password, it provides instructions to fix the issue.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Authentication token
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If authentication fails
|
||||||
|
"""
|
||||||
|
# First, try to login (user might already exist with correct password)
|
||||||
try:
|
try:
|
||||||
auth: str = login()
|
auth: str = login()
|
||||||
|
print(f"Successfully logged in as {EMAIL}")
|
||||||
return auth
|
return auth
|
||||||
except Exception as e:
|
except Exception as login_error:
|
||||||
error_msg = str(e)
|
login_error_msg = str(login_error)
|
||||||
if "Email and password do not match" in error_msg:
|
|
||||||
|
# If user doesn't exist, try to register
|
||||||
|
if "is not registered" in login_error_msg:
|
||||||
|
print(f"User {EMAIL} not found, attempting to register...")
|
||||||
|
try:
|
||||||
|
auth_token: Optional[str] = register()
|
||||||
|
if auth_token:
|
||||||
|
print(f"Successfully registered and logged in as {EMAIL}")
|
||||||
|
return auth_token
|
||||||
|
else:
|
||||||
|
# Try login if register didn't return auth token
|
||||||
|
auth: str = login()
|
||||||
|
print(f"Successfully registered and logged in as {EMAIL}")
|
||||||
|
return auth
|
||||||
|
except Exception as register_error:
|
||||||
|
raise Exception(
|
||||||
|
f"Failed to register user {EMAIL}: {register_error}"
|
||||||
|
) from register_error
|
||||||
|
|
||||||
|
# If user exists but password doesn't match
|
||||||
|
elif "Email and password do not match" in login_error_msg:
|
||||||
|
print(f"User {EMAIL} exists but password doesn't match. Attempting to delete and recreate...")
|
||||||
|
|
||||||
|
# Try to delete the user from database directly
|
||||||
|
if delete_user_from_db(EMAIL):
|
||||||
|
# Delay to ensure deletion is committed to database
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
# Now try to register and login
|
||||||
|
try:
|
||||||
|
print(f"Attempting to register user {EMAIL}...")
|
||||||
|
auth_token: Optional[str] = register()
|
||||||
|
if auth_token:
|
||||||
|
print(f"Successfully recreated user {EMAIL} with correct password")
|
||||||
|
return auth_token
|
||||||
|
else:
|
||||||
|
# Try login if register didn't return auth token
|
||||||
|
print(f"Registration completed, now attempting login...")
|
||||||
|
auth: str = login()
|
||||||
|
print(f"Successfully recreated user {EMAIL} with correct password")
|
||||||
|
return auth
|
||||||
|
except Exception as recreate_error:
|
||||||
|
recreate_error_msg = str(recreate_error)
|
||||||
|
print(f"Recreation failed: {recreate_error_msg}")
|
||||||
|
raise Exception(
|
||||||
|
f"Failed to recreate user after deletion: {recreate_error_msg}"
|
||||||
|
) from recreate_error
|
||||||
|
else:
|
||||||
|
# If database deletion failed, provide instructions
|
||||||
|
raise Exception(
|
||||||
|
f"Login failed: User {EMAIL} exists but password doesn't match.\n"
|
||||||
|
f"Automatic cleanup failed. To fix this issue:\n"
|
||||||
|
f"1. Manually delete the user from the database, OR\n"
|
||||||
|
f"2. Reset the password in the database to '123', OR\n"
|
||||||
|
f"3. Update EMAIL in configs.py to use a different test user\n"
|
||||||
|
f"Original error: {login_error_msg}"
|
||||||
|
) from login_error
|
||||||
|
|
||||||
|
# Other login errors
|
||||||
|
else:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"Login failed: User {EMAIL} exists but password doesn't match. "
|
f"Login failed with unexpected error: {login_error_msg}"
|
||||||
f"Please ensure the user has the correct password or delete the user first."
|
) from login_error
|
||||||
) from e
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
|
|
|
||||||
|
|
@ -477,8 +477,36 @@ def remove_users_from_team(
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|
||||||
|
|
||||||
|
def accept_team_invitation(
|
||||||
|
auth: Union[AuthBase, str, None],
|
||||||
|
tenant_id: str,
|
||||||
|
*,
|
||||||
|
headers: Dict[str, str] = HEADERS,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Accept a team invitation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auth: Authentication object (AuthBase subclass), token string, or None.
|
||||||
|
tenant_id: The tenant/team ID to accept invitation for.
|
||||||
|
headers: Optional HTTP headers. Defaults to HEADERS.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON response as a dictionary containing the acceptance result.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
requests.RequestException: If the HTTP request fails.
|
||||||
|
"""
|
||||||
|
url: str = f"{HOST_ADDRESS}{TEAM_API_URL}/update-request/{tenant_id}"
|
||||||
|
payload: Dict[str, bool] = {"accept": True}
|
||||||
|
res: requests.Response = requests.put(
|
||||||
|
url=url, headers=headers, auth=auth, json=payload
|
||||||
|
)
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
|
||||||
# DEPARTMENT MANAGEMENT
|
# DEPARTMENT MANAGEMENT
|
||||||
DEPARTMENT_API_URL: str = f"/{VERSION}/department"
|
DEPARTMENT_API_URL: str = f"/{VERSION}/department"
|
||||||
|
GROUP_API_URL: str = f"/{VERSION}/group"
|
||||||
|
|
||||||
|
|
||||||
def create_department(
|
def create_department(
|
||||||
|
|
@ -641,3 +669,30 @@ def list_department_members(
|
||||||
url=url, headers=headers, auth=auth
|
url=url, headers=headers, auth=auth
|
||||||
)
|
)
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|
||||||
|
|
||||||
|
# GROUP MANAGEMENT
|
||||||
|
def create_group(
|
||||||
|
auth: Union[AuthBase, str, None],
|
||||||
|
payload: Optional[Dict[str, Any]] = None,
|
||||||
|
*,
|
||||||
|
headers: Dict[str, str] = HEADERS,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Create a new group.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auth: Authentication object (AuthBase subclass), token string, or None.
|
||||||
|
payload: Optional JSON payload containing group data (e.g., name, tenant_id, description).
|
||||||
|
headers: Optional HTTP headers. Defaults to HEADERS.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON response as a dictionary containing the created group data.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
requests.RequestException: If the HTTP request fails.
|
||||||
|
"""
|
||||||
|
url: str = f"{HOST_ADDRESS}{GROUP_API_URL}/create"
|
||||||
|
res: requests.Response = requests.post(
|
||||||
|
url=url, headers=headers, auth=auth, json=payload
|
||||||
|
)
|
||||||
|
return res.json()
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,14 @@
|
||||||
#
|
#
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from common import (
|
from common import (
|
||||||
|
accept_team_invitation,
|
||||||
add_department_members,
|
add_department_members,
|
||||||
add_users_to_team,
|
add_users_to_team,
|
||||||
create_department,
|
create_department,
|
||||||
|
|
@ -187,6 +189,20 @@ class TestAddMembers:
|
||||||
assert set(res["data"]["added"]) == set(user_ids)
|
assert set(res["data"]["added"]) == set(user_ids)
|
||||||
assert len(res["data"]["failed"]) == 0
|
assert len(res["data"]["failed"]) == 0
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_add_member_missing_request_body(
|
||||||
|
self,
|
||||||
|
web_api_auth: RAGFlowWebApiAuth,
|
||||||
|
test_department: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test adding members without request body."""
|
||||||
|
res: dict[str, Any] = add_department_members(
|
||||||
|
web_api_auth, test_department["id"], None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res["code"] == 101
|
||||||
|
assert "required" in res["message"].lower() or "body" in res["message"].lower()
|
||||||
|
|
||||||
@pytest.mark.p1
|
@pytest.mark.p1
|
||||||
def test_add_member_missing_user_ids(
|
def test_add_member_missing_user_ids(
|
||||||
self,
|
self,
|
||||||
|
|
@ -200,7 +216,7 @@ class TestAddMembers:
|
||||||
)
|
)
|
||||||
|
|
||||||
assert res["code"] == 101
|
assert res["code"] == 101
|
||||||
assert "user_ids" in res["message"].lower() or "required" in res[
|
assert "user_ids" in res["message"].lower() or "non-empty array" in res[
|
||||||
"message"
|
"message"
|
||||||
].lower()
|
].lower()
|
||||||
|
|
||||||
|
|
@ -412,6 +428,9 @@ class TestAddMembers:
|
||||||
add_team_res: dict[str, Any] = add_users_to_team(web_api_auth, tenant_id, add_team_payload)
|
add_team_res: dict[str, Any] = add_users_to_team(web_api_auth, tenant_id, add_team_payload)
|
||||||
assert add_team_res["code"] == 0, "Failed to add user to team"
|
assert add_team_res["code"] == 0, "Failed to add user to team"
|
||||||
|
|
||||||
|
# Wait a bit for user creation and team addition to be committed
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
# Create another user to add to the department
|
# Create another user to add to the department
|
||||||
another_user_email: str = f"anotheruser_{uuid.uuid4().hex[:8]}@example.com"
|
another_user_email: str = f"anotheruser_{uuid.uuid4().hex[:8]}@example.com"
|
||||||
another_user_password: str = "TestPassword123!"
|
another_user_password: str = "TestPassword123!"
|
||||||
|
|
@ -431,8 +450,23 @@ class TestAddMembers:
|
||||||
)
|
)
|
||||||
assert add_another_team_res["code"] == 0, "Failed to add another user to team"
|
assert add_another_team_res["code"] == 0, "Failed to add another user to team"
|
||||||
|
|
||||||
|
# Wait a bit for team addition to be committed
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
# Login as the normal user (not admin/owner)
|
# Login as the normal user (not admin/owner)
|
||||||
normal_user_auth: RAGFlowWebApiAuth = login_as_user(normal_user_email, normal_user_password)
|
# Users with INVITE role should still be able to login
|
||||||
|
try:
|
||||||
|
normal_user_auth: RAGFlowWebApiAuth = login_as_user(normal_user_email, normal_user_password)
|
||||||
|
except Exception as e:
|
||||||
|
pytest.skip(f"Failed to login as normal user: {e}")
|
||||||
|
|
||||||
|
# Accept the invitation to join the team
|
||||||
|
accept_res: dict[str, Any] = accept_team_invitation(normal_user_auth, tenant_id)
|
||||||
|
if accept_res["code"] != 0:
|
||||||
|
pytest.skip(f"Failed to accept invitation: {accept_res.get('message', 'Unknown error')}")
|
||||||
|
|
||||||
|
# Wait a bit for invitation acceptance to be committed
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
# Try to add a member as the normal user (should fail)
|
# Try to add a member as the normal user (should fail)
|
||||||
add_payload: dict[str, list[str]] = {"user_ids": [another_user_id]}
|
add_payload: dict[str, list[str]] = {"user_ids": [another_user_id]}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,14 @@
|
||||||
#
|
#
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from common import (
|
from common import (
|
||||||
|
accept_team_invitation,
|
||||||
add_department_members,
|
add_department_members,
|
||||||
add_users_to_team,
|
add_users_to_team,
|
||||||
create_department,
|
create_department,
|
||||||
|
|
@ -337,6 +339,9 @@ class TestRemoveMember:
|
||||||
add_team_res: dict[str, Any] = add_users_to_team(web_api_auth, tenant_id, add_team_payload)
|
add_team_res: dict[str, Any] = add_users_to_team(web_api_auth, tenant_id, add_team_payload)
|
||||||
assert add_team_res["code"] == 0, "Failed to add user to team"
|
assert add_team_res["code"] == 0, "Failed to add user to team"
|
||||||
|
|
||||||
|
# Wait a bit for user creation and team addition to be committed
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
# Add another user to the department (so we have someone to remove)
|
# Add another user to the department (so we have someone to remove)
|
||||||
another_user_email: str = f"anotheruser_{uuid.uuid4().hex[:8]}@example.com"
|
another_user_email: str = f"anotheruser_{uuid.uuid4().hex[:8]}@example.com"
|
||||||
another_user_password: str = "TestPassword123!"
|
another_user_password: str = "TestPassword123!"
|
||||||
|
|
@ -356,6 +361,9 @@ class TestRemoveMember:
|
||||||
)
|
)
|
||||||
assert add_another_team_res["code"] == 0, "Failed to add another user to team"
|
assert add_another_team_res["code"] == 0, "Failed to add another user to team"
|
||||||
|
|
||||||
|
# Wait a bit for team addition to be committed
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
# Add another user to the department (as owner/admin)
|
# Add another user to the department (as owner/admin)
|
||||||
add_dept_payload: dict[str, list[str]] = {"user_ids": [another_user_id]}
|
add_dept_payload: dict[str, list[str]] = {"user_ids": [another_user_id]}
|
||||||
add_dept_res: dict[str, Any] = add_department_members(
|
add_dept_res: dict[str, Any] = add_department_members(
|
||||||
|
|
@ -364,7 +372,19 @@ class TestRemoveMember:
|
||||||
assert add_dept_res["code"] == 0, "Failed to add user to department"
|
assert add_dept_res["code"] == 0, "Failed to add user to department"
|
||||||
|
|
||||||
# Login as the normal user (not admin/owner)
|
# Login as the normal user (not admin/owner)
|
||||||
normal_user_auth: RAGFlowWebApiAuth = login_as_user(normal_user_email, normal_user_password)
|
# Users with INVITE role should still be able to login
|
||||||
|
try:
|
||||||
|
normal_user_auth: RAGFlowWebApiAuth = login_as_user(normal_user_email, normal_user_password)
|
||||||
|
except Exception as e:
|
||||||
|
pytest.skip(f"Failed to login as normal user: {e}")
|
||||||
|
|
||||||
|
# Accept the invitation to join the team
|
||||||
|
accept_res: dict[str, Any] = accept_team_invitation(normal_user_auth, tenant_id)
|
||||||
|
if accept_res["code"] != 0:
|
||||||
|
pytest.skip(f"Failed to accept invitation: {accept_res.get('message', 'Unknown error')}")
|
||||||
|
|
||||||
|
# Wait a bit for invitation acceptance to be committed
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
# Try to remove a member as the normal user (should fail)
|
# Try to remove a member as the normal user (should fail)
|
||||||
remove_res: dict[str, Any] = remove_department_member(
|
remove_res: dict[str, Any] = remove_department_member(
|
||||||
|
|
@ -398,3 +418,79 @@ class TestRemoveMember:
|
||||||
assert len(add_res["data"]["added"]) == 1
|
assert len(add_res["data"]["added"]) == 1
|
||||||
assert add_res["data"]["added"][0] == user_id
|
assert add_res["data"]["added"][0] == user_id
|
||||||
|
|
||||||
|
@pytest.mark.p2
|
||||||
|
def test_remove_multiple_members_sequentially(
|
||||||
|
self,
|
||||||
|
web_api_auth: RAGFlowWebApiAuth,
|
||||||
|
test_department: dict[str, Any],
|
||||||
|
test_team: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test removing multiple members sequentially."""
|
||||||
|
# Create multiple users
|
||||||
|
users = []
|
||||||
|
for i in range(3):
|
||||||
|
email = f"testuser{i}_{uuid.uuid4().hex[:8]}@example.com"
|
||||||
|
user_payload: dict[str, str] = {
|
||||||
|
"email": email,
|
||||||
|
"password": "TestPassword123!",
|
||||||
|
"nickname": f"Test User {i}",
|
||||||
|
}
|
||||||
|
user_res: dict[str, Any] = create_user(web_api_auth, user_payload)
|
||||||
|
if user_res["code"] == 0:
|
||||||
|
users.append({"email": email, "id": user_res["data"]["id"]})
|
||||||
|
|
||||||
|
if len(users) < 2:
|
||||||
|
pytest.skip("Need at least 2 test users")
|
||||||
|
|
||||||
|
# Add users to team
|
||||||
|
for user in users:
|
||||||
|
add_team_payload: dict[str, list[str]] = {"users": [user["email"]]}
|
||||||
|
add_users_to_team(web_api_auth, test_team["id"], add_team_payload)
|
||||||
|
|
||||||
|
# Add users to department
|
||||||
|
user_ids: list[str] = [user["id"] for user in users]
|
||||||
|
add_dept_payload: dict[str, list[str]] = {"user_ids": user_ids}
|
||||||
|
add_res: dict[str, Any] = add_department_members(
|
||||||
|
web_api_auth, test_department["id"], add_dept_payload
|
||||||
|
)
|
||||||
|
assert add_res["code"] == 0
|
||||||
|
assert len(add_res["data"]["added"]) == len(users)
|
||||||
|
|
||||||
|
# Remove all users sequentially
|
||||||
|
for user in users:
|
||||||
|
remove_res: dict[str, Any] = remove_department_member(
|
||||||
|
web_api_auth, test_department["id"], user["id"]
|
||||||
|
)
|
||||||
|
assert remove_res["code"] == 0
|
||||||
|
assert "removed" in remove_res["message"].lower() or "success" in remove_res["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p2
|
||||||
|
def test_remove_member_empty_string_user_id(
|
||||||
|
self,
|
||||||
|
web_api_auth: RAGFlowWebApiAuth,
|
||||||
|
test_department: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test removing a member with empty string user ID."""
|
||||||
|
res: dict[str, Any] = remove_department_member(
|
||||||
|
web_api_auth, test_department["id"], ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Empty string user ID may return 100 (EXCEPTION_ERROR) or 102 (DATA_ERROR)
|
||||||
|
assert res["code"] in [100, 102]
|
||||||
|
assert "not a member" in res["message"].lower() or "not found" in res["message"].lower() or "error" in res["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p2
|
||||||
|
def test_remove_member_special_characters_user_id(
|
||||||
|
self,
|
||||||
|
web_api_auth: RAGFlowWebApiAuth,
|
||||||
|
test_department: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test removing a member with special characters in user ID."""
|
||||||
|
invalid_user_id: str = "user@123_!@#$%"
|
||||||
|
res: dict[str, Any] = remove_department_member(
|
||||||
|
web_api_auth, test_department["id"], invalid_user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res["code"] == 102
|
||||||
|
assert "not a member" in res["message"].lower() or "not found" in res["message"].lower()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,485 @@
|
||||||
|
#
|
||||||
|
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from Cryptodome.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
|
||||||
|
from Cryptodome.PublicKey import RSA
|
||||||
|
|
||||||
|
from common import create_group, create_team, create_user, login_as_user
|
||||||
|
from configs import INVALID_API_TOKEN
|
||||||
|
from libs.auth import RAGFlowWebApiAuth
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helper Functions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_password(password: str) -> str:
|
||||||
|
"""Encrypt password using RSA for API calls.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
password: Plain text password to encrypt.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Encrypted password as a base64-encoded string.
|
||||||
|
"""
|
||||||
|
current_dir: str = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
project_base: str = os.path.abspath(os.path.join(current_dir, "..", "..", "..", ".."))
|
||||||
|
file_path: str = os.path.join(project_base, "conf", "public.pem")
|
||||||
|
|
||||||
|
with open(file_path, encoding="utf-8") as pem_file:
|
||||||
|
rsa_key: RSA.RsaKey = RSA.import_key(pem_file.read(), passphrase="Welcome")
|
||||||
|
|
||||||
|
cipher: Cipher_pkcs1_v1_5.PKCS115_Cipher = Cipher_pkcs1_v1_5.new(rsa_key)
|
||||||
|
password_base64: str = base64.b64encode(password.encode()).decode()
|
||||||
|
encrypted_password: bytes = cipher.encrypt(password_base64.encode())
|
||||||
|
return base64.b64encode(encrypted_password).decode()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test Classes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
class TestAuthorization:
|
||||||
|
"""Tests for authentication behavior during group creation."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("invalid_auth", "expected_code", "expected_message"),
|
||||||
|
[
|
||||||
|
# Endpoint now requires @login_required (JWT token auth)
|
||||||
|
(None, 401, "Unauthorized"),
|
||||||
|
(RAGFlowWebApiAuth(INVALID_API_TOKEN), 401, "Unauthorized"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_invalid_auth(
|
||||||
|
self,
|
||||||
|
invalid_auth: RAGFlowWebApiAuth | None,
|
||||||
|
expected_code: int,
|
||||||
|
expected_message: str,
|
||||||
|
web_api_auth: RAGFlowWebApiAuth,
|
||||||
|
) -> None:
|
||||||
|
"""Test group creation with invalid or missing authentication."""
|
||||||
|
# First create a team to use as tenant_id
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
if team_res["code"] != 0:
|
||||||
|
pytest.skip("Team creation failed, skipping auth test")
|
||||||
|
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Try to create group with invalid auth
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": "Test Group Auth",
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res: dict[str, Any] = create_group(invalid_auth, group_payload)
|
||||||
|
assert res["code"] == expected_code, res
|
||||||
|
if expected_message:
|
||||||
|
assert expected_message in res["message"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
class TestGroupCreate:
|
||||||
|
"""Comprehensive tests for group creation API."""
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_group_with_name_and_tenant_id(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group with name and tenant_id."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Create group
|
||||||
|
group_name: str = f"Test Group {uuid.uuid4().hex[:8]}"
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": group_name,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
assert res["code"] == 0, res
|
||||||
|
assert "data" in res
|
||||||
|
assert res["data"]["name"] == group_name
|
||||||
|
assert res["data"]["tenant_id"] == tenant_id
|
||||||
|
assert "id" in res["data"]
|
||||||
|
assert "created successfully" in res["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_group_with_description(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group with description."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Create group with description
|
||||||
|
group_name: str = f"Test Group {uuid.uuid4().hex[:8]}"
|
||||||
|
description: str = "This is a test group description"
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": group_name,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
assert res["code"] == 0, res
|
||||||
|
assert res["data"]["name"] == group_name
|
||||||
|
assert res["data"]["description"] == description
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_group_missing_name(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group without name."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Try to create group without name
|
||||||
|
group_payload: dict[str, str] = {"tenant_id": tenant_id}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
assert res["code"] == 101
|
||||||
|
assert "name" in res["message"].lower() or "required" in res[
|
||||||
|
"message"
|
||||||
|
].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_group_empty_name(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group with empty name."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Try to create group with empty name
|
||||||
|
group_payload: dict[str, str] = {"name": "", "tenant_id": tenant_id}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
assert res["code"] == 101
|
||||||
|
assert "name" in res["message"].lower() or "empty" in res[
|
||||||
|
"message"
|
||||||
|
].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_group_missing_tenant_id(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group without tenant_id."""
|
||||||
|
# Try to create group without tenant_id
|
||||||
|
group_payload: dict[str, str] = {"name": "Test Group"}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
assert res["code"] == 101
|
||||||
|
assert "tenant_id" in res["message"].lower() or "required" in res[
|
||||||
|
"message"
|
||||||
|
].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_group_invalid_tenant_id(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group with non-existent tenant_id."""
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": "Test Group Invalid Tenant",
|
||||||
|
"tenant_id": "non_existent_tenant_id_12345",
|
||||||
|
}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
# Permission check happens before tenant existence check,
|
||||||
|
# so invalid tenant_id results in permission error (108) not data error (102)
|
||||||
|
assert res["code"] == 108
|
||||||
|
assert "only team owners or admins" in res["message"].lower() or "permission" in res[
|
||||||
|
"message"
|
||||||
|
].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_group_name_too_long(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group with name exceeding 128 characters."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Try to create group with name too long
|
||||||
|
# Note: The API validates name length (max 128), so it should return an error
|
||||||
|
long_name: str = "A" * 129
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": long_name,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
# API validates length, so it should return an argument error
|
||||||
|
assert res["code"] == 101
|
||||||
|
assert "128" in res["message"] or "characters" in res["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_group_not_team_owner_or_admin(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group when user is not team owner or admin."""
|
||||||
|
# Create a team with the main user (owner)
|
||||||
|
team_name: str = f"Owner Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Create a second user with encrypted password (now supported!)
|
||||||
|
other_user_email: str = f"test_{uuid.uuid4().hex[:8]}@example.com"
|
||||||
|
other_user_password: str = "test123"
|
||||||
|
encrypted_password: str = encrypt_password(other_user_password)
|
||||||
|
|
||||||
|
user_payload: dict[str, str] = {
|
||||||
|
"nickname": "Other User",
|
||||||
|
"email": other_user_email,
|
||||||
|
"password": encrypted_password, # Now works with encryption!
|
||||||
|
}
|
||||||
|
user_res: dict[str, Any] = create_user(web_api_auth, user_payload)
|
||||||
|
assert user_res["code"] == 0, user_res
|
||||||
|
|
||||||
|
# Small delay to ensure user is fully created
|
||||||
|
import time
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Login as the other user
|
||||||
|
other_user_auth: RAGFlowWebApiAuth = login_as_user(
|
||||||
|
other_user_email, other_user_password
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try to create a group in the owner's team as the other user
|
||||||
|
group_name: str = f"Group {uuid.uuid4().hex[:8]}"
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": group_name,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res: dict[str, Any] = create_group(other_user_auth, group_payload)
|
||||||
|
|
||||||
|
# Should fail - user is not the team owner or admin
|
||||||
|
assert res["code"] != 0, (
|
||||||
|
"Non-owner/non-admin should not be able to create groups in another user's team"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify it's a permission-related error
|
||||||
|
# Common permission error codes: 108 (Permission denied), 403 (Forbidden), 104 (Permission Error), 102 (Authentication Error)
|
||||||
|
assert res["code"] in [108, 403, 104, 102], (
|
||||||
|
f"Expected permission error code (108, 403, 104, or 102), got: {res}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the error message indicates permission issue
|
||||||
|
assert "owner" in res["message"].lower() or "admin" in res["message"].lower() or "permission" in res["message"].lower(), (
|
||||||
|
f"Error message should indicate permission issue, got: {res['message']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_group_response_structure(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test that group creation returns the expected response structure."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Create group
|
||||||
|
group_name: str = f"Test Group Structure {uuid.uuid4().hex[:8]}"
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": group_name,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
assert res["code"] == 0
|
||||||
|
assert "data" in res
|
||||||
|
assert isinstance(res["data"], dict)
|
||||||
|
assert "id" in res["data"]
|
||||||
|
assert "name" in res["data"]
|
||||||
|
assert "tenant_id" in res["data"]
|
||||||
|
assert res["data"]["name"] == group_name
|
||||||
|
assert res["data"]["tenant_id"] == tenant_id
|
||||||
|
assert "message" in res
|
||||||
|
assert "created successfully" in res["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_multiple_groups_same_team(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating multiple groups for the same team."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Create first group
|
||||||
|
group_name_1: str = f"Group 1 {uuid.uuid4().hex[:8]}"
|
||||||
|
group_payload_1: dict[str, str] = {
|
||||||
|
"name": group_name_1,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res1: dict[str, Any] = create_group(web_api_auth, group_payload_1)
|
||||||
|
assert res1["code"] == 0, res1
|
||||||
|
group_id_1: str = res1["data"]["id"]
|
||||||
|
|
||||||
|
# Create second group
|
||||||
|
group_name_2: str = f"Group 2 {uuid.uuid4().hex[:8]}"
|
||||||
|
group_payload_2: dict[str, str] = {
|
||||||
|
"name": group_name_2,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res2: dict[str, Any] = create_group(web_api_auth, group_payload_2)
|
||||||
|
assert res2["code"] == 0, res2
|
||||||
|
group_id_2: str = res2["data"]["id"]
|
||||||
|
|
||||||
|
# Verify groups are different
|
||||||
|
assert group_id_1 != group_id_2
|
||||||
|
assert res1["data"]["name"] == group_name_1
|
||||||
|
assert res2["data"]["name"] == group_name_2
|
||||||
|
assert res1["data"]["tenant_id"] == tenant_id
|
||||||
|
assert res2["data"]["tenant_id"] == tenant_id
|
||||||
|
|
||||||
|
@pytest.mark.p1
|
||||||
|
def test_create_group_duplicate_name_same_tenant(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group with duplicate name in the same tenant."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Create first group
|
||||||
|
group_name: str = f"Duplicate Group {uuid.uuid4().hex[:8]}"
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": group_name,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res1: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
assert res1["code"] == 0, res1
|
||||||
|
|
||||||
|
# Try to create another group with the same name in the same tenant
|
||||||
|
res2: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
assert res2["code"] == 102 # DATA_ERROR
|
||||||
|
assert "already exists" in res2["message"].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p2
|
||||||
|
def test_create_group_with_whitespace_name(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group with whitespace-only name."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Try to create group with whitespace-only name
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": " ",
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
# Should fail validation
|
||||||
|
assert res["code"] == 101
|
||||||
|
assert "name" in res["message"].lower() or "empty" in res[
|
||||||
|
"message"
|
||||||
|
].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p2
|
||||||
|
def test_create_group_special_characters_in_name(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group with special characters in name."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Create group with special characters
|
||||||
|
group_name: str = f"Group-{uuid.uuid4().hex[:8]}_Test!"
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": group_name,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
# Should succeed if special chars are allowed
|
||||||
|
assert res["code"] in (0, 101)
|
||||||
|
|
||||||
|
@pytest.mark.p2
|
||||||
|
def test_create_group_empty_payload(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group with empty payload."""
|
||||||
|
group_payload: dict[str, Any] = {}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
assert res["code"] == 101
|
||||||
|
assert "required" in res["message"].lower() or "name" in res[
|
||||||
|
"message"
|
||||||
|
].lower()
|
||||||
|
|
||||||
|
@pytest.mark.p3
|
||||||
|
def test_create_group_unicode_name(
|
||||||
|
self, web_api_auth: RAGFlowWebApiAuth
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a group with unicode characters in name."""
|
||||||
|
# First create a team
|
||||||
|
team_name: str = f"Test Team {uuid.uuid4().hex[:8]}"
|
||||||
|
team_payload: dict[str, str] = {"name": team_name}
|
||||||
|
team_res: dict[str, Any] = create_team(web_api_auth, team_payload)
|
||||||
|
assert team_res["code"] == 0, team_res
|
||||||
|
tenant_id: str = team_res["data"]["id"]
|
||||||
|
|
||||||
|
# Create group with unicode name
|
||||||
|
group_name: str = f"组{uuid.uuid4().hex[:8]}"
|
||||||
|
group_payload: dict[str, str] = {
|
||||||
|
"name": group_name,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
}
|
||||||
|
res: dict[str, Any] = create_group(web_api_auth, group_payload)
|
||||||
|
# Should succeed if unicode is supported
|
||||||
|
assert res["code"] in (0, 101)
|
||||||
|
|
||||||
Loading…
Add table
Reference in a new issue