From 6d7bdbf897fdb64831a331ba144edd0d2e07964a Mon Sep 17 00:00:00 2001 From: Nikita Demidovich Date: Tue, 19 Jul 2022 18:12:19 +0300 Subject: [PATCH] feat: add a method for updating the user profile --- pybotx/bot/bot.py | 54 ++++++ pybotx/client/exceptions/users.py | 4 + .../client/users_api/update_user_profile.py | 87 +++++++++ pyproject.toml | 2 +- .../users_api/test_update_user_profile.py | 182 ++++++++++++++++++ 5 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 pybotx/client/users_api/update_user_profile.py create mode 100644 tests/client/users_api/test_update_user_profile.py diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index d3337464..fd947add 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -144,6 +144,10 @@ BotXAPISearchUserByOtherIdRequestPayload, SearchUserByOtherIdMethod, ) +from pybotx.client.users_api.update_user_profile import ( + BotXAPIUpdateUserProfileRequestPayload, + UpdateUsersProfileMethod, +) from pybotx.constants import BOTX_DEFAULT_TIMEOUT, STICKER_PACKS_PER_PAGE from pybotx.converters import optional_sequence_to_list from pybotx.image_validators import ( @@ -1137,6 +1141,56 @@ async def search_user_by_other_id( return botx_api_user_from_search.to_domain() + async def update_user_profile( + self, + *, + bot_id: UUID, + user_huid: UUID, + avatar: Missing[Union[IncomingFileAttachment, OutgoingAttachment]] = Undefined, + name: Missing[str] = Undefined, + public_name: Missing[str] = Undefined, + company: Missing[str] = Undefined, + company_position: Missing[str] = Undefined, + description: Missing[str] = Undefined, + department: Missing[str] = Undefined, + office: Missing[str] = Undefined, + manager: Missing[str] = Undefined, + ) -> None: + """Update user profile. + + :param bot_id: Bot which should perform the request. + :param user_huid: User huid whose profile needs to be updated. + :param avatar: New user avatar. + :param name: New user name. + :param public_name: New user public name. + :param company: New user company. + :param company_position: New user company position. + :param description: New user description. + :param department: New user department. + :param office: New user office. + :param manager: New user manager. + """ + method = UpdateUsersProfileMethod( + bot_id, + self._httpx_client, + self._bot_accounts_storage, + ) + + payload = BotXAPIUpdateUserProfileRequestPayload.from_domain( + user_huid=user_huid, + avatar=avatar, + name=name, + public_name=public_name, + company=company, + company_position=company_position, + description=description, + department=department, + office=office, + manager=manager, + ) + + await method.execute(payload) + # - SmartApps API - async def send_smartapp_event( self, diff --git a/pybotx/client/exceptions/users.py b/pybotx/client/exceptions/users.py index b3eed6b4..71d14361 100644 --- a/pybotx/client/exceptions/users.py +++ b/pybotx/client/exceptions/users.py @@ -3,3 +3,7 @@ class UserNotFoundError(BaseClientError): """User not found.""" + + +class InvalidProfileDataError(BaseClientError): + """Invalid profile data.""" diff --git a/pybotx/client/users_api/update_user_profile.py b/pybotx/client/users_api/update_user_profile.py new file mode 100644 index 00000000..510ec8ce --- /dev/null +++ b/pybotx/client/users_api/update_user_profile.py @@ -0,0 +1,87 @@ +from typing import Literal, Union +from uuid import UUID + +from pybotx.client.authorized_botx_method import AuthorizedBotXMethod +from pybotx.client.botx_method import response_exception_thrower +from pybotx.client.exceptions.users import InvalidProfileDataError, UserNotFoundError +from pybotx.missing import Missing, Undefined +from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel +from pybotx.models.attachments import ( + BotXAPIAttachment, + IncomingFileAttachment, + OutgoingAttachment, +) + + +class BotXAPIUpdateUserProfileRequestPayload(UnverifiedPayloadBaseModel): + user_huid: UUID + name: Missing[str] = Undefined + public_name: Missing[str] = Undefined + avatar: Missing[BotXAPIAttachment] = Undefined + company: Missing[str] = Undefined + company_position: Missing[str] = Undefined + description: Missing[str] = Undefined + department: Missing[str] = Undefined + office: Missing[str] = Undefined + manager: Missing[str] = Undefined + + @classmethod + def from_domain( + cls, + user_huid: UUID, + avatar: Missing[Union[IncomingFileAttachment, OutgoingAttachment]] = Undefined, + name: Missing[str] = Undefined, + public_name: Missing[str] = Undefined, + company: Missing[str] = Undefined, + company_position: Missing[str] = Undefined, + description: Missing[str] = Undefined, + department: Missing[str] = Undefined, + office: Missing[str] = Undefined, + manager: Missing[str] = Undefined, + ) -> "BotXAPIUpdateUserProfileRequestPayload": + api_avatar: Missing[BotXAPIAttachment] = Undefined + if avatar: + api_avatar = BotXAPIAttachment.from_file_attachment(avatar) + + return BotXAPIUpdateUserProfileRequestPayload( + user_huid=user_huid, + name=name, + public_name=public_name, + avatar=api_avatar, + company=company, + company_position=company_position, + description=description, + department=department, + office=office, + manager=manager, + ) + + +class BotXAPIUpdateUserProfileResponsePayload(VerifiedPayloadBaseModel): + status: Literal["ok"] + result: Literal[True] + + +class UpdateUsersProfileMethod(AuthorizedBotXMethod): + status_handlers = { + **AuthorizedBotXMethod.status_handlers, + 400: response_exception_thrower(InvalidProfileDataError), + 404: response_exception_thrower(UserNotFoundError), + } + + async def execute( + self, + payload: BotXAPIUpdateUserProfileRequestPayload, + ) -> BotXAPIUpdateUserProfileResponsePayload: + path = "/api/v3/botx/users/update_profile" + + response = await self._botx_method_call( + "PUT", + self._build_url(path), + json=payload.jsonable_dict(), + ) + + return self._verify_and_extract_api_model( + BotXAPIUpdateUserProfileResponsePayload, + response, + ) diff --git a/pyproject.toml b/pyproject.toml index 17c1fd82..582edf8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybotx" -version = "0.45.1" +version = "0.46.0" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", diff --git a/tests/client/users_api/test_update_user_profile.py b/tests/client/users_api/test_update_user_profile.py new file mode 100644 index 00000000..53e8e4d9 --- /dev/null +++ b/tests/client/users_api/test_update_user_profile.py @@ -0,0 +1,182 @@ +from http import HTTPStatus +from uuid import UUID + +import httpx +import pytest +from respx.router import MockRouter + +from pybotx import Bot, HandlerCollector, lifespan_wrapper +from pybotx.client.exceptions.users import InvalidProfileDataError +from pybotx.models.attachments import AttachmentImage +from pybotx.models.bot_account import BotAccountWithSecret +from pybotx.models.enums import AttachmentTypes + +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.mock_authorization, + pytest.mark.usefixtures("respx_mock"), +] + + +@pytest.fixture +def avatar() -> AttachmentImage: + return AttachmentImage( + type=AttachmentTypes.IMAGE, + filename="avatar.png", + size=len(b"Hello, world!"), + is_async_file=False, + content=b"Hello, world!", + ) + + +async def test__update_user_profile__minimal_update_succeed( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + endpoint = respx_mock.put( + f"https://{host}/api/v3/botx/users/update_profile", + headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, + json={ + "user_huid": "6fafda2c-6505-57a5-a088-25ea5d1d0364", + }, + ).mock( + return_value=httpx.Response( + HTTPStatus.OK, + json={ + "status": "ok", + "result": True, + }, + ), + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + await bot.update_user_profile( + bot_id=bot_id, + user_huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + ) + + # - Assert - + assert endpoint.called + + +async def test__update_user_profile__maximum_update_succeed( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, + avatar: AttachmentImage, +) -> None: + # - Arrange - + endpoint = respx_mock.put( + f"https://{host}/api/v3/botx/users/update_profile", + headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, + json={ + "avatar": { + "data": "data:image/png;base64,SGVsbG8sIHdvcmxkIQ==", + "file_name": "avatar.png", + }, + "company": "Doge Co", + "company_position": "Chief", + "department": "Commercy", + "description": "Just boss", + "manager": "Bob", + "name": "John Bork", + "office": "Moscow", + "public_name": "Johny B.", + "user_huid": "6fafda2c-6505-57a5-a088-25ea5d1d0364", + }, + ).mock( + return_value=httpx.Response( + HTTPStatus.OK, + json={ + "status": "ok", + "result": True, + }, + ), + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + await bot.update_user_profile( + bot_id=bot_id, + user_huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + avatar=avatar, + name="John Bork", + public_name="Johny B.", + company="Doge Co", + company_position="Chief", + description="Just boss", + department="Commercy", + office="Moscow", + manager="Bob", + ) + + # - Assert - + assert endpoint.called + + +async def test__update_user_profile__invalid_profile_data_error( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + endpoint = respx_mock.put( + f"https://{host}/api/v3/botx/users/update_profile", + headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, + json={ + "company": "Doge Co", + "company_position": "Chief", + "department": "Commercy", + "description": "Just boss", + "manager": "Bob", + "name": "John Bork", + "office": "Moscow", + "public_name": "Johny B.", + "user_huid": "6fafda2c-6505-57a5-a088-25ea5d1d0364", + }, + ).mock( + return_value=httpx.Response( + HTTPStatus.BAD_REQUEST, + json={ + "status": "error", + "reason": "invalid_profile", + "errors": [], + "error_data": { + "errors": {"field": "invalid"}, + "error_description": "Invalid profile data", + "user_huid": "6fafda2c-6505-57a5-a088-25ea5d1d0364", + }, + }, + ), + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(InvalidProfileDataError): + await bot.update_user_profile( + bot_id=bot_id, + user_huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + name="John Bork", + public_name="Johny B.", + company="Doge Co", + company_position="Chief", + description="Just boss", + department="Commercy", + office="Moscow", + manager="Bob", + ) + + # - Assert - + assert endpoint.called