Skip to content

Commit

Permalink
feat: add a method for updating the user profile
Browse files Browse the repository at this point in the history
  • Loading branch information
nidemidovich committed Jul 20, 2022
1 parent 3b34129 commit 6d7bdbf
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 1 deletion.
54 changes: 54 additions & 0 deletions pybotx/bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions pybotx/client/exceptions/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@

class UserNotFoundError(BaseClientError):
"""User not found."""


class InvalidProfileDataError(BaseClientError):
"""Invalid profile data."""
87 changes: 87 additions & 0 deletions pybotx/client/users_api/update_user_profile.py
Original file line number Diff line number Diff line change
@@ -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,
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
Expand Down
182 changes: 182 additions & 0 deletions tests/client/users_api/test_update_user_profile.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 6d7bdbf

Please sign in to comment.