Skip to content

Commit cc394c0

Browse files
committed
refactor: replace user provider with module facade pattern
1 parent 637bf34 commit cc394c0

8 files changed

Lines changed: 63 additions & 18 deletions

File tree

.importlinter

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ allow_indirect_imports = True
88
source_modules =
99
src.modules.todo
1010
forbidden_modules =
11+
src.modules.user.providers
1112
src.modules.user.application
1213
src.modules.user.domain
1314
src.modules.user.infrastructure
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from fastapi import Depends
22

33
from src.core.database.postgres.session import get_unit_of_work
4-
from src.modules.user.providers import UserModuleProvider
4+
from src.modules.user.facade import UserModuleFacade
55
from src.shared.unit_of_work import UnitOfWork
66

77

8-
def get_user_module_provider(
8+
def get_user_module_facade(
99
uow: UnitOfWork = Depends(get_unit_of_work),
10-
) -> UserModuleProvider:
11-
return UserModuleProvider(user_repository=uow.users)
10+
) -> UserModuleFacade:
11+
return UserModuleFacade(user_repository=uow.users)

src/modules/todo/application/detail_todo/handler.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
)
66
from src.modules.todo.presentation.schemas.response import TodoWithOwnerResponse
77
from src.modules.user import UserNotFoundError
8-
from src.modules.user.providers import UserModuleProvider
8+
from src.modules.user.facade import UserModuleFacade
99

1010

1111
class GetTodoDetailWithOwnerHandler:
1212
def __init__(
1313
self,
1414
todo_repo: TodoRepository,
15-
user_provider: UserModuleProvider,
15+
user_facade: UserModuleFacade,
1616
):
1717
self._todo_repo = todo_repo
18-
self._user_provider = user_provider
18+
self._user_facade = user_facade
1919

2020
async def execute(self, todo_id: int, user_id: int) -> TodoWithOwnerResponse:
2121
todo = await self._todo_repo.get_by_id(todo_id)
@@ -26,7 +26,7 @@ async def execute(self, todo_id: int, user_id: int) -> TodoWithOwnerResponse:
2626
"You do not have permission to view this todo"
2727
)
2828

29-
owner = await self._user_provider.get_user_profile(todo.user_id)
29+
owner = await self._user_facade.get_user_profile(todo.user_id)
3030
if not owner:
3131
raise UserNotFoundError("Todo owner not found")
3232

src/modules/todo/presentation/dependency.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from src.core.database.postgres.session import get_db, get_unit_of_work
55
from src.core.dependency.tenant import get_current_tenant_id
6-
from src.core.dependency.providers import get_user_module_provider
6+
from src.core.dependency.facades import get_user_module_facade
77
from src.modules.todo.application.create_todo.handler import CreateTodoHandler
88
from src.modules.todo.application.delete_todo.handler import DeleteTodoHandler
99
from src.modules.todo.application.detail_todo.handler import (
@@ -17,7 +17,7 @@
1717
from src.modules.todo.infrastructure.repositories.todo_repository import (
1818
SQLAlchemyTodoRepository,
1919
)
20-
from src.modules.user.providers import UserModuleProvider
20+
from src.modules.user.facade import UserModuleFacade
2121
from src.shared.unit_of_work import UnitOfWork
2222

2323

@@ -51,9 +51,9 @@ def get_delete_todo_handler(
5151

5252
def get_todo_detail_with_owner_handler(
5353
todo_repo: TodoRepository = Depends(get_todo_repository),
54-
user_provider: UserModuleProvider = Depends(get_user_module_provider),
54+
user_facade: UserModuleFacade = Depends(get_user_module_facade),
5555
) -> GetTodoDetailWithOwnerHandler:
56-
return GetTodoDetailWithOwnerHandler(todo_repo, user_provider=user_provider)
56+
return GetTodoDetailWithOwnerHandler(todo_repo, user_facade=user_facade)
5757

5858

5959
def get_todos_query_handler(

src/modules/user/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
UserAlreadyExistsError,
33
UserNotFoundError,
44
)
5-
from src.modules.user.providers import UserProfile
5+
from src.modules.user.facade import UserProfile
66

77
__all__ = [
88
"UserProfile",
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,30 @@
33
from src.modules.user.application.detail_user.handler import DetailUserQueryHandler
44
from src.modules.user.application.detail_user.query import DetailUserQuery
55
from src.modules.user.domain.repositories.user_repository import UserRepository
6+
from src.shared.facade import ModuleFacade
67

78

89
class UserProfile(BaseModel):
9-
id: str
10+
id: int
1011
email: str
1112
username: str | None = None
1213

1314

14-
class UserModuleProvider:
15+
class UserModuleFacade(ModuleFacade):
1516
def __init__(self, user_repository: UserRepository):
1617
self._user_detail_query = DetailUserQueryHandler(
1718
user_repository=user_repository
1819
)
1920

20-
async def get_user_profile(self, user_id: str) -> UserProfile | None:
21+
async def get_user_profile(self, user_id: int) -> UserProfile | None:
2122
user = await self._user_detail_query.execute(
22-
DetailUserQuery(user_id=str(user_id))
23+
DetailUserQuery(user_id=user_id)
2324
)
2425
if user is None:
2526
return None
2627

2728
return UserProfile(
28-
id=str(user.id),
29+
id=user.id,
2930
email=user.email,
3031
username=user.username,
3132
)

src/shared/facade.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from abc import ABC
2+
3+
4+
class ModuleFacade(ABC):
5+
"""Public contract a module exposes for cross-module communication.
6+
7+
Other modules depend only on a module's facade — never on its
8+
application, domain, or infrastructure internals.
9+
"""

tests/test_user_facade.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import asyncio
2+
3+
import pytest
4+
5+
from src.modules.user.domain.entities.user import User
6+
from src.modules.user.domain.exceptions.user_exception import UserNotFoundError
7+
from src.modules.user.facade import UserModuleFacade
8+
9+
10+
class FakeUserRepository:
11+
def __init__(self, user: User | None) -> None:
12+
self._user = user
13+
14+
async def get_by_id_with_relations(self, user_id: int) -> User | None:
15+
if self._user is None or self._user.id != user_id:
16+
return None
17+
return self._user
18+
19+
20+
def test_facade_returns_profile_with_int_id():
21+
facade = UserModuleFacade(FakeUserRepository(
22+
User(id=7, email="a@example.com", password_hash="x", username="alice")
23+
))
24+
profile = asyncio.run(facade.get_user_profile(7))
25+
assert profile is not None
26+
assert profile.id == 7
27+
assert profile.email == "a@example.com"
28+
assert profile.username == "alice"
29+
30+
31+
def test_facade_raises_user_not_found_for_unknown_user():
32+
facade = UserModuleFacade(FakeUserRepository(None))
33+
with pytest.raises(UserNotFoundError):
34+
asyncio.run(facade.get_user_profile(99))

0 commit comments

Comments
 (0)