From 21f6cb9e830aa75ffc6a554cfa5bea925303a5b0 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 1 Oct 2026 11:26:19 +0300 Subject: [PATCH] Update tests and Throw InvalidArgumentException when building an empty comment body --- src/Api.php | 19 ++++-- tests/ApiTest.php | 52 +++++++++++++++ tests/ModelFactoryTest.php | 45 +++++++++++++ tests/UpdateDispatcherTest.php | 111 +++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 4 deletions(-) diff --git a/src/Api.php b/src/Api.php index 71f1f77..73b1396 100644 --- a/src/Api.php +++ b/src/Api.php @@ -894,7 +894,7 @@ class Api ?int $count = null, ): array { $query = [ - 'comment_ids' => $commentIds !== null ? implode(',', $commentIds) : null, + 'comment_ids' => ($commentIds === null || $commentIds === []) ? null : implode(',', $commentIds), 'before' => $before, 'after' => $after, 'count' => $count, @@ -938,10 +938,11 @@ class Api * @param string|null $text Comment text. * @param MessageFormat|null $format Comment format. * @param MessageLink|null $link Link to a comment to reply to or forward. - * @param bool $disableLinkPreview If false, server will not generate media preview for links in text. + * @param bool $disableLinkPreview If true, server will not generate media preview for links in text. * * @return CommentMessage * @throws ClientApiException + * @throws InvalidArgumentException * @throws NetworkException * @throws ReflectionException * @throws SerializationException @@ -957,7 +958,7 @@ class Api $this->client->request( self::METHOD_POST, sprintf(self::ACTION_MESSAGE_COMMENTS, $messageId), - ['disable_link_preview' => $disableLinkPreview], + $disableLinkPreview ? ['disable_link_preview' => true] : [], $this->buildNewCommentBody($text, $format, $link), ) ); @@ -974,6 +975,7 @@ class Api * * @return Result * @throws ClientApiException + * @throws InvalidArgumentException * @throws NetworkException * @throws ReflectionException * @throws SerializationException @@ -1405,6 +1407,7 @@ class Api * @param MessageLink|null $link * * @return array + * @throws InvalidArgumentException */ private function buildNewCommentBody(?string $text, ?MessageFormat $format, ?MessageLink $link): array { @@ -1414,6 +1417,14 @@ class Api 'link' => $link, ]; - return array_filter($body, fn($item) => $item !== null); + $body = array_filter($body, fn($item) => $item !== null); + + if ($body === []) { + throw new InvalidArgumentException( + 'At least one of $text, $format or $link must be provided to send or edit a comment.', + ); + } + + return $body; } } diff --git a/tests/ApiTest.php b/tests/ApiTest.php index e993b85..73eac81 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -1382,6 +1382,21 @@ final class ApiTest extends TestCase $this->assertSame([], $this->api->getComments('mid.post')); } + #[Test] + public function getCommentsTreatsEmptyCommentIdsListAsNoFilter(): void + { + $this->clientMock->expects($this->once()) + ->method('request') + ->with('GET', '/messages/mid.post/comments', []) + ->willReturn(['messages' => []]); + + $this->modelFactoryMock->expects($this->once()) + ->method('createCommentMessages') + ->willReturn([]); + + $this->assertSame([], $this->api->getComments('mid.post', [])); + } + #[Test] public function getCommentByIdRequestsTheSingleComment(): void { @@ -1427,6 +1442,21 @@ final class ApiTest extends TestCase $this->assertSame($expectedComment, $result); } + #[Test] + public function sendCommentOmitsDisableLinkPreviewWhenFalse(): void + { + $this->clientMock->expects($this->once()) + ->method('request') + ->with('POST', '/messages/mid.post/comments', [], ['text' => 'Nice post']) + ->willReturn(['message' => $this->commentData()]); + + $this->modelFactoryMock->expects($this->once()) + ->method('createCommentMessageFromSendResponse') + ->willReturn($this->comment()); + + $this->api->sendComment('mid.post', 'Nice post'); + } + #[Test] public function editCommentPutsNewTextForTheComment(): void { @@ -1445,6 +1475,28 @@ final class ApiTest extends TestCase $this->assertSame($expectedResult, $this->api->editComment('mid.post', 'mid.comment', 'Edited')); } + #[Test] + public function sendCommentWithoutAnyFieldThrowsInvalidArgumentException(): void + { + $this->clientMock->expects($this->never())->method('request'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('At least one of $text, $format or $link must be provided'); + + $this->api->sendComment('mid.post'); + } + + #[Test] + public function editCommentWithoutAnyFieldThrowsInvalidArgumentException(): void + { + $this->clientMock->expects($this->never())->method('request'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('At least one of $text, $format or $link must be provided'); + + $this->api->editComment('mid.post', 'mid.comment'); + } + #[Test] public function deleteCommentDeletesByCommentId(): void { diff --git a/tests/ModelFactoryTest.php b/tests/ModelFactoryTest.php index 79303b1..86d0bd8 100644 --- a/tests/ModelFactoryTest.php +++ b/tests/ModelFactoryTest.php @@ -966,6 +966,51 @@ final class ModelFactoryTest extends TestCase $this->assertSame(7, $updateList->marker); } + #[Test] + public function createUpdateListSkipsCommentAndBotPermissionUpdatesThatDoNotFit(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->exactly(4))->method('warning'); + $factory = new ModelFactory($logger); + + $updateList = $factory->createUpdateList([ + 'updates' => [ + // TypeError: required `message` is missing + ['update_type' => 'comment_created', 'timestamp' => 1], + // TypeError: required `message` is missing + ['update_type' => 'comment_edited', 'timestamp' => 2], + // TypeError: required `chat_id` is missing + ['update_type' => 'comment_removed', 'timestamp' => 3, 'message_id' => 'mid.c'], + // ValueError: unknown permission value + [ + 'update_type' => 'bot_admin_permissions_changed', + 'timestamp' => 4, + 'chat_id' => -100, + 'user_id' => 50, + 'bot_id' => 999, + 'is_channel' => true, + 'is_admin' => true, + 'permissions' => ['not_a_permission'], + ], + // valid comment_removed survives + [ + 'update_type' => 'comment_removed', + 'timestamp' => 5, + 'message_id' => 'mid.ok', + 'chat_id' => -100, + 'user_id' => 50, + 'post_id' => 'mid.post', + ], + ], + 'marker' => 9, + ]); + + $this->assertCount(1, $updateList->updates); + $this->assertInstanceOf(CommentRemovedUpdate::class, $updateList->updates[0]); + $this->assertSame('mid.ok', $updateList->updates[0]->messageId); + $this->assertSame(9, $updateList->marker); + } + public static function nonArrayPayloadProvider(): array { return [ diff --git a/tests/UpdateDispatcherTest.php b/tests/UpdateDispatcherTest.php index 1506008..e4df528 100644 --- a/tests/UpdateDispatcherTest.php +++ b/tests/UpdateDispatcherTest.php @@ -5,12 +5,17 @@ declare(strict_types=1); namespace BushlanovDev\MaxMessengerBot\Tests; use BushlanovDev\MaxMessengerBot\Api; +use BushlanovDev\MaxMessengerBot\Enums\ChatAdminPermission; use BushlanovDev\MaxMessengerBot\Enums\ChatType; use BushlanovDev\MaxMessengerBot\Enums\UpdateType; use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\MessageBody; use BushlanovDev\MaxMessengerBot\Models\Recipient; +use BushlanovDev\MaxMessengerBot\Models\Updates\BotAdminPermissionsChangedUpdate; use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate; +use BushlanovDev\MaxMessengerBot\Models\Updates\CommentCreatedUpdate; +use BushlanovDev\MaxMessengerBot\Models\Updates\CommentEditedUpdate; +use BushlanovDev\MaxMessengerBot\Models\Updates\CommentRemovedUpdate; use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate; use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto; use BushlanovDev\MaxMessengerBot\UpdateDispatcher; @@ -28,6 +33,10 @@ use PHPUnit\Framework\TestCase; #[UsesClass(MessageBody::class)] #[UsesClass(Recipient::class)] #[UsesClass(MessageCreatedUpdate::class)] +#[UsesClass(CommentCreatedUpdate::class)] +#[UsesClass(CommentEditedUpdate::class)] +#[UsesClass(CommentRemovedUpdate::class)] +#[UsesClass(BotAdminPermissionsChangedUpdate::class)] final class UpdateDispatcherTest extends TestCase { private Api $apiMock; @@ -117,4 +126,106 @@ final class UpdateDispatcherTest extends TestCase $this->assertFalse($commandCalled, 'onCommand handler should not be called for a regular message.'); $this->assertTrue($messageHandlerCalled, 'onMessageCreated handler was not called.'); } + + #[Test] + public function onCommentCreatedDispatchesCommentCreatedUpdate(): void + { + $wasCalled = false; + $message = new Message( + time(), + new Recipient(ChatType::Channel, null, -100), + new MessageBody('mid.c1', 1, 'A comment', null, null), + new UserWithPhoto(50, 'Anna', null, null, false, time(), null, null, null), + null, + null, + null, + ); + $update = new CommentCreatedUpdate(time(), $message); + + $this->dispatcher->onCommentCreated( + function ($receivedUpdate, $receivedApi) use (&$wasCalled, $update) { + $this->assertSame($update, $receivedUpdate); + $this->assertSame($this->apiMock, $receivedApi); + $wasCalled = true; + }, + ); + + $this->dispatcher->dispatch($update); + + $this->assertTrue($wasCalled, 'onCommentCreated handler was not called.'); + } + + #[Test] + public function onCommentEditedDispatchesCommentEditedUpdate(): void + { + $wasCalled = false; + $message = new Message( + time(), + new Recipient(ChatType::Channel, null, -100), + new MessageBody('mid.c1', 2, 'Edited comment', null, null), + new UserWithPhoto(50, 'Anna', null, null, false, time(), null, null, null), + null, + null, + null, + ); + $update = new CommentEditedUpdate(time(), $message); + + $this->dispatcher->onCommentEdited( + function ($receivedUpdate, $receivedApi) use (&$wasCalled, $update) { + $this->assertSame($update, $receivedUpdate); + $this->assertSame($this->apiMock, $receivedApi); + $wasCalled = true; + }, + ); + + $this->dispatcher->dispatch($update); + + $this->assertTrue($wasCalled, 'onCommentEdited handler was not called.'); + } + + #[Test] + public function onCommentRemovedDispatchesCommentRemovedUpdate(): void + { + $wasCalled = false; + $update = new CommentRemovedUpdate(time(), 'mid.c1', -100, 50, 'mid.post'); + + $this->dispatcher->onCommentRemoved( + function ($receivedUpdate, $receivedApi) use (&$wasCalled, $update) { + $this->assertSame($update, $receivedUpdate); + $this->assertSame($this->apiMock, $receivedApi); + $wasCalled = true; + }, + ); + + $this->dispatcher->dispatch($update); + + $this->assertTrue($wasCalled, 'onCommentRemoved handler was not called.'); + } + + #[Test] + public function onBotAdminPermissionsChangedDispatchesPermissionsUpdate(): void + { + $wasCalled = false; + $update = new BotAdminPermissionsChangedUpdate( + time(), + -100, + 50, + 999, + true, + true, + [ChatAdminPermission::ReadAllMessages, ChatAdminPermission::Write], + ); + + $this->dispatcher->onBotAdminPermissionsChanged( + function ($receivedUpdate, $receivedApi) use (&$wasCalled, $update) { + $this->assertSame($update, $receivedUpdate); + $this->assertSame($this->apiMock, $receivedApi); + $wasCalled = true; + }, + ); + + $this->dispatcher->dispatch($update); + + $this->assertTrue($wasCalled, 'onBotAdminPermissionsChanged handler was not called.'); + } }