Skip to content

Commit 3cfbc1b

Browse files
benjaminpiliaBenjamin PILIAleoguillaume
authored
refacto(clean-architecture): refacto DELETE /v1/admin/users/{user_id} endpoint toward clean architecture (#898)
* refacto(clean-architecture): delete_user * refacto delete_user * Update unit coverage badge * refacto after review * Update unit coverage badge --------- Co-authored-by: Benjamin PILIA <benjamin.pilia@protonmail.com> Co-authored-by: benjaminpilia <benjaminpilia@users.noreply.github.com> Co-authored-by: Léo Guillaume <62661249+leoguillaume@users.noreply.github.com> Co-authored-by: leoguillaume <leoguillaume@users.noreply.github.com>
1 parent f62dbce commit 3cfbc1b

25 files changed

Lines changed: 631 additions & 173 deletions

File tree

.github/badges/coverage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"schemaVersion":1,"label":"coverage","message":"56.11%","color":"red"}
1+
{"schemaVersion":1,"label":"coverage","message":"56.14%","color":"red"}

api/dependencies.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
)
3535
from api.use_cases.admin.roles import CreateRoleUseCase, DeleteRoleUseCase, GetRolesUseCase, GetRoleUseCase, UpdateRoleUseCase
3636
from api.use_cases.admin.routers import CreateRouterUseCase, DeleteRouterUseCase, GetOneRouterUseCase, GetRoutersUseCase, UpdateRouterUseCase
37-
from api.use_cases.admin.users import CreateUserUseCase, GetOneUserUseCase, GetUsersUseCase
37+
from api.use_cases.admin.users import CreateUserUseCase, DeleteUserUseCase, GetOneUserUseCase, GetUsersUseCase
3838
from api.use_cases.health import GetHealthModelsUseCase
3939
from api.use_cases.models import GetModelsUseCase, GetModelUseCase
4040
from api.utils.configuration import configuration
@@ -166,6 +166,15 @@ def get_users_use_case_factory(postgres_session: AsyncSession = Depends(get_post
166166
return GetUsersUseCase(user_repository=_user_repository(postgres_session), user_with_role_query=_user_with_role_query(postgres_session))
167167

168168

169+
def delete_user_use_case_factory(postgres_session: AsyncSession = Depends(get_postgres_session)) -> DeleteUserUseCase:
170+
return DeleteUserUseCase(
171+
user_repository=_user_repository(postgres_session),
172+
user_with_role_query=_user_with_role_query(postgres_session),
173+
router_repository=_router_repository(postgres_session),
174+
provider_repository=_provider_repository(postgres_session),
175+
)
176+
177+
169178
# roles use cases
170179
def create_role_use_case_factory(postgres_session: AsyncSession = Depends(get_postgres_session)) -> CreateRoleUseCase:
171180
return CreateRoleUseCase(

api/domain/provider/_providerrepository.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,7 @@ async def update_provider(self, provider: Provider) -> Provider | ProviderAlread
5252
@abstractmethod
5353
async def get_all_providers(self) -> list[Provider]:
5454
pass
55+
56+
@abstractmethod
57+
async def get_provider_ids_by_user_id(self, user_id: int) -> list[int]:
58+
pass

api/domain/router/_routerrepository.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ async def get_aliases(self, filtered_aliases: list[str] | None = None) -> list[s
6161
@abstractmethod
6262
async def update_router(self, router: Router) -> Router | RouterNameAlreadyExistsError:
6363
pass
64+
65+
@abstractmethod
66+
async def get_router_ids_by_user_id(self, user_id: int) -> list[int]:
67+
pass

api/domain/user/_userrepository.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from api.domain.organization.errors import OrganizationNotFoundError
55
from api.domain.role.errors import RoleNotFoundError
66
from api.domain.user.entities import User, UserPage, UserSortField
7-
from api.domain.user.errors import UserAlreadyExistsError, UserNotFoundError
7+
from api.domain.user.errors import DeleteUserWithProvidersError, DeleteUserWithRoutersError, UserAlreadyExistsError, UserNotFoundError
88

99

1010
class UserRepository(ABC):
@@ -49,7 +49,7 @@ async def update_user(self, user: User) -> User | UserNotFoundError | UserAlread
4949
pass
5050

5151
@abstractmethod
52-
async def delete_user(self, user_id: int) -> User | UserNotFoundError:
52+
async def delete_user(self, user_id: int) -> User | UserNotFoundError | DeleteUserWithRoutersError | DeleteUserWithProvidersError:
5353
pass
5454

5555
@abstractmethod

api/domain/user/errors.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,15 @@ class UserIsNotAdminError:
2020
@dataclass
2121
class UserExpiredError:
2222
pass
23+
24+
25+
@dataclass
26+
class DeleteUserWithProvidersError:
27+
user_id: int
28+
provider_ids: list[int] | None
29+
30+
31+
@dataclass
32+
class DeleteUserWithRoutersError:
33+
user_id: int
34+
router_ids: list[int] | None

api/endpoints/admin/users.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,6 @@
1111
from api.utils.variables import EndpointRoute
1212

1313

14-
@router.delete(
15-
path=EndpointRoute.ADMIN_USERS + "/{user_id}",
16-
dependencies=[Security(dependency=AccessController(permissions=[PermissionType.ADMIN]))],
17-
status_code=204,
18-
)
19-
async def delete_user(
20-
request: Request,
21-
user_id: int = Path(description="The ID of the user to delete."),
22-
postgres_session: AsyncSession = Depends(get_postgres_session),
23-
) -> Response:
24-
"""
25-
Delete a user.
26-
"""
27-
await global_context.identity_access_manager.delete_user(postgres_session=postgres_session, user_id=user_id)
28-
29-
return Response(status_code=204)
30-
31-
3214
@router.patch(
3315
path=EndpointRoute.ADMIN_USERS + "/{user}",
3416
dependencies=[Security(dependency=AccessController(permissions=[PermissionType.ADMIN]))],

api/helpers/_identityaccessmanager.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525
from api.utils.exceptions import (
2626
DeleteOrganizationWithUsersException,
2727
DeleteRoleWithUsersException,
28-
DeleteUserWithProvidersException,
29-
DeleteUserWithRoutersException,
3028
InvalidCurrentPasswordException,
3129
InvalidTokenExpirationException,
3230
OrganizationAlreadyExistsException,
@@ -268,23 +266,6 @@ async def create_user(
268266

269267
return user_id
270268

271-
@staticmethod
272-
async def delete_user(postgres_session: AsyncSession, user_id: int) -> None:
273-
result = await postgres_session.execute(statement=select(UserTable.id).where(UserTable.id == user_id))
274-
try:
275-
result.scalar_one()
276-
except NoResultFound:
277-
raise UserNotFoundException()
278-
279-
try:
280-
await postgres_session.execute(statement=delete(table=UserTable).where(UserTable.id == user_id))
281-
except IntegrityError as e:
282-
if "provider_user_id_fkey" in str(e.orig):
283-
raise DeleteUserWithProvidersException()
284-
if "router_user_id_fkey" in str(e.orig):
285-
raise DeleteUserWithRoutersException()
286-
await postgres_session.commit()
287-
288269
async def update_user(
289270
self,
290271
postgres_session: AsyncSession,

api/infrastructure/fastapi/endpoints/admin/users.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,33 @@
44

55
from fastapi import Body, Depends, Path, Query, Security
66

7-
from api.dependencies import create_user_use_case_factory, get_one_user_use_case_factory, get_request_context, get_users_use_case_factory
7+
from api.dependencies import (
8+
create_user_use_case_factory,
9+
delete_user_use_case_factory,
10+
get_one_user_use_case_factory,
11+
get_request_context,
12+
get_users_use_case_factory,
13+
)
814
from api.domain import SortOrder
915
from api.domain.organization.errors import OrganizationNotFoundError
1016
from api.domain.role.errors import RoleNotFoundError
1117
from api.domain.user.entities import UserSortField
12-
from api.domain.user.errors import UserAlreadyExistsError, UserExpiredError, UserIsNotAdminError, UserNotFoundError
18+
from api.domain.user.errors import (
19+
DeleteUserWithProvidersError,
20+
DeleteUserWithRoutersError,
21+
UserAlreadyExistsError,
22+
UserExpiredError,
23+
UserIsNotAdminError,
24+
UserNotFoundError,
25+
)
1326
from api.infrastructure.fastapi.access import get_current_key
1427
from api.infrastructure.fastapi.context import RequestContext
1528
from api.infrastructure.fastapi.documentation import get_documentation_responses
1629
from api.infrastructure.fastapi.endpoints.admin import router
1730
from api.infrastructure.fastapi.endpoints.exceptions import (
1831
AccountExpiredHTTPException,
32+
DeleteUserWithProvidersHTTPException,
33+
DeleteUserWithRoutersHTTPException,
1934
InternalServerHTTPException,
2035
NotAdminUserHTTPException,
2136
OrganizationNotFoundHTTPException,
@@ -28,6 +43,9 @@
2843
CreateUserCommand,
2944
CreateUserUseCase,
3045
CreateUserUseCaseSuccess,
46+
DeleteUserCommand,
47+
DeleteUserUseCase,
48+
DeleteUserUseCaseSuccess,
3149
GetOneUserCommand,
3250
GetOneUserUseCase,
3351
GetOneUserUseCaseSuccess,
@@ -184,3 +202,53 @@ async def get_users(
184202
raise AccountExpiredHTTPException()
185203
case _ as unreachable:
186204
assert_never(unreachable)
205+
206+
207+
@router.delete(
208+
path=EndpointRoute.ADMIN_USERS + "/{user_id}",
209+
dependencies=[Security(dependency=get_current_key)],
210+
status_code=200,
211+
responses=get_documentation_responses(
212+
[
213+
UserNotFoundHTTPException,
214+
DeleteUserWithRoutersHTTPException,
215+
DeleteUserWithProvidersHTTPException,
216+
]
217+
),
218+
)
219+
async def delete_user(
220+
user_id: int = Path(description="The ID of the user to delete."),
221+
delete_user_use_case: DeleteUserUseCase = Depends(delete_user_use_case_factory),
222+
request_context: ContextVar[RequestContext] = Depends(get_request_context),
223+
) -> UserResponse:
224+
command = DeleteUserCommand(
225+
authenticated_user_id=request_context.get().user_id,
226+
user_id=user_id,
227+
)
228+
try:
229+
result = await delete_user_use_case.execute(command)
230+
except Exception as e:
231+
logger.exception(
232+
"Unexpected error while executing delete_user use case",
233+
extra={
234+
"authenticated_user_id": command.authenticated_user_id,
235+
"user_id": command.user_id,
236+
"error_type": type(e).__name__,
237+
},
238+
)
239+
raise InternalServerHTTPException()
240+
match result:
241+
case DeleteUserUseCaseSuccess(user=user):
242+
return UserResponse.model_validate(user, from_attributes=True)
243+
case UserNotFoundError(id=not_found_id):
244+
raise UserNotFoundHTTPException(user_id=not_found_id)
245+
case DeleteUserWithRoutersError(router_ids=router_ids):
246+
raise DeleteUserWithRoutersHTTPException(router_ids=router_ids)
247+
case DeleteUserWithProvidersError(provider_ids=provider_ids):
248+
raise DeleteUserWithProvidersHTTPException(provider_ids=provider_ids)
249+
case UserIsNotAdminError():
250+
raise NotAdminUserHTTPException()
251+
case UserExpiredError():
252+
raise AccountExpiredHTTPException()
253+
case _ as unreachable:
254+
assert_never(unreachable)

api/infrastructure/fastapi/endpoints/exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,22 @@ def __init__(self, role_id: int, number_of_users: int) -> None:
164164
super().__init__(status_code=409, detail=f"Role {role_id} has {number_of_users} users and cannot be removed.")
165165

166166

167+
class DeleteUserWithRoutersHTTPException(HTTPException):
168+
status_code = 409
169+
detail = "User cannot be deleted because the user owns routers: {router_ids}."
170+
171+
def __init__(self, router_ids: list[int] | None) -> None:
172+
super().__init__(status_code=409, detail=f"User cannot be deleted because the user owns routers: {router_ids}.")
173+
174+
175+
class DeleteUserWithProvidersHTTPException(HTTPException):
176+
status_code = 409
177+
detail = "User cannot be deleted because the user owns providers: {provider_ids}."
178+
179+
def __init__(self, provider_ids: list[int] | None) -> None:
180+
super().__init__(status_code=409, detail=f"User cannot be deleted because the user owns providers: {provider_ids}.")
181+
182+
167183
# 413
168184

169185

0 commit comments

Comments
 (0)