Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/apps/user_management_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ async def signup(request: UserSignUpRequest):
try:
user_data = await signup_user_with_invitation(email=request.email,
password=request.password,
invite_code=request.invite_code)
invite_code=request.invite_code,
auto_login=request.auto_login)
success_message = "🎉 User account registered successfully! Please start experiencing the AI assistant service."
return JSONResponse(status_code=HTTPStatus.OK,
content={"message":success_message, "data":user_data})
Expand Down
1 change: 1 addition & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class UserSignUpRequest(BaseModel):
email: EmailStr
password: str = Field(..., min_length=6)
invite_code: Optional[str] = None
auto_login: Optional[bool] = True # Whether to return session after signup


class UserSignInRequest(BaseModel):
Expand Down
13 changes: 7 additions & 6 deletions backend/services/user_management_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,12 @@ async def check_auth_service_health():

async def signup_user_with_invitation(email: EmailStr,
password: str,
invite_code: Optional[str] = None):
invite_code: Optional[str] = None,
auto_login: Optional[bool] = True):
"""User registration with invitation code support"""
client = get_supabase_client()
logging.info(
f"Receive registration request: email={email}, invite_code={'provided' if invite_code else 'not provided'}")
f"Receive registration request: email={email}, invite_code={'provided' if invite_code else 'not provided'}, auto_login={auto_login}")

# Default user role is USER
user_role = "USER"
Expand Down Expand Up @@ -228,23 +229,23 @@ async def signup_user_with_invitation(email: EmailStr,
f"Failed to use invitation code {invite_code} for user {user_id}: {str(e)}")

logging.info(
f"User {email} registered successfully, role: {user_role}, tenant: {tenant_id}")
f"User {email} registered successfully, role: {user_role}, tenant: {tenant_id}, auto_login={auto_login}")

if user_role == "ADMIN":
await generate_tts_stt_4_admin(tenant_id, user_id)

# Initialize tool list for the new tenant (only once per tenant)
await init_tool_list_for_tenant(tenant_id, user_id)

return await parse_supabase_response(False, response, user_role)
return await parse_supabase_response(False, response, user_role, auto_login)
else:
logging.error(
"Supabase registration request returned no user object")
raise UserRegistrationException(
"Registration service is temporarily unavailable, please try again later")


async def parse_supabase_response(is_admin, response, user_role):
async def parse_supabase_response(is_admin, response, user_role, auto_login: bool = True):
"""Parse Supabase response and build standardized user registration response"""
user_data = {
"id": response.user.id,
Expand All @@ -253,7 +254,7 @@ async def parse_supabase_response(is_admin, response, user_role):
}

session_data = None
if response.session:
if response.session and auto_login:
session_data = {
"access_token": response.session.access_token,
"refresh_token": response.session.refresh_token,
Expand Down
1 change: 1 addition & 0 deletions frontend/services/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export const authService = {
email,
password,
invite_code: inviteCode || null,
auto_login: autoLogin,
}),
});

Expand Down
57 changes: 55 additions & 2 deletions test/backend/app/test_user_management_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,34 @@ def test_signup_success_regular_user(self):
mock_signup.assert_called_once_with(
email="test@example.com",
password="password123",
invite_code=None
invite_code=None,
auto_login=True
)

def test_signup_success_regular_user_with_auto_login_false(self):
"""Test successful regular user registration with auto_login=false"""
with patch('apps.user_management_app.signup_user_with_invitation') as mock_signup:
mock_signup.return_value = {"user_id": "123", "email": "test@example.com"}

response = client.post(
"/user/signup",
json={
"email": "test@example.com",
"password": "password123",
"invite_code": None,
"auto_login": False
}
)

assert response.status_code == HTTPStatus.OK
data = response.json()
assert "registered successfully" in data["message"]
assert "data" in data
mock_signup.assert_called_once_with(
email="test@example.com",
password="password123",
invite_code=None,
auto_login=False
)

def test_signup_success_admin_user(self):
Expand All @@ -141,7 +168,33 @@ def test_signup_success_admin_user(self):
mock_signup.assert_called_once_with(
email="admin@example.com",
password="password123",
invite_code="admin_code"
invite_code="admin_code",
auto_login=True
)

def test_signup_success_admin_user_with_auto_login_false(self):
"""Test successful admin user registration with auto_login=false (tenant management scenario)"""
with patch('apps.user_management_app.signup_user_with_invitation') as mock_signup:
mock_signup.return_value = {"user_id": "123", "email": "admin@example.com"}

response = client.post(
"/user/signup",
json={
"email": "admin@example.com",
"password": "password123",
"invite_code": "admin_code",
"auto_login": False
}
)

assert response.status_code == HTTPStatus.OK
data = response.json()
assert "registered successfully" in data["message"]
mock_signup.assert_called_once_with(
email="admin@example.com",
password="password123",
invite_code="admin_code",
auto_login=False
)

