Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/conode/application/manage_profile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
UpdateCurrentUserProfileInteractor,
UpdateCurrentUserProfileRequestDTO,
)
from .update_user_profile import (
UpdateUserProfileInteractor,
UpdateUserProfileRequestDTO,
)

__all__ = (
"UpdateCurrentUserProfileInteractor",
"UpdateCurrentUserProfileRequestDTO",
"UpdateUserProfileInteractor",
"UpdateUserProfileRequestDTO",
)
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ async def execute(self, request: UpdateCurrentUserProfileRequestDTO) -> None:
username=request.username,
bio=request.bio,
)

await self.user_repository.update(user)
44 changes: 44 additions & 0 deletions src/conode/application/manage_profile/update_user_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from dataclasses import dataclass

from conode.application.interfaces.repositories import UserRepository
from conode.application.interfaces.transaction_manager import TransactionManager
from conode.application.services.access_control import AccessControlService
from conode.domain.user import UserId


@dataclass(frozen=True, slots=True, kw_only=True)
class UpdateUserProfileRequestDTO:
first_name: str
last_name: str
username: str
bio: str


@dataclass
class UpdateUserProfileInteractor:
access_control_service: AccessControlService
transaction_manager: TransactionManager
user_repository: UserRepository

async def execute(
self, user_id: UserId, request: UpdateUserProfileRequestDTO
) -> None:
async with self.transaction_manager:
user = await self.access_control_service.get_authorized_user()

if user.id == user_id:
target = user
else:
self.access_control_service.ensure_user_can_manipulate_user_profiles(
user
)
target = await self.user_repository.get_by_id(user_id)

target.update_profile(
username=request.username,
first_name=request.first_name,
last_name=request.last_name,
bio=request.bio,
)

await self.user_repository.update(target)
6 changes: 5 additions & 1 deletion src/conode/application/services/access_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
RolePermission,
RolePermissionEntityId,
)
from conode.domain.user import Email, User, UserId
from conode.domain.user import Email, User, UserId, UserSystemRole

type RolesPermissions = list[RolePermission]

Expand Down Expand Up @@ -289,3 +289,7 @@ async def ensure_user_can_manipulate_groups(
raise NotEnoughRightsError(
"Not enough rights to perform operation", None
)

def ensure_user_can_manipulate_user_profiles(self, user: User) -> None:
if user.system_role != UserSystemRole.ADMIN:
raise NotEnoughRightsError("Not enough rights to perform operation", None)
38 changes: 21 additions & 17 deletions src/conode/bootstrap/di/providers/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
from conode.application.manage_edge import CreateEdgeInteractor, DeleteEdgeInteractor
from conode.application.manage_group import CreateGroupInteractor, DeleteGroupInteractor
from conode.application.manage_node import CreateNodeInteractor, DeleteNodeInteractor
from conode.application.manage_profile import UpdateCurrentUserProfileInteractor
from conode.application.manage_profile import (
UpdateCurrentUserProfileInteractor,
UpdateUserProfileInteractor,
)
from conode.application.manage_role import (
CreateRoleInteractor,
DeleteRoleInteractor,
Expand Down Expand Up @@ -63,41 +66,42 @@

class ApplicationProvider(Provider):
provides = provide_all(
UpdateNodeInteractor,
AccessControlService,
OfferAcceptanceService,
OfferSendingService,
DeleteEdgeInteractor,
UpdateEdgeWeightInteractor,
IncrementEdgeWeightInteractor,
GetGroupByIdInteractor,
GetContextByIdInteractor,
UpdateCurrentUserProfileInteractor,
SendOfferToCompanyInteractor,
IncrementEdgeWeightInteractor,
DecrementEdgeWeightInteractor,
SendOfferToCompanyInteractor,
RevokeRoleFromUserInteractor,
UpdateUserProfileInteractor,
GetNodeNeighboursInteractor,
GetUserByUsernameInteractor,
UpdateEdgeWeightInteractor,
FindShortestPathInteractor,
GetNodesByGroupInteractor,
RegisterCompanyInteractor,
GetCurrentUserInteractor,
CreateContextInteractor,
GetContextByIdInteractor,
GiveRoleToUserInteractor,
VerifyCompanyInteractor,
DeleteContextInteractor,
CreateContextInteractor,
OfferAcceptanceService,
GetGroupByIdInteractor,
DeclineOfferInteractor,
AcceptOfferInteractor,
CreateGroupInteractor,
DeleteGroupInteractor,
UpdateNodeInteractor,
AccessControlService,
DeleteEdgeInteractor,
CreateNodeInteractor,
DeleteRoleInteractor,
RoleManagmentService,
UpdateRoleInteractor,
DeleteNodeInteractor,
AcceptOfferInteractor,
DeclineOfferInteractor,
DetachNodeInteractor,
GetNodeNeighboursInteractor,
CreateRoleInteractor,
AttachNodeInteractor,
CreateEdgeInteractor,
GiveRoleToUserInteractor,
RevokeRoleFromUserInteractor,
OfferSendingService,
scope=Scope.REQUEST,
)
26 changes: 25 additions & 1 deletion src/conode/presentation/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,31 @@ class UpdateCurrentUserProfileRequest(BaseModel):
max_length=MAX_ALLOWED_LAST_NAME_LENGTH,
),
]
password: Annotated[str, Field(min_length=7, max_length=100)]
bio: Annotated[str, Field(max_length=MAX_ALLOWED_BIO_LENGTH)]


