From 8c898a0b3a71be328c901bf15794b5a3dadafe36 Mon Sep 17 00:00:00 2001 From: dmitrymihalev Date: Tue, 24 Jun 2025 14:50:58 +0500 Subject: [PATCH 1/6] feat: Add method to create thread to Bot --- pybotx/__init__.py | 6 + pybotx/bot/bot.py | 31 ++- pybotx/client/chats_api/create_thread.py | 58 +++++ pybotx/client/exceptions/chats.py | 23 ++ pyproject.toml | 4 +- tests/client/chats_api/test_create_thread.py | 214 +++++++++++++++++++ 6 files changed, 329 insertions(+), 7 deletions(-) create mode 100644 pybotx/client/chats_api/create_thread.py create mode 100644 tests/client/chats_api/test_create_thread.py diff --git a/pybotx/__init__.py b/pybotx/__init__.py index e14e9439..a7ea961c 100644 --- a/pybotx/__init__.py +++ b/pybotx/__init__.py @@ -39,6 +39,9 @@ ChatCreationError, ChatCreationProhibitedError, InvalidUsersListError, + ThreadCreationError, + ThreadCreationEventNotFoundError, + ThreadCreationProhibitedError, ) from pybotx.client.exceptions.common import ( ChatNotFoundError, @@ -260,6 +263,9 @@ "SyncSmartAppEventHandlerFunc", "SyncSmartAppEventHandlerNotFoundError", "SyncSourceTypes", + "ThreadCreationError", + "ThreadCreationEventNotFoundError", + "ThreadCreationProhibitedError", "UnknownBotAccountError", "UnknownSystemEventError", "UnsupportedBotAPIVersionError", diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index 0cd21e17..887a0ee7 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -1,17 +1,13 @@ from asyncio import Task +from collections.abc import AsyncIterable, AsyncIterator, Iterator, Mapping, Sequence from contextlib import asynccontextmanager from datetime import datetime from types import SimpleNamespace from typing import ( Any, - AsyncIterable, - AsyncIterator, Dict, - Iterator, List, - Mapping, Optional, - Sequence, Set, Tuple, Union, @@ -57,6 +53,10 @@ BotXAPICreateChatRequestPayload, CreateChatMethod, ) +from pybotx.client.chats_api.create_thread import ( + BotXAPICreateThreadRequestPayload, + CreateThreadMethod, +) from pybotx.client.chats_api.disable_stealth import ( BotXAPIDisableStealthRequestPayload, DisableStealthMethod, @@ -1176,6 +1176,27 @@ async def create_chat( return botx_api_chat_id.to_domain() + async def create_thread(self, bot_id: UUID, sync_id: UUID) -> UUID: + """ + Create thread. + + :param bot_id: Bot which should perform the request. + :param sync_id: Message for which thread should be created + + :return: Created thread uuid. + """ + + method = CreateThreadMethod( + bot_id, + self._httpx_client, + self._bot_accounts_storage, + ) + + payload = BotXAPICreateThreadRequestPayload.from_domain(sync_id=sync_id) + botx_api_thread_id = await method.execute(payload) + + return botx_api_thread_id.to_domain() + async def pin_message( self, *, diff --git a/pybotx/client/chats_api/create_thread.py b/pybotx/client/chats_api/create_thread.py new file mode 100644 index 00000000..981215d9 --- /dev/null +++ b/pybotx/client/chats_api/create_thread.py @@ -0,0 +1,58 @@ +import typing +from typing import Literal +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.chats import ( + ThreadCreationError, + ThreadCreationEventNotFoundError, + ThreadCreationProhibitedError, +) +from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel + + +class BotXAPICreateThreadRequestPayload(UnverifiedPayloadBaseModel): + sync_id: UUID + + @classmethod + def from_domain(cls, sync_id: UUID) -> "BotXAPICreateThreadRequestPayload": + return cls(sync_id=sync_id) + + +class BotXAPIThreadIdResult(VerifiedPayloadBaseModel): + thread_id: UUID + + +class BotXAPICreateThreadResponsePayload(VerifiedPayloadBaseModel): + status: Literal["ok"] + result: BotXAPIThreadIdResult + + def to_domain(self) -> UUID: + return self.result.thread_id + + +class CreateThreadMethod(AuthorizedBotXMethod): + status_handlers: typing.ClassVar = { + **AuthorizedBotXMethod.status_handlers, + 403: response_exception_thrower(ThreadCreationProhibitedError), + 404: response_exception_thrower(ThreadCreationEventNotFoundError), + 422: response_exception_thrower(ThreadCreationError), + } + + async def execute( + self, + payload: BotXAPICreateThreadRequestPayload, + ) -> BotXAPICreateThreadResponsePayload: + path = "/api/v3/botx/chats/create_thread" + + response = await self._botx_method_call( + "POST", + self._build_url(path), + json=payload.jsonable_dict(), + ) + + return self._verify_and_extract_api_model( + BotXAPICreateThreadResponsePayload, + response, + ) diff --git a/pybotx/client/exceptions/chats.py b/pybotx/client/exceptions/chats.py index 4134b5a0..8dc57e32 100644 --- a/pybotx/client/exceptions/chats.py +++ b/pybotx/client/exceptions/chats.py @@ -15,3 +15,26 @@ class ChatCreationProhibitedError(BaseClientError): class ChatCreationError(BaseClientError): """Error while chat creation.""" + + +class ThreadCreationError(BaseClientError): + """Error while thread creation (invalid scheme).""" + + +class ThreadCreationProhibitedError(BaseClientError): + """ + 1. Bot has no permissions to create thread + 2. Threads are not allowed for that message + 3. Bot is not a chat member where message is located + 4. Message is located in personal chat + 5. Usupported event type + 6. Unsuppoerted chat type + 7. Thread is already created + 8. No access for message + 9. Message in stealth mode + 10. Message is deleted + """ + + +class ThreadCreationEventNotFoundError(BaseClientError): + """Event not found""" diff --git a/pyproject.toml b/pyproject.toml index 6bb84255..bfdfe41e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [tool.poetry] name = "pybotx" -version = "0.75.1" +version = "0.75.2" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", "Maxim Gorbachev ", "Alexander Samoylenko ", - "Arseniy Zhiltsov " + "Arseniy Zhiltsov ", ] readme = "README.md" repository = "https://github.com/ExpressApp/pybotx" diff --git a/tests/client/chats_api/test_create_thread.py b/tests/client/chats_api/test_create_thread.py new file mode 100644 index 00000000..74ee9b05 --- /dev/null +++ b/tests/client/chats_api/test_create_thread.py @@ -0,0 +1,214 @@ +from http import HTTPStatus +from uuid import UUID + +import httpx +import pytest +from respx.router import MockRouter + +from pybotx import ( + Bot, + BotAccountWithSecret, + HandlerCollector, + ThreadCreationError, + ThreadCreationEventNotFoundError, + ThreadCreationProhibitedError, + lifespan_wrapper, +) + +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.mock_authorization, + pytest.mark.usefixtures("respx_mock"), +] + +ENDPOINT = "api/v3/botx/chats/create_thread" + + +async def test__create_chat__succeed( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + sync_id = "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3" + thread_id = "2a8c0d1e-c4d1-4308-b024-6e1a9f4a4b6d" + endpoint = respx_mock.post( + f"https://{host}/{ENDPOINT}", + headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, + json={"sync_id": sync_id}, + ).mock( + return_value=httpx.Response( + HTTPStatus.OK, + json={ + "status": "ok", + "result": {"thread_id": thread_id}, + }, + ), + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + created_thread_id = await bot.create_thread(bot_id=bot_id, sync_id=UUID(sync_id)) + + # - Assert - + assert str(created_thread_id) == thread_id + assert endpoint.called + + +@pytest.mark.parametrize( + "return_json, response_status, expected_exc_type", + ( + ( + { + "status": "error", + "reason": "thread_creation_is_prohibited", + "errors": ["This bot is not allowed to create thread"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "threads_not_enabled", + "errors": ["Threads not enabled for this chat"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "bot_is_not_a_chat_member", + "errors": ["This bot is not a chat member"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "can_not_create_for_personal_chat", + "errors": ["This is personal chat"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "unsupported_event_type", + "errors": ["This event type is unsupported"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "unsupported_chat_type", + "errors": ["This chat type is unsupported"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "thread_already_created", + "errors": ["Thread already created"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "no_access_for_message", + "errors": ["There is no access for this message"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "event_in_stealth_mode", + "errors": ["This event is in stealth mode"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "event_already_deleted", + "errors": ["This event already deleted"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "event_not_found", + "errors": ["Event not found"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.NOT_FOUND, + ThreadCreationEventNotFoundError, + ), + ( + { + "status": "error", + "reason": "|specified reason|", + "errors": ["|specified errors|"], + "error_data": {}, + }, + HTTPStatus.UNPROCESSABLE_ENTITY, + ThreadCreationError, + ), + ), +) +async def test__create_thread__botx_error_raised( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, + return_json, + response_status, + expected_exc_type, +) -> None: + # - Arrange - + sync_id = "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3" + endpoint = respx_mock.post( + f"https://{host}/{ENDPOINT}", + headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, + json={"sync_id": sync_id}, + ).mock(return_value=httpx.Response(response_status, json=return_json)) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(expected_exc_type) as exc: + await bot.create_thread( + bot_id=bot_id, sync_id=UUID(sync_id) + ) + + # - Assert - + assert endpoint.called + assert return_json["reason"] in str(exc.value) From ba3f62a5d479f7eedad26d16af894aff7af6b1a2 Mon Sep 17 00:00:00 2001 From: dmitrymihalev Date: Tue, 24 Jun 2025 15:52:50 +0500 Subject: [PATCH 2/6] fix: linter errors --- pybotx/client/chats_api/create_thread.py | 2 +- tests/client/chats_api/test_create_thread.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pybotx/client/chats_api/create_thread.py b/pybotx/client/chats_api/create_thread.py index 981215d9..9d46e7fd 100644 --- a/pybotx/client/chats_api/create_thread.py +++ b/pybotx/client/chats_api/create_thread.py @@ -33,7 +33,7 @@ def to_domain(self) -> UUID: class CreateThreadMethod(AuthorizedBotXMethod): - status_handlers: typing.ClassVar = { + status_handlers = { **AuthorizedBotXMethod.status_handlers, 403: response_exception_thrower(ThreadCreationProhibitedError), 404: response_exception_thrower(ThreadCreationEventNotFoundError), diff --git a/tests/client/chats_api/test_create_thread.py b/tests/client/chats_api/test_create_thread.py index 74ee9b05..25555f8f 100644 --- a/tests/client/chats_api/test_create_thread.py +++ b/tests/client/chats_api/test_create_thread.py @@ -1,4 +1,5 @@ from http import HTTPStatus +from typing import Any from uuid import UUID import httpx @@ -51,7 +52,10 @@ async def test__create_chat__succeed( # - Act - async with lifespan_wrapper(built_bot) as bot: - created_thread_id = await bot.create_thread(bot_id=bot_id, sync_id=UUID(sync_id)) + created_thread_id = await bot.create_thread( + bot_id=bot_id, + sync_id=UUID(sync_id), + ) # - Assert - assert str(created_thread_id) == thread_id @@ -188,9 +192,9 @@ async def test__create_thread__botx_error_raised( host: str, bot_id: UUID, bot_account: BotAccountWithSecret, - return_json, - response_status, - expected_exc_type, + return_json: dict[str, Any], + response_status: int, + expected_exc_type: type[BaseException], ) -> None: # - Arrange - sync_id = "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3" @@ -206,7 +210,8 @@ async def test__create_thread__botx_error_raised( async with lifespan_wrapper(built_bot) as bot: with pytest.raises(expected_exc_type) as exc: await bot.create_thread( - bot_id=bot_id, sync_id=UUID(sync_id) + bot_id=bot_id, + sync_id=UUID(sync_id), ) # - Assert - From 9ddebbbfe120e925ea73fad235ffc63582d8834d Mon Sep 17 00:00:00 2001 From: dmitrymihalev Date: Tue, 24 Jun 2025 15:57:38 +0500 Subject: [PATCH 3/6] fix: remove duplicate exception --- pybotx/__init__.py | 1 - pybotx/client/chats_api/create_thread.py | 5 ++--- pybotx/client/exceptions/chats.py | 4 ---- tests/client/chats_api/test_create_thread.py | 4 ++-- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/pybotx/__init__.py b/pybotx/__init__.py index a7ea961c..456feb01 100644 --- a/pybotx/__init__.py +++ b/pybotx/__init__.py @@ -40,7 +40,6 @@ ChatCreationProhibitedError, InvalidUsersListError, ThreadCreationError, - ThreadCreationEventNotFoundError, ThreadCreationProhibitedError, ) from pybotx.client.exceptions.common import ( diff --git a/pybotx/client/chats_api/create_thread.py b/pybotx/client/chats_api/create_thread.py index 9d46e7fd..f7370b41 100644 --- a/pybotx/client/chats_api/create_thread.py +++ b/pybotx/client/chats_api/create_thread.py @@ -1,4 +1,3 @@ -import typing from typing import Literal from uuid import UUID @@ -6,9 +5,9 @@ from pybotx.client.botx_method import response_exception_thrower from pybotx.client.exceptions.chats import ( ThreadCreationError, - ThreadCreationEventNotFoundError, ThreadCreationProhibitedError, ) +from pybotx.client.exceptions.event import EventNotFoundError from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel @@ -36,7 +35,7 @@ class CreateThreadMethod(AuthorizedBotXMethod): status_handlers = { **AuthorizedBotXMethod.status_handlers, 403: response_exception_thrower(ThreadCreationProhibitedError), - 404: response_exception_thrower(ThreadCreationEventNotFoundError), + 404: response_exception_thrower(EventNotFoundError), 422: response_exception_thrower(ThreadCreationError), } diff --git a/pybotx/client/exceptions/chats.py b/pybotx/client/exceptions/chats.py index 8dc57e32..e5fe156d 100644 --- a/pybotx/client/exceptions/chats.py +++ b/pybotx/client/exceptions/chats.py @@ -34,7 +34,3 @@ class ThreadCreationProhibitedError(BaseClientError): 9. Message in stealth mode 10. Message is deleted """ - - -class ThreadCreationEventNotFoundError(BaseClientError): - """Event not found""" diff --git a/tests/client/chats_api/test_create_thread.py b/tests/client/chats_api/test_create_thread.py index 25555f8f..5bc08311 100644 --- a/tests/client/chats_api/test_create_thread.py +++ b/tests/client/chats_api/test_create_thread.py @@ -9,9 +9,9 @@ from pybotx import ( Bot, BotAccountWithSecret, + EventNotFoundError, HandlerCollector, ThreadCreationError, - ThreadCreationEventNotFoundError, ThreadCreationProhibitedError, lifespan_wrapper, ) @@ -173,7 +173,7 @@ async def test__create_chat__succeed( "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, }, HTTPStatus.NOT_FOUND, - ThreadCreationEventNotFoundError, + EventNotFoundError, ), ( { From 45f072be5c5e2d43c5af850caaf2139a52540fd6 Mon Sep 17 00:00:00 2001 From: dmitrymihalev Date: Tue, 24 Jun 2025 16:01:03 +0500 Subject: [PATCH 4/6] fix: docstring format --- pybotx/client/exceptions/chats.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pybotx/client/exceptions/chats.py b/pybotx/client/exceptions/chats.py index e5fe156d..50a2d3a1 100644 --- a/pybotx/client/exceptions/chats.py +++ b/pybotx/client/exceptions/chats.py @@ -23,6 +23,8 @@ class ThreadCreationError(BaseClientError): class ThreadCreationProhibitedError(BaseClientError): """ + Error while permission checks. + 1. Bot has no permissions to create thread 2. Threads are not allowed for that message 3. Bot is not a chat member where message is located From 7fe0eca76c8d185c83df31bf92892d0510473394 Mon Sep 17 00:00:00 2001 From: dmitrymihalev Date: Wed, 25 Jun 2025 18:41:55 +0500 Subject: [PATCH 5/6] add enum value for thread chat type --- pybotx/models/enums.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pybotx/models/enums.py b/pybotx/models/enums.py index 05bc35fa..d56f61d7 100644 --- a/pybotx/models/enums.py +++ b/pybotx/models/enums.py @@ -59,6 +59,7 @@ class ChatTypes(AutoName): PERSONAL_CHAT = auto() GROUP_CHAT = auto() CHANNEL = auto() + THREAD = auto() class SyncSourceTypes(AutoName): @@ -92,6 +93,7 @@ class APIChatTypes(Enum): CHAT = "chat" GROUP_CHAT = "group_chat" CHANNEL = "channel" + THREAD = "thread" class BotAPICommandTypes(StrEnum): @@ -295,6 +297,7 @@ def convert_chat_type_from_domain(chat_type: ChatTypes) -> APIChatTypes: ChatTypes.PERSONAL_CHAT: APIChatTypes.CHAT, ChatTypes.GROUP_CHAT: APIChatTypes.GROUP_CHAT, ChatTypes.CHANNEL: APIChatTypes.CHANNEL, + ChatTypes.THREAD: APIChatTypes.THREAD, } converted_type = chat_types_mapping.get(chat_type) @@ -323,6 +326,7 @@ def convert_chat_type_to_domain( APIChatTypes.CHAT: ChatTypes.PERSONAL_CHAT, APIChatTypes.GROUP_CHAT: ChatTypes.GROUP_CHAT, APIChatTypes.CHANNEL: ChatTypes.CHANNEL, + APIChatTypes.THREAD: ChatTypes.THREAD, } converted_type: Optional[IncomingChatTypes] From bd10418b7a0ea0f156f49df0d33982748cbf9d69 Mon Sep 17 00:00:00 2001 From: dmitrymihalev Date: Thu, 26 Jun 2025 20:19:26 +0500 Subject: [PATCH 6/6] Add test cases, move duplicating code to fixture, fix name of test --- tests/client/chats_api/test_create_thread.py | 92 ++++++++++++++------ 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/tests/client/chats_api/test_create_thread.py b/tests/client/chats_api/test_create_thread.py index 5bc08311..bfb56fbd 100644 --- a/tests/client/chats_api/test_create_thread.py +++ b/tests/client/chats_api/test_create_thread.py @@ -1,9 +1,10 @@ from http import HTTPStatus -from typing import Any +from typing import Any, Callable from uuid import UUID import httpx import pytest +from respx import Route from respx.router import MockRouter from pybotx import ( @@ -25,27 +26,44 @@ ENDPOINT = "api/v3/botx/chats/create_thread" -async def test__create_chat__succeed( +@pytest.fixture +def sync_id() -> str: + return "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3" + + +@pytest.fixture +def create_mocked_endpoint( respx_mock: MockRouter, host: str, + sync_id: str, +) -> Callable[[dict[str, Any], int], Route]: + def mocked_endpoint(json_response: dict[str, Any], status_code: int) -> Route: + return respx_mock.post( + f"https://{host}/{ENDPOINT}", + headers={ + "Authorization": "Bearer token", + "Content-Type": "application/json", + }, + json={"sync_id": sync_id}, + ).mock(return_value=httpx.Response(status_code, json=json_response)) + + return mocked_endpoint + + +async def test__create_thread__succeed( + create_mocked_endpoint: Callable[[dict[str, Any], int], Route], + sync_id: str, bot_id: UUID, bot_account: BotAccountWithSecret, ) -> None: # - Arrange - - sync_id = "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3" thread_id = "2a8c0d1e-c4d1-4308-b024-6e1a9f4a4b6d" - endpoint = respx_mock.post( - f"https://{host}/{ENDPOINT}", - headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, - json={"sync_id": sync_id}, - ).mock( - return_value=httpx.Response( - HTTPStatus.OK, - json={ - "status": "ok", - "result": {"thread_id": thread_id}, - }, - ), + endpoint = create_mocked_endpoint( + { + "status": "ok", + "result": {"thread_id": thread_id}, + }, + HTTPStatus.OK, ) built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) @@ -185,11 +203,39 @@ async def test__create_chat__succeed( HTTPStatus.UNPROCESSABLE_ENTITY, ThreadCreationError, ), + ( + { + "status": "error", + "reason": None, + "errors": [], + "error_data": {}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "errors": [], + "error_data": {}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "unexpected reason", + "errors": [], + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), ), ) async def test__create_thread__botx_error_raised( - respx_mock: MockRouter, - host: str, + create_mocked_endpoint: Callable[[dict[str, Any], int], Route], + sync_id: str, bot_id: UUID, bot_account: BotAccountWithSecret, return_json: dict[str, Any], @@ -197,13 +243,7 @@ async def test__create_thread__botx_error_raised( expected_exc_type: type[BaseException], ) -> None: # - Arrange - - sync_id = "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3" - endpoint = respx_mock.post( - f"https://{host}/{ENDPOINT}", - headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, - json={"sync_id": sync_id}, - ).mock(return_value=httpx.Response(response_status, json=return_json)) - + endpoint = create_mocked_endpoint(return_json, response_status) built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) # - Act - @@ -216,4 +256,6 @@ async def test__create_thread__botx_error_raised( # - Assert - assert endpoint.called - assert return_json["reason"] in str(exc.value) + + if return_json.get("reason"): + assert return_json["reason"] in str(exc.value)