def test_signup_no_invite_code_exception(self):
Expand Down
186 changes: 182 additions & 4 deletions test/backend/services/test_user_management_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ async def test_signup_user_with_admin_invite_code(self, mock_get_client, mock_us
mock_insert_tenant.assert_called_once_with(user_id="user-123", tenant_id="tenant_id", user_role="ADMIN", user_email="admin@example.com")
mock_use_invite.assert_called_once_with("ADMIN123", "user-123")
mock_add_groups.assert_called_once_with("user-123", [1, 2, 3], "user-123")
mock_parse_response.assert_called_once_with(False, mock_response, "ADMIN")
mock_parse_response.assert_called_once_with(False, mock_response, "ADMIN", True)
# Verify init_tool_list_for_tenant was called
mock_init_tools.assert_called_once_with("tenant_id", "user-123")

Expand Down Expand Up @@ -637,7 +637,7 @@ async def test_signup_user_with_dev_invite_code(self, mock_get_client, mock_use_
mock_insert_tenant.assert_called_once_with(user_id="user-456", tenant_id="tenant_id", user_role="DEV", user_email="dev@example.com")
mock_use_invite.assert_called_once_with("DEV456", "user-456")
mock_add_groups.assert_called_once_with("user-456", [4, 5], "user-456")
mock_parse_response.assert_called_once_with(False, mock_response, "DEV")
mock_parse_response.assert_called_once_with(False, mock_response, "DEV", True)
# Verify init_tool_list_for_tenant was called
mock_init_tools.assert_called_once_with("tenant_id", "user-456")

Expand Down Expand Up @@ -738,7 +738,7 @@ async def test_signup_user_with_admin_invite_role_assignment(self, mock_check_av
# Verify ADMIN role was assigned and TTS/STT generation was called
mock_insert_tenant.assert_called_with(user_id="user-123", tenant_id="tenant_id", user_role="ADMIN", user_email="admin@example.com")
mock_generate_tts.assert_called_once_with("tenant_id", "user-123")
mock_parse.assert_called_with(False, mock_response, "ADMIN")
mock_parse.assert_called_with(False, mock_response, "ADMIN", True)
# Verify init_tool_list_for_tenant was called
mock_init_tools.assert_called_once_with("tenant_id", "user-123")

Expand Down Expand Up @@ -774,7 +774,7 @@ async def test_signup_user_with_dev_invite_role_assignment(self, mock_check_avai

# Verify DEV role was assigned and TTS/STT generation was NOT called
mock_insert_tenant.assert_called_with(user_id="user-123", tenant_id="tenant_id", user_role="DEV", user_email="dev@example.com")
mock_parse.assert_called_with(False, mock_response, "DEV")
mock_parse.assert_called_with(False, mock_response, "DEV", True)
# Verify init_tool_list_for_tenant was called
mock_init_tools.assert_called_once_with("tenant_id", "user-123")

Expand All @@ -789,6 +789,97 @@ async def test_signup_user_with_invite_code_validation_exception_conversion(self

self.assertIn("Invalid invitation code: Database connection failed", str(context.exception))

@patch('backend.services.user_management_service.add_user_to_groups')
@patch('backend.services.user_management_service.parse_supabase_response')
@patch('backend.services.user_management_service.generate_tts_stt_4_admin')
@patch('backend.services.user_management_service.insert_user_tenant')
@patch('backend.services.user_management_service.get_invitation_by_code')
@patch('backend.services.user_management_service.check_invitation_available')
@patch('backend.services.user_management_service.use_invitation_code')
@patch('backend.services.user_management_service.get_supabase_client')
async def test_signup_user_with_auto_login_false(self, mock_get_client, mock_use_invite,
mock_check_available, mock_get_invite_code,
mock_insert_tenant, mock_generate_tts, mock_parse_response, mock_add_groups):
"""Test user signup with auto_login=False (tenant admin creation scenario)"""
# Setup mocks
mock_client = MagicMock()
mock_user = MagicMock()
mock_user.id = "user-123"
mock_response = MagicMock()
mock_response.user = mock_user
mock_client.auth.sign_up.return_value = mock_response
mock_get_client.return_value = mock_client

# Mock invitation code validation
mock_check_available.return_value = True
mock_get_invite_code.return_value = {
"invitation_id": 1,
"code_type": "ADMIN_INVITE",
"group_ids": [],
"tenant_id": "tenant_id"
}
mock_use_invite.return_value = {"invitation_id": 1, "code_type": "ADMIN_INVITE", "group_ids": []}
mock_parse_response.return_value = {"user": "admin_data", "session": None}
mock_add_groups.return_value = []

# Call with auto_login=False
with patch('backend.services.user_management_service.init_tool_list_for_tenant', new_callable=AsyncMock) as mock_init_tools:
result = await signup_user_with_invitation(
"admin@example.com",
"password123",
invite_code="ADMIN123",
auto_login=False
)

# Verify parse_supabase_response was called with auto_login=False
mock_parse_response.assert_called_once_with(False, mock_response, "ADMIN", False)
# Verify init_tool_list_for_tenant was called
mock_init_tools.assert_called_once_with("tenant_id", "user-123")

@patch('backend.services.user_management_service.add_user_to_groups')
@patch('backend.services.user_management_service.parse_supabase_response')
@patch('backend.services.user_management_service.generate_tts_stt_4_admin')
@patch('backend.services.user_management_service.insert_user_tenant')
@patch('backend.services.user_management_service.get_invitation_by_code')
@patch('backend.services.user_management_service.check_invitation_available')
@patch('backend.services.user_management_service.use_invitation_code')
@patch('backend.services.user_management_service.get_supabase_client')
async def test_signup_user_with_auto_login_default(self, mock_get_client, mock_use_invite,
mock_check_available, mock_get_invite_code,
mock_insert_tenant, mock_generate_tts, mock_parse_response, mock_add_groups):
"""Test user signup with default auto_login (True)"""
# Setup mocks
mock_client = MagicMock()
mock_user = MagicMock()
mock_user.id = "user-123"
mock_response = MagicMock()
mock_response.user = mock_user
mock_client.auth.sign_up.return_value = mock_response
mock_get_client.return_value = mock_client

# Mock invitation code validation
mock_check_available.return_value = True
mock_get_invite_code.return_value = {
"invitation_id": 1,
"code_type": "ADMIN_INVITE",
"group_ids": [],
"tenant_id": "tenant_id"
}
mock_use_invite.return_value = {"invitation_id": 1, "code_type": "ADMIN_INVITE", "group_ids": []}
mock_parse_response.return_value = {"user": "admin_data", "session": "session_data"}
mock_add_groups.return_value = []

# Call without auto_login parameter (should default to True)
with patch('backend.services.user_management_service.init_tool_list_for_tenant', new_callable=AsyncMock) as mock_init_tools:
result = await signup_user_with_invitation(
"admin@example.com",
"password123",
invite_code="ADMIN123"
)

# Verify parse_supabase_response was called with default auto_login=True
mock_parse_response.assert_called_once_with(False, mock_response, "ADMIN", True)


class TestParseSupabaseResponse(unittest.IsolatedAsyncioTestCase):
"""Test parse_supabase_response"""
Expand Down Expand Up @@ -853,6 +944,93 @@ async def test_parse_response_without_session(self):
}
self.assertEqual(result, expected)

@patch('backend.services.user_management_service.get_jwt_expiry_seconds')
@patch('backend.services.user_management_service.calculate_expires_at')
async def test_parse_response_with_session_but_auto_login_false(self, mock_calc_expires, mock_get_expiry):
"""Test parsing response with session but auto_login=False (tenant admin creation scenario)"""
mock_user = MagicMock()
mock_user.id = "user-123"
mock_user.email = "admin@example.com"

mock_session = MagicMock()
mock_session.access_token = "access-token"
mock_session.refresh_token = "refresh-token"

mock_response = MagicMock()
mock_response.user = mock_user
mock_response.session = mock_session

mock_calc_expires.return_value = "2024-01-01T00:00:00Z"
mock_get_expiry.return_value = 3600

# When auto_login=False, session should be None even if Supabase returns session
result = await parse_supabase_response(False, mock_response, "ADMIN", auto_login=False)

expected = {
"user": {
"id": "user-123",
"email": "admin@example.com",
"role": "ADMIN"
},
"session": None, # Session should be suppressed when auto_login=False
"registration_type": "user"
}
self.assertEqual(result, expected)

@patch('backend.services.user_management_service.get_jwt_expiry_seconds')
@patch('backend.services.user_management_service.calculate_expires_at')
async def test_parse_response_with_session_and_auto_login_true(self, mock_calc_expires, mock_get_expiry):
"""Test parsing response with session and auto_login=True (normal signup scenario)"""
mock_user = MagicMock()
mock_user.id = "user-123"
mock_user.email = "test@example.com"

mock_session = MagicMock()
mock_session.access_token = "access-token"
mock_session.refresh_token = "refresh-token"

mock_response = MagicMock()
mock_response.user = mock_user
mock_response.session = mock_session

mock_calc_expires.return_value = "2024-01-01T00:00:00Z"
mock_get_expiry.return_value = 3600

# When auto_login=True, session should be included
result = await parse_supabase_response(False, mock_response, "USER", auto_login=True)

expected = {
"user": {
"id": "user-123",
"email": "test@example.com",
"role": "USER"
},
"session": {
"access_token": "access-token",
"refresh_token": "refresh-token",
"expires_at": "2024-01-01T00:00:00Z",
"expires_in_seconds": 3600
},
"registration_type": "user"
}
self.assertEqual(result, expected)

async def test_parse_response_default_auto_login_true(self):
"""Test that auto_login defaults to True when not specified"""
mock_user = MagicMock()
mock_user.id = "user-123"
mock_user.email = "test@example.com"

mock_response = MagicMock()
mock_response.user = mock_user
mock_response.session = None # No session from Supabase

# Call without auto_login parameter (should default to True)
result = await parse_supabase_response(False, mock_response, "user")

# Session should be None because Supabase didn't return it
self.assertIsNone(result["session"])


class TestGenerateTtsStt4Admin(unittest.IsolatedAsyncioTestCase):
"""Test generate_tts_stt_4_admin"""
Expand Down
Loading