Skip to content

Commit bc26bdc

Browse files
committed
Refactor user repository and service to handle password hashing and align with new DTOs
1 parent 212d1f3 commit bc26bdc

File tree

8 files changed

+79
-35
lines changed

8 files changed

+79
-35
lines changed

conduit/domain/dtos/user.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ class CreateUserDTO:
4848
password: str
4949

5050

51+
@dataclass(frozen=True)
52+
class CreateUserRecordDTO:
53+
username: str
54+
email: str
55+
password_hash: str
56+
57+
5158
@dataclass(frozen=True)
5259
class LoginUserDTO:
5360
email: str
@@ -61,3 +68,12 @@ class UpdateUserDTO:
6168
password: str | None = None
6269
bio: str | None = None
6370
image_url: str | None = None
71+
72+
73+
@dataclass(frozen=True)
74+
class UpdateUserRecordDTO:
75+
username: str | None = None
76+
email: str | None = None
77+
password_hash: str | None = None
78+
bio: str | None = None
79+
image_url: str | None = None

conduit/domain/repositories/user.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
from collections.abc import Collection, Mapping
33
from typing import Any
44

5-
from conduit.domain.dtos.user import CreateUserDTO, UpdateUserDTO, UserDTO
5+
from conduit.domain.dtos.user import CreateUserRecordDTO, UpdateUserRecordDTO, UserDTO
66

77

88
class IUserRepository(abc.ABC):
99
"""User repository interface."""
1010

1111
@abc.abstractmethod
12-
async def add(self, session: Any, create_item: CreateUserDTO) -> UserDTO: ...
12+
async def add(self, session: Any, create_item: CreateUserRecordDTO) -> UserDTO: ...
1313

1414
@abc.abstractmethod
1515
async def get_or_none(self, session: Any, user_id: int) -> UserDTO | None: ...
@@ -40,5 +40,5 @@ async def get_by_username(self, session: Any, username: str) -> UserDTO: ...
4040

4141
@abc.abstractmethod
4242
async def update(
43-
self, session: Any, user_id: int, update_item: UpdateUserDTO
43+
self, session: Any, user_id: int, update_item: UpdateUserRecordDTO
4444
) -> UserDTO: ...

conduit/infrastructure/repositories/user.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,23 @@
55
from sqlalchemy.ext.asyncio import AsyncSession
66

77
from conduit.core.exceptions import UserNotFoundException
8-
from conduit.domain.dtos.user import CreateUserDTO, UpdateUserDTO, UserDTO
8+
from conduit.domain.dtos.user import CreateUserRecordDTO, UpdateUserRecordDTO, UserDTO
99
from conduit.domain.repositories.user import IUserRepository
1010
from conduit.infrastructure.models import User
11-
from conduit.services.password import get_password_hash
1211

1312

1413
class UserRepository(IUserRepository):
1514
"""Repository for User model."""
1615

