Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 37 additions & 21 deletions src/ModelFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use ReflectionException;
use TypeError;
use ValueError;

/**
* Creates DTOs from raw associative arrays returned by the API client.
Expand Down Expand Up @@ -375,27 +377,41 @@ public function createUpdateList(array $data): UpdateList
*/
public function createUpdate(array $data): AbstractUpdate
{
return match (UpdateType::tryFrom($data['update_type'] ?? '')) {
UpdateType::MessageCreated => MessageCreatedUpdate::fromArray($data),
UpdateType::MessageCallback => MessageCallbackUpdate::fromArray($data),
UpdateType::MessageEdited => MessageEditedUpdate::fromArray($data),
UpdateType::MessageRemoved => MessageRemovedUpdate::fromArray($data),
UpdateType::BotAdded => BotAddedToChatUpdate::fromArray($data),
UpdateType::BotRemoved => BotRemovedFromChatUpdate::fromArray($data),
UpdateType::DialogMuted => DialogMutedUpdate::fromArray($data),
UpdateType::DialogUnmuted => DialogUnmutedUpdate::fromArray($data),
UpdateType::DialogCleared => DialogClearedUpdate::fromArray($data),
UpdateType::DialogRemoved => DialogRemovedUpdate::fromArray($data),
UpdateType::UserAdded => UserAddedToChatUpdate::fromArray($data),
UpdateType::UserRemoved => UserRemovedFromChatUpdate::fromArray($data),
UpdateType::BotStarted => BotStartedUpdate::fromArray($data),
UpdateType::BotStopped => BotStoppedUpdate::fromArray($data),
UpdateType::ChatTitleChanged => ChatTitleChangedUpdate::fromArray($data),
UpdateType::MessageChatCreated => MessageChatCreatedUpdate::fromArray($data),
default => throw new LogicException(
'Unknown or unsupported update type received: ' . ($data['update_type'] ?? 'none')
),
};
try {
return match (UpdateType::tryFrom($data['update_type'] ?? '')) {
UpdateType::MessageCreated => MessageCreatedUpdate::fromArray($data),
UpdateType::MessageCallback => MessageCallbackUpdate::fromArray($data),
UpdateType::MessageEdited => MessageEditedUpdate::fromArray($data),
UpdateType::MessageRemoved => MessageRemovedUpdate::fromArray($data),
UpdateType::BotAdded => BotAddedToChatUpdate::fromArray($data),
UpdateType::BotRemoved => BotRemovedFromChatUpdate::fromArray($data),
UpdateType::DialogMuted => DialogMutedUpdate::fromArray($data),
UpdateType::DialogUnmuted => DialogUnmutedUpdate::fromArray($data),
UpdateType::DialogCleared => DialogClearedUpdate::fromArray($data),
UpdateType::DialogRemoved => DialogRemovedUpdate::fromArray($data),
UpdateType::UserAdded => UserAddedToChatUpdate::fromArray($data),
UpdateType::UserRemoved => UserRemovedFromChatUpdate::fromArray($data),
UpdateType::BotStarted => BotStartedUpdate::fromArray($data),
UpdateType::BotStopped => BotStoppedUpdate::fromArray($data),
UpdateType::ChatTitleChanged => ChatTitleChangedUpdate::fromArray($data),
UpdateType::MessageChatCreated => MessageChatCreatedUpdate::fromArray($data),
default => throw new LogicException(
'Unknown or unsupported update type received: ' . ($data['update_type'] ?? 'none')
),
};
} catch (TypeError|ValueError $e) {
// The update type is known, but its payload does not fit the model: the API has
// changed a field. Skip it like an unsupported update, so a webhook or a long-polling
// loop does not stop on it, but log it louder.
$message = sprintf(
'Failed to parse update of type %s: %s',
is_string($data['update_type'] ?? null) ? $data['update_type'] : 'none',
$e->getMessage(),
);
$this->logger->warning($message, ['payload' => $data, 'exception' => $e]);

throw new LogicException($message, 0, $e);
}
}

/**
Expand Down
57 changes: 57 additions & 0 deletions tests/ModelFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,63 @@ public function createAttachmentSuccessfullyCreatesVariousTypes(
$assertionCallback($this, $attachment);
}

#[Test]
public function createUpdateTurnsPayloadThatDoesNotFitTheModelIntoLogicException(): void
{
$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())
->method('warning')
->with($this->stringStartsWith('Failed to parse update of type bot_started: '));
$factory = new ModelFactory($logger);

try {
// `user` is required by BotStartedUpdate
$factory->createUpdate(['update_type' => 'bot_started', 'timestamp' => 1, 'chat_id' => 123]);
$this->fail('LogicException expected');
} catch (LogicException $e) {
$this->assertInstanceOf(\TypeError::class, $e->getPrevious());
}
}

#[Test]
public function createUpdateListSkipsUpdatesThatDoNotFitTheirModel(): void
{
$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->exactly(2))->method('warning');
$factory = new ModelFactory($logger);

$updateList = $factory->createUpdateList([
'updates' => [
// TypeError: a required field is missing
['update_type' => 'bot_started', 'timestamp' => 1, 'chat_id' => 123],
// ValueError: an enum value the library does not know yet
[
'update_type' => 'message_created',
'timestamp' => 2,
'message' => [
'timestamp' => 2,
'body' => ['mid' => 'mid.1', 'seq' => 1],
'recipient' => ['chat_type' => 'some_new_chat_type'],
],
],
[
'update_type' => 'message_created',
'timestamp' => 3,
'message' => [
'timestamp' => 3,
'body' => ['mid' => 'mid.2', 'seq' => 2],
'recipient' => ['chat_type' => 'dialog'],
],
],
],
'marker' => 7,
]);

$this->assertCount(1, $updateList->updates);
$this->assertInstanceOf(MessageCreatedUpdate::class, $updateList->updates[0]);
$this->assertSame(7, $updateList->marker);
}

#[Test]
public function createUpdateListCatchesAndLogsLogicException(): void
{
Expand Down
23 changes: 23 additions & 0 deletions tests/WebhookHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#[UsesClass(Recipient::class)]
#[UsesClass(AbstractUpdate::class)]
#[UsesClass(MessageCreatedUpdate::class)]
#[UsesClass(ModelFactory::class)]
final class WebhookHandlerTest extends TestCase
{
use PHPMock;
Expand Down Expand Up @@ -226,4 +227,26 @@ function (string $message, array $context = []) use (&$callIndex, $payload, $exc

$handler->handle($request);
}

#[Test]
public function handleSkipsUpdateThatDoesNotFitItsModel(): void
{
// `user` is required by BotStartedUpdate: before, the TypeError escaped the handler
$payload = '{"update_type":"bot_started","timestamp":123,"chat_id":1}';
$request = $this->createMockRequest($payload, self::SECRET);

$this->loggerMock->expects($this->once())
->method('warning')
->with($this->stringStartsWith('Failed to parse update of type bot_started: '));

$handlerWasCalled = false;
$this->dispatcher->addHandler(UpdateType::BotStarted, function () use (&$handlerWasCalled) {
$handlerWasCalled = true;
});

$handler = new WebhookHandler($this->dispatcher, new ModelFactory($this->loggerMock), $this->loggerMock, self::SECRET);
$handler->handle($request);

$this->assertFalse($handlerWasCalled);
}
}
Loading