diff --git a/README.md b/README.md index db365270..7e46b5cb 100644 --- a/README.md +++ b/README.md @@ -234,16 +234,11 @@ collector = HandlerCollector() @collector.sync_smartapp_event async def handle_sync_smartapp_event( event: SmartAppEvent, bot: Bot, -) -> SyncSmartAppEventResponsePayload: +) -> BotAPISyncSmartAppEventResultResponse: print(f"Got sync smartapp event: {event}") - return SyncSmartAppEventResponsePayload.from_domain( - ref=event.ref, - smartapp_id=event.bot.id, - chat_id=event.chat.id, + return BotAPISyncSmartAppEventResultResponse.from_domain( data={}, - opts={}, files=[], - encrypted=True, ) ``` diff --git a/pybotx/__init__.py b/pybotx/__init__.py index 3aac90e0..cb0539cb 100644 --- a/pybotx/__init__.py +++ b/pybotx/__init__.py @@ -64,9 +64,6 @@ SmartappManifest, SmartappManifestWebParams, ) -from pybotx.client.smartapps_api.sync_smartapp_event import ( - SyncSmartAppEventResponsePayload, -) from pybotx.client.stickers_api.exceptions import ( InvalidEmojiError, InvalidImageError, @@ -126,6 +123,11 @@ from pybotx.models.smartapps import SmartApp from pybotx.models.status import BotMenu, StatusRecipient from pybotx.models.stickers import Sticker, StickerPack +from pybotx.models.sync_smartapp_event import ( + BotAPISyncSmartAppEventErrorResponse, + BotAPISyncSmartAppEventResponse, + BotAPISyncSmartAppEventResultResponse, +) from pybotx.models.system_events.added_to_chat import AddedToChatEvent from pybotx.models.system_events.chat_created import ChatCreatedEvent, ChatCreatedMember from pybotx.models.system_events.chat_deleted_by_user import ChatDeletedByUserEvent @@ -152,6 +154,9 @@ "BotAPIBotDisabledErrorData", "BotAPIBotDisabledResponse", "BotAPIMethodFailedCallback", + "BotAPISyncSmartAppEventErrorResponse", + "BotAPISyncSmartAppEventResponse", + "BotAPISyncSmartAppEventResultResponse", "BotAPIUnverifiedRequestErrorData", "BotAPIUnverifiedRequestResponse", "BotAccount", @@ -228,7 +233,6 @@ "RequestHeadersNotProvidedError", "SmartApp", "SmartAppEvent", - "SyncSmartAppEventResponsePayload", "SmartappManifest", "SmartappManifestWebLayoutChoices", "SmartappManifestWebParams", diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index ca563512..b311cf32 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -148,9 +148,6 @@ BotXAPISmartAppsListRequestPayload, SmartAppsListMethod, ) -from pybotx.client.smartapps_api.sync_smartapp_event import ( - SyncSmartAppEventResponsePayload, -) from pybotx.client.smartapps_api.upload_file import ( UploadFileMethod as SmartappsUploadFileMethod, ) @@ -221,7 +218,7 @@ ensure_file_content_is_png, ensure_sticker_image_size_valid, ) -from pybotx.logger import logger, pformat_jsonable_obj, trim_file_data_in_incoming_json +from pybotx.logger import log_incoming_request, logger, pformat_jsonable_obj from pybotx.missing import Missing, MissingOptional, Undefined from pybotx.models.async_files import File from pybotx.models.attachments import IncomingFileAttachment, OutgoingAttachment @@ -245,10 +242,11 @@ build_bot_status_response, ) from pybotx.models.stickers import Sticker, StickerPack, StickerPackFromList -from pybotx.models.system_events.smartapp_event import ( - BotAPISmartAppEvent, - SmartAppEvent, +from pybotx.models.sync_smartapp_event import ( + BotAPISyncSmartAppEvent, + BotAPISyncSmartAppEventResponse, ) +from pybotx.models.system_events.smartapp_event import SmartAppEvent from pybotx.models.users import UserFromCSV, UserFromSearch MissingOptionalAttachment = MissingOptional[ @@ -299,16 +297,9 @@ def async_execute_raw_bot_command( logging_command: bool = True, ) -> None: if logging_command: - logger.opt(lazy=True).debug( - "Got command: {command}", - command=lambda: pformat_jsonable_obj( - trim_file_data_in_incoming_json(raw_bot_command), - ), - ) + log_incoming_request(raw_bot_command, message="Got command: ") if verify_request: - if request_headers is None: - raise RequestHeadersNotProvidedError self._verify_request(request_headers) try: @@ -338,23 +329,19 @@ async def sync_execute_raw_smartapp_event( verify_request: bool = True, request_headers: Optional[Mapping[str, str]] = None, logging_command: bool = True, - ) -> SyncSmartAppEventResponsePayload: + ) -> BotAPISyncSmartAppEventResponse: if logging_command: - logger.opt(lazy=True).debug( - "Got sync smartapp event: {command}", - command=lambda: pformat_jsonable_obj( - trim_file_data_in_incoming_json(raw_smartapp_event), - ), + log_incoming_request( + raw_smartapp_event, + message="Got sync smartapp event: ", ) if verify_request: - if request_headers is None: - raise RequestHeadersNotProvidedError self._verify_request(request_headers) try: - bot_api_smartapp_event: BotAPISmartAppEvent = parse_obj_as( - BotAPISmartAppEvent, + bot_api_smartapp_event: BotAPISyncSmartAppEvent = parse_obj_as( + BotAPISyncSmartAppEvent, raw_smartapp_event, ) except ValidationError as validation_exc: @@ -368,7 +355,7 @@ async def sync_execute_raw_smartapp_event( async def sync_execute_smartapp_event( self, smartapp_event: SmartAppEvent, - ) -> SyncSmartAppEventResponsePayload: + ) -> BotAPISyncSmartAppEventResponse: self._bot_accounts_storage.ensure_bot_id_exists(smartapp_event.bot.id) return await self._handler_collector.handle_sync_smartapp_event( self, @@ -2028,7 +2015,13 @@ async def collect_metric( ) await method.execute(payload) - def _verify_request(self, headers: Mapping[str, str]) -> None: # noqa: WPS238 + def _verify_request( # noqa: WPS231, WPS238 + self, + headers: Optional[Mapping[str, str]], + ) -> None: + if headers is None: + raise RequestHeadersNotProvidedError + authorization_header = headers.get("authorization") if not authorization_header: raise UnverifiedRequestError("The authorization token was not provided.") diff --git a/pybotx/bot/handler.py b/pybotx/bot/handler.py index ea9faec3..22345003 100644 --- a/pybotx/bot/handler.py +++ b/pybotx/bot/handler.py @@ -2,12 +2,10 @@ from functools import partial from typing import TYPE_CHECKING, Awaitable, Callable, List, Literal, TypeVar, Union -from pybotx.client.smartapps_api.sync_smartapp_event import ( - SyncSmartAppEventResponsePayload, -) from pybotx.models.commands import BotCommand from pybotx.models.message.incoming_message import IncomingMessage from pybotx.models.status import StatusRecipient +from pybotx.models.sync_smartapp_event import BotAPISyncSmartAppEventResponse from pybotx.models.system_events.added_to_chat import AddedToChatEvent from pybotx.models.system_events.chat_created import ChatCreatedEvent from pybotx.models.system_events.chat_deleted_by_user import ChatDeletedByUserEvent @@ -29,7 +27,7 @@ SyncSmartAppEventHandlerFunc = Callable[ [SmartAppEvent, "Bot"], - Awaitable[SyncSmartAppEventResponsePayload], + Awaitable[BotAPISyncSmartAppEventResponse], ] IncomingMessageHandlerFunc = HandlerFunc[IncomingMessage] diff --git a/pybotx/bot/handler_collector.py b/pybotx/bot/handler_collector.py index c203110c..5b5d88ca 100644 --- a/pybotx/bot/handler_collector.py +++ b/pybotx/bot/handler_collector.py @@ -33,14 +33,12 @@ ExceptionMiddleware, ) from pybotx.client.smartapps_api.exceptions import SyncSmartAppEventHandlerNotFoundError -from pybotx.client.smartapps_api.sync_smartapp_event import ( - SyncSmartAppEventResponsePayload, -) from pybotx.converters import optional_sequence_to_list from pybotx.logger import logger from pybotx.models.commands import BotCommand, SystemEvent from pybotx.models.message.incoming_message import IncomingMessage from pybotx.models.status import BotMenu, StatusRecipient +from pybotx.models.sync_smartapp_event import BotAPISyncSmartAppEventResponse from pybotx.models.system_events.added_to_chat import AddedToChatEvent from pybotx.models.system_events.chat_created import ChatCreatedEvent from pybotx.models.system_events.chat_deleted_by_user import ChatDeletedByUserEvent @@ -126,7 +124,7 @@ async def handle_sync_smartapp_event( self, bot: "Bot", smartapp_event: SmartAppEvent, - ) -> SyncSmartAppEventResponsePayload: + ) -> BotAPISyncSmartAppEventResponse: if not isinstance(smartapp_event, SmartAppEvent): raise NotImplementedError( f"Unsupported event type for sync smartapp event: `{smartapp_event}`", diff --git a/pybotx/client/smartapps_api/smartapp_event.py b/pybotx/client/smartapps_api/smartapp_event.py index abf7c61f..e8649ac5 100644 --- a/pybotx/client/smartapps_api/smartapp_event.py +++ b/pybotx/client/smartapps_api/smartapp_event.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Literal, Type, TypeVar +from typing import Any, Dict, List, Literal from uuid import UUID from pybotx.client.authorized_botx_method import AuthorizedBotXMethod @@ -7,11 +7,6 @@ from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel from pybotx.models.async_files import APIAsyncFile, File, convert_async_file_from_domain -TBotXAPISmartAppEventRequestPayload = TypeVar( - "TBotXAPISmartAppEventRequestPayload", - bound="BotXAPISmartAppEventRequestPayload", -) - class BotXAPISmartAppEventRequestPayload(UnverifiedPayloadBaseModel): ref: MissingOptional[UUID] @@ -25,7 +20,7 @@ class BotXAPISmartAppEventRequestPayload(UnverifiedPayloadBaseModel): @classmethod def from_domain( - cls: Type[TBotXAPISmartAppEventRequestPayload], + cls, ref: MissingOptional[UUID], smartapp_id: UUID, chat_id: UUID, @@ -33,7 +28,7 @@ def from_domain( opts: Missing[Dict[str, Any]], files: Missing[List[File]], encrypted: bool, - ) -> TBotXAPISmartAppEventRequestPayload: + ) -> "BotXAPISmartAppEventRequestPayload": api_async_files: Missing[List[APIAsyncFile]] = Undefined if files: api_async_files = [convert_async_file_from_domain(file) for file in files] diff --git a/pybotx/client/smartapps_api/sync_smartapp_event.py b/pybotx/client/smartapps_api/sync_smartapp_event.py deleted file mode 100644 index 99cd788d..00000000 --- a/pybotx/client/smartapps_api/sync_smartapp_event.py +++ /dev/null @@ -1,7 +0,0 @@ -from pybotx.client.smartapps_api.smartapp_event import ( - BotXAPISmartAppEventRequestPayload, -) - - -class SyncSmartAppEventResponsePayload(BotXAPISmartAppEventRequestPayload): - """The response payload to a synchronous smartapp event at /smartapps/request.""" diff --git a/pybotx/logger.py b/pybotx/logger.py index a52a3f70..3a12f95f 100644 --- a/pybotx/logger.py +++ b/pybotx/logger.py @@ -41,6 +41,15 @@ def trim_file_data_in_incoming_json(json_body: Dict[str, Any]) -> Dict[str, Any] return json_body +def log_incoming_request(request: Dict[str, Any], *, message: str = "") -> None: + logger.opt(lazy=True).debug( + message + "{command}", + command=lambda: pformat_jsonable_obj( + trim_file_data_in_incoming_json(request), + ), + ) + + def setup_logger() -> "Logger": return _logger diff --git a/pybotx/models/bot_account.py b/pybotx/models/bot_account.py index 48200525..7fe3a468 100644 --- a/pybotx/models/bot_account.py +++ b/pybotx/models/bot_account.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from functools import cached_property +from typing import Optional from urllib.parse import urlparse from uuid import UUID @@ -9,7 +10,7 @@ @dataclass class BotAccount: id: UUID - host: str + host: Optional[str] class BotAccountWithSecret(BaseModel): diff --git a/pybotx/models/sync_smartapp_event.py b/pybotx/models/sync_smartapp_event.py new file mode 100644 index 00000000..ea012c5b --- /dev/null +++ b/pybotx/models/sync_smartapp_event.py @@ -0,0 +1,148 @@ +import json +from typing import Any, Dict, List, Optional, Union +from uuid import UUID + +from pybotx.missing import Missing, Undefined +from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel +from pybotx.models.async_files import ( + APIAsyncFile, + File, + convert_async_file_from_domain, + convert_async_file_to_domain, +) +from pybotx.models.bot_account import BotAccount +from pybotx.models.chats import Chat +from pybotx.models.enums import ( + BotAPIClientPlatforms, + ChatTypes, + convert_client_platform_to_domain, +) +from pybotx.models.message.incoming_message import UserDevice, UserSender +from pybotx.models.system_events.smartapp_event import SmartAppEvent + + +class BotAPISyncSmartAppSender(VerifiedPayloadBaseModel): + user_huid: UUID + udid: Optional[UUID] + platform: Optional[BotAPIClientPlatforms] + + +class BotAPISyncSmartAppPayload(VerifiedPayloadBaseModel): + data: Dict[str, Any] + files: List[APIAsyncFile] + + +class BotAPISyncSmartAppEvent(VerifiedPayloadBaseModel): + bot_id: UUID + group_chat_id: UUID + sender_info: BotAPISyncSmartAppSender + method: str + payload: BotAPISyncSmartAppPayload + + def to_domain(self, raw_smartapp_event: Dict[str, Any]) -> SmartAppEvent: + platform = ( + convert_client_platform_to_domain(self.sender_info.platform) + if self.sender_info.platform + else None + ) + + device = UserDevice( + platform=platform, + manufacturer=None, + device_name=None, + os=None, + pushes=None, + timezone=None, + permissions=None, + platform_package_id=None, + app_version=None, + locale=None, + ) + + sender = UserSender( + huid=self.sender_info.user_huid, + udid=self.sender_info.udid, + device=device, + ad_login=None, + ad_domain=None, + username=None, + is_chat_admin=None, + is_chat_creator=None, + ) + + return SmartAppEvent( + bot=BotAccount(id=self.bot_id, host=None), + chat=Chat( + id=self.group_chat_id, + type=ChatTypes.PERSONAL_CHAT, + ), + sender=sender, + data={ + "method": self.method, + "type": "smartapp_rpc", + "params": self.payload.data, + }, + ref=None, + smartapp_id=self.bot_id, + opts=None, + files=[convert_async_file_to_domain(file) for file in self.payload.files], + smartapp_api_version=None, + raw_command=raw_smartapp_event, + ) + + +class BotAPISyncSmartAppEventResultResponse(UnverifiedPayloadBaseModel): + data: Any + files: List[APIAsyncFile] + + @classmethod + def from_domain( + cls, + data: Any, + files: Missing[List[File]] = Undefined, + ) -> "BotAPISyncSmartAppEventResultResponse": + api_async_files: List[APIAsyncFile] = [] + if files: + api_async_files = [convert_async_file_from_domain(file) for file in files] + + return cls( + data=data, + files=api_async_files, + ) + + def jsonable_dict(self) -> Dict[str, Any]: + return { + "status": "ok", + "result": json.loads(self.json()), + } + + +class BotAPISyncSmartAppEventErrorResponse(UnverifiedPayloadBaseModel): + reason: str + errors: List[Any] + error_data: Dict[str, Any] + + @classmethod + def from_domain( + cls, + reason: Missing[str] = Undefined, + errors: Missing[List[Any]] = Undefined, + error_data: Missing[Dict[str, Any]] = Undefined, + ) -> "BotAPISyncSmartAppEventErrorResponse": + return cls( + reason="smartapp_error" if reason is Undefined else reason, + errors=[] if errors is Undefined else errors, + error_data={} if error_data is Undefined else error_data, + ) + + def jsonable_dict(self) -> Dict[str, Any]: + return { + "status": "error", + **json.loads(self.json()), + } + + +BotAPISyncSmartAppEventResponse = Union[ + BotAPISyncSmartAppEventResultResponse, + BotAPISyncSmartAppEventErrorResponse, +] diff --git a/pybotx/models/system_events/smartapp_event.py b/pybotx/models/system_events/smartapp_event.py index 4bdcbbf2..1925a4e9 100644 --- a/pybotx/models/system_events/smartapp_event.py +++ b/pybotx/models/system_events/smartapp_event.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Dict, List, Literal +from typing import Any, Dict, List, Literal, Optional from uuid import UUID from pydantic import Field @@ -37,11 +37,11 @@ class SmartAppEvent(BotCommandBase): sender: Event sender. """ - ref: UUID + ref: Optional[UUID] smartapp_id: UUID data: Dict[str, Any] # noqa: WPS110 - opts: Dict[str, Any] - smartapp_api_version: int + opts: Optional[Dict[str, Any]] + smartapp_api_version: Optional[int] files: List[File] chat: Chat sender: UserSender diff --git a/pyproject.toml b/pyproject.toml index c6444d3c..d3b5b0aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybotx" -version = "0.68.1" +version = "0.69.0" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", diff --git a/tests/conftest.py b/tests/conftest.py index 9e2d1eeb..8f57ae1f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,12 +21,12 @@ HandlerCollector, IncomingMessage, SmartAppEvent, - SyncSmartAppEventResponsePayload, UserDevice, UserSender, ) from pybotx.bot.bot_accounts_storage import BotAccountsStorage from pybotx.logger import logger +from pybotx.models.sync_smartapp_event import BotAPISyncSmartAppEventResultResponse @pytest.fixture(autouse=True) @@ -235,62 +235,32 @@ def decorator( bot_id: Optional[UUID] = None, group_chat_id: Optional[UUID] = None, user_huid: Optional[UUID] = None, - host: Optional[str] = None, - attachment: Optional[Dict[str, Any]] = None, async_file: Optional[Dict[str, Any]] = None, method: Optional[str] = None, params: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: return { - "sync_id": "a465f0f3-1354-491c-8f11-f400164295cb", - "command": { - "body": "system:smartapp_event", - "data": { - "ref": "6fafda2c-6505-57a5-a088-25ea5d1d0364", - "smartapp_id": str(bot_id) - if bot_id - else "8dada2c8-67a6-4434-9dec-570d244e78ee", - "data": { - "type": "smartapp_rpc", - "method": method or "folders.get", - "params": params or {}, - }, - "opts": {"option": "test_option"}, - "smartapp_api_version": 1, - }, - "command_type": "system", - "metadata": {}, - }, - "async_files": [async_file] if async_file else [], - "attachments": [attachment] if attachment else [], - "entities": [], - "from": { - "user_huid": str(user_huid) - if user_huid - else "b9197d3a-d855-5d34-ba8a-eff3a975ab20", - "user_udid": None, - "group_chat_id": str(group_chat_id) + "bot_id": str(bot_id) if bot_id else "8dada2c8-67a6-4434-9dec-570d244e78ee", + "group_chat_id": ( + str(group_chat_id) if group_chat_id - else "dea55ee4-7a9f-5da0-8c73-079f400ee517", - "host": host or "cts.example.com", - "ad_login": None, - "ad_domain": None, - "username": None, - "chat_type": "group_chat", - "manufacturer": None, - "device": None, - "device_software": None, - "device_meta": {}, - "platform": None, - "platform_package_id": None, - "is_admin": False, - "is_creator": False, - "app_version": None, - "locale": "en", + else "30dc1980-643a-00ad-37fc-7cc10d74e935" + ), + "sender_info": { + "user_huid": ( + str(user_huid) + if user_huid + else "f16cdc5f-6366-5552-9ecd-c36290ab3d11" + ), + "platform": "web", + "udid": "49eac56a-c0d8-51d7-863e-925028f05110", + }, + "method": method or "list.get", + "payload": { + "ref": "6fafda2c-6505-57a5-a088-25ea5d1d0364", + "data": params or {}, + "files": [async_file] if async_file else [], }, - "bot_id": str(bot_id) if bot_id else "8dada2c8-67a6-4434-9dec-570d244e78ee", - "proto_version": 4, - "source_sync_id": None, } return decorator @@ -370,15 +340,10 @@ def collector_with_sync_smartapp_event_handler() -> HandlerCollector: async def handle_sync_smartapp_event( event: SmartAppEvent, _: Bot, - ) -> SyncSmartAppEventResponsePayload: - return SyncSmartAppEventResponsePayload.from_domain( - ref=event.ref, - smartapp_id=event.bot.id, - chat_id=event.chat.id, + ) -> BotAPISyncSmartAppEventResultResponse: + return BotAPISyncSmartAppEventResultResponse.from_domain( data=event.data, - opts={}, files=event.files, - encrypted=True, ) return collector diff --git a/tests/test_base_command.py b/tests/test_base_command.py index 5de76535..a361a76d 100644 --- a/tests/test_base_command.py +++ b/tests/test_base_command.py @@ -190,7 +190,7 @@ async def test__sync_execute_raw_smartapp_event__not_logging_incoming_request( # - Act - async with lifespan_wrapper(built_bot) as bot: with loguru_caplog.at_level(logging.DEBUG): - bot.async_execute_raw_bot_command( + await bot.sync_execute_raw_smartapp_event( payload, verify_request=False, logging_command=False, diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 442e64a8..566b98f0 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import List +from typing import Any, Callable, Dict, List, Optional from uuid import UUID import httpx @@ -16,7 +16,6 @@ HandlerCollector, IncomingMessage, SmartAppEvent, - SyncSmartAppEventResponsePayload, UnknownBotAccountError, UnverifiedRequestError, build_bot_disabled_response, @@ -25,6 +24,10 @@ from pybotx.bot.api.responses.unverified_request import ( build_unverified_request_response, ) +from pybotx.models.sync_smartapp_event import ( + BotAPISyncSmartAppEventErrorResponse, + BotAPISyncSmartAppEventResultResponse, +) # - Bot setup - collector = HandlerCollector() @@ -39,22 +42,18 @@ async def debug_handler(message: IncomingMessage, bot: Bot) -> None: async def handle_sync_smartapp_event( event: SmartAppEvent, _: Bot, -) -> SyncSmartAppEventResponsePayload: - return SyncSmartAppEventResponsePayload.from_domain( - ref=event.ref, - smartapp_id=event.bot.id, - chat_id=event.chat.id, - data=event.data, - opts={}, +) -> BotAPISyncSmartAppEventResultResponse: + return BotAPISyncSmartAppEventResultResponse.from_domain( + data=event.data["params"], files=event.files, - encrypted=True, ) def bot_factory( bot_accounts: List[BotAccountWithSecret], + bot_collector: Optional[HandlerCollector] = None, ) -> Bot: - return Bot(collectors=[collector], bot_accounts=bot_accounts) + return Bot(collectors=[bot_collector or collector], bot_accounts=bot_accounts) # - FastAPI integration - @@ -417,71 +416,82 @@ def test__web_app__unverified_request_response( } -def test__web_app__sync_smartapp_event(bot: Bot) -> None: +def test__web_app__sync_smartapp_event__success(bot: Bot, bot_id: UUID) -> None: # - Arrange - request_payload = { - "sync_id": "a465f0f3-1354-491c-8f11-f400164295cb", - "command": { - "body": "system:smartapp_event", - "data": { - "ref": "6fafda2c-6505-57a5-a088-25ea5d1d0364", - "smartapp_id": "8dada2c8-67a6-4434-9dec-570d244e78ee", - "data": { - "type": "smartapp_rpc", - "method": "folders.get", - "params": { - "q": 1, - }, + "bot_id": str(bot_id), + "group_chat_id": "8dada2c8-67a6-4434-9dec-570d244e78ee", + "sender_info": { + "user_huid": "ab103983-6001-44e9-889e-d55feb295494", + "platform": "web", + "udid": "49eac56a-c0d8-51d7-863e-925028f05110", + }, + "method": "list.get", + "payload": { + "data": {"category_id": 1}, + "files": [ + { + "file": "/uploads/files/b0232da0bf3d406eb5653e37b2bb6517.bin", + "file_name": "cts1-test.ast-innovation.ru.har", + "file_size": 349372, + "file_hash": "qVSzEUJITWP+TgCvcF3UCzQrBaY3RHqB92CHObz4E70=", + "file_mime_type": "application/octet-stream", + "chunk_size": 2097152, + "file_encryption_algo": "stream", + "file_id": "a0ec914f-8235-5021-9b8d-05c3cd303536", + "type": "document", }, - "opts": {"option": "test_option"}, - "smartapp_api_version": 1, - }, - "command_type": "system", - "metadata": {}, + ], }, - "async_files": [ - { - "type": "image", - "file": "https://link.to/file", - "file_mime_type": "image/png", - "file_name": "pass.png", - "file_preview": "https://link.to/preview", - "file_preview_height": 300, - "file_preview_width": 300, - "file_size": 1502345, - "file_hash": "Jd9r+OKpw5y+FSCg1xNTSUkwEo4nCW1Sn1AkotkOpH0=", - "file_encryption_algo": "stream", - "chunk_size": 2097152, - "file_id": "8dada2c8-67a6-4434-9dec-570d244e78ee", - }, - ], - "attachments": [], - "entities": [], - "from": { - "user_huid": "b9197d3a-d855-5d34-ba8a-eff3a975ab20", - "user_udid": None, - "group_chat_id": "dea55ee4-7a9f-5da0-8c73-079f400ee517", - "host": "cts.example.com", - "ad_login": None, - "ad_domain": None, - "username": None, - "chat_type": "group_chat", - "manufacturer": None, - "device": None, - "device_software": None, - "device_meta": {}, - "platform": None, - "platform_package_id": None, - "is_admin": False, - "is_creator": False, - "app_version": None, - "locale": "en", + } + + # - Act - + with TestClient(fastapi_factory(bot)) as test_client: + response = test_client.post( + "/smartapps/request", + json=request_payload, + ) + + # - Assert - + assert response.status_code == HTTPStatus.OK + assert response.json() == { + "status": "ok", + "result": { + "data": {"category_id": 1}, + "files": [ + { + "file": "/uploads/files/b0232da0bf3d406eb5653e37b2bb6517.bin", + "file_name": "cts1-test.ast-innovation.ru.har", + "file_size": 349372, + "file_hash": "qVSzEUJITWP+TgCvcF3UCzQrBaY3RHqB92CHObz4E70=", + "file_mime_type": "application/octet-stream", + "file_id": "a0ec914f-8235-5021-9b8d-05c3cd303536", + "type": "document", + }, + ], }, - "bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46", - "proto_version": 4, - "source_sync_id": None, } + +def test__web_app__sync_smartapp_event__error( + bot_id: UUID, + bot_account: BotAccountWithSecret, + api_sync_smartapp_event_factory: Callable[..., Dict[str, Any]], +) -> None: + # - Arrange - + request_payload = api_sync_smartapp_event_factory(bot_id=bot_id) + local_collector = HandlerCollector() + + @local_collector.sync_smartapp_event + async def handle_sync_smartapp_event_with_error( + *_: Any, + ) -> BotAPISyncSmartAppEventErrorResponse: + return BotAPISyncSmartAppEventErrorResponse.from_domain( + errors=[{"id": "Error", "reason": "some error"}], + ) + + bot = bot_factory(bot_accounts=[bot_account], bot_collector=local_collector) + # - Act - with TestClient(fastapi_factory(bot)) as test_client: response = test_client.post( @@ -492,22 +502,8 @@ def test__web_app__sync_smartapp_event(bot: Bot) -> None: # - Assert - assert response.status_code == HTTPStatus.OK assert response.json() == { - "ref": "6fafda2c-6505-57a5-a088-25ea5d1d0364", - "smartapp_id": "24348246-6791-4ac0-9d86-b948cd6a0e46", - "group_chat_id": "dea55ee4-7a9f-5da0-8c73-079f400ee517", - "data": {"type": "smartapp_rpc", "method": "folders.get", "params": {"q": 1}}, - "opts": {}, - "smartapp_api_version": 1, - "async_files": [ - { - "type": "image", - "file": "https://link.to/file", - "file_mime_type": "image/png", - "file_id": "8dada2c8-67a6-4434-9dec-570d244e78ee", - "file_name": "pass.png", - "file_size": 1502345, - "file_hash": "Jd9r+OKpw5y+FSCg1xNTSUkwEo4nCW1Sn1AkotkOpH0=", - }, - ], - "encrypted": True, + "status": "error", + "reason": "smartapp_error", + "errors": [{"id": "Error", "reason": "some error"}], + "error_data": {}, }