17-
async def add(self, session: AsyncSession, create_item: CreateUserDTO) -> UserDTO:
16+
async def add(
17+
self, session: AsyncSession, create_item: CreateUserRecordDTO
18+
) -> UserDTO:
1819
query = (
1920
insert(User)
2021
.values(
2122
username=create_item.username,
2223
email=create_item.email,
23-
password_hash=get_password_hash(create_item.password),
24+
password_hash=create_item.password_hash,
2425
image_url="https://api.realworld.io/images/smiley-cyrus.jpeg",
2526
bio="",
2627
created_at=datetime.now(),
@@ -78,7 +79,7 @@ async def get_by_username(self, session: AsyncSession, username: str) -> UserDTO
7879
return self._to_user_dto(user)
7980

8081
async def update(
81-
self, session: AsyncSession, user_id: int, update_item: UpdateUserDTO
82+
self, session: AsyncSession, user_id: int, update_item: UpdateUserRecordDTO
8283
) -> UserDTO:
8384
query = (
8485
update(User)
@@ -90,8 +91,8 @@ async def update(
9091
query = query.values(username=update_item.username)
9192
if update_item.email is not None:
9293
query = query.values(email=update_item.email)
93-
if update_item.password is not None:
94-
query = query.values(password_hash=get_password_hash(update_item.password))
94+
if update_item.password_hash is not None:
95+
query = query.values(password_hash=update_item.password_hash)
9596
if update_item.bio is not None:
9697
query = query.values(bio=update_item.bio)
9798
if update_item.image_url is not None:

conduit/services/user.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
)
1111
from conduit.domain.dtos.user import (
1212
CreateUserDTO,
13+
CreateUserRecordDTO,
1314
UpdatedUserDTO,
1415
UpdateUserDTO,
16+
UpdateUserRecordDTO,
1517
UserDTO,
1618
)
1719
from conduit.domain.repositories.user import IUserRepository
1820
from conduit.domain.services.user import IUserService
21+
from conduit.services.password import get_password_hash
1922

2023

2124
class UserService(IUserService):
@@ -37,9 +40,14 @@ async def create_user(
3740
):
3841
raise UserNameAlreadyTakenException()
3942

43+
create_user_record = CreateUserRecordDTO(
44+
username=user_to_create.username,
45+
email=user_to_create.email,
46+
password_hash=get_password_hash(user_to_create.password),
47+
)
4048
try:
4149
return await self._user_repo.add(
42-
session=session, create_item=user_to_create
50+
session=session, create_item=create_user_record
4351
)
4452
except (NoResultFound, MultipleResultsFound) as exc:
4553
raise UserCreateException() from exc
@@ -78,8 +86,19 @@ async def update_user(
7886
):
7987
raise EmailAlreadyTakenException()
8088

89+
update_record = UpdateUserRecordDTO(
90+
username=user_to_update.username,
91+
email=user_to_update.email,
92+
password_hash=(
93+
get_password_hash(user_to_update.password)
94+
if user_to_update.password is not None
95+
else None
96+
),
97+
bio=user_to_update.bio,
98+
image_url=user_to_update.image_url,
99+
)
81100
updated_user = await self._user_repo.update(
82-
session=session, user_id=current_user.id, update_item=user_to_update
101+
session=session, user_id=current_user.id, update_item=update_record
83102
)
84103
return UpdatedUserDTO(
85104
id=updated_user.id,

tests/api/routes/test_article.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from conduit.api.schemas.responses.article import ArticleResponse
66
from conduit.domain.dtos.article import ArticleDTO
7+
from conduit.domain.services.user import IUserService
78
from conduit.infrastructure.repositories.article import ArticleRepository
8-
from conduit.infrastructure.repositories.user import UserRepository
99
from tests.utils import create_another_test_article, create_another_test_user
1010

1111

@@ -117,11 +117,11 @@ async def test_user_can_retrieve_article_if_exists(
117117
async def test_user_can_not_delete_foreign_article(
118118
authorized_test_client: AsyncClient,
119119
session: AsyncSession,
120-
user_repository: UserRepository,
120+
user_service: IUserService,
121121
article_repository: ArticleRepository,
122122
) -> None:
123123
new_user = await create_another_test_user(
124-
session=session, user_repository=user_repository
124+
session=session, user_service=user_service
125125
)
126126
new_article = await create_another_test_article(
127127
session=session, article_repository=article_repository, author_id=new_user.id
@@ -134,11 +134,11 @@ async def test_user_can_not_delete_foreign_article(
134134
async def test_user_can_not_update_foreign_article(
135135
authorized_test_client: AsyncClient,
136136
session: AsyncSession,
137-
user_repository: UserRepository,
137+
user_service: IUserService,
138138
article_repository: ArticleRepository,
139139
) -> None:
140140
new_user = await create_another_test_user(
141-
session=session, user_repository=user_repository
141+
session=session, user_service=user_service
142142
)
143143
new_article = await create_another_test_article(
144144
session=session, article_repository=article_repository, author_id=new_user.id

tests/api/routes/test_profile.py

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

55
from conduit.api.schemas.responses.profile import ProfileResponse
66
from conduit.domain.dtos.user import UserDTO
7-
from conduit.infrastructure.repositories.user import UserRepository
7+
from conduit.domain.services.user import IUserService
88
from tests.utils import create_another_test_user
99

1010

@@ -50,11 +50,11 @@ async def test_authenticated_user_cant_follow_own_profile(
5050
async def test_authenticated_user_cant_follow_another_profile(
5151
authorized_test_client: AsyncClient,
5252
test_user: UserDTO,
53-
user_repository: UserRepository,
53+
user_service: IUserService,
5454
session: AsyncSession,
5555
) -> None:
5656
new_user = await create_another_test_user(
57-
session=session, user_repository=user_repository
57+
session=session, user_service=user_service
5858
)
5959
response = await authorized_test_client.post(
6060
url=f"/profiles/{new_user.username}/follow"
@@ -67,11 +67,11 @@ async def test_authenticated_user_cant_follow_another_profile(
6767
async def test_authenticated_user_cant_follow_already_followed_profile(
6868
authorized_test_client: AsyncClient,
6969
test_user: UserDTO,
70-
user_repository: UserRepository,
70+
user_service: IUserService,
7171
session: AsyncSession,
7272
) -> None:
7373
new_user = await create_another_test_user(
74-
session=session, user_repository=user_repository
74+
session=session, user_service=user_service
7575
)
7676
response = await authorized_test_client.post(
7777
url=f"/profiles/{new_user.username}/follow"
@@ -88,11 +88,11 @@ async def test_authenticated_user_cant_follow_already_followed_profile(
8888
@pytest.mark.anyio
8989
async def test_authenticated_user_cant_unfollow_not_followed_profile(
9090
authorized_test_client: AsyncClient,
91-
user_repository: UserRepository,
91+
user_service: IUserService,
9292
session: AsyncSession,
9393
) -> None:
9494
new_user = await create_another_test_user(
95-
session=session, user_repository=user_repository
95+
session=session, user_service=user_service
9696
)
9797
response = await authorized_test_client.delete(
9898
url=f"/profiles/{new_user.username}/follow"
@@ -103,11 +103,11 @@ async def test_authenticated_user_cant_unfollow_not_followed_profile(
103103
@pytest.mark.anyio
104104
async def test_authenticated_user_can_unfollow_followed_profile(
105105
authorized_test_client: AsyncClient,
106-
user_repository: UserRepository,
106+
user_service: IUserService,
107107
session: AsyncSession,
108108
) -> None:
109109
new_user = await create_another_test_user(
110-
session=session, user_repository=user_repository
110+
session=session, user_service=user_service
111111
)
112112
response = await authorized_test_client.post(
113113
url=f"/profiles/{new_user.username}/follow"
@@ -125,11 +125,11 @@ async def test_authenticated_user_can_unfollow_followed_profile(
125125
@pytest.mark.anyio
126126
async def test_authenticated_user_can_unfollow_already_unfollowed_profile(
127127
authorized_test_client: AsyncClient,
128-
user_repository: UserRepository,
128+
user_service: IUserService,
129129
session: AsyncSession,
130130
) -> None:
131131
new_user = await create_another_test_user(
132-
session=session, user_repository=user_repository
132+
session=session, user_service=user_service
133133
)
134134
response = await authorized_test_client.post(
135135
url=f"/profiles/{new_user.username}/follow"

tests/conftest.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from conduit.domain.dtos.user import CreateUserDTO, UserDTO
2020
from conduit.domain.repositories.article import IArticleRepository
2121
from conduit.domain.repositories.user import IUserRepository
22+
from conduit.domain.services.user import IUserService
2223
from conduit.infrastructure.models import Base
2324

2425
SetupFixture: TypeAlias = None
@@ -100,6 +101,11 @@ def auth_token_service(di_container: Container) -> IAuthTokenService:
100101
return di_container.auth_token_service()
101102

102103

104+
@pytest.fixture
105+
def user_service(di_container: Container) -> IUserService:
106+
return di_container.user_service()
107+
108+
103109
@pytest.fixture
104110
def user_to_create() -> CreateUserDTO:
105111
return CreateUserDTO(username="test", email="test@gmail.com", password="password")
@@ -131,11 +137,11 @@ def not_exists_user() -> UserDTO:
131137

132138
@pytest.fixture
133139
async def test_user(
134-
session: AsyncSession,
135-
user_repository: IUserRepository,
136-
user_to_create: CreateUserDTO,
140+
session: AsyncSession, user_service: IUserService, user_to_create: CreateUserDTO
137141
) -> UserDTO:
138-
return await user_repository.add(session=session, create_item=user_to_create)
142+
return await user_service.create_user(
143+
session=session, user_to_create=user_to_create
144+
)
139145

140146

141147
@pytest.fixture

tests/utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@
22

33
from conduit.domain.dtos.article import ArticleRecordDTO, CreateArticleDTO
44
from conduit.domain.dtos.user import CreateUserDTO, UserDTO
5+
from conduit.domain.services.user import IUserService
56
from conduit.infrastructure.repositories.article import ArticleRepository
6-
from conduit.infrastructure.repositories.user import UserRepository
77

88

99
async def create_another_test_user(
10-
session: AsyncSession, user_repository: UserRepository
10+
session: AsyncSession, user_service: IUserService
1111
) -> UserDTO:
1212
create_user_dto = CreateUserDTO(
1313
username="temp-user", email="temp-user@gmail.com", password="password"
1414
)
15-
return await user_repository.add(session=session, create_item=create_user_dto)
15+
return await user_service.create_user(
16+
session=session, user_to_create=create_user_dto
17+
)
1618

1719

1820
async def create_another_test_article(

0 commit comments

Comments
 (0)