From 6e0c1b1f44462867132435be984dcd96eaee3ad2 Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Tue, 14 May 2024 12:32:38 +0200 Subject: [PATCH 1/7] Update @todo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7ddb7b..b78010e 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Tasks to do for v1.0: - [x] Add documentation and examples - [x] Support for channel bots - [ ] Improve test coverage -- [ ] Improve developer experience (cleaner API, better error handling) +- [ ] Improve developer experience (cleaner API (similar method in Dialog and DialogManager)) - [ ] Reach message type validation - [ ] Reach API to validate message types and content - [ ] Support `\Iterator`s and/or `\Generator`s for Dialog steps From 045bb542f01eeb92612b4ed35dd344a0f09a5a54 Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Tue, 14 May 2024 12:35:51 +0200 Subject: [PATCH 2/7] Fix type issue --- src/Exceptions/ControlFlow/SwitchToAnotherDialog.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Exceptions/ControlFlow/SwitchToAnotherDialog.php b/src/Exceptions/ControlFlow/SwitchToAnotherDialog.php index e571098..9fb99c3 100644 --- a/src/Exceptions/ControlFlow/SwitchToAnotherDialog.php +++ b/src/Exceptions/ControlFlow/SwitchToAnotherDialog.php @@ -13,7 +13,7 @@ */ final class SwitchToAnotherDialog extends \LogicException implements DialogControlFlowException { - public ?Dialog $nextDialog = null; + public Dialog $nextDialog; private function __construct(Dialog $nextDialog) { From 88d3870ce1635a18b2a54d64bdb3a5b734e3d26d Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Tue, 14 May 2024 12:44:20 +0200 Subject: [PATCH 3/7] Update docs to avoid confusion between Dialog and DialogManager APIs --- README.md | 19 +++++++++++-------- src/Dialog.php | 7 ++++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b78010e..ac0c6f0 100644 --- a/README.md +++ b/README.md @@ -126,11 +126,8 @@ final class TelegramWebhookController } ``` - ### `Dialog` class API -- `start(\Telegram\Bot\Objects\Update $update)` - Start the dialog from the first step -- `proceed(\Telegram\Bot\Objects\Update $update)` - Proceed the dialog to the next step - `isEnd()` - Check the end of the dialog - 🔐 `end()` - End dialog - 🔐 `jump(string $stepName)` - Jump to the particular step, where `$step` is the `public` method name @@ -140,12 +137,18 @@ final class TelegramWebhookController ### `DialogManager` class API -ℹī¸ `Dialogs` [Facade](https://laravel.com/docs/master/facades) proxies calls to `DialogManager` class. +`DialogManager` is in charge of: + - storing and recovering Dialog instances between steps/requests + - running Dialog steps (using Dialog public API) + - switching/activating Dialogs + +For Laravel apps, the package provides `Dialogs` Facade, that proxies calls to `DialogManager` class. -- `setBot(\Telegram\Bot\Api $bot)` - Use non-default Bot for API calls -- `activate(\KootLabs\TelegramBotDialogs\Dialog $dialog)` - Activate a new Dialog (without running it) -- `proceed(\Telegram\Bot\Objects\Update $update)` - Run the next step handler for the existing Dialog -- `exists(\Telegram\Bot\Objects\Update $update)` - Check for existing Dialog +`DialogManager` public API: +- `activate(\KootLabs\TelegramBotDialogs\Dialog $dialog)` - Activate a new Dialog (without running it). The same user/chat may have few open Dialogs, DialogManager should know which one is active. +- `proceed(\Telegram\Bot\Objects\Update $update)` - Run the next step handler for the active Dialog (if exists) +- `exists(\Telegram\Bot\Objects\Update $update)` - Check for existing Dialog for a given Update (based on chat_id and optional user_id) +- `setBot(\Telegram\Bot\Api $bot)` - Use non-default Bot for Telegram Bot API calls ## ToDo diff --git a/src/Dialog.php b/src/Dialog.php index 1a9e3a4..c517daf 100644 --- a/src/Dialog.php +++ b/src/Dialog.php @@ -59,7 +59,10 @@ final public function setBot(Api $bot): void $this->bot = $bot; } - /** Start Dialog from the begging. */ + /** + * @deprecated Will be removed in v1.0. + * Start Dialog from the begging. + */ final public function start(Update $update): void { $this->next = 0; @@ -67,6 +70,8 @@ final public function start(Update $update): void } /** + * @internal Should be called by {@see \KootLabs\TelegramBotDialogs\DialogManager::proceed}, + * please do not call this method directly. * @throws \KootLabs\TelegramBotDialogs\Exceptions\InvalidDialogStep * @throws \Telegram\Bot\Exceptions\TelegramSDKException */ From 01dbcc2934a8112b5ac8c813957a72b57809121c Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Tue, 14 May 2024 14:43:31 +0200 Subject: [PATCH 4/7] Add tests for DialogManager::exists and DialogManager::activate --- composer.json | 1 + tests/DialogManagerTest.php | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/DialogManagerTest.php diff --git a/composer.json b/composer.json index 05afbb3..7983a4f 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.54", "phpunit/phpunit": "^10.5 || ^11.0", + "symfony/cache": "^7.0", "vimeo/psalm": "^5.24" }, "minimum-stability": "dev", diff --git a/tests/DialogManagerTest.php b/tests/DialogManagerTest.php new file mode 100644 index 0000000..3f1df75 --- /dev/null +++ b/tests/DialogManagerTest.php @@ -0,0 +1,58 @@ +instantiateDialogManager(); + $dialog = new HelloExampleDialog(42); + + $dialogManager->activate($dialog); + $exist = $dialogManager->exists(new Update(['message' => ['chat' => ['id' => 42]]])); + + $this->assertTrue($exist); + } + + #[Test] + public function it_do_not_find_dialog_if_it_ts_not_activated(): void + { + $dialogManager = $this->instantiateDialogManager(); + + $exist = $dialogManager->exists(new Update(['message' => ['chat' => ['id' => 42]]])); + + $this->assertFalse($exist); + } + + #[Test] + public function it_do_not_find_dialog_if_it_ts_not_activated_for_the_current_chat(): void + { + $dialogManager = $this->instantiateDialogManager(); + $dialog = new HelloExampleDialog(42); + + $dialogManager->activate($dialog); + $exist = $dialogManager->exists(new Update(['message' => ['chat' => ['id' => 43]]])); + + $this->assertFalse($exist); + } +} From 4dc7b5c5ecaca7eb5c07f2793504fbb5bedd9359 Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Tue, 14 May 2024 14:44:15 +0200 Subject: [PATCH 5/7] Fix coding style issues --- src/Exceptions/ControlFlow/DialogControlFlowException.php | 4 +--- tests/DialogManagerTest.php | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Exceptions/ControlFlow/DialogControlFlowException.php b/src/Exceptions/ControlFlow/DialogControlFlowException.php index d1c2242..25c2adb 100644 --- a/src/Exceptions/ControlFlow/DialogControlFlowException.php +++ b/src/Exceptions/ControlFlow/DialogControlFlowException.php @@ -7,6 +7,4 @@ use KootLabs\TelegramBotDialogs\Exceptions\DialogException; /** @internal */ -interface DialogControlFlowException extends DialogException -{ -} \ No newline at end of file +interface DialogControlFlowException extends DialogException {} diff --git a/tests/DialogManagerTest.php b/tests/DialogManagerTest.php index 3f1df75..895452d 100644 --- a/tests/DialogManagerTest.php +++ b/tests/DialogManagerTest.php @@ -1,4 +1,6 @@ - Date: Tue, 14 May 2024 14:46:36 +0200 Subject: [PATCH 6/7] Fix tests on PHP 8.0 and 8.1 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 7983a4f..7e5273a 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ ], "homepage": "https://github.com/koot-labs/telegram-bot-dialogs", "require": { - "php": ">=8.0", + "php": ">=8.0.2", "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", "irazasyed/telegram-bot-sdk": "^3.1", "predis/predis": "^1.0 || ^2.0", @@ -19,7 +19,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.54", "phpunit/phpunit": "^10.5 || ^11.0", - "symfony/cache": "^7.0", + "symfony/cache": "^6.0 || ^7.0", "vimeo/psalm": "^5.24" }, "minimum-stability": "dev", From 18e2cd601d403fc2cf2fe9b413987ef64bc6e46f Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Tue, 14 May 2024 15:14:07 +0200 Subject: [PATCH 7/7] Fix generateDialogKeyUserBounded and add more tests --- phpunit.xml | 1 + src/DialogManager.php | 2 +- tests/DialogManagerTest.php | 42 ++++++++++++++++++++------ tests/DialogSerializationTest.php | 48 ++++++++++++++++++++++++++++++ tests/Fakes/FakeBot.php | 21 +++++++++++++ tests/Fakes/FakeHttp.php | 49 +++++++++++++++++++++++++++++++ 6 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 tests/DialogSerializationTest.php create mode 100644 tests/Fakes/FakeBot.php create mode 100644 tests/Fakes/FakeHttp.php diff --git a/phpunit.xml b/phpunit.xml index 116f43f..fcf1561 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,6 +5,7 @@ executionOrder="depends,defects" beStrictAboutOutputDuringTests="true" beStrictAboutCoverageMetadata="true" + displayDetailsOnTestsThatTriggerWarnings="true" failOnRisky="true" failOnWarning="true" cacheDirectory=".phpunit.cache" diff --git a/src/DialogManager.php b/src/DialogManager.php index 17fe680..e4ab69f 100644 --- a/src/DialogManager.php +++ b/src/DialogManager.php @@ -147,8 +147,8 @@ private function readDialogState(string $key): Dialog private function generateDialogKeyUserBounded(Update $update): string { return implode('-', [ - $update->getMessage()->from->id, $update->getChat()->id, + $update->getMessage()->from->id, ]); } diff --git a/tests/DialogManagerTest.php b/tests/DialogManagerTest.php index 895452d..ec19e59 100644 --- a/tests/DialogManagerTest.php +++ b/tests/DialogManagerTest.php @@ -16,6 +16,9 @@ #[CoversClass(\KootLabs\TelegramBotDialogs\DialogManager::class)] final class DialogManagerTest extends TestCase { + private const RANDOM_CHAT_ID = 42; + private const RANDOM_USER_ID = 110; + private function instantiateDialogManager(): DialogManager { return new DialogManager( @@ -28,33 +31,54 @@ private function instantiateDialogManager(): DialogManager public function it_finds_an_activated_dialog(): void { $dialogManager = $this->instantiateDialogManager(); - $dialog = new HelloExampleDialog(42); + $dialog = new HelloExampleDialog(self::RANDOM_CHAT_ID); $dialogManager->activate($dialog); - $exist = $dialogManager->exists(new Update(['message' => ['chat' => ['id' => 42]]])); + $exist = $dialogManager->exists(new Update(['message' => ['chat' => ['id' => self::RANDOM_CHAT_ID]]])); $this->assertTrue($exist); } #[Test] - public function it_do_not_find_dialog_if_it_ts_not_activated(): void + public function it_finds_a_user_bounded_activated_dialog(): void { $dialogManager = $this->instantiateDialogManager(); + $dialog = new HelloExampleDialog(self::RANDOM_CHAT_ID, null, self::RANDOM_USER_ID); - $exist = $dialogManager->exists(new Update(['message' => ['chat' => ['id' => 42]]])); + $dialogManager->activate($dialog); + $existResult = $dialogManager->exists(new Update(['message' => [ + 'chat' => ['id' => self::RANDOM_CHAT_ID], + 'from' => ['id' => self::RANDOM_USER_ID], + ]])); - $this->assertFalse($exist); + $this->assertTrue($existResult); } #[Test] - public function it_do_not_find_dialog_if_it_ts_not_activated_for_the_current_chat(): void + public function it_do_not_find_dialog_if_it_is_not_activated(): void { $dialogManager = $this->instantiateDialogManager(); - $dialog = new HelloExampleDialog(42); + $existResult = $dialogManager->exists(new Update(['message' => [ + 'chat' => ['id' => self::RANDOM_CHAT_ID], + 'from' => ['id' => self::RANDOM_USER_ID], + ]])); + + $this->assertFalse($existResult); + } + + #[Test] + public function it_do_not_find_dialog_if_it_ts_not_activated_for_the_current_chat(): void + { + $dialogManager = $this->instantiateDialogManager(); + $dialog = new HelloExampleDialog(self::RANDOM_CHAT_ID); $dialogManager->activate($dialog); - $exist = $dialogManager->exists(new Update(['message' => ['chat' => ['id' => 43]]])); - $this->assertFalse($exist); + $existResult = $dialogManager->exists(new Update(['message' => [ + 'chat' => ['id' => 43], + 'from' => ['id' => self::RANDOM_USER_ID]], + ])); + + $this->assertFalse($existResult); } } diff --git a/tests/DialogSerializationTest.php b/tests/DialogSerializationTest.php new file mode 100644 index 0000000..0b7c7a5 --- /dev/null +++ b/tests/DialogSerializationTest.php @@ -0,0 +1,48 @@ +createBotWithQueuedResponse(); + $dialog = new HelloExampleDialog(self::RANDOM_CHAT_ID, $bot); + + $unserializedDialog = unserialize(serialize($dialog)); + + $this->assertSame($dialog->getChatId(), $unserializedDialog->getChatId()); + $this->assertSame($dialog->getUserId(), $unserializedDialog->getUserId()); + $this->assertSame($dialog->ttl(), $unserializedDialog->ttl()); + $this->assertSame($dialog->isStart(), $unserializedDialog->isStart()); + } + + #[Test] + public function it_can_be_serialized_and_unserialized_on_step_after_first(): void + { + $bot = $this->createBotWithQueuedResponse(); + $dialog = new HelloExampleDialog(self::RANDOM_CHAT_ID, $bot); + $dialog->proceed(new Update([])); + + $unserializedDialog = unserialize(serialize($dialog)); + + $this->assertSame($dialog->getChatId(), $unserializedDialog->getChatId()); + $this->assertSame($dialog->getUserId(), $unserializedDialog->getUserId()); + $this->assertSame($dialog->ttl(), $unserializedDialog->ttl()); + $this->assertSame($dialog->isStart(), $unserializedDialog->isStart()); + } +} diff --git a/tests/Fakes/FakeBot.php b/tests/Fakes/FakeBot.php new file mode 100644 index 0000000..41c5796 --- /dev/null +++ b/tests/Fakes/FakeBot.php @@ -0,0 +1,21 @@ +getGuzzleHttpClient([$this->makeFakeServerResponse($data, $statusCode, $headers)]) + ); + } +} \ No newline at end of file diff --git a/tests/Fakes/FakeHttp.php b/tests/Fakes/FakeHttp.php new file mode 100644 index 0000000..1cb4179 --- /dev/null +++ b/tests/Fakes/FakeHttp.php @@ -0,0 +1,49 @@ +createClientWithQueuedResponse($responsesToQueue); + + return new GuzzleHttpClient($client); + } + + protected function createClientWithQueuedResponse(array $responsesToQueue): Client + { + $this->history = []; + $handler = HandlerStack::create(new MockHandler($responsesToQueue)); + $handler->push(Middleware::history($this->history)); + + return new Client(['handler' => $handler]); + } + + public function makeFakeServerResponse(array $data, int $statusCode = 200, array $headers = []): Response + { + return new Response( + $statusCode, + $headers, + json_encode([ + 'ok' => true, + 'result' => $data, + ], \JSON_THROW_ON_ERROR) + ); + } +} \ No newline at end of file