class UpdateUserProfileRequest(BaseModel):
username: Annotated[
str,
Field(
min_length=MIN_ALLOWED_USERNAME_LENGTH,
max_length=MAX_ALLOWED_USERNAME_LENGTH,
),
]
first_name: Annotated[
str,
Field(
min_length=MIN_ALLOWED_FIRST_NAME_LENGTH,
max_length=MAX_ALLOWED_FIRST_NAME_LENGTH,
),
]
last_name: Annotated[
str,
Field(
min_length=MIN_ALLOWED_LAST_NAME_LENGTH,
max_length=MAX_ALLOWED_LAST_NAME_LENGTH,
),
]
bio: Annotated[str, Field(max_length=MAX_ALLOWED_BIO_LENGTH)]


Expand Down
20 changes: 20 additions & 0 deletions src/conode/presentation/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from conode.application.manage_profile import (
UpdateCurrentUserProfileInteractor,
UpdateCurrentUserProfileRequestDTO,
UpdateUserProfileInteractor,
UpdateUserProfileRequestDTO,
)
from conode.application.manage_user_rights import (
GiveRoleToUserInteractor,
Expand All @@ -19,6 +21,7 @@
from conode.domain.user import UserId
from conode.presentation.schemas.user import (
UpdateCurrentUserProfileRequest,
UpdateUserProfileRequest,
UserSchema,
)

Expand All @@ -40,6 +43,23 @@ async def update_current_user_profile(
)


@router.put("/me/profile/{user_id}", status_code=HTTPStatus.NO_CONTENT)
async def update_user_profile(
user_id: UserId,
request: UpdateUserProfileRequest,
interactor: FromDishka[UpdateUserProfileInteractor],
) -> None:
await interactor.execute(
user_id,
UpdateUserProfileRequestDTO(
username=request.username,
first_name=request.first_name,
last_name=request.last_name,
bio=request.bio,
),
)


@router.get("/me/profile")
async def get_current_user_profile(
interactor: FromDishka[GetCurrentUserInteractor],
Expand Down
94 changes: 94 additions & 0 deletions tests/e2e/user/test_user_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from http import HTTPStatus

import pytest
from dirty_equals import IsPartialDataclass, IsPartialDict
from httpx import AsyncClient

from conode.application.interfaces.repositories import UserRepository
from conode.domain.user import Bio, FirstName, LastName, Username
from tests.factories.common import authorization_headers
from tests.factories.models import UserFactory
from tests.factories.schemas import (
UpdateUserProfileRequestFactory,
)


@pytest.mark.asyncio
async def test_update_foreign_user_profile_ok(
transport: AsyncClient,
user_factory: UserFactory,
user_repository: UserRepository,
) -> None:
users = (await user_factory.build(admin=True), await user_factory.build())

request = UpdateUserProfileRequestFactory.build()

response = await transport.put(
f"/users/me/profile/{users[1].user.id}",
json=request.model_dump(mode="json"),
headers=authorization_headers(users[0].access_token),
)
result = await user_repository.get_by_id(users[1].user.id)

assert response.status_code == HTTPStatus.NO_CONTENT
assert result == IsPartialDataclass(
first_name=FirstName(request.first_name),
last_name=LastName(request.last_name),
username=Username(request.username),
bio=Bio(request.bio),
)


@pytest.mark.asyncio
async def test_update_self_user_profile_ok(
transport: AsyncClient,
user_factory: UserFactory,
user_repository: UserRepository,
) -> None:
user = await user_factory.build()

request = UpdateUserProfileRequestFactory.build()

response = await transport.put(
f"/users/me/profile/{user.user.id}",
json=request.model_dump(mode="json"),
headers=authorization_headers(user.access_token),
)
result = await user_repository.get_by_id(user.user.id)

assert response.status_code == HTTPStatus.NO_CONTENT
assert result == IsPartialDataclass(
first_name=FirstName(request.first_name),
last_name=LastName(request.last_name),
username=Username(request.username),
bio=Bio(request.bio),
)


@pytest.mark.asyncio
async def test_update_foreign_user_profile_without_correct_rights(
transport: AsyncClient,
user_factory: UserFactory,
user_repository: UserRepository,
) -> None:
users = (await user_factory.build(), await user_factory.build())

request = UpdateUserProfileRequestFactory.build()

response = await transport.put(
f"/users/me/profile/{users[1].user.id}",
json=request.model_dump(mode="json"),
headers=authorization_headers(users[0].access_token),
)
result = await user_repository.get_by_id(users[1].user.id)

assert response.status_code == HTTPStatus.FORBIDDEN
assert response.json() == IsPartialDict(
detail="Not enough rights to perform operation", meta=None
)
assert result == IsPartialDataclass(
first_name=users[1].user.first_name,
last_name=users[1].user.last_name,
username=users[1].user.username,
bio=users[1].user.bio,
)
2 changes: 2 additions & 0 deletions tests/factories/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
)
from .user import (
UpdateCurrentUserProfileRequestFactory,
UpdateUserProfileRequestFactory,
)

__all__ = (
Expand All @@ -20,4 +21,5 @@
"RegisterCompanyRequestFactory",
"UpdateCurrentUserProfileRequestFactory",
"UpdateNodeRequestFactory",
"UpdateUserProfileRequestFactory",
)
4 changes: 4 additions & 0 deletions tests/factories/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

from conode.presentation.schemas.user import (
UpdateCurrentUserProfileRequest,
UpdateUserProfileRequest,
)


class UpdateCurrentUserProfileRequestFactory(
ModelFactory[UpdateCurrentUserProfileRequest]
): ...


class UpdateUserProfileRequestFactory(ModelFactory[UpdateUserProfileRequest]): ...
Loading