From 8fa63fa10b40890972f22f0a7e298364fcdc518d Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 31 Jul 2025 09:31:33 +0300 Subject: [PATCH] Added addAdmins & addMembers & editMessage & answerOnCallback --- README.md | 119 ++++++++--- src/Api.php | 240 ++++++++++++++++++++-- src/Exceptions/NetworkException.php | 5 - src/Exceptions/SerializationException.php | 5 - src/Models/ChatAdmin.php | 25 +++ tests/ApiTest.php | 227 +++++++++++++++++++- tests/Models/ChatAdminTest.php | 31 +++ 7 files changed, 593 insertions(+), 59 deletions(-) create mode 100644 src/Models/ChatAdmin.php create mode 100644 tests/Models/ChatAdminTest.php diff --git a/README.md b/README.md index c04c8ce..9826cd8 100644 --- a/README.md +++ b/README.md @@ -14,60 +14,115 @@ ## Быстрый старт -> Если вы новичок, то можете прочитать [официальную документацию](https://dev.max.ru/), написанную разработчиками Max +> Если вы новичок, то можете прочитать [официальную документацию](https://dev.max.ru/), написанную разработчиками Max. ### Получение токена Откройте диалог с [MasterBot](https://max.ru/MasterBot), следуйте инструкциям и создайте нового бота. После создания бота MasterBot отправит вам токен. +### Установка библиотеки + +```bash +composer require bushlanov-dev/max-bot-api-client-php +``` + +### Использование + +Отправка сообщения с клавиатурой + +```php +$api = new \BushlanovDev\MaxMessengerBot\Api('YOUR_BOT_API_TOKEN'); + +$api->sendMessage( + userId: 123, // ID пользователя получателя сообщения + chatId: 321, // Или ID чата, в который нужно отправить сообщение + text: 'Привет!', // Текст сообщения, вы можете использовать HTML или Markdown + attachments: [ + new InlineKeyboardAttachmentRequest([ + [new CallbackButton('Нажми меня!', 'payload_button1')], + [new LinkButton('Нажми меня!', 'https://example.com')], + ]), + ], + format: MessageFormat::Markdown, // Формат сообщения (Markdown или HTML) +); +``` + +Подписка на вэб хуки + +```php +$api->subscribe( + new \BushlanovDev\MaxMessengerBot\Models\SubscriptionRequest( + 'https://example.com/webhook', // URL на который будут приходить хуки + 'super_secret', // Секретная фраза для проверки хуков + [ + // Типы хуков которые вы хотите получать (либо ничего не указывать, чтобы получать все) + \BushlanovDev\MaxMessengerBot\Enums\UpdateType::MessageCreated, + ], + ), +); +``` + +Обработка хуков + +```php +$webhookHandler = $api->createWebhookHandler(); + +$webhookHandler->addHandler(UpdateType::BotStarted, function (BotStartedUpdate $update, Api $api) { + $api->sendMessage( + chatId: $update->chatId, + text: 'Я запущен!', + ); +}); +``` + ## Реализованные методы #### Bots -- [x] `GET /me` (`getBotInfo`) — *Получение информации о боте.* -- [ ] `PATCH /me` (`editBotInfo`) — *Редактирование информации о боте.* +- [x] `GET /me` (`getBotInfo`) - *Получение информации о боте.* +- [ ] `PATCH /me` (`editBotInfo`) - *Редактирование информации о боте.* #### Chats -- [x] `GET /chats` (`getChats`) — *Получение списка всех чатов бота.* -- [x] `GET /chats/{chatLink}` (`getChatByLink`) — *Получение информации о чате по ссылке.* -- [x] `GET /chats/{chatId}` (`getChat`) — *Получение информации о чате по ID.* -- [ ] `PATCH /chats/{chatId}` (`editChat`) — *Редактирование информации о чате.* -- [x] `DELETE /chats/{chatId}` (`deleteChat`) — *Удаление чата.* -- [x] `POST /chats/{chatId}/actions` (`sendAction`) — *Отправка действия в чат (например, "печатает...").* -- [x] `GET /chats/{chatId}/pin` (`getPinnedMessage`) — *Получение закрепленного сообщения.* -- [x] `PUT /chats/{chatId}/pin` (`pinMessage`) — *Закрепление сообщения.* -- [x] `DELETE /chats/{chatId}/pin` (`unpinMessage`) — *Открепление сообщения.* -- [x] `GET /chats/{chatId}/members/me` (`getMembership`) — *Получение информации о членстве бота в чате.* -- [x] `DELETE /chats/{chatId}/members/me` (`leaveChat`) — *Выход бота из чата.* -- [x] `GET /chats/{chatId}/members/admins` (`getAdmins`) — *Получение администраторов чата.* -- [ ] `POST /chats/{chatId}/members/admins` (`postAdmins`) — *Назначение администраторов чата.* -- [x] `DELETE /chats/{chatId}/members/admins/{userId}` (`deleteAdmins`) — *Снятие прав администратора.* -- [x] `GET /chats/{chatId}/members` (`getMembers`) — *Получение участников чата.* -- [ ] `POST /chats/{chatId}/members` (`addMembers`) — *Добавление участников в чат.* -- [x] `DELETE /chats/{chatId}/members` (`removeMember`) — *Удаление участника из чата.* +- [x] `GET /chats` (`getChats`) - *Получение списка всех чатов бота.* +- [x] `GET /chats/{chatLink}` (`getChatByLink`) - *Получение информации о чате по ссылке.* +- [x] `GET /chats/{chatId}` (`getChat`) - *Получение информации о чате по ID.* +- [ ] `PATCH /chats/{chatId}` (`editChat`) - *Редактирование информации о чате.* +- [x] `DELETE /chats/{chatId}` (`deleteChat`) - *Удаление чата.* +- [x] `POST /chats/{chatId}/actions` (`sendAction`) - *Отправка действия в чат (например, "печатает...").* +- [x] `GET /chats/{chatId}/pin` (`getPinnedMessage`) - *Получение закрепленного сообщения.* +- [x] `PUT /chats/{chatId}/pin` (`pinMessage`) - *Закрепление сообщения.* +- [x] `DELETE /chats/{chatId}/pin` (`unpinMessage`) - *Открепление сообщения.* +- [x] `GET /chats/{chatId}/members/me` (`getMembership`) - *Получение информации о членстве бота в чате.* +- [x] `DELETE /chats/{chatId}/members/me` (`leaveChat`) - *Выход бота из чата.* +- [x] `GET /chats/{chatId}/members/admins` (`getAdmins`) - *Получение администраторов чата.* +- [x] `POST /chats/{chatId}/members/admins` (`addAdmins`) - *Назначение администраторов чата.* +- [x] `DELETE /chats/{chatId}/members/admins/{userId}` (`deleteAdmins`) - *Снятие прав администратора.* +- [x] `GET /chats/{chatId}/members` (`getMembers`) - *Получение участников чата.* +- [x] `POST /chats/{chatId}/members` (`addMembers`) - *Добавление участников в чат.* +- [x] `DELETE /chats/{chatId}/members` (`deleteMember`) - *Удаление участника из чата.* #### Subscriptions -- [x] `GET /subscriptions` (`getSubscriptions`) — *Получение списка Webhook-подписок.* -- [x] `POST /subscriptions` (`subscribe`) — *Создание Webhook-подписки.* -- [x] `DELETE /subscriptions` (`unsubscribe`) — *Удаление Webhook-подписки.* -- [x] `GET /updates` (`getUpdates`) — *Получение обновлений через Long-Polling.* +- [x] `GET /subscriptions` (`getSubscriptions`) - *Получение списка Webhook-подписок.* +- [x] `POST /subscriptions` (`subscribe`) - *Создание Webhook-подписки.* +- [x] `DELETE /subscriptions` (`unsubscribe`) - *Удаление Webhook-подписки.* +- [x] `GET /updates` (`getUpdates`) - *Получение обновлений через Long-Polling.* #### Upload -- [x] `POST /uploads` (`getUploadUrl`) — *Получение URL для загрузки файла.* +- [x] `POST /uploads` (`getUploadUrl`) - *Получение URL для загрузки файла.* #### Messages -- [x] `GET /messages` (`getMessages`) — *Получение списка сообщений из чата.* -- [x] `POST /messages` (`sendMessage`) — *Отправка сообщения.* -- [ ] `PUT /messages` (`editMessage`) — *Редактирование сообщения.* -- [x] `DELETE /messages` (`deleteMessage`) — *Удаление сообщения.* -- [x] `GET /messages/{messageId}` (`getMessageById`) — *Получение сообщения по ID.* -- [ ] `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) — *Получение детальной информации о видео.* -- [ ] `POST /answers` (`answerOnCallback`) — *Ответ на нажатие callback-кнопки.* +- [x] `GET /messages` (`getMessages`) - *Получение списка сообщений из чата.* +- [x] `POST /messages` (`sendMessage`) - *Отправка сообщения.* +- [x] `PUT /messages` (`editMessage`) - *Редактирование сообщения.* +- [x] `DELETE /messages` (`deleteMessage`) - *Удаление сообщения.* +- [x] `GET /messages/{messageId}` (`getMessageById`) - *Получение сообщения по ID.* +- [ ] `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) - *Получение детальной информации о видео.* +- [x] `POST /answers` (`answerOnCallback`) - *Ответ на нажатие callback-кнопки.* ## Лицензия diff --git a/src/Api.php b/src/Api.php index b85a102..4907f0f 100644 --- a/src/Api.php +++ b/src/Api.php @@ -20,6 +20,7 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\PhotoAttachmentRequ use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\VideoAttachmentRequest; use BushlanovDev\MaxMessengerBot\Models\BotInfo; use BushlanovDev\MaxMessengerBot\Models\Chat; +use BushlanovDev\MaxMessengerBot\Models\ChatAdmin; use BushlanovDev\MaxMessengerBot\Models\ChatList; use BushlanovDev\MaxMessengerBot\Models\ChatMember; use BushlanovDev\MaxMessengerBot\Models\ChatMembersList; @@ -65,6 +66,7 @@ class Api private const string ACTION_CHATS_MEMBERS_ADMINS_ID = '/chats/%d/members/admins/%d'; private const string ACTION_CHATS_MEMBERS = '/chats/%d/members'; private const string ACTION_UPDATES = '/updates'; + private const string ACTION_ANSWERS = '/answers'; private readonly ClientApiInterface $client; @@ -348,7 +350,7 @@ class Api } /** - * Sends a message to a chat. + * Sends a message to a chat or user. * * @param int|null $userId Fill this parameter if you want to send message to user. * @param int|null $chatId Fill this if you send message to chat. @@ -381,27 +383,76 @@ class Api 'disable_link_preview' => $disableLinkPreview, ]; - $body = [ - 'text' => $text, - 'format' => $format?->value, - 'notify' => $notify, - 'link' => $link, - 'attachments' => $attachments !== null ? array_map( - fn(AbstractModel $attachment) => $attachment->toArray(), - $attachments, - ) : null, - ]; - $response = $this->client->request( self::METHOD_POST, self::ACTION_MESSAGES, array_filter($query, fn($item) => null !== $item), - array_filter($body, fn($item) => null !== $item), + $this->buildNewMessageBody($text, $attachments, $format, $link, $notify), ); return $this->modelFactory->createMessage($response['message']); } + /** + * Sends a message to a user. + * + * @param int|null $userId Fill this parameter if you want to send message to user. + * @param string|null $text Message text. + * @param AbstractAttachmentRequest[]|null $attachments Message attachments. + * @param MessageFormat|null $format Message format. + * @param MessageLink|null $link Link to message. + * @param bool $notify If false, chat participants would not be notified. + * @param bool $disableLinkPreview If false, server will not generate media preview for links in text. + * + * @return Message + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + * @codeCoverageIgnore + */ + public function sendUserMessage( + ?int $userId = null, + ?string $text = null, + ?array $attachments = null, + ?MessageFormat $format = null, + ?MessageLink $link = null, + bool $notify = true, + bool $disableLinkPreview = false, + ): Message { + return $this->sendMessage($userId, null, $text, $attachments, $format, $link, $notify, $disableLinkPreview); + } + + /** + * Sends a message to a chat. + * + * @param int|null $chatId Fill this if you send message to chat. + * @param string|null $text Message text. + * @param AbstractAttachmentRequest[]|null $attachments Message attachments. + * @param MessageFormat|null $format Message format. + * @param MessageLink|null $link Link to message. + * @param bool $notify If false, chat participants would not be notified. + * @param bool $disableLinkPreview If false, server will not generate media preview for links in text. + * + * @return Message + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + * @codeCoverageIgnore + */ + public function sendChatMessage( + ?int $chatId = null, + ?string $text = null, + ?array $attachments = null, + ?MessageFormat $format = null, + ?MessageLink $link = null, + bool $notify = true, + bool $disableLinkPreview = false, + ): Message { + return $this->sendMessage(null, $chatId, $text, $attachments, $format, $link, $notify, $disableLinkPreview); + } + /** * Returns the URL for the subsequent file upload. * @@ -779,7 +830,7 @@ class Api [ 'message_id' => $messageId, 'notify' => $notify, - ] + ], ) ); } @@ -877,7 +928,7 @@ class Api * @throws ReflectionException * @throws SerializationException */ - public function removeMember(int $chatId, int $userId, bool $block = false): Result + public function deleteMember(int $chatId, int $userId, bool $block = false): Result { return $this->modelFactory->createResult( $this->client->request( @@ -890,4 +941,163 @@ class Api ) ); } + + /** + * Sets the administrators for a chat. + * + * @param int $chatId The identifier of the chat. + * @param ChatAdmin[] $admins An array of ChatAdmin objects representing the users and their permissions. + * + * @return Result + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + */ + public function addAdmins(int $chatId, array $admins): Result + { + return $this->modelFactory->createResult( + $this->client->request( + self::METHOD_POST, + sprintf(self::ACTION_CHATS_MEMBERS_ADMINS, $chatId), + [], + ['admins' => array_map(fn(ChatAdmin $admin) => $admin->toArray(), $admins)], + ) + ); + } + + /** + * Adds members to a chat. The bot may require additional permissions. + * + * @param int $chatId The identifier of the chat. + * @param int[] $userIds An array of user identifiers to add to the chat. + * + * @return Result + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + */ + public function addMembers(int $chatId, array $userIds): Result + { + return $this->modelFactory->createResult( + $this->client->request( + self::METHOD_POST, + sprintf(self::ACTION_CHATS_MEMBERS, $chatId), + [], + ['user_ids' => $userIds], + ) + ); + } + + /** + * Sends an answer to a callback query. This should be called after a user clicks an inline button. + * + * @param string $callbackId The identifier of the callback query. + * @param string|null $notification A short text notification to show to the user. + * @param string|null $text If provided, the original message will be edited with this text. + * @param AbstractAttachmentRequest[]|null $attachments New attachments for the edited message. + * @param MessageLink|null $link New link for the edited message. + * @param MessageFormat|null $format Formatting for the new message text. + * @param bool $notify Notification setting for the edited message. + * + * @return Result + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + */ + public function answerOnCallback( + string $callbackId, + ?string $notification = null, + ?string $text = null, + ?array $attachments = null, + ?MessageLink $link = null, + ?MessageFormat $format = null, + bool $notify = true, + ): Result { + $answerBody = ['notification' => $notification]; + if ($text !== null || $attachments !== null || $link !== null) { + $answerBody['message'] = $this->buildNewMessageBody($text, $attachments, $format, $link, $notify); + } + + return $this->modelFactory->createResult( + $this->client->request( + self::METHOD_POST, + self::ACTION_ANSWERS, + ['callback_id' => $callbackId], + array_filter($answerBody, fn($value) => $value !== null) + ) + ); + } + + /** + * Edits a message that was previously sent by the bot. + * Note on attachments: + * - To leave attachments unchanged, pass `null` (default). + * - To remove all attachments, pass an empty array `[]`. + * + * @param string $messageId The identifier of the message to edit. + * @param string|null $text New message text. + * @param AbstractAttachmentRequest[]|null $attachments New message attachments. + * @param MessageFormat|null $format Formatting for the new message text. + * @param MessageLink|null $link New link for the edited message. + * @param bool $notify Notification setting for the edited message. + * + * @return Result + * @throws ClientApiException + * @throws NetworkException + * @throws ReflectionException + * @throws SerializationException + */ + public function editMessage( + string $messageId, + ?string $text = null, + ?array $attachments = null, + ?MessageFormat $format = null, + ?MessageLink $link = null, + bool $notify = true, + ): Result { + return $this->modelFactory->createResult( + $this->client->request( + self::METHOD_PUT, + self::ACTION_MESSAGES, + ['message_id' => $messageId], + $this->buildNewMessageBody($text, $attachments, $format, $link, $notify), + ) + ); + } + + /** + * A helper to build the 'NewMessageBody' array structure consistently. + * + * @param string|null $text + * @param AbstractAttachmentRequest[]|null $attachments + * @param MessageFormat|null $format + * @param MessageLink|null $link + * @param bool $notify + * + * @return array + * @throws ReflectionException + */ + private function buildNewMessageBody( + ?string $text, + ?array $attachments, + ?MessageFormat $format, + ?MessageLink $link, + bool $notify, + ): array { + $body = [ + 'text' => $text, + 'format' => $format?->value, + 'notify' => $notify, + 'link' => $link, + 'attachments' => $attachments !== null ? array_map( + fn(AbstractModel $attachment) => $attachment->toArray(), + $attachments, + ) : null, + ]; + + return array_filter($body, fn($item) => $item !== null); + } } diff --git a/src/Exceptions/NetworkException.php b/src/Exceptions/NetworkException.php index ed3f136..1f48070 100644 --- a/src/Exceptions/NetworkException.php +++ b/src/Exceptions/NetworkException.php @@ -5,12 +5,7 @@ declare(strict_types=1); namespace BushlanovDev\MaxMessengerBot\Exceptions; use RuntimeException; -use Throwable; class NetworkException extends RuntimeException { - public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) - { - parent::__construct($message, $code, $previous); - } } diff --git a/src/Exceptions/SerializationException.php b/src/Exceptions/SerializationException.php index 8525d60..d5acc42 100644 --- a/src/Exceptions/SerializationException.php +++ b/src/Exceptions/SerializationException.php @@ -5,12 +5,7 @@ declare(strict_types=1); namespace BushlanovDev\MaxMessengerBot\Exceptions; use LogicException; -use Throwable; class SerializationException extends LogicException { - public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) - { - parent::__construct($message, $code, $previous); - } } diff --git a/src/Models/ChatAdmin.php b/src/Models/ChatAdmin.php new file mode 100644 index 0000000..03ceaa8 --- /dev/null +++ b/src/Models/ChatAdmin.php @@ -0,0 +1,25 @@ +with($rawResponse) ->willReturn($expectedResult); - $result = $this->api->removeMember($chatId, $userId); + $result = $this->api->deleteMember($chatId, $userId); $this->assertSame($expectedResult, $result); } @@ -1872,8 +1875,228 @@ final class ApiTest extends TestCase ->with($rawResponse) ->willReturn($expectedResult); - $result = $this->api->removeMember($chatId, $userId, true); + $result = $this->api->deleteMember($chatId, $userId, true); $this->assertSame($expectedResult, $result); } + + #[Test] + public function postAdminsCallsClientWithCorrectBody(): void + { + $chatId = 12345; + $uri = sprintf('/chats/%d/members/admins', $chatId); + $admins = [ + new ChatAdmin(101, [ChatAdminPermission::Write]), + new ChatAdmin(202, [ChatAdminPermission::PinMessage]), + ]; + + $expectedBody = [ + 'admins' => [ + ['user_id' => 101, 'permissions' => ['write']], + ['user_id' => 202, 'permissions' => ['pin_message']], + ], + ]; + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('POST', $uri, [], $expectedBody) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->addAdmins($chatId, $admins); + $this->assertSame($expectedResult, $result); + } + + #[Test] + public function addMembersCallsClientWithCorrectBody(): void + { + $chatId = 12345; + $userIds = [101, 202, 303]; + $uri = sprintf('/chats/%d/members', $chatId); + $expectedBody = ['user_ids' => $userIds]; + + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with( + self::equalTo('POST'), + self::equalTo($uri), + self::equalTo([]), + self::equalTo($expectedBody), + ) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->addMembers($chatId, $userIds); + $this->assertSame($expectedResult, $result); + } + + #[Test] + public function answerOnCallbackWithNotificationOnly(): void + { + $callbackId = 'cb.123.abc'; + $notification = 'Action confirmed!'; + $expectedQuery = ['callback_id' => $callbackId]; + $expectedBody = ['notification' => $notification]; + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('POST', '/answers', $expectedQuery, $expectedBody) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->answerOnCallback($callbackId, $notification); + $this->assertSame($expectedResult, $result); + } + + #[Test] + public function answerOnCallbackWithMessageEdit(): void + { + $callbackId = 'cb.456.def'; + $newText = 'Message updated!'; + $expectedQuery = ['callback_id' => $callbackId]; + $expectedBody = [ + 'message' => [ + 'text' => $newText, + 'notify' => true, + ], + ]; + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('POST', '/answers', $expectedQuery, $expectedBody) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->answerOnCallback(callbackId: $callbackId, text: $newText); + $this->assertSame($expectedResult, $result); + } + + #[Test] + public function answerOnCallbackWithBothMessageAndNotification(): void + { + $callbackId = 'cb.789.ghi'; + $notification = 'Done!'; + $newText = 'Updated!'; + $expectedQuery = ['callback_id' => $callbackId]; + $expectedBody = [ + 'notification' => $notification, + 'message' => [ + 'text' => $newText, + 'notify' => true, + ], + ]; + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('POST', '/answers', $expectedQuery, $expectedBody) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->answerOnCallback( + callbackId: $callbackId, + notification: $notification, + text: $newText + ); + $this->assertSame($expectedResult, $result); + } + + #[Test] + public function editMessageCallsClientWithCorrectParameters(): void + { + $messageId = 'mid.123.abc'; + $newText = 'This is the edited text.'; + $expectedQuery = ['message_id' => $messageId]; + $expectedBody = [ + 'text' => $newText, + 'notify' => true, + ]; + + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('PUT', '/messages', $expectedQuery, $expectedBody) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->editMessage($messageId, $newText); + $this->assertSame($expectedResult, $result); + } + + #[Test] + public function editMessageCanClearAttachmentsWithEmptyArray(): void + { + $messageId = 'mid.456.def'; + $expectedQuery = ['message_id' => $messageId]; + $expectedBody = [ + 'attachments' => [], + 'notify' => true, + ]; + + $rawResponse = ['success' => true]; + $expectedResult = new Result(true, null); + + $this->clientMock + ->expects($this->once()) + ->method('request') + ->with('PUT', '/messages', $expectedQuery, $expectedBody) + ->willReturn($rawResponse); + + $this->modelFactoryMock + ->expects($this->once()) + ->method('createResult') + ->with($rawResponse) + ->willReturn($expectedResult); + + $result = $this->api->editMessage($messageId, attachments: []); + $this->assertSame($expectedResult, $result); + } } diff --git a/tests/Models/ChatAdminTest.php b/tests/Models/ChatAdminTest.php new file mode 100644 index 0000000..9671125 --- /dev/null +++ b/tests/Models/ChatAdminTest.php @@ -0,0 +1,31 @@ + 123, + 'permissions' => ['write', 'pin_message'], + ]; + + $this->assertEquals($expected, $admin->toArray()); + } +}