Coverage for mcpgateway / services / personal_team_service.py: 100%
78 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 03:05 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 03:05 +0000
1# -*- coding: utf-8 -*-
2"""Location: ./mcpgateway/services/personal_team_service.py
3Copyright 2025
4SPDX-License-Identifier: Apache-2.0
5Authors: Mihai Criveti
7Personal Team Service.
8This module provides automatic personal team creation and management
9for email-based user authentication system.
11Examples:
12 >>> from unittest.mock import Mock
13 >>> service = PersonalTeamService(Mock())
14 >>> isinstance(service, PersonalTeamService)
15 True
16 >>> hasattr(service, 'db')
17 True
18"""
20# Standard
21from typing import Optional
23# Third-Party
24from sqlalchemy.orm import Session
26# First-Party
27from mcpgateway.config import settings
28from mcpgateway.db import EmailTeam, EmailTeamMember, EmailTeamMemberHistory, EmailUser, utc_now
29from mcpgateway.services.logging_service import LoggingService
30from mcpgateway.utils.create_slug import slugify
32# Initialize logging
33logging_service = LoggingService()
34logger = logging_service.get_logger(__name__)
37class PersonalTeamService:
38 """Service for managing personal teams.
40 This service handles automatic creation of personal teams for users
41 and manages team membership for personal workspaces.
43 Attributes:
44 db (Session): SQLAlchemy database session
46 Examples:
47 >>> from unittest.mock import Mock
48 >>> service = PersonalTeamService(Mock())
49 >>> service.__class__.__name__
50 'PersonalTeamService'
51 >>> hasattr(service, 'db')
52 True
53 """
55 def __init__(self, db: Session):
56 """Initialize the personal team service.
58 Args:
59 db: SQLAlchemy database session
61 Examples:
62 >>> from unittest.mock import Mock
63 >>> service = PersonalTeamService(Mock())
64 >>> hasattr(service, 'db') and service.db is not None or service.db is None
65 True
66 """
67 self.db = db
69 async def create_personal_team(self, user: EmailUser) -> EmailTeam:
70 """Create a personal team for a user.
72 Args:
73 user: EmailUser instance for whom to create personal team
75 Returns:
76 EmailTeam: The created personal team
78 Raises:
79 ValueError: If user already has a personal team
80 Exception: If team creation fails
82 Examples:
83 Personal team creation is handled automatically during user registration.
84 The team name is derived from the user's full name or email.
86 After creation, a record is inserted into EmailTeamMemberHistory to track the membership event.
88 Note:
89 This method is async and cannot be directly called with 'await' in doctest. To test async methods, use an event loop in real tests.
91 # Example (not executable in doctest):
92 # import asyncio
93 # team = asyncio.run(service.create_personal_team(user))
94 """
95 try:
96 # Check if user already has a personal team
97 existing_team = self.db.query(EmailTeam).filter(EmailTeam.created_by == user.email, EmailTeam.is_personal.is_(True), EmailTeam.is_active.is_(True)).first()
99 if existing_team:
100 logger.warning(f"User {user.email} already has a personal team: {existing_team.id}")
101 raise ValueError(f"User {user.email} already has a personal team")
103 # Generate team name from user's display name
104 display_name = user.get_display_name()
105 team_name = f"{display_name}'s Team"
107 # Create team slug — use prefix from config if set, otherwise derive from display name
108 prefix = slugify(settings.personal_team_prefix.strip()) if settings.personal_team_prefix.strip() else ""
109 if prefix:
110 email_slug = user.email.replace("@", "-").replace(".", "-").lower()
111 team_slug = f"{prefix}-{email_slug}"
112 else:
113 team_slug = slugify(team_name)
115 # Create the personal team
116 team = EmailTeam(
117 name=team_name,
118 slug=team_slug, # Will be auto-generated by event listener if not set
119 description=f"Personal workspace for {user.email}",
120 created_by=user.email,
121 is_personal=True,
122 visibility="private",
123 is_active=True,
124 )
126 self.db.add(team)
127 self.db.flush() # Get the team ID
129 # Add the user as the owner of their personal team
130 membership = EmailTeamMember(team_id=team.id, user_email=user.email, role="owner", joined_at=utc_now(), is_active=True)
132 self.db.add(membership)
133 self.db.flush() # Get the membership ID
134 # Insert history record
135 history = EmailTeamMemberHistory(team_member_id=membership.id, team_id=team.id, user_email=user.email, role="owner", action="added", action_by=user.email, action_timestamp=utc_now())
136 self.db.add(history)
137 self.db.commit()
139 logger.info(f"Created personal team '{team.name}' for user {user.email}")
140 return team
142 except Exception as e:
143 self.db.rollback()
144 logger.error(f"Failed to create personal team for {user.email}: {e}")
145 raise
147 async def get_personal_team(self, user_email: str) -> Optional[EmailTeam]:
148 """Get the personal team for a user.
150 Args:
151 user_email: Email address of the user
153 Returns:
154 EmailTeam: The user's personal team or None if not found
156 Examples:
157 >>> import asyncio
158 >>> from unittest.mock import Mock
159 >>> service = PersonalTeamService(Mock())
160 >>> asyncio.iscoroutinefunction(service.get_personal_team)
161 True
162 """
163 try:
164 team = self.db.query(EmailTeam).filter(EmailTeam.created_by == user_email, EmailTeam.is_personal.is_(True), EmailTeam.is_active.is_(True)).first()
165 return team
167 except Exception as e:
168 logger.error(f"Failed to get personal team for {user_email}: {e}")
169 return None
171 async def ensure_personal_team(self, user: EmailUser) -> Optional[EmailTeam]:
172 """Ensure a user has a personal team, creating one if needed.
174 Args:
175 user: EmailUser instance
177 Returns:
178 EmailTeam: The user's personal team (existing or newly created), or None if auto-creation is disabled
180 Raises:
181 Exception: If team creation or retrieval fails
183 Examples:
184 >>> import asyncio
185 >>> from unittest.mock import Mock
186 >>> service = PersonalTeamService(Mock())
187 >>> asyncio.iscoroutinefunction(service.ensure_personal_team)
188 True
189 """
190 try:
191 # Try to get existing personal team
192 team = await self.get_personal_team(user.email)
194 if team is None:
195 if not getattr(settings, "auto_create_personal_teams", True):
196 return None
197 # Create personal team if it doesn't exist
198 logger.info(f"Creating missing personal team for user {user.email}")
199 team = await self.create_personal_team(user)
201 return team
203 except ValueError:
204 # User already has a team, get it
205 team = await self.get_personal_team(user.email)
206 if team is None:
207 raise Exception(f"Failed to get or create personal team for {user.email}")
208 return team
210 def is_personal_team(self, team_id: str) -> bool:
211 """Check if a team is a personal team.
213 Args:
214 team_id: ID of the team to check
216 Returns:
217 bool: True if the team is a personal team, False otherwise
219 Examples:
220 >>> from unittest.mock import Mock
221 >>> service = PersonalTeamService(Mock())
222 >>> callable(service.is_personal_team)
223 True
224 """
225 try:
226 team = self.db.query(EmailTeam).filter(EmailTeam.id == team_id, EmailTeam.is_active.is_(True)).first()
227 return team is not None and team.is_personal
229 except Exception as e:
230 logger.error(f"Failed to check if team {team_id} is personal: {e}")
231 return False
233 async def delete_personal_team(self, team_id: str) -> bool:
234 """Delete a personal team (not allowed).
236 Personal teams cannot be deleted, only deactivated.
238 Args:
239 team_id: ID of the team to delete
241 Returns:
242 bool: False (personal teams cannot be deleted)
244 Raises:
245 ValueError: Always, as personal teams cannot be deleted
247 Examples:
248 >>> import asyncio
249 >>> from unittest.mock import Mock
250 >>> service = PersonalTeamService(Mock())
251 >>> asyncio.iscoroutinefunction(service.delete_personal_team)
252 True
253 """
254 if self.is_personal_team(team_id):
255 raise ValueError("Personal teams cannot be deleted")
256 return False
258 async def get_personal_team_owner(self, team_id: str) -> Optional[str]:
259 """Get the owner email of a personal team.
261 Args:
262 team_id: ID of the personal team
264 Returns:
265 str: Owner email address or None if not found
267 Examples:
268 Used for access control and team management operations.
269 """
270 try:
271 team = self.db.query(EmailTeam).filter(EmailTeam.id == team_id, EmailTeam.is_personal.is_(True), EmailTeam.is_active.is_(True)).first()
272 return team.created_by if team else None
274 except Exception as e:
275 logger.error(f"Failed to get personal team owner for {team_id}: {e}")
276 return None