Skip to content

Commit 171f7bf

Browse files
committed
test: Add user_service test class
1 parent cacfcde commit 171f7bf

File tree

8 files changed

+79
-116
lines changed

8 files changed

+79
-116
lines changed

app/api/user_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ async def create(
9393
The endpoint creates a new user with the provided user data and returns the created user's details.
9494
"""
9595
token_data = request.state.jwt_user
96-
_log.debug(f"UserApi Creating user: {user_create_data} {token_data}")
97-
result = await user_service.create(user_create_data)
96+
_log.debug(f"UserApi Creating user: {user_create_data} TokenUser:{token_data}")
97+
result = await user_service.create(user_create_data, token_data)
9898
if result is None:
9999
_log.error(f"UserApi User not created")
100100
raise HTTPException(status_code=400, detail="User not created")

app/entity/user_entity.py

Lines changed: 8 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,27 @@
1-
from datetime import datetime, timezone
2-
from typing import Optional
1+
from datetime import datetime
32

43
from beanie import Document
54
from pydantic import EmailStr
65

7-
from app.schema.user_dto import UserDTO, UserCreate, UserUpdate
8-
96

107
class User(Document):
11-
user_id: str
8+
user_id: str | None = None
129
username: str
1310
first_name: str
1411
last_name: str
1512
email: EmailStr
16-
hashed_password: Optional[str] = None
17-
is_active: bool = False
13+
hashed_password: str | None = None
14+
is_active: bool | None = None
1815
roles: list[str] | None = None
19-
created_by: Optional[str] = None
20-
created_date: Optional[datetime] = None
16+
created_by: str | None = None
17+
created_date: datetime | None = None
2118
last_updated_by: str | None = None
22-
last_updated_date: datetime = datetime.now(timezone.utc)
23-
age: Optional[int] = None
19+
last_updated_date: datetime | None = None
20+
age: int | None = None
2421

2522
class Settings:
2623
name = "app_user"
2724
validate_on_save = True
2825

29-
@staticmethod
30-
def from_create(user_create: UserCreate):
31-
return User(
32-
user_id="",
33-
username=user_create.username,
34-
first_name=user_create.first_name,
35-
last_name=user_create.last_name,
36-
email=user_create.email,
37-
hashed_password="",
38-
is_active=user_create.is_active,
39-
roles=user_create.roles,
40-
created_by="system",
41-
created_date=datetime.now().isoformat(),
42-
last_updated_by="system",
43-
last_updated_date=datetime.now().isoformat(),
44-
)
45-
46-
@staticmethod
47-
def from_update(user_update: UserUpdate):
48-
return User(
49-
user_id="", # Not allowed to update
50-
username="", # Not allowed to update
51-
first_name=user_update.first_name,
52-
last_name=user_update.last_name,
53-
email=user_update.email,
54-
hashed_password="", # Not allowed to update
55-
is_active=user_update.is_active,
56-
roles=user_update.roles,
57-
created_by="", # Not allowed to update
58-
created_date=None, # Not allowed to update
59-
last_updated_by="system",
60-
last_updated_date=datetime.now(timezone.utc),
61-
)
62-
63-
@staticmethod
64-
def from_dto(user_dto: UserDTO):
65-
return User(
66-
user_id=user_dto.user_id,
67-
username=user_dto.username,
68-
first_name=user_dto.first_name,
69-
last_name=user_dto.last_name,
70-
email=user_dto.email,
71-
is_active=user_dto.is_active,
72-
roles=user_dto.roles,
73-
created_by=user_dto.created_by,
74-
created_date=user_dto.created_date,
75-
last_updated_by=user_dto.last_updated_by,
76-
last_updated_date=user_dto.last_updated_date,
77-
)
78-
7926
def __str__(self):
8027
return f"User: {self.user_id}, {self.username}, {self.first_name} {self.last_name}, {self.email}, {self.is_active}, {self.roles}"

app/middleware/security_middleware.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from app.conf.app_settings import security_settings, server_settings
66
from app.security import auth_handler
7+
from app.security.jwt_token import JWTUser
78

89
API_PREFIX = server_settings.CONTEXT_PATH
910
ALLOWED_PATHS = [resource for resource in security_settings.ALLOWED_PATHS]
@@ -38,6 +39,6 @@ def _is_valid_token(token: str) -> bool:
3839
return token is not None and auth_handler.is_valid_token(token.replace("Bearer ", ""))
3940

4041
@staticmethod
41-
def _get_user_from_token(token: str):
42+
def _get_user_from_token(token: str) -> JWTUser:
4243
"""Retrieve user information from the token."""
4344
return auth_handler.get_jwt_user_from_token(token.replace("Bearer ", ""))

app/repository/user_repository.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ async def create(self, user: User) -> User:
2828

2929
async def update(self, user: User) -> User | None:
3030
_log.debug(f"UserRepository Updating user")
31+
if user.user_id is None:
32+
raise BusinessException(ErrorCodes.INVALID_PAYLOAD, "User id is required for update")
3133
result = await user.replace()
3234
_log.debug(f"UserRepository User updated")
3335
return result
@@ -74,7 +76,7 @@ async def find(self, query: str | None = None, page: int = 0, size: int = 10, so
7476
else:
7577
query = json.loads(query)
7678

77-
total_count = await self.count(query)
79+
total_count = await User.find(query).count()
7880
if total_count == 0:
7981
return PageResponse(content=[], page=page, size=size, total=total_count)
8082

app/security/jwt_token.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class JWTUser(BaseModel):
99
sub: str = Field(alias="sub", title="Username")
1010
email: str = Field(alias="email", title="Email")
1111
scopes: list[str] = Field(alias="scopes", title="Roles")
12-
expires: float = Field(alias="exp", title="Expires of the token")
12+
exp: float = Field(alias="exp", title="Expires of the token")
1313
token: str = Field(alias="token", title="Access Token")
1414

1515

app/service/user_service.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from app.errors.business_exception import BusinessException, ErrorCodes
1111
from app.repository.user_repository import UserRepository
1212
from app.schema.user_dto import UserDTO, UserCreate, UserUpdate
13+
from app.security.jwt_token import JWTUser
1314
from app.service import email_service
1415
from app.utils.pass_util import PasswordUtil
1516

@@ -62,13 +63,16 @@ async def user_create_validation(self, user_create: UserCreate):
6263
raise BusinessException(ErrorCodes.ALREADY_EXISTS,
6364
f"User with email already exists: {user_create.email}")
6465

65-
async def create(self, user_create: UserCreate) -> UserDTO:
66+
async def create(self, user_create: UserCreate, token_data: JWTUser) -> UserDTO:
6667
_log.debug(f"UserService Creating user: {user_create} with: {type(user_create)}")
6768

6869
try:
69-
user = User.from_create(user_create)
70+
hashed_password = PasswordUtil().hash_password(user_create.password)
71+
user = User(**user_create.model_dump(), hashed_password = hashed_password)
72+
user.created_by = token_data.sub
73+
user.last_updated_by = token_data.sub
7074
user.user_id = str(uuid.uuid4())
71-
user.hashed_password = PasswordUtil().hash_password(user_create.password)
75+
7276
await self.user_create_validation(user_create)
7377
final_user = await self.repository.create(user)
7478
result = UserDTO.model_validate(final_user)
@@ -107,7 +111,7 @@ async def find(self, query, page: int, size: int, sort: str) -> PageResponse[Use
107111
_log.debug("UserService Users retrieved")
108112
return page_response
109113

110-
async def update(self, user_id: str, user_update: UserUpdate) -> Optional[UserDTO]:
114+
async def update(self, user_id: str, user_update: UserUpdate, token_data: JWTUser) -> Optional[UserDTO]:
111115
_log.debug(f"UserService Updating user: {user_id} with: {type(user_update)}")
112116

113117
if not user_update:
@@ -123,6 +127,7 @@ async def update(self, user_id: str, user_update: UserUpdate) -> Optional[UserDT
123127
if getattr(user_update, attr) is not None:
124128
setattr(user_entity, attr, getattr(user_update, attr))
125129

130+
user_entity.last_updated_by = token_data.sub
126131
final_user = await self.repository.update(user_entity)
127132
result = UserDTO.model_validate(final_user)
128133
_log.debug("UserService User updated")

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
asyncio_default_fixture_loop_scope = function

test/service/test_user_service.py

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# python unittest for user_service layer with mocking repository
2+
import unittest
23
from datetime import datetime
3-
from unittest.mock import AsyncMock
4+
from unittest.mock import AsyncMock, patch, MagicMock
45

5-
import pytest
6+
from beanie import init_beanie
7+
from mongomock_motor import AsyncMongoMockClient
68

7-
from app.schema.user_dto import UserCreate, UserUpdate, UserDTO
9+
from app.entity import User
10+
from app.schema.user_dto import UserCreate, UserUpdate
11+
from app.schema.user_dto import UserDTO
812
from app.security.jwt_token import JWTUser
913
from app.service.user_service import UserService
10-
from unittest.mock import AsyncMock, patch
11-
from app.schema.user_dto import UserDTO
12-
from app.service.user_service import send_creation_email
1314

1415

1516
# region init entity
@@ -20,7 +21,7 @@ def _get_jwt_user():
2021
sub="system",
2122
email="system@system.com",
2223
scopes=["system"],
23-
expires=1000,
24+
exp=1000,
2425
token="token",
2526
)
2627

@@ -63,54 +64,59 @@ def _get_dto():
6364
)
6465

6566

67+
def _get_mock_dto():
68+
dto = MagicMock()
69+
dto.first_name = "test"
70+
dto.last_name = "test"
71+
dto.email = "test@test.com"
72+
dto.is_active = True
73+
dto.roles = ["test"]
74+
dto.user_id = "test"
75+
dto.username = "test"
76+
dto.created_by = "system"
77+
dto.created_date = datetime(2024, 1, 1)
78+
dto.last_updated_by = "system"
79+
dto.last_updated_date = datetime(2024, 1, 1)
80+
dto.password = "password"
81+
return dto
82+
83+
6684
def _get_service(repository):
6785
return UserService(user_repository=repository)
6886

6987

7088
# endregion init entity
7189

7290

73-
# success case for create
74-
@pytest.mark.asyncio
75-
async def test_given_valid_create_entity_when_create_then_return_dto():
76-
# Arrange
77-
@pytest.mark.asyncio
78-
async def test_send_creation_email_success():
91+
# region test_service
92+
class TestUserService(unittest.IsolatedAsyncioTestCase):
93+
94+
async def asyncSetUp(self):
95+
self.mock_repository = AsyncMock()
96+
self.service = _get_service(self.mock_repository)
97+
self.client = AsyncMongoMockClient()
98+
await init_beanie(document_models=[User], database=self.client.get_database(name="pyfapi"))
99+
100+
async def asyncTearDown(self):
101+
await self.client.drop_database("pyfapi")
102+
103+
@patch("app.service.user_service.send_creation_email", new_callable=AsyncMock)
104+
async def test_given_create_entity_when_create_then_send_email_success(self, mock_send_email):
79105
# Arrange
80-
user = _get_dto()
81-
with patch("app.service.email_service.send_email", new_callable=AsyncMock) as mock_send_email:
82-
# Act
83-
await send_creation_email(user)
84-
85-
# Assert
86-
mock_send_email.assert_called_once_with(
87-
"john.doe@example.com",
88-
"Welcome to the TestApp",
89-
"Hello John,\n\n"
90-
"Welcome to the TestApp. Your account has been created successfully.\n\n"
91-
"Please visit http://testapp.com to login to your account.\n\n"
92-
"TestApp Team."
93-
)
94-
95-
96-
@pytest.mark.asyncio
97-
async def test_send_creation_email_failure():
98-
# Arrange
99-
user = _get_dto()
100-
with patch("app.service.email_service.send_email", new_callable=AsyncMock) as mock_send_email:
101-
mock_send_email.side_effect = Exception("Email service failure")
106+
create_entity = _get_create_entity()
107+
jwt_user = _get_jwt_user()
108+
109+
fvo = _get_mock_dto()
110+
111+
self.mock_repository.create.return_value = fvo
102112

103113
# Act
104-
with pytest.raises(Exception) as exc_info:
105-
await send_creation_email(user)
114+
result = await self.service.create(create_entity, jwt_user)
106115

107116
# Assert
108-
assert str(exc_info.value) == "Email service failure"
109-
mock_send_email.assert_called_once_with(
110-
"john.doe@example.com",
111-
"Welcome to the TestApp",
112-
"Hello John,\n\n"
113-
"Welcome to the TestApp. Your account has been created successfully.\n\n"
114-
"Please visit http://testapp.com to login to your account.\n\n"
115-
"TestApp Team."
116-
)
117+
self.mock_repository.create.assert_called_once()
118+
mock_send_email.assert_called_once_with(UserDTO.model_validate(fvo))
119+
self.assertEqual(result.user_id, fvo.user_id)
120+
self.assertEqual(result, UserDTO.model_validate(fvo))
121+
122+
# endregion test_service

0 commit comments

Comments
 (0)