Compare commits

...

57 Commits

Author SHA1 Message Date
Alex f788038189 Update doc 2026-07-05 13:57:03 +03:00
Alex 22b328b0c3 Update doc 2026-07-02 22:32:39 +03:00
Alex daa3d297da Update doc 2026-07-02 22:19:34 +03:00
Alex a40e47ebc0 Update doc 2026-07-02 22:17:38 +03:00
Alex 99f66483f3 Update doc 2026-07-02 22:13:24 +03:00
Alex e5a6ec40fc Update composer 2026-06-27 16:40:46 +03:00
Alex 9eb8783f40 Merge pull request #36 from tofers/patch-1
Change url address
2026-06-27 09:38:23 +03:00
Konstantin 40de64593b Change url address
Для корректной работы ваших чат-ботов и мини-приложений до 19 июля 2026 необходимо перенаправить HTTP-запросы с домена platform-api.max.ru на platform-api2.max.ru, а также добавить сертификат Минцифры в список доверенных
2026-06-25 12:53:49 +03:00
Alex dfe2147cd1 tag 1.6.3 2026-04-26 20:36:38 +03:00
Alex 3d958f3f92 Update tests 2026-04-26 20:28:07 +03:00
Alex f40dc12d0c Merge pull request #32 from imperiumbee/master
New button type clipboard
2026-04-26 19:04:43 +03:00
imperiumbee b726c96366 Merge branch 'BushlanovDev:master' into master 2026-04-25 18:41:05 +03:00
imperiumbee c41221cec7 New button type clipboard 2026-04-25 18:38:55 +03:00
Alex 4fdfca53ee tag 1.6.2 2026-04-17 20:21:29 +03:00
Alex 9df8ed8817 Merge pull request #31 from spyke113/master
add `psr/log` versions
2026-04-16 14:46:34 +03:00
szuev 455b36ba2a add psr/log versions 2026-04-16 13:05:58 +03:00
Alex efd6261490 Added chat admin permissions 2026-04-15 22:10:15 +03:00
Alex dd295efc3d tag 1.6.0 2026-03-22 09:23:35 +03:00
Alex 85231b0962 Removed deprecated DataAttachment 2026-03-21 09:44:40 +03:00
Alex 40742ddc60 Removed deprecated ReplyKeyboardAttachment #27 2026-03-20 22:55:36 +03:00
Alex 1e48123ee3 Updated webhook documentation #26 2026-03-13 20:54:49 +03:00
Alex affe904bed tag 1.5.1 2026-03-07 09:47:01 +03:00
Alex 6c4e0c8ca7 Merge pull request #24 from imperiumbee/master
Added payload param to openAppButton
2026-03-07 09:25:53 +03:00
imperiumbee 3eb3e07bfa Added payload param to openAppButton 2026-03-06 11:12:20 +03:00
Alex ab7ea25796 Месседжер MAX следит за пользователями VPN 2026-03-05 15:24:24 +03:00
Alex 2a97a15889 1.5.0 2025-12-20 20:36:59 +03:00
Alex e9904cec71 Merge pull request #23 from BushlanovDev/1.5.0
1.5.0
2025-12-20 20:35:00 +03:00
Alex 67463a6b64 Replacing the User object with UserWithPhoto #22 2025-12-19 13:30:36 +03:00
Alex 3c6da8e8f6 Added MessageButton & ChatAdminPermission::EditLink & alias in ChatAdmin & participants in Chat & first_name in BotPatch 2025-12-18 19:50:58 +03:00
Alex cbb4cbcf0e Removed the typo 2025-12-10 21:51:13 +03:00
Alex a7773c0b93 Added AttachmentNotReadyException (#19) 2025-12-06 15:15:39 +03:00
Alex 79d406da27 ShareAttachmentRequestPayload & PhotoAttachmentRequestPayload update exception rule (#18) 2025-12-05 13:53:07 +03:00
Alex ef3b76394c tag 1.4.0 2025-12-04 08:44:10 +03:00
Alex 2ecb5c7b04 Merge pull request #17 from BushlanovDev/i15
Silent mode for errors of unsupported event types #15
2025-12-04 08:41:54 +03:00
Alex 2e7e9d987f Added DialogUnmuted & DialogCleared & DialogRemoved update types 2025-12-03 23:05:09 +03:00
Alex cf415b70e9 Added DialogMuted & BotStopped update types 2025-12-02 20:00:34 +03:00
Alex a94dfe0df4 Silent mode for errors of unsupported event types #15 2025-12-01 20:11:47 +03:00
Alex 03b1f158d9 Disabled api version #16 2025-11-30 17:10:00 +03:00
Alex d853faacf7 tag 1.3.2 2025-10-21 09:05:51 +03:00
Alex 576f02efbd Merge pull request #14 from BushlanovDev/i12
fix 12
2025-10-21 09:02:44 +03:00
Alex da7a07145b Fix send message response object 2025-10-20 21:27:54 +03:00
Alex 7cdbfc0f47 Update Message model 2025-10-08 23:14:06 +03:00
Alex f29344d2ec Update Message model 2025-10-07 23:56:57 +03:00
Alex 3012272497 Merge pull request #11 from BushlanovDev/fix10
Fix authorization with header #10
2025-10-04 21:54:19 +03:00
Alex 0dc10bd279 Fix authorization with header #10 2025-10-04 21:51:14 +03:00
Alex 8419a7892d Merge remote-tracking branch 'origin/master' 2025-09-29 19:28:41 +03:00
Alex ddc57d7ab8 Change api domain and authorization with header 2025-09-29 19:28:20 +03:00
Alex d0f044c7bd Change api domain and authorization with header 2025-09-29 19:25:28 +03:00
Alex 31f109db7e Merge pull request #8 from BushlanovDev/docs
Update documentation
2025-09-16 21:25:38 +03:00
Alex e1375acabb Update documentation 2025-09-16 21:22:04 +03:00
Alex 098aa44825 Update documentation 2025-09-15 23:04:15 +03:00
Alex 6137b2ea6e Merge pull request #7 from BushlanovDev/docs
Update documentation
2025-09-14 15:55:33 +03:00
Alex 122afcc7c0 Merge branch 'master' into docs 2025-09-14 15:44:19 +03:00
Alex 4d56119fa2 Disable deprecation error for unit test 2025-09-13 19:53:43 +03:00
Alex ae53f51854 Disable deprecation error for unit test 2025-09-13 19:26:13 +03:00
Alex b87b41ff8f Update documentation 2025-09-12 22:06:23 +03:00
Alex 493be1a9a0 Add php8.5 to github action 2025-09-10 21:25:52 +03:00
99 changed files with 6374 additions and 720 deletions
+1 -12
View File
@@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: [ "8.3", "8.4" ]
php-versions: [ "8.3", "8.4", "8.5" ]
name: PHP ${{ matrix.php-versions }} Test on ubuntu-latest
steps:
@@ -43,17 +43,6 @@ jobs:
php-version: ${{ matrix.php-versions }}
coverage: xdebug
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install composer dependencies
run: composer install --prefer-dist --no-interaction
+55 -3
View File
@@ -9,12 +9,15 @@
> [!CAUTION]
> На мой взгляд `Max Messenger` является ни чем иным как малварью, созданной для слежки за гражданами РФ. Настоятельно
> не рекомендую использовать его на реальных устройствах, с настоящим номером телефона, и для личной переписки.
> не рекомендую использовать его на реальных устройствах, с настоящим номером телефона, и для личной переписки.
> Обязательно к прочтению - [Месседжер MAX следит за пользователями VPN](https://habr.com/ru/articles/1006666/)
## Быстрый старт
> Если вы новичок, то можете прочитать [официальную документацию](https://dev.max.ru/), написанную разработчиками Max.
> ℹ️ С полной документацией [вы можете ознакомиться тут](./docs/README.md).
### Получение токена
Откройте диалог с [MasterBot](https://max.ru/MasterBot), следуйте инструкциям и создайте нового бота. После создания
@@ -42,6 +45,55 @@ composer require bushlanov-dev/max-bot-api-client-php
### Использование
> [!NOTE]
> С 19 июля 2026 основной домен api изменится с platform-api.max.ru на platform-api2.max.ru и начнет использовать чебурнетовский сертификат!
> Вам необходимо либо установить сертификат, либо отключить его проверку, оба варианта описаны ниже.
Установка сертификата на примере ОС Ubuntu
```bash
# корневой сертификат
curl -k -O "https://gu-st.ru/content/Other/doc/russian_trusted_root_ca.cer"
# промежуточный сертификат
curl -k -O "https://gu-st.ru/content/Other/doc/russian_trusted_sub_ca.cer"
sudo cp russian_trusted_root_ca.cer /usr/local/share/ca-certificates/russian_trusted_root_ca.crt
sudo cp russian_trusted_sub_ca.cer /usr/local/share/ca-certificates/russian_trusted_sub_ca.crt
sudo update-ca-certificates
```
> [!TIP]
> **Гибкая настройка Guzzle**
> Мах часто меняют домен API а теперь еще и сертификат.
> Если вы не хотите или не можете установить сертификат на прямую в систему, можно собрать объект API с кастомным Guzzle клиентом и отключить проверку сертификата.
> Во всех остальных случаях достаточно минимального $api = new Api('YOUR_BOT_API_TOKEN');
```php
$guzzle = new \GuzzleHttp\Client([
'timeout' => 10,
'connect_timeout' => 5,
'read_timeout' => 10,
'headers' => ['User-Agent' => 'max-bot-api-client-php'],
'verify' => false, // Отключить проверку либо путь до сертификата '/path/to/cert.pem'
]);
$httpFactory = new \GuzzleHttp\Psr7\HttpFactory();
$client = new \BushlanovDev\MaxMessengerBot\Client(
accessToken: 'YOUR_BOT_API_TOKEN',
httpClient: $guzzle,
requestFactory: $httpFactory,
streamFactory: $httpFactory,
baseUrl: 'https://platform-api2.max.ru',
);
$api = new BushlanovDev\MaxMessengerBot\Api(
client: $client,
modelFactory: new BushlanovDev\MaxMessengerBot\ModelFactory(),
);
```
Отправка сообщения с клавиатурой
```php
@@ -102,7 +154,7 @@ $dispatcher->addHandler(UpdateType::BotStarted, function (BotStartedUpdate $upda
```php
$api->subscribe(
url: 'https://example.com/webhook', // URL на который будут приходить хуки
url: 'https://example.com/webhook', // HTTPS URL на который будут приходить хуки
secret: 'super_secret', // Секретная фраза для проверки хуков
updateTypes: [
// Типы хуков которые вы хотите получать (либо ничего не указывать, чтобы получать все)
@@ -133,7 +185,7 @@ $handler->handle();
#### Chats
- [x] `GET /chats` (`getChats`) - [*Получение списка всех чатов бота.*](./docs/README.md#Получение-списка-всех-чатов-бота)
- [x] ~~`GET /chats` (`getChats`) - [*Получение списка всех чатов бота.*](./docs/README.md#Получение-списка-всех-чатов-бота-deprecated)~~ (deprecated)
- [x] `GET /chats/{chatLink}` (`getChatByLink`) - [*Получение информации о чате по ссылке.*](./docs/README.md#Получение-информации-о-чате-по-ссылке)
- [x] `GET /chats/{chatId}` (`getChat`) - [*Получение информации о чате по ID.*](./docs/README.md#Получение-информации-о-чате-по-ID)
- [x] `PATCH /chats/{chatId}` (`editChat`) - [*Редактирование информации о чате.*](./docs/README.md#Редактирование-информации-о-чате)
+4 -4
View File
@@ -28,15 +28,15 @@
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0||^2.0",
"psr/log": "^3.0"
"psr/log": "^1.1||^2.0||^3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.77",
"bushlanov-dev/php-coverage-badger": "^2.1",
"laravel/framework": "^11.0",
"laravel/framework": "^12.0",
"mikey179/vfsstream": "^1.6",
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0",
"orchestra/testbench": "^10.0",
"php-mock/php-mock-phpunit": "^2.13",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.0",
@@ -66,7 +66,7 @@
}
},
"scripts": {
"analyse": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=256M",
"analyse": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=512M",
"format": "vendor/bin/php-cs-fixer fix --allow-risky=yes src",
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage",
+226 -39
View File
@@ -1,13 +1,12 @@
- [Быстрый старт](#Быстрый-старт)
- [Получение токена](#Получение-токена)
- [Установка библиотеки](#Установка-библиотеки)
- [Установка библиотеки в Laravel](#Установка-библиотеки-в-Laravel)
- [Инициализация бота](#Инициализация-бота)
- [Информация о боте](#Информация-о-боте)
- `GET /me` (`getBotInfo`) - [*Получение информации о боте.*](#Получение-информации-о-боте)
- `PATCH /me` (`editBotInfo`) - [*Редактирование информации о боте.*](#Редактирование-информации-о-боте)
- [Чаты](#Чаты)
- `GET /chats` (`getChats`) - [*Получение списка всех чатов бота.*](#Получение-списка-всех-чатов-бота)
- ~~`GET /chats` (`getChats`)~~ - [*Получение списка всех чатов бота.*](#Получение-списка-всех-чатов-бота-deprecated) (deprecated)
- `GET /chats/{chatLink}` (`getChatByLink`) - [*Получение информации о чате по ссылке.*](#Получение-информации-о-чате-по-ссылке)
- `GET /chats/{chatId}` (`getChat`) - [*Получение информации о чате по ID.*](#Получение-информации-о-чате-по-ID)
- `PATCH /chats/{chatId}` (`editChat`) - [*Редактирование информации о чате.*](#Редактирование-информации-о-чате)
@@ -40,6 +39,18 @@
- `GET /messages/{messageId}` (`getMessageById`) - [*Получение сообщения по ID.*](#Получение-сообщения-по-ID)
- `GET /videos/{videoToken}` (`getVideoAttachmentDetails`) - [*Получение детальной информации о видео.*](#Получение-детальной-информации-о-видео)
- `POST /answers` (`answerOnCallback`) - [*Ответ на нажатие callback-кнопки.*](#Ответ-на-нажатие-callback-кнопки)
- [Laravel](#Laravel)
- [Регистрация пакета](#Регистрация-пакета)
- [Настройка](#Настройка)
- [Использование](#Использование)
- [Artisan команды](#Artisan-команды)
- [Подписка на Webhook](#Подписка-на-Webhook)
- [Удаление подписки](#Удаление-подписки)
- [Список подписок](#Список-подписок)
- [Обработка хуков](#Обработка-хуков)
- [Long Polling](#Long-Polling)
- [Handler Classes](#Handler-Classes)
- [Тестирование](#Тестирование)
## Быстрый старт
@@ -56,36 +67,6 @@
composer require bushlanov-dev/max-bot-api-client-php
```
### Установка библиотеки в Laravel
Пользователи Laravel могут зарегистрировать сервис провайдер и фасад в `config/app.php`:
```php
'providers' => [
// ...
BushlanovDev\MaxMessengerBot\Laravel\MaxBotServiceProvider::class,
],
// ...
'aliases' => [
// ...
'MaxBot' => BushlanovDev\MaxMessengerBot\Laravel\MaxBotFacade::class,
],
```
При не необходимости опубликовать конфиг выполните следующую команду
```bash
php artisan vendor:publish --provider="BushlanovDev\MaxMessengerBot\Laravel\MaxBotServiceProvider"
```
Для работы вам потребуется внести следующие настройки в `.env`
```env
MAXBOT_ACCESS_TOKEN=your_bot_access_token_here
MAXBOT_WEBHOOK_SECRET=your_webhook_secret_here
MAXBOT_LOGGING_ENABLED=true
```
### Инициализация бота
Единственной обязательной настройкой является токен вашего бота.
@@ -103,10 +84,27 @@ $api = new Api('YOUR_BOT_API_TOKEN');
Так же вы можете создать экземпляр бота гибко настроив все зависимости под свои нужды.
```php
$api = new Api(
client: new Client(...),
modelFactory: new ModelFactory(),
logger: new YourPsrLogger(),
$guzzle = new \GuzzleHttp\Client([
'timeout' => 10,
'connect_timeout' => 5,
'read_timeout' => 10,
'headers' => ['User-Agent' => 'max-bot-api-client-php'],
// 'verify' => false, // Отключить проверку либо путь до сертификата '/path/to/cert.pem'
]);
$httpFactory = new \GuzzleHttp\Psr7\HttpFactory();
$client = new \BushlanovDev\MaxMessengerBot\Client(
accessToken: 'YOUR_BOT_API_TOKEN',
httpClient: $guzzle,
requestFactory: $httpFactory,
streamFactory: $httpFactory,
baseUrl: 'https://platform-api2.max.ru',
);
$api = new BushlanovDev\MaxMessengerBot\Api(
client: $client,
modelFactory: new BushlanovDev\MaxMessengerBot\ModelFactory(),
);
```
@@ -138,7 +136,7 @@ $botInfo = $api->editBotInfo(
## Чаты
### Получение списка всех чатов бота
### Получение списка всех чатов бота (deprecated)
Возвращает информацию о чатах, в которых участвовал бот.
Результат включает список чатов и маркер для перехода к следующей странице.
@@ -294,11 +292,11 @@ $subscriptions = $api->getSubscriptions();
Подписывает бота на получение обновлений через WebHook.
После вызова этого метода бот будет получать уведомления о новых событиях в чатах на указанный URL.
Ваш сервер должен прослушивать один из следующих портов: 80, 8080, 443, 8443, 16384-32383.
Судя по обновленной [документации](https://dev.max.ru/docs-api/methods/POST/subscriptions) поддерживается только 443 порт!
```php
$api->subscribe(
url: 'https://example.com/webhook', // URL на который будут приходить хуки. Должен начинаться с http(s)://
url: 'https://example.com/webhook', // HTTPS URL на который будут приходить хуки. Должен начинаться с https://
secret: 'super_secret', // Секретная фраза для проверки хуков (необязательно)
updateTypes: [UpdateType::MessageCreated], // Типы хуков которые вы хотите получать (либо ничего не указывать, чтобы получать все)
);
@@ -444,3 +442,192 @@ $api->answerOnCallback(
notify: true, // Заполните это, если хотите просто отправить одноразовое уведомление пользователю (необязательно)
);
```
## Laravel
### Регистрация пакета
Пакет будет автоматически обнаружен Laravel.
Если автоматическое обнаружение отключено можно зарегистрировать сервис провайдер и фасад в `config/app.php`:
```php
'providers' => [
// ...
BushlanovDev\MaxMessengerBot\Laravel\MaxBotServiceProvider::class,
],
// ...
'aliases' => [
// ...
'MaxBot' => BushlanovDev\MaxMessengerBot\Laravel\MaxBotFacade::class,
],
```
### Настройка
При необходимости опубликовать конфиг выполните следующую команду:
```bash
php artisan vendor:publish --provider="BushlanovDev\MaxMessengerBot\Laravel\MaxBotServiceProvider"
```
Для работы вам потребуется добавить ваш токен в файл `.env`
```env
MAXBOT_ACCESS_TOKEN=your_bot_access_token_here
MAXBOT_WEBHOOK_SECRET=your_webhook_secret_here
```
### Использование
Все методы бота доступны через фасад MaxBot, например:
```php
use MaxBot;
// Отправка сообщения пользователю
MaxBot::sendUserMessage(123456, 'Hello from Laravel!');
// Получение обновления
$updates = MaxBot::getUpdates();
```
### Artisan команды
#### Подписка на Webhook
```bash
# Подписка на получение обновлений
php artisan maxbot:webhook:subscribe https://yourapp.com/bot/webhook
# С верификацией
php artisan maxbot:webhook:subscribe https://yourapp.com/bot/webhook --secret=your_secret_key
# Подписка только на определенные типы событий
php artisan maxbot:webhook:subscribe https://yourapp.com/bot/webhook --types=message_created --types=message_callback
```
#### Удаление подписки
```bash
php artisan maxbot:webhook:unsubscribe https://yourapp.com/bot/webhook
```
#### Список подписок
```bash
php artisan maxbot:webhook:list
```
### Обработка хуков
Создайте контроллер для обработчика:
```php
use Illuminate\Http\Request;
use BushlanovDev\MaxMessengerBot\Laravel\MaxBotManager;
class WebhookController extends Controller
{
public function handle(Request $request, MaxBotManager $botManager)
{
// Обработчик сообщений
$botManager->onMessageCreated(function (MessageCreatedUpdate $update) {
$message = $update->message;
// ...
});
// Обработчик команды /start
$botManager->onCommand('start', function (MessageCreatedUpdate $update) {
// ...
});
// Using Laravel container bindings
$botManager->onMessageCreated(MessageHandler::class);
$botManager->onCommand('help', HelpCommandHandler::class);
return $botManager->handleWebhook($request);
}
}
```
Добавьте маршрут в `routes/web.php`:
```php
Route::post('/bot/webhook', [WebhookController::class, 'handle']);
```
### Long Polling
Создайте artisan команду для получения long polling обновлений:
```php
use Illuminate\Console\Command;
use BushlanovDev\MaxMessengerBot\Laravel\MaxBotManager;
class BotPollingCommand extends Command
{
protected $signature = 'bot:polling';
protected $description = 'Start bot long polling';
public function handle(MaxBotManager $botManager)
{
$this->info('Starting bot polling...');
$botManager->startLongPolling();
}
}
```
### Handler Classes
Примеры классов обработчиков обновлений:
```php
class MessageHandler
{
public function handle(MessageCreatedUpdate $update)
{
$message = $update->message;
$text = $message->body?->text;
if ($text) {
app(Api::class)->sendMessage(
userId: $message->sender->userId,
text: "You said: $text",
);
}
}
}
class HelpCommandHandler
{
public function handle(MessageCreatedUpdate $update)
{
app(Api::class)->sendMessage(
userId: $update->message->sender->userId,
text: "This is help message",
);
}
}
```
### Тестирование
Вы можете использовать мок API для тестирования:
```php
use BushlanovDev\MaxMessengerBot\Api;
class BotTest extends TestCase
{
public function test_bot_sends_message()
{
$apiMock = $this->createMock(Api::class);
$apiMock->expects($this->once())
->method('sendUserMessage')
->with(123456, 'Hello!');
$this->app->instance(Api::class, $apiMock);
// Your test code here
}
}
```
+4659
View File
File diff suppressed because one or more lines are too long
+13 -3
View File
@@ -1,14 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
bootstrap="tests/bootstrap.php"
testdox="false"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
requireCoverageMetadata="true"
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
displayDetailsOnPhpunitDeprecations="true"
failOnPhpunitDeprecation="true"
failOnDeprecation="false"
failOnPhpunitDeprecation="false"
displayDetailsOnPhpunitDeprecations="false"
displayDetailsOnTestsThatTriggerDeprecations="false"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
@@ -25,4 +31,8 @@
<directory suffix=".php">src/Exceptions</directory>
</exclude>
</source>
<php>
<ini name="error_reporting" value="E_ALL &amp; ~E_DEPRECATED"/>
</php>
</phpunit>
+6 -7
View File
@@ -48,11 +48,11 @@ use RuntimeException;
*/
class Api
{
public const string LIBRARY_VERSION = '1.2.1';
public const string LIBRARY_VERSION = '1.6.4';
public const string API_VERSION = '0.0.6';
public const string API_VERSION = '1.2.5';
private const string API_BASE_URL = 'https://botapi.max.ru';
private const string API_BASE_URL = 'https://platform-api2.max.ru';
private const string METHOD_GET = 'GET';
private const string METHOD_POST = 'POST';
@@ -128,13 +128,13 @@ class Api
$httpFactory,
$httpFactory,
self::API_BASE_URL,
self::API_VERSION,
null,
$this->logger,
);
}
$this->client = $client;
$this->modelFactory = $modelFactory ?? new ModelFactory();
$this->modelFactory = $modelFactory ?? new ModelFactory($this->logger);
$this->updateDispatcher = new UpdateDispatcher($this);
}
@@ -363,7 +363,7 @@ class Api
$this->buildNewMessageBody($text, $attachments, $format, $link, $notify),
);
return $this->modelFactory->createMessage($response['message']);
return $this->modelFactory->createMessageFromSendResponse($response);
}
/**
@@ -685,7 +685,6 @@ class Api
return null;
}
return $this->modelFactory->createMessage($response['message']);
}
+28 -4
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot;
use BushlanovDev\MaxMessengerBot\Exceptions\AttachmentNotReadyException;
use BushlanovDev\MaxMessengerBot\Exceptions\ForbiddenException;
use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException;
use BushlanovDev\MaxMessengerBot\Exceptions\MethodNotAllowedException;
@@ -60,7 +61,6 @@ final readonly class Client implements ClientApiInterface
*/
public function request(string $method, string $uri, array $queryParams = [], array $body = []): array
{
$queryParams['access_token'] = $this->accessToken;
if (!empty($this->apiVersion)) {
$queryParams['v'] = $this->apiVersion;
}
@@ -72,7 +72,9 @@ final readonly class Client implements ClientApiInterface
]);
$fullUrl = $this->baseUrl . $uri . '?' . http_build_query($queryParams);
$request = $this->requestFactory->createRequest($method, $fullUrl);
$request = $this->requestFactory
->createRequest($method, $fullUrl)
->withHeader('Authorization', $this->accessToken);
if (!empty($body)) {
try {
@@ -143,6 +145,7 @@ final readonly class Client implements ClientApiInterface
$request = $this->requestFactory
->createRequest('POST', $uri)
->withHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary)
->withHeader('Authorization', $this->accessToken)
->withBody($bodyStream);
try {
@@ -198,7 +201,8 @@ final readonly class Client implements ClientApiInterface
->withBody($chunkStream)
->withHeader('Content-Type', 'application/octet-stream')
->withHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"')
->withHeader('Content-Range', "bytes {$startByte}-{$endByte}/{$fileSize}");
->withHeader('Content-Range', "bytes {$startByte}-{$endByte}/{$fileSize}")
->withHeader('Authorization', $this->accessToken);
try {
$response = $this->httpClient->sendRequest($request);
@@ -249,7 +253,27 @@ final readonly class Client implements ClientApiInterface
404 => new NotFoundException($errorMessage, $errorCode, $response),
405 => new MethodNotAllowedException($errorMessage, $errorCode, $response),
429 => new RateLimitExceededException($errorMessage, $errorCode, $response),
default => new ClientApiException($errorMessage, $errorCode, $response, $statusCode),
default => $this->mapErrorCodeToException($errorMessage, $errorCode, $response, $statusCode),
};
}
/**
* @param string $message
* @param string $errorCode
* @param ResponseInterface $response
* @param int|null $httpStatusCode
*
* @return ClientApiException
*/
private function mapErrorCodeToException(
string $message,
string $errorCode,
ResponseInterface $response,
?int $httpStatusCode = null,
): ClientApiException {
return match ($errorCode) {
'attachment.not.ready' => new AttachmentNotReadyException($message, $errorCode, $response, $httpStatusCode),
default => new ClientApiException($message, $errorCode, $response, $httpStatusCode),
};
}
}
-2
View File
@@ -13,8 +13,6 @@ enum AttachmentType: string
case Sticker = 'sticker';
case Contact = 'contact';
case InlineKeyboard = 'inline_keyboard';
case ReplyKeyboard = 'reply_keyboard';
case Location = 'location';
case Share = 'share';
case Data = 'data';
}
+5
View File
@@ -15,4 +15,9 @@ enum ChatAdminPermission: string
case ChangeChatInfo = 'change_chat_info';
case PinMessage = 'pin_message';
case Write = 'write';
case CanCall = 'can_call';
case EditLink = 'edit_link';
case PostEditDeleteMessage = 'post_edit_delete_message';
case EditMessage = 'edit_message';
case DeleteMessage = 'delete_message';
}
+1
View File
@@ -13,4 +13,5 @@ enum InlineButtonType: string
case OpenApp = 'open_app';
case Message = 'message';
case Chat = 'chat';
case Clipboard = 'clipboard';
}
+5
View File
@@ -15,9 +15,14 @@ enum UpdateType: string
case MessageRemoved = 'message_removed';
case BotAdded = 'bot_added';
case BotRemoved = 'bot_removed';
case DialogMuted = 'dialog_muted';
case DialogUnmuted = 'dialog_unmuted';
case DialogCleared = 'dialog_cleared';
case DialogRemoved = 'dialog_removed';
case UserAdded = 'user_added';
case UserRemoved = 'user_removed';
case BotStarted = 'bot_started';
case BotStopped = 'bot_stopped';
case ChatTitleChanged = 'chat_title_changed';
case MessageChatCreated = 'message_chat_created';
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Exceptions;
/**
* Exception thrown when an attachment is not yet ready for use.
* This typically occurs when trying to use an attachment that is still being processed.
*/
class AttachmentNotReadyException extends ClientApiException
{
}
+69 -4
View File
@@ -27,7 +27,7 @@ use Throwable;
* Provides convenient methods for integrating Max Bot with Laravel applications.
* Handles webhook processing, long polling, and event dispatching within Laravel context.
*/
class MaxBotManager
readonly class MaxBotManager
{
/**
* @param Container $container
@@ -35,9 +35,9 @@ class MaxBotManager
* @param UpdateDispatcher $dispatcher
*/
public function __construct(
private readonly Container $container,
private readonly Api $api,
private readonly UpdateDispatcher $dispatcher,
private Container $container,
private Api $api,
private UpdateDispatcher $dispatcher,
) {
}
@@ -229,6 +229,58 @@ class MaxBotManager
$this->dispatcher->onBotRemoved($this->resolveHandler($handler));
}
/**
* Register a dialog mute handler.
*
* @param callable|string $handler Can be a closure, callable, or Laravel container binding.
*
* @throws BindingResolutionException
* @codeCoverageIgnore
*/
public function onDialogMuted(callable|string $handler): void
{
$this->dispatcher->onDialogMuted($this->resolveHandler($handler));
}
/**
* Register a dialog unmute handler.
*
* @param callable|string $handler Can be a closure, callable, or Laravel container binding.
*
* @throws BindingResolutionException
* @codeCoverageIgnore
*/
public function onDialogUnmuted(callable|string $handler): void
{
$this->dispatcher->onDialogUnmuted($this->resolveHandler($handler));
}
/**
* Register a dialog cleared handler.
*
* @param callable|string $handler Can be a closure, callable, or Laravel container binding.
*
* @throws BindingResolutionException
* @codeCoverageIgnore
*/
public function onDialogCleared(callable|string $handler): void
{
$this->dispatcher->onDialogCleared($this->resolveHandler($handler));
}
/**
* Register a dialog removed handler.
*
* @param callable|string $handler Can be a closure, callable, or Laravel container binding.
*
* @throws BindingResolutionException
* @codeCoverageIgnore
*/
public function onDialogRemoved(callable|string $handler): void
{
$this->dispatcher->onDialogRemoved($this->resolveHandler($handler));
}
/**
* Register a user added handler.
*
@@ -268,6 +320,19 @@ class MaxBotManager
$this->dispatcher->onBotStarted($this->resolveHandler($handler));
}
/**
* Register a bot stopped handler.
*
* @param callable|string $handler Can be a closure, callable, or Laravel container binding.
*
* @throws BindingResolutionException
* @codeCoverageIgnore
*/
public function onBotStopped(callable|string $handler): void
{
$this->dispatcher->onBotStopped($this->resolveHandler($handler));
}
/**
* Register a chat title changed handler.
*
+28 -7
View File
@@ -76,14 +76,20 @@ class MaxBotServiceProvider extends ServiceProvider
$guzzle,
$httpFactory,
$httpFactory,
$config->get('maxbot.base_url', 'https://botapi.max.ru'),
$config->get('maxbot.api_version', Api::API_VERSION),
$config->get('maxbot.base_url', 'https://platform-api.max.ru'),
$config->get('maxbot.api_version'),
$logger,
);
});
$this->app->singleton(ModelFactory::class, function () {
return new ModelFactory();
$this->app->singleton(ModelFactory::class, function (Application $app) {
/** @var Config $config */
$config = $app->make(Config::class);
$logger = $config->get('maxbot.logging.enabled', false)
? $app->make(LoggerInterface::class)
: new NullLogger();
return new ModelFactory($logger);
});
$this->app->singleton(Api::class, function (Application $app) {
@@ -97,11 +103,15 @@ class MaxBotServiceProvider extends ServiceProvider
);
}
$logger = $config->get('maxbot.logging.enabled', false)
? $app->make(LoggerInterface::class)
: new NullLogger();
return new Api(
$accessToken,
$app->make(ClientApiInterface::class),
$app->make(ModelFactory::class),
$app->make(LoggerInterface::class),
$logger,
);
});
@@ -114,19 +124,30 @@ class MaxBotServiceProvider extends ServiceProvider
$config = $app->make(Config::class);
$secret = $config->get('maxbot.webhook_secret');
$logger = $config->get('maxbot.logging.enabled', false)
? $app->make(LoggerInterface::class)
: new NullLogger();
return new WebhookHandler(
$app->make(UpdateDispatcher::class),
$app->make(ModelFactory::class),
$app->make(LoggerInterface::class),
$logger,
$secret,
);
});
$this->app->bind(LongPollingHandler::class, function (Application $app) {
/** @var Config $config */
$config = $app->make(Config::class);
$logger = $config->get('maxbot.logging.enabled', false)
? $app->make(LoggerInterface::class)
: new NullLogger();
return new LongPollingHandler(
$app->make(Api::class),
$app->make(UpdateDispatcher::class),
$app->make(LoggerInterface::class),
$logger,
);
});
+63 -16
View File
@@ -14,7 +14,9 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\AudioAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\AbstractInlineButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\CallbackButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\ChatButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\ClipboardButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\LinkButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\MessageButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\OpenAppButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\RequestContactButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\RequestGeoLocationButton;
@@ -23,12 +25,10 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendContactBut
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendGeoLocationButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendMessageButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\ContactAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\DataAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\FileAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\InlineKeyboardAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\LocationAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\PhotoAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\ReplyKeyboardAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\ShareAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\StickerAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\VideoAttachment;
@@ -56,7 +56,12 @@ use BushlanovDev\MaxMessengerBot\Models\Updates\AbstractUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotAddedToChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotRemovedFromChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStoppedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\ChatTitleChangedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogClearedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogMutedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogRemovedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogUnmutedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCallbackUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageChatCreatedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate;
@@ -67,13 +72,25 @@ use BushlanovDev\MaxMessengerBot\Models\Updates\UserRemovedFromChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
use BushlanovDev\MaxMessengerBot\Models\VideoAttachmentDetails;
use LogicException;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use ReflectionException;
/**
* Creates DTOs from raw associative arrays returned by the API client.
*/
class ModelFactory
readonly class ModelFactory
{
private LoggerInterface $logger;
/**
* @param LoggerInterface|null $logger PSR LoggerInterface.
*/
public function __construct(?LoggerInterface $logger = null)
{
$this->logger = $logger ?? new NullLogger();
}
/**
* Simple response to request.
*
@@ -128,6 +145,33 @@ class ModelFactory
: [];
}
/**
* Creates a Message from the specific response structure of the sendMessage endpoint.
*
* @param array<string, mixed> $data The raw response from the client.
*
* @return Message
* @throws ReflectionException
*/
public function createMessageFromSendResponse(array $data): Message
{
$messageData = $data['message'];
$topLevelData = [
'chat_id' => $data['chat_id'] ?? null,
'recipient_id' => $data['recipient_id'] ?? null,
'message_id' => $data['message_id'] ?? null,
];
$messageData = array_merge($messageData, array_filter($topLevelData, fn($value) => $value !== null));
if (isset($messageData['message']) && is_array($messageData['message'])) {
$messageData['body'] = $messageData['message'];
unset($messageData['message']);
}
return $this->createMessage($messageData);
}
/**
* Message.
*
@@ -197,14 +241,6 @@ class ModelFactory
public function createAttachment(array $data): AbstractAttachment
{
$attachmentType = AttachmentType::tryFrom($data['type'] ?? '');
if ($attachmentType === AttachmentType::ReplyKeyboard
&& isset($data['buttons']) && is_array($data['buttons'])) {
$data['buttons'] = array_map(
fn($rowOfButtons) => array_map([$this, 'createReplyButton'], $rowOfButtons),
$data['buttons'],
);
}
if ($attachmentType === AttachmentType::InlineKeyboard
&& isset($data['payload']['buttons']) && is_array($data['payload']['buttons'])) {
$data['payload']['buttons'] = array_map(
@@ -214,8 +250,6 @@ class ModelFactory
}
return match ($attachmentType) {
AttachmentType::Data => DataAttachment::fromArray($data),
AttachmentType::Share => ShareAttachment::fromArray($data),
AttachmentType::Image => PhotoAttachment::fromArray($data),
AttachmentType::Video => VideoAttachment::fromArray($data),
AttachmentType::Audio => AudioAttachment::fromArray($data),
@@ -223,8 +257,8 @@ class ModelFactory
AttachmentType::Sticker => StickerAttachment::fromArray($data),
AttachmentType::Contact => ContactAttachment::fromArray($data),
AttachmentType::InlineKeyboard => InlineKeyboardAttachment::fromArray($data),
AttachmentType::ReplyKeyboard => ReplyKeyboardAttachment::fromArray($data),
AttachmentType::Location => LocationAttachment::fromArray($data),
AttachmentType::Share => ShareAttachment::fromArray($data),
default => throw new LogicException('Unknown or unsupported attachment type: ' . ($data['type'] ?? 'none')),
};
}
@@ -267,7 +301,11 @@ class ModelFactory
InlineButtonType::RequestGeoLocation => RequestGeoLocationButton::fromArray($data),
InlineButtonType::Chat => ChatButton::fromArray($data),
InlineButtonType::OpenApp => OpenAppButton::fromArray($data),
default => throw new LogicException('Unknown or unsupported inline button type: ' . ($data['type'] ?? 'none')),
InlineButtonType::Clipboard => ClipboardButton::fromArray($data),
InlineButtonType::Message => MessageButton::fromArray($data),
default => throw new LogicException(
'Unknown or unsupported inline button type: ' . ($data['type'] ?? 'none')
),
};
}
@@ -312,7 +350,11 @@ class ModelFactory
if (isset($data['updates']) && is_array($data['updates'])) {
foreach ($data['updates'] as $updateData) {
// Here we delegate the creation of a specific update to another factory method
$updateObjects[] = $this->createUpdate($updateData);
try {
$updateObjects[] = $this->createUpdate($updateData);
} catch (LogicException $e) {
$this->logger->debug($e->getMessage(), ['payload' => $updateData, 'exception' => $e]);
}
}
}
@@ -340,9 +382,14 @@ class ModelFactory
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(
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
final readonly class User extends AbstractModel
abstract readonly class AbstractUser extends AbstractModel
{
/**
* @param int $userId Users identifier.
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline;
use BushlanovDev\MaxMessengerBot\Enums\InlineButtonType;
/**
* A button that, when clicked, copies text to the clipboard.
*/
final readonly class ClipboardButton extends AbstractInlineButton
{
public ?string $payload;
/**
* @param string $text Visible button text (1 to 128 characters).
* @param string $payload Text which will be copied.
*/
public function __construct(string $text, string $payload)
{
parent::__construct(InlineButtonType::Clipboard, $text);
$this->payload = $payload;
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline;
use BushlanovDev\MaxMessengerBot\Enums\InlineButtonType;
/**
* Button send text in chat from user.
*/
final readonly class MessageButton extends AbstractInlineButton
{
/**
* @param string $text Text sending in chat from user.
*/
public function __construct(string $text)
{
parent::__construct(InlineButtonType::Message, $text);
}
}
@@ -13,17 +13,20 @@ final readonly class OpenAppButton extends AbstractInlineButton
{
public ?string $webApp;
public ?int $contactId;
public ?string $payload;
/**
* @param string $text Visible button text (1 to 128 characters).
* @param string|null $webApp The public name (username) of the bot or a link to it, whose mini-application should be launched.
* @param int|null $contactId The ID of the bot whose mini-app should be launched.
* @param string|null $payload Init param which would be passed in mini-application initData (1 to 512 characters, allowed A-Z, a-z, 0-9, _(underscore) , - (minus) ).
*/
public function __construct(string $text, ?string $webApp = null, ?int $contactId = null)
public function __construct(string $text, ?string $webApp = null, ?int $contactId = null, ?string $payload = null)
{
parent::__construct(InlineButtonType::OpenApp, $text);
$this->webApp = $webApp;
$this->contactId = $contactId;
$this->payload = $payload;
}
}
-21
View File
@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments;
use BushlanovDev\MaxMessengerBot\Enums\AttachmentType;
/**
* Represents an attachment containing a payload from a SendMessageButton.
*/
final readonly class DataAttachment extends AbstractAttachment
{
/**
* @param string $data The payload from the button.
*/
public function __construct(public string $data)
{
parent::__construct(AttachmentType::Data);
}
}
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads;
use BushlanovDev\MaxMessengerBot\Models\AbstractModel;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* Payload of a contact attachment.
@@ -14,11 +14,11 @@ final readonly class ContactAttachmentPayload extends AbstractModel
{
/**
* @param string|null $vcfInfo User info in VCF format.
* @param User|null $maxInfo User info if the contact is a Max user.
* @param UserWithPhoto|null $maxInfo User info if the contact is a Max user.
*/
public function __construct(
public ?string $vcfInfo,
public ?User $maxInfo,
public ?UserWithPhoto $maxInfo,
) {
}
}
@@ -23,9 +23,9 @@ final readonly class PhotoAttachmentRequestPayload extends AbstractAttachmentReq
#[ArrayOf(PhotoToken::class)]
public ?array $photos = null,
) {
if (count(array_filter([$this->url, $this->token, $this->photos])) !== 1) {
if ($this->url === null && $this->token === null && $this->photos === null) {
throw new InvalidArgumentException(
'Provide exactly one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.'
'Provide one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.'
);
}
}
@@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\AbstractReplyButton;
final readonly class ReplyKeyboardAttachmentRequestPayload extends AbstractAttachmentRequestPayload
{
/**
* @param AbstractReplyButton[][] $buttons Two-dimensional array of buttons.
* @param bool $direct Applicable only for chats.
* @param int|null $directUserId If set, reply keyboard will only be shown to this participant.
*/
public function __construct(
public array $buttons,
public bool $direct = false,
public ?int $directUserId = null,
) {
}
}
@@ -19,9 +19,9 @@ final readonly class ShareAttachmentRequestPayload extends AbstractAttachmentReq
public ?string $url = null,
public ?string $token = null,
) {
if (count(array_filter([$this->url, $this->token])) !== 1) {
if ($this->url === null && $this->token === null) {
throw new InvalidArgumentException(
'Provide exactly one of "url" or "token" for ShareAttachmentRequestPayload.'
'Provide one of "url" or "token" for ShareAttachmentRequestPayload.'
);
}
}
@@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments;
use BushlanovDev\MaxMessengerBot\Enums\AttachmentType;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\AbstractReplyButton;
final readonly class ReplyKeyboardAttachment extends AbstractAttachment
{
/**
* @param AbstractReplyButton[][] $buttons
*/
public function __construct(public array $buttons)
{
parent::__construct(AttachmentType::ReplyKeyboard);
}
}
@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Requests;
use BushlanovDev\MaxMessengerBot\Enums\AttachmentType;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\AbstractReplyButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ReplyKeyboardAttachmentRequestPayload;
final readonly class ReplyKeyboardAttachmentRequest extends AbstractAttachmentRequest
{
/**
* @param AbstractReplyButton[][] $buttons
* @param bool $direct
* @param int|null $directUserId
*/
public function __construct(
array $buttons,
bool $direct = false,
?int $directUserId = null
) {
parent::__construct(
AttachmentType::ReplyKeyboard,
new ReplyKeyboardAttachmentRequestPayload($buttons, $direct, $directUserId),
);
}
}
+8 -7
View File
@@ -9,7 +9,7 @@ use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
/**
* Information about the current bot.
*/
final readonly class BotInfo extends AbstractModel
final readonly class BotInfo extends AbstractUser
{
/**
* @param int $userId ID user.
@@ -24,17 +24,18 @@ final readonly class BotInfo extends AbstractModel
* @param BotCommand[]|null $commands Commands supported by the bot (up to 32 elements).
*/
public function __construct(
public int $userId,
public string $firstName,
public ?string $lastName,
public ?string $username,
public bool $isBot,
public int $lastActivityTime,
int $userId,
string $firstName,
?string $lastName,
?string $username,
bool $isBot,
int $lastActivityTime,
public ?string $description,
public ?string $avatarUrl,
public ?string $fullAvatarUrl,
#[ArrayOf(BotCommand::class)]
public ?array $commands,
) {
parent::__construct($userId, $firstName, $lastName, $username, $isBot, $lastActivityTime);
}
}
+4 -2
View File
@@ -9,12 +9,14 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequ
/**
* Represents the data to patch for a bot. Instantiate with named arguments.
*
* Example: new BotPatch(name: 'New Bot Name', description: null);
* Example: new BotPatch(first_name: 'New Bot Name', description: null);
*
* @property-read string|null $name
* @property-read string|null $first_name
* @property-read string|null $last_name
* @property-read string|null $description
* @property-read BotCommand[]|null $commands
* @property-read PhotoAttachmentRequestPayload|null $photo
* @property-read string|null $name @deprecated Use first_name
*/
final readonly class BotPatch extends AbstractPatchModel
{
+2 -2
View File
@@ -13,13 +13,13 @@ final readonly class Callback extends AbstractModel
* @param int $timestamp Unix-time when user pressed the button.
* @param string $callbackId Identifier of the callback, unique for the message.
* @param string $payload Payload from the pressed button.
* @param User $user User who pressed the button.
* @param UserWithPhoto $user User who pressed the button.
*/
public function __construct(
public int $timestamp,
public string $callbackId,
public string $payload,
public User $user,
public UserWithPhoto $user,
) {
}
}
+2
View File
@@ -28,6 +28,7 @@ final readonly class Chat extends AbstractModel
* @param int|null $messagesCount Messages count in chat. Only for group chats and channels. Not available for dialogs.
* @param string|null $chatMessageId Identifier of message that contains `chat` button initialized chat.
* @param Message|null $pinnedMessage Pinned message in chat or channel. Returned only when single chat is requested.
* @param array<string, int>|null $participants List of participants in chat. Returned only when single chat is requested.
*/
public function __construct(
public int $chatId,
@@ -45,6 +46,7 @@ final readonly class Chat extends AbstractModel
public ?int $messagesCount,
public ?string $chatMessageId,
public ?Message $pinnedMessage,
public ?array $participants,
) {
}
}
+2
View File
@@ -15,11 +15,13 @@ final readonly class ChatAdmin extends AbstractModel
/**
* @param int $userId The identifier of the user to be made an admin.
* @param ChatAdminPermission[] $permissions The list of permissions to grant to the user.
* @param string|null $alias Alias of the user.
*/
public function __construct(
public int $userId,
#[ArrayOf(ChatAdminPermission::class)]
public array $permissions,
public ?string $alias = null,
) {
}
}
+10 -7
View File
@@ -10,7 +10,7 @@ use BushlanovDev\MaxMessengerBot\Enums\ChatAdminPermission;
/**
* Represents a member of a chat, including their user information and chat-specific status.
*/
final readonly class ChatMember extends AbstractModel
final readonly class ChatMember extends AbstractUser
{
/**
* @param int $userId User's identifier.
@@ -27,14 +27,15 @@ final readonly class ChatMember extends AbstractModel
* @param bool $isAdmin True if this member is an administrator of the chat.
* @param int $joinTime The time the user joined the chat.
* @param ChatAdminPermission[]|null $permissions A list of permissions if the member is an admin, otherwise null.
* @param string|null $alias Alias of the user.
*/
public function __construct(
public int $userId,
public string $firstName,
public ?string $lastName,
public ?string $username,
public bool $isBot,
public int $lastActivityTime,
int $userId,
string $firstName,
?string $lastName,
?string $username,
bool $isBot,
int $lastActivityTime,
public ?string $description,
public ?string $avatarUrl,
public ?string $fullAvatarUrl,
@@ -44,6 +45,8 @@ final readonly class ChatMember extends AbstractModel
public int $joinTime,
#[ArrayOf(ChatAdminPermission::class)]
public ?array $permissions,
public ?string $alias,
) {
parent::__construct($userId, $firstName, $lastName, $username, $isBot, $lastActivityTime);
}
}
+2 -2
View File
@@ -14,13 +14,13 @@ final readonly class LinkedMessage extends AbstractModel
/**
* @param MessageLinkType $type Type of linked message (forward or reply).
* @param MessageBody $message The body of the original message.
* @param User|null $sender The sender of the original message. Can be null if posted on behalf of a channel.
* @param UserWithPhoto|null $sender The sender of the original message. Can be null if posted on behalf of a channel.
* @param int|null $chatId The chat where the message was originally posted (for forwarded messages).
*/
public function __construct(
public MessageLinkType $type,
public MessageBody $message,
public ?User $sender,
public ?UserWithPhoto $sender,
public ?int $chatId,
) {
}
+8 -2
View File
@@ -13,19 +13,25 @@ final readonly class Message extends AbstractModel
* @param int $timestamp Unix-time when message was created.
* @param Recipient $recipient Message recipient. Could be user or chat.
* @param MessageBody|null $body Body of created message. Text + attachments.
* @param User|null $sender User who sent this message. Can be null if message has been posted on behalf of a channel.
* @param UserWithPhoto|null $sender User who sent this message. Can be null if message has been posted on behalf of a channel.
* @param string|null $url Message public URL. Can be null for dialogs or non-public chats/channels.
* @param LinkedMessage|null $link Forwarded or replied message.
* @param MessageStat|null $stat Message statistics. Available only for channels.
* @param int|null $chatId Chat identifier.
* @param int|null $recipientId User identifier, if message was sent to user.
* @param string|null $messageId Unique identifier of message.
*/
public function __construct(
public int $timestamp,
public Recipient $recipient,
public ?MessageBody $body,
public ?User $sender,
public ?UserWithPhoto $sender,
public ?string $url,
public ?LinkedMessage $link,
public ?MessageStat $stat,
public ?int $chatId = null,
public ?int $recipientId = null,
public ?string $messageId = null,
) {
}
}
+3 -3
View File
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* You will receive this update when the bot has been added to a chat.
@@ -15,13 +15,13 @@ final readonly class BotAddedToChatUpdate extends AbstractUpdate
/**
* @param int $timestamp Unix-time when the event has occurred.
* @param int $chatId Chat ID where the bot was added.
* @param User $user User who added the bot to the chat.
* @param UserWithPhoto $user User who added the bot to the chat.
* @param bool $isChannel Indicates whether the bot has been added to a channel or not.
*/
public function __construct(
int $timestamp,
public int $chatId,
public User $user,
public UserWithPhoto $user,
public bool $isChannel,
) {
parent::__construct(UpdateType::BotAdded, $timestamp);
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* You will receive this update when the bot has been removed from a chat.
@@ -15,13 +15,13 @@ final readonly class BotRemovedFromChatUpdate extends AbstractUpdate
/**
* @param int $timestamp Unix-time when the event has occurred.
* @param int $chatId Chat identifier the bot was removed from.
* @param User $user User who removed the bot from the chat.
* @param UserWithPhoto $user User who removed the bot from the chat.
* @param bool $isChannel Indicates whether the bot has been removed from a channel or not.
*/
public function __construct(
int $timestamp,
public int $chatId,
public User $user,
public UserWithPhoto $user,
public bool $isChannel,
) {
parent::__construct(UpdateType::BotRemoved, $timestamp);
+3 -3
View File
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* Bot gets this type of update as soon as user pressed `Start` button.
@@ -15,14 +15,14 @@ final readonly class BotStartedUpdate extends AbstractUpdate
/**
* @param int $timestamp Unix-time when event has occurred.
* @param int $chatId Dialog identifier where event has occurred.
* @param User $user User pressed the 'Start' button.
* @param UserWithPhoto $user User pressed the 'Start' button.
* @param string|null $payload Additional data from deep-link passed on bot startup.
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
*/
public function __construct(
int $timestamp,
public int $chatId,
public User $user,
public UserWithPhoto $user,
public ?string $payload,
public ?string $userLocale,
) {
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* The bot receives this type of update as soon as the user stops the bot.
*/
final readonly class BotStoppedUpdate extends AbstractUpdate
{
/**
* @param int $timestamp Unix-time when event has occurred.
* @param int $chatId Dialog identifier where event has occurred.
* @param UserWithPhoto $user User pressed the 'Start' button.
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
*/
public function __construct(
int $timestamp,
public int $chatId,
public UserWithPhoto $user,
public ?string $userLocale,
) {
parent::__construct(UpdateType::BotStopped, $timestamp);
}
}
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* Bot gets this type of update as soon as the title has been changed in a chat.
@@ -15,13 +15,13 @@ final readonly class ChatTitleChangedUpdate extends AbstractUpdate
/**
* @param int $timestamp Unix-time when the event has occurred.
* @param int $chatId Chat identifier where the event has occurred.
* @param User $user User who changed the title.
* @param UserWithPhoto $user User who changed the title.
* @param string $title The new title.
*/
public function __construct(
int $timestamp,
public int $chatId,
public User $user,
public UserWithPhoto $user,
public string $title,
) {
parent::__construct(UpdateType::ChatTitleChanged, $timestamp);
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* Event clearing dialog history.
*/
final readonly class DialogClearedUpdate extends AbstractUpdate
{
/**
* @param int $timestamp Unix-time when event has occurred.
* @param int $chatId Dialog identifier where event has occurred.
* @param UserWithPhoto $user User pressed the 'Start' button.
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
*/
public function __construct(
int $timestamp,
public int $chatId,
public UserWithPhoto $user,
public ?string $userLocale,
) {
parent::__construct(UpdateType::DialogCleared, $timestamp);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* Event when a user mutes a conversation with a bot.
*/
final readonly class DialogMutedUpdate extends AbstractUpdate
{
/**
* @param int $timestamp Unix-time when event has occurred.
* @param int $chatId Dialog identifier where event has occurred.
* @param UserWithPhoto $user User pressed the 'Start' button.
* @param int|null $mutedUntil The time in Unix format before which the dialog was disabled.
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
*/
public function __construct(
int $timestamp,
public int $chatId,
public UserWithPhoto $user,
public ?int $mutedUntil,
public ?string $userLocale,
) {
parent::__construct(UpdateType::DialogMuted, $timestamp);
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* Event deleting a chat.
*/
final readonly class DialogRemovedUpdate extends AbstractUpdate
{
/**
* @param int $timestamp Unix-time when event has occurred.
* @param int $chatId Dialog identifier where event has occurred.
* @param UserWithPhoto $user User pressed the 'Start' button.
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
*/
public function __construct(
int $timestamp,
public int $chatId,
public UserWithPhoto $user,
public ?string $userLocale,
) {
parent::__construct(UpdateType::DialogRemoved, $timestamp);
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* Event of enabling notifications in a dialog.
*/
final readonly class DialogUnmutedUpdate extends AbstractUpdate
{
/**
* @param int $timestamp Unix-time when event has occurred.
* @param int $chatId Dialog identifier where event has occurred.
* @param UserWithPhoto $user User pressed the 'Start' button.
* @param string|null $userLocale Current user locale in IETF BCP 47 format.
*/
public function __construct(
int $timestamp,
public int $chatId,
public UserWithPhoto $user,
public ?string $userLocale,
) {
parent::__construct(UpdateType::DialogUnmuted, $timestamp);
}
}
+3 -3
View File
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* You will receive this update when a user has been added to a chat where the bot is an administrator.
@@ -15,7 +15,7 @@ final readonly class UserAddedToChatUpdate extends AbstractUpdate
/**
* @param int $timestamp Unix-time when the event has occurred.
* @param int $chatId Chat identifier where the event has occurred.
* @param User $user User who was added to the chat.
* @param UserWithPhoto $user User who was added to the chat.
* @param int|null $inviterId User who added the new user to the chat.
* Can be `null` if the user joined via a link.
* @param bool $isChannel Indicates whether the user has been added to a channel or not.
@@ -23,7 +23,7 @@ final readonly class UserAddedToChatUpdate extends AbstractUpdate
public function __construct(
int $timestamp,
public int $chatId,
public User $user,
public UserWithPhoto $user,
public ?int $inviterId,
public bool $isChannel,
) {
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
/**
* You will receive this update when a user has been removed from a chat where the bot is an administrator.
@@ -15,7 +15,7 @@ final readonly class UserRemovedFromChatUpdate extends AbstractUpdate
/**
* @param int $timestamp Unix-time when the event has occurred.
* @param int $chatId Chat identifier where the event has occurred.
* @param User $user User who was removed from the chat.
* @param UserWithPhoto $user User who was removed from the chat.
* @param int|null $adminId Administrator who removed the user from the chat.
* Can be `null` if the user left the chat themselves.
* @param bool $isChannel Indicates whether the user has been removed from a channel or not.
@@ -23,7 +23,7 @@ final readonly class UserRemovedFromChatUpdate extends AbstractUpdate
public function __construct(
int $timestamp,
public int $chatId,
public User $user,
public UserWithPhoto $user,
public ?int $adminId,
public bool $isChannel,
) {
+8 -7
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
final readonly class UserWithPhoto extends AbstractModel
final readonly class UserWithPhoto extends AbstractUser
{
/**
* @param int $userId Users identifier.
@@ -18,15 +18,16 @@ final readonly class UserWithPhoto extends AbstractModel
* @param string|null $fullAvatarUrl URL of avatar of a bigger size.
*/
public function __construct(
public int $userId,
public string $firstName,
public ?string $lastName,
public ?string $username,
public bool $isBot,
public int $lastActivityTime,
int $userId,
string $firstName,
?string $lastName,
?string $username,
bool $isBot,
int $lastActivityTime,
public ?string $description,
public ?string $avatarUrl,
public ?string $fullAvatarUrl,
) {
parent::__construct($userId, $firstName, $lastName, $username, $isBot, $lastActivityTime);
}
}
+65
View File
@@ -164,6 +164,58 @@ final class UpdateDispatcher
return $this->addHandler(UpdateType::BotRemoved, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::DialogMuted, $handler).
*
* @param callable(Models\Updates\DialogMutedUpdate, Api): void $handler
*
* @return $this
* @codeCoverageIgnore
*/
public function onDialogMuted(callable $handler): self
{
return $this->addHandler(UpdateType::DialogMuted, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::DialogUnmuted, $handler).
*
* @param callable(Models\Updates\DialogUnmutedUpdate, Api): void $handler
*
* @return $this
* @codeCoverageIgnore
*/
public function onDialogUnmuted(callable $handler): self
{
return $this->addHandler(UpdateType::DialogUnmuted, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::DialogCleared, $handler).
*
* @param callable(Models\Updates\DialogClearedUpdate, Api): void $handler
*
* @return $this
* @codeCoverageIgnore
*/
public function onDialogCleared(callable $handler): self
{
return $this->addHandler(UpdateType::DialogCleared, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::DialogRemoved, $handler).
*
* @param callable(Models\Updates\DialogRemovedUpdate, Api): void $handler
*
* @return $this
* @codeCoverageIgnore
*/
public function onDialogRemoved(callable $handler): self
{
return $this->addHandler(UpdateType::DialogRemoved, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::UserAdded, $handler).
*
@@ -203,6 +255,19 @@ final class UpdateDispatcher
return $this->addHandler(UpdateType::BotStarted, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::BotStopped, $handler).
*
* @param callable(Models\Updates\BotStoppedUpdate, Api): void $handler
*
* @return $this
* @codeCoverageIgnore
*/
public function onBotStopped(callable $handler): self
{
return $this->addHandler(UpdateType::BotStopped, $handler);
}
/**
* A convenient alias for addHandler(UpdateType::ChatTitleChanged, $handler).
*
+6 -3
View File
@@ -69,9 +69,12 @@ final readonly class WebhookHandler
throw new SerializationException('Failed to decode webhook body as JSON.', 0, $e);
}
$update = $this->modelFactory->createUpdate($data);
$this->dispatcher->dispatch($update);
try {
$update = $this->modelFactory->createUpdate($data);
$this->dispatcher->dispatch($update);
} catch (\LogicException $e) {
$this->logger->debug($e->getMessage(), ['payload' => $payload, 'exception' => $e]);
}
if (!headers_sent()) {
http_response_code(200);
+2
View File
@@ -10,6 +10,7 @@ use BushlanovDev\MaxMessengerBot\LongPollingHandler;
use BushlanovDev\MaxMessengerBot\ModelFactory;
use BushlanovDev\MaxMessengerBot\UpdateDispatcher;
use BushlanovDev\MaxMessengerBot\WebhookHandler;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -19,6 +20,7 @@ use Psr\Log\LoggerInterface;
use ReflectionClass;
#[CoversClass(Api::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(UpdateDispatcher::class)]
#[UsesClass(WebhookHandler::class)]
#[UsesClass(LongPollingHandler::class)]
+61 -29
View File
@@ -47,7 +47,7 @@ use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\Recipient;
use BushlanovDev\MaxMessengerBot\Models\Result;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use BushlanovDev\MaxMessengerBot\Models\Subscription;
use BushlanovDev\MaxMessengerBot\Models\UpdateList;
use BushlanovDev\MaxMessengerBot\Models\Updates\AbstractUpdate;
@@ -62,6 +62,7 @@ use InvalidArgumentException;
use LogicException;
use org\bovigo\vfs\vfsStream;
use phpmock\phpunit\PHPMock;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\PreserveGlobalState;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
@@ -75,6 +76,7 @@ use ReflectionClass;
use RuntimeException;
#[CoversClass(Api::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(Result::class)]
#[UsesClass(Client::class)]
#[UsesClass(BotInfo::class)]
@@ -82,7 +84,7 @@ use RuntimeException;
#[UsesClass(Message::class)]
#[UsesClass(MessageBody::class)]
#[UsesClass(Recipient::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
#[UsesClass(CallbackButton::class)]
#[UsesClass(InlineKeyboardAttachmentRequestPayload::class)]
#[UsesClass(InlineKeyboardAttachmentRequest::class)]
@@ -118,6 +120,7 @@ use RuntimeException;
#[UsesClass(VideoAttachmentDetails::class)]
#[UsesClass(VideoUrls::class)]
#[UsesClass(UpdateDispatcher::class)]
#[UsesClass(ModelFactory::class)]
final class ApiTest extends TestCase
{
use PHPMock;
@@ -336,7 +339,7 @@ final class ApiTest extends TestCase
$apiResponse = [
'message' => [
'timestamp' => time(),
'body' => ['mid' => 'mid.456.xyz', 'seq' => 101, 'text' => $text],
'message' => ['mid' => 'mid.456.xyz', 'seq' => 101, 'text' => $text],
'recipient' => ['chat_type' => 'dialog', 'user_id' => 123, 'chat_id' => null],
'sender' => [
'user_id' => 123,
@@ -348,9 +351,18 @@ final class ApiTest extends TestCase
],
'url' => 'https://max.ru/message/123',
],
'chat_id' => 20414985,
'recipient_id' => 4328369,
'message_id' => 'mid.456.xyz',
];
$expectedMessageObject = Message::fromArray($apiResponse['message']);
$finalMessageData = $apiResponse['message'];
$finalMessageData['body'] = $finalMessageData['message'];
unset($finalMessageData['message']);
$finalMessageData['chat_id'] = $apiResponse['chat_id'];
$finalMessageData['recipient_id'] = $apiResponse['recipient_id'];
$finalMessageData['message_id'] = $apiResponse['message_id'];
$expectedMessageObject = Message::fromArray($finalMessageData);
$this->clientMock
->expects($this->once())
@@ -360,8 +372,8 @@ final class ApiTest extends TestCase
$this->modelFactoryMock
->expects($this->once())
->method('createMessage')
->with($apiResponse['message'])
->method('createMessageFromSendResponse')
->with($apiResponse)
->willReturn($expectedMessageObject);
$result = $this->api->sendMessage(
@@ -416,12 +428,17 @@ final class ApiTest extends TestCase
$apiResponse = [
'message' => [
'timestamp' => time(),
'body' => ['mid' => 'mid.test.123', 'seq' => 1, 'text' => $text],
'message' => ['mid' => 'mid.test.123', 'seq' => 1, 'text' => $text],
'recipient' => ['chat_type' => 'dialog', 'user_id' => 123, 'chat_id' => null],
]
],
'message_id' => 'mid.test.123',
];
$expectedMessageObject = Message::fromArray($apiResponse['message']);
$finalMessageData = $apiResponse['message'];
$finalMessageData['body'] = $finalMessageData['message'];
unset($finalMessageData['message']);
$finalMessageData['message_id'] = $apiResponse['message_id'];
$expectedMessageObject = Message::fromArray($finalMessageData);
$this->clientMock
->expects($this->once())
@@ -436,8 +453,8 @@ final class ApiTest extends TestCase
$this->modelFactoryMock
->expects($this->once())
->method('createMessage')
->with($apiResponse['message'])
->method('createMessageFromSendResponse')
->with($apiResponse)
->willReturn($expectedMessageObject);
$result = $this->api->sendMessage(
@@ -803,11 +820,15 @@ final class ApiTest extends TestCase
$apiResponse = [
'message' => [
'timestamp' => time(),
'body' => ['mid' => 'mid.sticker.1', 'seq' => 10],
'message' => ['mid' => 'mid.sticker.1', 'seq' => 10],
'recipient' => ['chat_type' => 'dialog', 'user_id' => $chatId],
]
];
$expectedMessageObject = Message::fromArray($apiResponse['message']);
$finalMessageData = $apiResponse['message'];
$finalMessageData['body'] = $finalMessageData['message'];
unset($finalMessageData['message']);
$expectedMessageObject = Message::fromArray($finalMessageData);
$this->clientMock->expects($this->once())
->method('request')
@@ -815,8 +836,8 @@ final class ApiTest extends TestCase
->willReturn($apiResponse);
$this->modelFactoryMock->expects($this->once())
->method('createMessage')
->with($apiResponse['message'])
->method('createMessageFromSendResponse')
->with($apiResponse)
->willReturn($expectedMessageObject);
$result = $this->api->sendMessage(chatId: $chatId, attachments: [$stickerRequest]);
@@ -849,11 +870,14 @@ final class ApiTest extends TestCase
$apiResponse = [
'message' => [
'timestamp' => time(),
'body' => ['mid' => 'mid.contact.1', 'seq' => 11],
'message' => ['mid' => 'mid.contact.1', 'seq' => 11],
'recipient' => ['chat_type' => 'dialog', 'user_id' => $chatId],
]
];
$expectedMessageObject = Message::fromArray($apiResponse['message']);
$finalMessageData = $apiResponse['message'];
$finalMessageData['body'] = $finalMessageData['message'];
unset($finalMessageData['message']);
$expectedMessageObject = Message::fromArray($finalMessageData);
$this->clientMock->expects($this->once())
->method('request')
@@ -861,8 +885,8 @@ final class ApiTest extends TestCase
->willReturn($apiResponse);
$this->modelFactoryMock->expects($this->once())
->method('createMessage')
->with($apiResponse['message'])
->method('createMessageFromSendResponse')
->with($apiResponse)
->willReturn($expectedMessageObject);
$result = $this->api->sendMessage(chatId: $chatId, attachments: [$contactRequest]);
@@ -895,11 +919,14 @@ final class ApiTest extends TestCase
$apiResponse = [
'message' => [
'timestamp' => time(),
'body' => ['mid' => 'mid.location.1', 'seq' => 12],
'message' => ['mid' => 'mid.location.1', 'seq' => 12],
'recipient' => ['chat_type' => 'dialog', 'user_id' => $chatId],
]
];
$expectedMessageObject = Message::fromArray($apiResponse['message']);
$finalMessageData = $apiResponse['message'];
$finalMessageData['body'] = $finalMessageData['message'];
unset($finalMessageData['message']);
$expectedMessageObject = Message::fromArray($finalMessageData);
$this->clientMock->expects($this->once())
->method('request')
@@ -907,8 +934,8 @@ final class ApiTest extends TestCase
->willReturn($apiResponse);
$this->modelFactoryMock->expects($this->once())
->method('createMessage')
->with($apiResponse['message'])
->method('createMessageFromSendResponse')
->with($apiResponse)
->willReturn($expectedMessageObject);
$result = $this->api->sendMessage(chatId: $chatId, attachments: [$locationRequest]);
@@ -940,11 +967,14 @@ final class ApiTest extends TestCase
$apiResponse = [
'message' => [
'timestamp' => time(),
'body' => ['mid' => 'mid.share.1', 'seq' => 13],
'message' => ['mid' => 'mid.share.1', 'seq' => 13],
'recipient' => ['chat_type' => 'dialog', 'user_id' => $chatId],
]
];
$expectedMessageObject = Message::fromArray($apiResponse['message']);
$finalMessageData = $apiResponse['message'];
$finalMessageData['body'] = $finalMessageData['message'];
unset($finalMessageData['message']);
$expectedMessageObject = Message::fromArray($finalMessageData);
$this->clientMock->expects($this->once())
->method('request')
@@ -952,8 +982,8 @@ final class ApiTest extends TestCase
->willReturn($apiResponse);
$this->modelFactoryMock->expects($this->once())
->method('createMessage')
->with($apiResponse['message'])
->method('createMessageFromSendResponse')
->with($apiResponse)
->willReturn($expectedMessageObject);
$result = $this->api->sendMessage(chatId: $chatId, attachments: [$shareRequest]);
@@ -1639,12 +1669,14 @@ final class ApiTest extends TestCase
$admins = [
new ChatAdmin(101, [ChatAdminPermission::Write]),
new ChatAdmin(202, [ChatAdminPermission::PinMessage]),
new ChatAdmin(303, [ChatAdminPermission::EditMessage, ChatAdminPermission::CanCall], 'SuperUser'),
];
$expectedBody = [
'admins' => [
['user_id' => 101, 'permissions' => ['write']],
['user_id' => 202, 'permissions' => ['pin_message']],
['user_id' => 101, 'permissions' => ['write'], 'alias' => null],
['user_id' => 202, 'permissions' => ['pin_message'], 'alias' => null],
['user_id' => 303, 'permissions' => ['edit_message', 'can_call'], 'alias' => 'SuperUser'],
],
];
$rawResponse = ['success' => true];
+93 -15
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests;
use BushlanovDev\MaxMessengerBot\Client;
use BushlanovDev\MaxMessengerBot\Exceptions\AttachmentNotReadyException;
use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException;
use BushlanovDev\MaxMessengerBot\Exceptions\ForbiddenException;
use BushlanovDev\MaxMessengerBot\Exceptions\MethodNotAllowedException;
@@ -15,6 +16,7 @@ use BushlanovDev\MaxMessengerBot\Exceptions\SerializationException;
use BushlanovDev\MaxMessengerBot\Exceptions\UnauthorizedException;
use GuzzleHttp\Psr7\HttpFactory;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
@@ -31,11 +33,12 @@ use Psr\Http\Message\StreamInterface;
use Psr\Log\LoggerInterface;
#[CoversClass(Client::class)]
#[AllowMockObjectsWithoutExpectations]
final class ClientTest extends TestCase
{
private const string FAKE_TOKEN = '12345:abcdef';
private const string API_VERSION = '0.0.6';
private const string API_BASE_URL = 'https://botapi.max.ru';
private const string API_BASE_URL = 'https://platform-api.max.ru';
private MockObject&ClientInterface $httpClientMock;
private MockObject&RequestFactoryInterface $requestFactoryMock;
@@ -96,7 +99,6 @@ final class ClientTest extends TestCase
{
$uri = '/me';
$expectedUrl = self::API_BASE_URL . $uri . '?' . http_build_query([
'access_token' => self::FAKE_TOKEN,
'v' => self::API_VERSION,
]);
$responsePayload = ['id' => 987, 'name' => 'TestBot'];
@@ -108,6 +110,12 @@ final class ClientTest extends TestCase
->with('GET', $expectedUrl)
->willReturn($this->requestMock);
$this->requestMock
->expects($this->once())
->method('withHeader')
->with('Authorization', self::FAKE_TOKEN)
->willReturn($this->requestMock);
$this->httpClientMock
->expects($this->once())
->method('sendRequest')
@@ -138,8 +146,7 @@ final class ClientTest extends TestCase
];
$responsePayload = ['success' => true];
$expectedUrl = self::API_BASE_URL . $uri . '?' . http_build_query([
'access_token' => self::FAKE_TOKEN,
'v' => self::API_VERSION
'v' => self::API_VERSION,
]);
$this->requestFactoryMock
@@ -157,11 +164,23 @@ final class ClientTest extends TestCase
}))
->willReturn($this->requestMock);
$headerCallCount = 0;
$this->requestMock
->expects($this->once())
->expects($this->exactly(2))
->method('withHeader')
->with('Content-Type', 'application/json; charset=utf-8')
->willReturn($this->requestMock);
->willReturnCallback(function (string $header, string $value) use (&$headerCallCount) {
if ($headerCallCount === 0) {
$this->assertSame('Authorization', $header);
$this->assertSame(self::FAKE_TOKEN, $value);
} elseif ($headerCallCount === 1) {
$this->assertSame('Content-Type', $header);
$this->assertSame('application/json; charset=utf-8', $value);
}
$headerCallCount++;
return $this->requestMock;
});
$this->responseMock->method('getStatusCode')->willReturn(200);
$this->streamMock->method('__toString')->willReturn(json_encode($responsePayload));
@@ -226,6 +245,12 @@ final class ClientTest extends TestCase
public static function apiErrorProvider(): array
{
return [
'400 Attachment Not Ready' => [
400,
AttachmentNotReadyException::class,
'attachment.not.ready',
'Key: errors.process.attachment.file.not.processed',
],
'400 Bad Request' => [400, ClientApiException::class, 'bad.request', 'Invalid parameters'],
'401 Unauthorized' => [401, UnauthorizedException::class, 'verify.token', 'Invalid access_token'],
'403 Forbidden' => [403, ForbiddenException::class, 'access.denied', 'You don\'t have permissions'],
@@ -298,11 +323,23 @@ final class ClientTest extends TestCase
)
->willReturn($this->requestMock);
$headerCallCount = 0;
$this->requestMock
->expects($this->once())
->expects($this->exactly(2))
->method('withHeader')
->with($this->stringStartsWith('Content-Type'), $this->stringStartsWith('multipart/form-data'))
->willReturn($this->requestMock);
->willReturnCallback(function (string $header, string $value) use (&$headerCallCount) {
if ($headerCallCount === 0) {
$this->assertSame('Content-Type', $header);
$this->assertStringStartsWith('multipart/form-data; boundary=', $value);
} elseif ($headerCallCount === 1) {
$this->assertSame('Authorization', $header);
$this->assertSame(self::FAKE_TOKEN, $value);
}
$headerCallCount++;
return $this->requestMock;
});
$this->requestFactoryMock
->expects($this->once())
@@ -330,9 +367,26 @@ final class ClientTest extends TestCase
rewind($tmpFileHandle);
$this->requestFactoryMock->method('createRequest')->willReturn($this->requestMock);
$this->requestMock->method('withHeader')->willReturn($this->requestMock);
$headerCallCount = 0;
$this->requestMock
->expects($this->exactly(2))
->method('withHeader')
->willReturnCallback(function (string $header, string $value) use (&$headerCallCount) {
if ($headerCallCount === 0) {
$this->assertSame('Content-Type', $header);
$this->assertStringStartsWith('multipart/form-data; boundary=', $value);
} elseif ($headerCallCount === 1) {
$this->assertSame('Authorization', $header);
$this->assertSame(self::FAKE_TOKEN, $value);
}
$headerCallCount++;
return $this->requestMock;
});
$this->requestMock->method('withBody')->willReturn($this->requestMock);
// $this->httpClientMock->method('sendRequest')->willReturn($this->responseMock);
$this->responseMock->method('getStatusCode')->willReturn(200);
$this->streamMock->method('__toString')->willReturn(json_encode($responsePayload));
@@ -466,7 +520,30 @@ final class ClientTest extends TestCase
$this->requestFactoryMock->method('createRequest')->willReturn($this->requestMock);
$this->requestMock->method('withBody')->willReturnSelf();
$this->requestMock->method('withHeader')->willReturnSelf();
$headerCallCount = 0;
$this->requestMock
->expects($this->exactly(4))
->method('withHeader')
->willReturnCallback(function (string $header, string $value) use (&$headerCallCount, $fileName, $fileSize) {
if ($headerCallCount === 0) {
$this->assertSame('Content-Type', $header);
$this->assertSame('application/octet-stream', $value);
} elseif ($headerCallCount === 1) {
$this->assertSame('Content-Disposition', $header);
$this->assertSame('attachment; filename="' . $fileName . '"', $value);
} elseif ($headerCallCount === 2) {
$this->assertSame('Content-Range', $header);
$this->assertSame("bytes 0-8/{$fileSize}", $value);
} elseif ($headerCallCount === 3) {
$this->assertSame('Authorization', $header);
$this->assertSame(self::FAKE_TOKEN, $value);
}
$headerCallCount++;
return $this->requestMock;
});
$this->httpClientMock
->expects($this->once())
@@ -486,7 +563,8 @@ final class ClientTest extends TestCase
#[Test]
public function resumableUploadSuccessfullyUploadsMultipleChunks(): void
{
$fileContents = str_repeat('A', 3 * 1024 * 1024); // 3 MB
$chunkSize = 1024 * 1024;
$fileContents = str_repeat('A', 3 * $chunkSize); // 3 MB
$fileResource = fopen('php://memory', 'w+');
fwrite($fileResource, $fileContents);
rewind($fileResource);
@@ -513,7 +591,7 @@ final class ClientTest extends TestCase
->method('__toString')
->willReturnOnConsecutiveCalls('', '', '<retval>1</retval>');
$result = $this->client->resumableUpload($uploadUrl, $fileResource, $fileName, $fileSize, 1024 * 1024);
$result = $this->client->resumableUpload($uploadUrl, $fileResource, $fileName, $fileSize, $chunkSize);
$this->assertSame('<retval>1</retval>', $result);
fclose($fileResource);
@@ -7,6 +7,7 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Laravel\Commands;
use BushlanovDev\MaxMessengerBot\Laravel\Commands\PollingStartCommand;
use BushlanovDev\MaxMessengerBot\Laravel\MaxBotManager;
use Illuminate\Support\Facades\Log;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -15,6 +16,7 @@ use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Tester\CommandTester;
#[CoversClass(PollingStartCommand::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(MaxBotManager::class)]
final class PollingStartCommandTest extends TestCase
{
@@ -9,6 +9,7 @@ use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Laravel\Commands\WebhookListCommand;
use BushlanovDev\MaxMessengerBot\Models\Subscription;
use Illuminate\Support\Facades\Log;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -17,6 +18,7 @@ use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Tester\CommandTester;
#[CoversClass(WebhookListCommand::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(Api::class)]
#[UsesClass(Subscription::class)]
final class WebhookListCommandTest extends TestCase
@@ -10,6 +10,7 @@ use BushlanovDev\MaxMessengerBot\Laravel\Commands\WebhookSubscribeCommand;
use BushlanovDev\MaxMessengerBot\Models\Result;
use Illuminate\Contracts\Config\Repository as Config;
use Illuminate\Support\Facades\Log;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -18,6 +19,7 @@ use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Tester\CommandTester;
#[CoversClass(WebhookSubscribeCommand::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(Api::class)]
#[UsesClass(Result::class)]
final class WebhookSubscribeCommandTest extends TestCase
@@ -8,6 +8,7 @@ use BushlanovDev\MaxMessengerBot\Api;
use BushlanovDev\MaxMessengerBot\Laravel\Commands\WebhookUnsubscribeCommand;
use BushlanovDev\MaxMessengerBot\Models\Result;
use Illuminate\Support\Facades\Log;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -16,6 +17,7 @@ use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Tester\CommandTester;
#[CoversClass(WebhookUnsubscribeCommand::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(Api::class)]
#[UsesClass(Result::class)]
final class WebhookUnsubscribeCommandTest extends TestCase
+2
View File
@@ -20,6 +20,7 @@ use Illuminate\Container\Container;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Facade;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -28,6 +29,7 @@ use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
#[CoversClass(MaxBotManager::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(Message::class)]
#[UsesClass(MessageBody::class)]
#[UsesClass(Recipient::class)]
+123 -1
View File
@@ -22,6 +22,7 @@ use InvalidArgumentException;
use LogicException;
use Orchestra\Testbench\TestCase;
use phpmock\phpunit\PHPMock;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\PreserveGlobalState;
@@ -33,11 +34,13 @@ use Psr\Log\NullLogger;
use ReflectionClass;
#[CoversClass(MaxBotServiceProvider::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(Api::class)]
#[UsesClass(Client::class)]
#[UsesClass(UpdateDispatcher::class)]
#[UsesClass(MaxBotManager::class)]
#[UsesClass(WebhookHandler::class)]
#[UsesClass(ModelFactory::class)]
final class MaxBotServiceProviderTest extends TestCase
{
use PHPMock;
@@ -92,6 +95,8 @@ final class MaxBotServiceProviderTest extends TestCase
#[PreserveGlobalState(false)]
public function itThrowsExceptionWhenGuzzleIsMissing(): void
{
error_reporting(E_ALL & ~E_DEPRECATED);
$this->expectException(LogicException::class);
$this->expectExceptionMessage(
'Guzzle HTTP client is required. Please run "composer require guzzlehttp/guzzle".'
@@ -210,6 +215,105 @@ final class MaxBotServiceProviderTest extends TestCase
$this->assertInstanceOf(NullLogger::class, $actualLogger);
}
#[Test]
public function modelFactoryIsConfiguredWithApplicationLoggerWhenLoggingIsEnabled(): void
{
$this->app['config']->set('maxbot.logging.enabled', true);
$mockLogger = $this->createMock(LoggerInterface::class);
$this->app->instance(LoggerInterface::class, $mockLogger);
/** @var ModelFactory $factory */
$factory = $this->app->make(ModelFactory::class);
$reflection = new ReflectionClass($factory);
$loggerProp = $reflection->getProperty('logger');
$actualLogger = $loggerProp->getValue($factory);
$this->assertSame($mockLogger, $actualLogger);
}
#[Test]
public function modelFactoryIsConfiguredWithNullLoggerWhenLoggingIsDisabled(): void
{
$this->app['config']->set('maxbot.logging.enabled', false);
/** @var ModelFactory $factory */
$factory = $this->app->make(ModelFactory::class);
$reflection = new ReflectionClass($factory);
$loggerProp = $reflection->getProperty('logger');
$actualLogger = $loggerProp->getValue($factory);
$this->assertInstanceOf(NullLogger::class, $actualLogger);
}
#[Test]
public function webhookHandlerIsConfiguredWithApplicationLoggerWhenLoggingIsEnabled(): void
{
$this->app['config']->set('maxbot.logging.enabled', true);
$mockLogger = $this->createMock(LoggerInterface::class);
$this->app->instance(LoggerInterface::class, $mockLogger);
/** @var WebhookHandler $handler */
$handler = $this->app->make(WebhookHandler::class);
$reflection = new ReflectionClass($handler);
$loggerProp = $reflection->getProperty('logger');
$actualLogger = $loggerProp->getValue($handler);
$this->assertSame($mockLogger, $actualLogger);
}
#[Test]
public function webhookHandlerIsConfiguredWithNullLoggerWhenLoggingIsDisabled(): void
{
$this->app['config']->set('maxbot.logging.enabled', false);
/** @var WebhookHandler $handler */
$handler = $this->app->make(WebhookHandler::class);
$reflection = new ReflectionClass($handler);
$loggerProp = $reflection->getProperty('logger');
$actualLogger = $loggerProp->getValue($handler);
$this->assertInstanceOf(NullLogger::class, $actualLogger);
}
#[Test]
public function longPollingHandlerIsConfiguredWithApplicationLoggerWhenLoggingIsEnabled(): void
{
$this->app['config']->set('maxbot.logging.enabled', true);
$mockLogger = $this->createMock(LoggerInterface::class);
$this->app->instance(LoggerInterface::class, $mockLogger);
/** @var LongPollingHandler $handler */
$handler = $this->app->make(LongPollingHandler::class);
$reflection = new ReflectionClass($handler);
$loggerProp = $reflection->getProperty('logger');
$actualLogger = $loggerProp->getValue($handler);
$this->assertSame($mockLogger, $actualLogger);
}
#[Test]
public function longPollingHandlerIsConfiguredWithNullLoggerWhenLoggingIsDisabled(): void
{
$this->app['config']->set('maxbot.logging.enabled', false);
/** @var LongPollingHandler $handler */
$handler = $this->app->make(LongPollingHandler::class);
$reflection = new ReflectionClass($handler);
$loggerProp = $reflection->getProperty('logger');
$actualLogger = $loggerProp->getValue($handler);
$this->assertInstanceOf(NullLogger::class, $actualLogger);
}
#[Test]
public function webhookHandlerIsConfiguredWithSecretFromConfig(): void
{
@@ -238,10 +342,28 @@ final class MaxBotServiceProviderTest extends TestCase
$this->assertSame($this->app->make(ClientApiInterface::class), $clientProp->getValue($api));
$this->assertSame($this->app->make(ModelFactory::class), $factoryProp->getValue($api));
$this->assertSame($this->app->make(LoggerInterface::class), $loggerProp->getValue($api));
$this->assertInstanceOf(NullLogger::class, $loggerProp->getValue($api));
$this->assertSame($this->app->make(UpdateDispatcher::class), $dispatcherProp->getValue($api));
}
#[Test]
public function apiIsConfiguredWithApplicationLoggerWhenLoggingIsEnabled(): void
{
$this->app['config']->set('maxbot.logging.enabled', true);
$mockLogger = $this->createMock(LoggerInterface::class);
$this->app->instance(LoggerInterface::class, $mockLogger);
/** @var Api $api */
$api = $this->app->make(Api::class);
$reflection = new ReflectionClass($api);
$loggerProp = $reflection->getProperty('logger');
$actualLogger = $loggerProp->getValue($api);
$this->assertSame($mockLogger, $actualLogger);
}
/**
* @return array<string, array{0: string}>
*/
+6 -4
View File
@@ -11,11 +11,12 @@ use BushlanovDev\MaxMessengerBot\LongPollingHandler;
use BushlanovDev\MaxMessengerBot\Models\UpdateList;
use BushlanovDev\MaxMessengerBot\Models\Updates\AbstractUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use BushlanovDev\MaxMessengerBot\UpdateDispatcher;
use Error;
use Exception;
use phpmock\phpunit\PHPMock;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\PreserveGlobalState;
@@ -26,11 +27,12 @@ use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
#[CoversClass(LongPollingHandler::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(UpdateDispatcher::class)]
#[UsesClass(UpdateList::class)]
#[UsesClass(AbstractUpdate::class)]
#[UsesClass(BotStartedUpdate::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class LongPollingHandlerTest extends TestCase
{
use PHPMock;
@@ -77,7 +79,7 @@ final class LongPollingHandlerTest extends TestCase
public static function processUpdatesProvider(): array
{
$user = new User(1, 'Test', null, null, false, time());
$user = new UserWithPhoto(1, 'Test', null, null, false, time(), null, null, null);
$update1 = new BotStartedUpdate(time(), 1, $user, null, null);
$update2 = new BotStartedUpdate(time(), 2, $user, null, null);
@@ -167,7 +169,7 @@ final class LongPollingHandlerTest extends TestCase
$dispatcher = new UpdateDispatcher($apiMock, $loggerMock);
$user = new User(1, 'Test', null, null, false, time());
$user = new UserWithPhoto(1, 'Test', null, null, false, time(), null, null, null);
$updateToFail = new BotStartedUpdate(time(), 1, $user, null, null);
$updateToSucceed = new BotStartedUpdate(time(), 2, $user, null, null);
+89 -44
View File
@@ -18,7 +18,6 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\RequestGeoLoc
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\AbstractReplyButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendContactButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendMessageButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\DataAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\InlineKeyboardAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\LocationAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\KeyboardPayload;
@@ -26,7 +25,6 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentPayl
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequestPayload;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ShareAttachmentRequestPayload;
use BushlanovDev\MaxMessengerBot\Models\Attachments\PhotoAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\ReplyKeyboardAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\ShareAttachment;
use BushlanovDev\MaxMessengerBot\Models\BotCommand;
use BushlanovDev\MaxMessengerBot\Models\BotInfo;
@@ -42,7 +40,6 @@ use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\Recipient;
use BushlanovDev\MaxMessengerBot\Models\Result;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\Subscription;
use BushlanovDev\MaxMessengerBot\Models\UpdateList;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
@@ -59,6 +56,7 @@ use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
#[CoversClass(ModelFactory::class)]
#[UsesClass(BotInfo::class)]
@@ -69,7 +67,6 @@ use PHPUnit\Framework\TestCase;
#[UsesClass(Message::class)]
#[UsesClass(MessageBody::class)]
#[UsesClass(Recipient::class)]
#[UsesClass(User::class)]
#[UsesClass(UpdateList::class)]
#[UsesClass(BotStartedUpdate::class)]
#[UsesClass(MessageCreatedUpdate::class)]
@@ -86,7 +83,6 @@ use PHPUnit\Framework\TestCase;
#[UsesClass(PhotoAttachmentRequestPayload::class)]
#[UsesClass(VideoUrls::class)]
#[UsesClass(AbstractAttachment::class)]
#[UsesClass(DataAttachment::class)]
#[UsesClass(ShareAttachment::class)]
#[UsesClass(ShareAttachmentRequestPayload::class)]
#[UsesClass(LinkMarkup::class)]
@@ -97,7 +93,6 @@ use PHPUnit\Framework\TestCase;
#[UsesClass(AbstractReplyButton::class)]
#[UsesClass(SendContactButton::class)]
#[UsesClass(SendMessageButton::class)]
#[UsesClass(ReplyKeyboardAttachment::class)]
#[UsesClass(AbstractInlineButton::class)]
#[UsesClass(ChatButton::class)]
#[UsesClass(RequestContactButton::class)]
@@ -220,6 +215,36 @@ final class ModelFactoryTest extends TestCase
$this->assertSame(UpdateType::MessageCreated, $subscriptions[0]->updateTypes[0]);
}
#[Test]
public function createMessageFromSendResponseCorrectlyTransformsData(): void
{
$apiResponse = [
'message' => [
'recipient' => ['chat_id' => 20414985, 'chat_type' => 'dialog', 'user_id' => 4328369],
'timestamp' => 1760089962345,
'sender' => [
'user_id' => 24480184,
'first_name' => 'Autobot',
'is_bot' => true,
'last_activity_time' => 1760089962354,
],
'message' => ['mid' => 'mid.xyz', 'seq' => 1153, 'text' => '123123'],
],
'chat_id' => 20414985,
'recipient_id' => 4328369,
'message_id' => 'mid.xyz',
];
$message = $this->factory->createMessageFromSendResponse($apiResponse);
$this->assertInstanceOf(Message::class, $message);
$this->assertInstanceOf(MessageBody::class, $message->body);
$this->assertSame('mid.xyz', $message->body->mid);
$this->assertSame(20414985, $message->chatId);
$this->assertSame(4328369, $message->recipientId);
$this->assertSame('mid.xyz', $message->messageId);
}
#[Test]
public function createMessage(): void
{
@@ -251,7 +276,7 @@ final class ModelFactoryTest extends TestCase
$this->assertInstanceOf(Message::class, $message);
$this->assertInstanceOf(MessageBody::class, $message->body);
$this->assertInstanceOf(Recipient::class, $message->recipient);
$this->assertInstanceOf(User::class, $message->sender);
$this->assertInstanceOf(UserWithPhoto::class, $message->sender);
}
#[Test]
@@ -427,6 +452,7 @@ final class ModelFactoryTest extends TestCase
'is_admin' => true,
'join_time' => 1678000000,
'permissions' => ['pin_message', 'write'],
'alias' => null,
];
$chatMember = $this->factory->createChatMember($rawData);
@@ -563,7 +589,6 @@ final class ModelFactoryTest extends TestCase
'seq' => 102,
'text' => 'Message with data attachment',
'attachments' => [
['type' => 'data', 'data' => 'payload_from_reply_button'],
[
'type' => 'share',
'payload' => ['url' => 'http://a.com'],
@@ -585,16 +610,13 @@ final class ModelFactoryTest extends TestCase
$this->assertInstanceOf(Message::class, $message);
$this->assertInstanceOf(MessageBody::class, $message->body);
$this->assertIsArray($attachments);
$this->assertCount(3, $attachments);
$this->assertCount(2, $attachments);
$this->assertInstanceOf(DataAttachment::class, $attachments[0]);
$this->assertSame('payload_from_reply_button', $attachments[0]->data);
$this->assertInstanceOf(ShareAttachment::class, $message->body->attachments[0]);
$this->assertSame('Test Share', $attachments[0]->title);
$this->assertInstanceOf(ShareAttachment::class, $message->body->attachments[1]);
$this->assertSame('Test Share', $attachments[1]->title);
$this->assertInstanceOf(PhotoAttachment::class, $attachments[2]);
$this->assertSame(1, $attachments[2]->payload->photoId);
$this->assertInstanceOf(PhotoAttachment::class, $attachments[1]);
$this->assertSame(1, $attachments[1]->payload->photoId);
}
#[Test]
@@ -635,26 +657,6 @@ final class ModelFactoryTest extends TestCase
$this->factory->createMarkupElement(['type' => 'brand_new_unsupported_type']);
}
#[Test]
public function createAttachmentCorrectlyHydratesReplyKeyboard(): void
{
$data = [
'type' => 'reply_keyboard',
'buttons' => [
[['type' => 'message', 'text' => 'Hello']],
[['type' => 'user_contact', 'text' => 'My Contact']]
]
];
$attachment = $this->factory->createAttachment($data);
$this->assertInstanceOf(ReplyKeyboardAttachment::class, $attachment);
$this->assertCount(2, $attachment->buttons);
$this->assertInstanceOf(SendMessageButton::class, $attachment->buttons[0][0]);
$this->assertInstanceOf(SendContactButton::class, $attachment->buttons[1][0]);
$this->assertSame('My Contact', $attachment->buttons[1][0]->text);
}
#[Test]
public function createReplyButtonThrowsExceptionForUnknownType(): void
{
@@ -739,14 +741,6 @@ final class ModelFactoryTest extends TestCase
public static function attachmentTypeProvider(): array
{
return [
'Data Attachment' => [
['type' => 'data', 'data' => 'test_payload'],
DataAttachment::class,
function (TestCase $test, DataAttachment $attachment) {
$test->assertSame('test_payload', $attachment->data);
}
],
'Location Attachment' => [
['type' => 'location', 'latitude' => 55.751244, 'longitude' => 37.618423],
LocationAttachment::class,
@@ -788,4 +782,55 @@ final class ModelFactoryTest extends TestCase
$assertionCallback($this, $attachment);
}
#[Test]
public function createUpdateListCatchesAndLogsLogicException(): void
{
$loggerMock = $this->createMock(LoggerInterface::class);
$factory = $this->getMockBuilder(ModelFactory::class)
->setConstructorArgs([$loggerMock])
->onlyMethods(['createUpdate'])
->getMock();
$validUpdateData = [
'update_type' => 'bot_started',
'timestamp' => 2,
'chat_id' => 123,
'user' => [
'user_id' => 123,
'first_name' => 'John',
'is_bot' => false,
'last_activity_time' => 2,
],
'payload' => 'start_payload',
'user_locale' => 'ru-RU',
];
$invalidUpdateData = ['update_type' => 'unknown_type'];
$rawData = [
'updates' => [$validUpdateData, $invalidUpdateData],
'marker' => 123,
];
$exception = new LogicException('Unknown or unsupported update type received: unknown_type');
$factory->expects($this->exactly(2))
->method('createUpdate')
->willReturnCallback(function ($data) use ($validUpdateData, $invalidUpdateData, $exception) {
if ($data === $validUpdateData) {
return BotStartedUpdate::fromArray($data);
}
if ($data === $invalidUpdateData) {
throw $exception;
}
return null;
});
$loggerMock->expects($this->once())
->method('debug')
->with($exception->getMessage(), ['payload' => $invalidUpdateData, 'exception' => $exception]);
$updateList = $factory->createUpdateList($rawData);
$this->assertCount(1, $updateList->updates);
$this->assertInstanceOf(BotStartedUpdate::class, $updateList->updates[0]);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments\Buttons\Inline;
use BushlanovDev\MaxMessengerBot\Enums\InlineButtonType;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\ClipboardButton;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
#[CoversClass(ClipboardButton::class)]
final class ClipboardButtonTest extends TestCase
{
#[Test]
public function toArraySerializesCorrectly(): void
{
$button = new ClipboardButton('display', 'copy');
$expectedArray = [
'payload' => 'copy',
'type' => InlineButtonType::Clipboard->value,
'text' => 'display',
];
$resultArray = $button->toArray();
$this->assertSame($expectedArray, $resultArray);
}
#[Test]
public function fromArrayHydratesCorrectly(): void
{
$data = [
'type' => 'clipboard',
'text' => 'display',
'payload' => 'copy'
];
$button = ClipboardButton::fromArray($data);
$this->assertInstanceOf(ClipboardButton::class, $button);
$this->assertSame(InlineButtonType::Clipboard, $button->type);
$this->assertSame('display', $button->text);
$this->assertSame('copy', $button->payload);
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments\Buttons\Inline;
use BushlanovDev\MaxMessengerBot\Enums\InlineButtonType;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Inline\MessageButton;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
#[CoversClass(MessageButton::class)]
final class MessageButtonTest extends TestCase
{
#[Test]
public function toArraySerializesCorrectly(): void
{
$button = new MessageButton('Test Button');
$expectedArray = [
'type' => InlineButtonType::Message->value,
'text' => 'Test Button',
];
$resultArray = $button->toArray();
$this->assertSame($expectedArray, $resultArray);
}
}
@@ -16,11 +16,12 @@ final class OpenAppButtonTest extends TestCase
#[Test]
public function toArraySerializesCorrectly(): void
{
$button = new OpenAppButton('Test Button', 'MyWebApp', 123);
$button = new OpenAppButton('Test Button', 'MyWebApp', 123, 'somePayload');
$expectedArray = [
'web_app' => 'MyWebApp',
'contact_id' => 123,
'payload' => 'somePayload',
'type' => InlineButtonType::OpenApp->value,
'text' => 'Test Button',
];
@@ -38,6 +39,7 @@ final class OpenAppButtonTest extends TestCase
'text' => 'Launch',
'web_app' => 'SomeApp',
'contact_id' => 456,
'payload' => 'somePayload'
];
$button = OpenAppButton::fromArray($data);
@@ -47,5 +49,6 @@ final class OpenAppButtonTest extends TestCase
$this->assertSame('Launch', $button->text);
$this->assertSame('SomeApp', $button->webApp);
$this->assertSame(456, $button->contactId);
$this->assertSame('somePayload', $button->payload);
}
}
@@ -6,7 +6,7 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments;
use BushlanovDev\MaxMessengerBot\Models\Attachments\ContactAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ContactAttachmentPayload;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase;
#[CoversClass(ContactAttachment::class)]
#[UsesClass(ContactAttachmentPayload::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class ContactAttachmentTest extends TestCase
{
#[Test]
@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments;
use BushlanovDev\MaxMessengerBot\Enums\AttachmentType;
use BushlanovDev\MaxMessengerBot\Models\Attachments\AbstractAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\DataAttachment;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(DataAttachment::class)]
#[UsesClass(AbstractAttachment::class)]
final class DataAttachmentTest extends TestCase
{
#[Test]
public function canBeCreatedAndSerialized(): void
{
$attachment = new DataAttachment('some_payload_from_button');
$this->assertSame(AttachmentType::Data, $attachment->type);
$this->assertSame('some_payload_from_button', $attachment->data);
$expectedArray = [
'type' => 'data',
'data' => 'some_payload_from_button',
];
$this->assertEquals($expectedArray, $attachment->toArray());
}
#[Test]
public function canBeCreatedFromArray(): void
{
$data = [
'type' => 'data',
'data' => 'payload123',
];
$attachment = DataAttachment::fromArray($data);
$this->assertInstanceOf(DataAttachment::class, $attachment);
$this->assertSame(AttachmentType::Data, $attachment->type);
$this->assertSame('payload123', $attachment->data);
}
}
@@ -5,20 +5,20 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments\Payloads;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ContactAttachmentPayload;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ContactAttachmentPayload::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class ContactAttachmentPayloadTest extends TestCase
{
#[Test]
public function canBeCreatedAndSerialized(): void
{
$user = User::fromArray(
$user = UserWithPhoto::fromArray(
['user_id' => 101, 'first_name' => 'MaxUser', 'is_bot' => false, 'last_activity_time' => time()]
);
$payload = new ContactAttachmentPayload('vcf_info_string', $user);
@@ -9,7 +9,6 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequ
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoToken;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
@@ -77,32 +76,12 @@ final class PhotoAttachmentRequestPayloadTest extends TestCase
$this->assertEquals($expectedArray, $payload->toArray());
}
/**
* Data provider for invalid constructor arguments.
*
* @return array<string, array{0: string|null, 1: string|null, 2: array|null}>
*/
public static function invalidPayloadProvider(): array
{
return [
'all null (no arguments)' => [null, null, null],
'url and token provided' => ['https://a.com', 'token123', null],
'url and photos provided' => ['https://a.com', null, [new PhotoToken('t')]],
'token and photos provided' => [null, 'token123', [new PhotoToken('t')]],
'all three arguments provided' => ['https://a.com', 'token123', [new PhotoToken('t')]],
];
}
#[Test]
#[DataProvider('invalidPayloadProvider')]
public function constructorThrowsExceptionForInvalidArguments(
?string $url,
?string $token,
?array $photos
): void {
public function constructorThrowsExceptionForInvalidArguments(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Provide exactly one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.');
$this->expectExceptionMessage('Provide one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.');
new PhotoAttachmentRequestPayload($url, $token, $photos);
new PhotoAttachmentRequestPayload(null, null, null);
}
}
@@ -1,99 +0,0 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments\Payloads;
use BushlanovDev\MaxMessengerBot\Enums\Intent;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendContactButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendMessageButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ReplyKeyboardAttachmentRequestPayload;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ReplyKeyboardAttachmentRequestPayload::class)]
#[UsesClass(SendMessageButton::class)]
#[UsesClass(SendContactButton::class)]
final class ReplyKeyboardAttachmentRequestPayloadTest extends TestCase
{
#[Test]
public function toArrayWithDefaults(): void
{
$buttons = [
[new SendMessageButton('Help')],
[new SendContactButton('Share Contact')],
];
$payload = new ReplyKeyboardAttachmentRequestPayload($buttons);
$expected = [
'buttons' => [
[
[
'type' => 'message',
'text' => 'Help',
'payload' => null,
'intent' => 'default',
],
],
[
[
'type' => 'user_contact',
'text' => 'Share Contact',
],
],
],
'direct' => false,
'direct_user_id' => null,
];
$this->assertEquals($expected, $payload->toArray());
}
#[Test]
public function toArrayWithAllParameters(): void
{
$buttons = [
[new SendMessageButton('Confirm', 'confirm-action', Intent::Positive)],
];
$payload = new ReplyKeyboardAttachmentRequestPayload(
buttons: $buttons,
direct: true,
directUserId: 987654321,
);
$expected = [
'buttons' => [
[
[
'type' => 'message',
'text' => 'Confirm',
'payload' => 'confirm-action',
'intent' => 'positive',
],
],
],
'direct' => true,
'direct_user_id' => 987654321,
];
$this->assertEquals($expected, $payload->toArray());
}
#[Test]
public function toArrayWithEmptyButtons(): void
{
$payload = new ReplyKeyboardAttachmentRequestPayload(buttons: []);
$expected = [
'buttons' => [],
'direct' => false,
'direct_user_id' => null,
];
$this->assertEquals($expected, $payload->toArray());
}
}
@@ -38,24 +38,12 @@ final class ShareAttachmentRequestPayloadTest extends TestCase
$this->assertEquals($expectedArray, $payload->toArray());
}
/**
* @return array<string, array{0: ?string, 1: ?string}>
*/
public static function invalidPayloadProvider(): array
{
return [
'both null' => [null, null],
'both set' => ['https://a.com', 'token123'],
];
}
#[Test]
#[DataProvider('invalidPayloadProvider')]
public function constructorThrowsExceptionForInvalidArguments(?string $url, ?string $token): void
public function constructorThrowsExceptionForInvalidArguments(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Provide exactly one of "url" or "token" for ShareAttachmentRequestPayload.');
$this->expectExceptionMessage('Provide one of "url" or "token" for ShareAttachmentRequestPayload.');
new ShareAttachmentRequestPayload($url, $token);
new ShareAttachmentRequestPayload(null, null);
}
}
@@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments;
use BushlanovDev\MaxMessengerBot\ModelFactory;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\AbstractReplyButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendContactButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendMessageButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\ReplyKeyboardAttachment;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ReplyKeyboardAttachment::class)]
#[UsesClass(AbstractReplyButton::class)]
#[UsesClass(SendContactButton::class)]
#[UsesClass(SendMessageButton::class)]
#[UsesClass(ModelFactory::class)]
final class ReplyKeyboardAttachmentTest extends TestCase
{
#[Test]
public function canBeCreatedFromArrayViaFactory(): void
{
$data = [
'type' => 'reply_keyboard',
'buttons' => [
[['type' => 'message', 'text' => 'Hello']],
[['type' => 'user_contact', 'text' => 'My Contact']]
]
];
$factory = new ModelFactory();
$attachment = $factory->createAttachment($data);
$this->assertInstanceOf(ReplyKeyboardAttachment::class, $attachment);
$this->assertCount(2, $attachment->buttons);
$this->assertInstanceOf(SendContactButton::class, $attachment->buttons[1][0]);
$this->assertSame('My Contact', $attachment->buttons[1][0]->text);
}
}
@@ -98,29 +98,12 @@ final class PhotoAttachmentRequestTest extends TestCase
$this->assertEquals($expectedArray, $request->toArray());
}
/**
* @return array<string, array{0: string|null, 1: string|null, 2: array|null}>
*/
public static function invalidPayloadProvider(): array
{
return [
'no arguments' => [null, null, null],
'url and token' => ['http://a.com', 'token123', null],
'token and photos' => [null, 'token123', [new PhotoToken('t')]],
'all arguments' => ['http://a.com', 'token123', [new PhotoToken('t')]],
];
}
#[Test]
#[DataProvider('invalidPayloadProvider')]
public function payloadThrowsExceptionWhenNotExactlyOneArgumentIsProvided(
?string $url,
?string $token,
?array $photos
): void {
public function payloadThrowsExceptionWhenNotExactlyOneArgumentIsProvided(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Provide exactly one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.');
$this->expectExceptionMessage('Provide one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.');
new PhotoAttachmentRequestPayload($url, $token, $photos);
new PhotoAttachmentRequestPayload(null, null, null);
}
}
@@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments\Requests;
use BushlanovDev\MaxMessengerBot\Enums\Intent;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendContactButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\SendMessageButton;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ReplyKeyboardAttachmentRequestPayload;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\ReplyKeyboardAttachmentRequest;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ReplyKeyboardAttachmentRequest::class)]
#[UsesClass(SendMessageButton::class)]
#[UsesClass(SendContactButton::class)]
#[UsesClass(ReplyKeyboardAttachmentRequestPayload::class)]
final class ReplyKeyboardAttachmentRequestTest extends TestCase
{
#[Test]
public function toArray(): void
{
$buttons = [
[new SendMessageButton('Help', 'help_payload', Intent::Positive)],
[new SendContactButton('Share Contact')],
];
$request = new ReplyKeyboardAttachmentRequest(buttons: $buttons, direct: true);
$expectedArray = [
'type' => 'reply_keyboard',
'payload' => [
'buttons' => [
[
[
'type' => 'message',
'text' => 'Help',
'payload' => 'help_payload',
'intent' => 'positive',
],
],
[
[
'type' => 'user_contact',
'text' => 'Share Contact',
],
],
],
'direct' => true,
'direct_user_id' => null,
],
];
$this->assertEquals($expectedArray, $request->toArray());
}
}
@@ -9,7 +9,6 @@ use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ShareAttachmentRequ
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\ShareAttachmentRequest;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
@@ -42,21 +41,11 @@ final class ShareAttachmentRequestTest extends TestCase
$this->assertEquals($expected, $request->toArray());
}
/** @return array<string, array{0: ?string, 1: ?string}> */
public static function invalidPayloadProvider(): array
{
return [
'both null' => [null, null],
'both set' => ['https://a.com', 'token123'],
];
}
#[Test]
#[DataProvider('invalidPayloadProvider')]
public function payloadThrowsExceptionForInvalidArguments(?string $url, ?string $token): void
public function payloadThrowsExceptionForInvalidArguments(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Provide exactly one of "url" or "token" for ShareAttachmentRequestPayload.');
new ShareAttachmentRequestPayload($url, $token);
$this->expectExceptionMessage('Provide one of "url" or "token" for ShareAttachmentRequestPayload.');
new ShareAttachmentRequestPayload(null, null);
}
}
+7
View File
@@ -58,4 +58,11 @@ final class BotPatchTest extends TestCase
$patch = new BotPatch();
$this->assertEmpty($patch->toArray());
}
#[Test]
public function toArrayFirstAndLastName(): void
{
$patch = new BotPatch(first_name: 'New Name', last_name: null);
$this->assertEquals(['first_name' => 'New Name', 'last_name' => null], $patch->toArray());
}
}
+6 -3
View File
@@ -5,14 +5,14 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\Models\Callback;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(Callback::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class CallbackTest extends TestCase
{
#[Test]
@@ -29,6 +29,9 @@ final class CallbackTest extends TestCase
'last_activity_time' => 1678886000,
'last_name' => null,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
];
@@ -38,7 +41,7 @@ final class CallbackTest extends TestCase
$this->assertSame(1678886400, $callback->timestamp);
$this->assertSame('cb.12345.abc', $callback->callbackId);
$this->assertSame('button_1_pressed', $callback->payload);
$this->assertInstanceOf(User::class, $callback->user);
$this->assertInstanceOf(UserWithPhoto::class, $callback->user);
$this->assertSame(101, $callback->user->userId);
$this->assertEquals($data, $callback->toArray());
}
+1
View File
@@ -24,6 +24,7 @@ final class ChatAdminTest extends TestCase
$expected = [
'user_id' => 123,
'permissions' => ['write', 'pin_message'],
'alias' => null,
];
$this->assertEquals($expected, $admin->toArray());
+2
View File
@@ -34,6 +34,7 @@ final class ChatMemberTest extends TestCase
'is_admin' => true,
'join_time' => 1678000000,
'permissions' => ['pin_message', 'write'],
'alias' => null,
];
$member = ChatMember::fromArray($data);
@@ -66,6 +67,7 @@ final class ChatMemberTest extends TestCase
'is_admin' => false,
'join_time' => 1678000001,
'permissions' => null,
'alias' => null,
];
$member = ChatMember::fromArray($data);
+5
View File
@@ -48,6 +48,10 @@ final class ChatTest extends TestCase
],
'messages_count' => 100,
'chat_message_id' => 'mid.123',
'participants' => [
'456' => 1678886400000,
'789' => 1678886500000,
],
];
$chat = Chat::fromArray($data);
@@ -99,5 +103,6 @@ final class ChatTest extends TestCase
$this->assertNull($chat->dialogWithUser);
$this->assertNull($chat->messagesCount);
$this->assertNull($chat->chatMessageId);
$this->assertNull($chat->participants);
}
}
+3 -3
View File
@@ -7,7 +7,7 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\Enums\MessageLinkType;
use BushlanovDev\MaxMessengerBot\Models\LinkedMessage;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -15,7 +15,7 @@ use PHPUnit\Framework\TestCase;
#[CoversClass(LinkedMessage::class)]
#[UsesClass(MessageBody::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class LinkedMessageTest extends TestCase
{
#[Test]
@@ -46,7 +46,7 @@ final class LinkedMessageTest extends TestCase
$this->assertSame(MessageLinkType::Reply, $linkedMessage->type);
$this->assertInstanceOf(MessageBody::class, $linkedMessage->message);
$this->assertSame('mid.original.123', $linkedMessage->message->mid);
$this->assertInstanceOf(User::class, $linkedMessage->sender);
$this->assertInstanceOf(UserWithPhoto::class, $linkedMessage->sender);
$this->assertSame(101, $linkedMessage->sender->userId);
$this->assertSame(98765, $linkedMessage->chatId);
}
+2 -2
View File
@@ -15,7 +15,7 @@ use BushlanovDev\MaxMessengerBot\Models\Markup\StrongMarkup;
use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\Recipient;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -32,7 +32,7 @@ use PHPUnit\Framework\TestCase;
#[UsesClass(StrongMarkup::class)]
#[UsesClass(Message::class)]
#[UsesClass(Recipient::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class MessageBodyTest extends TestCase
{
#[Test]
+9 -3
View File
@@ -9,7 +9,7 @@ use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\MessageStat;
use BushlanovDev\MaxMessengerBot\Models\Recipient;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -18,7 +18,7 @@ use PHPUnit\Framework\TestCase;
#[CoversClass(Message::class)]
#[UsesClass(MessageBody::class)]
#[UsesClass(Recipient::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
#[UsesClass(LinkedMessage::class)]
#[UsesClass(MessageStat::class)]
final class MessageTest extends TestCase
@@ -41,6 +41,9 @@ final class MessageTest extends TestCase
'markup' => null,
],
'sender' =>[
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
'user_id' => 123,
'first_name' => 'John',
'last_name' => 'Doe',
@@ -64,6 +67,9 @@ final class MessageTest extends TestCase
'stat' => [
'views' => 500,
],
'chat_id' => null,
'recipient_id' => null,
'message_id' => null,
];
$message = Message::fromArray($data);
@@ -72,7 +78,7 @@ final class MessageTest extends TestCase
$this->assertSame($data['timestamp'], $message->timestamp);
$this->assertInstanceOf(MessageBody::class, $message->body);
$this->assertInstanceOf(Recipient::class, $message->recipient);
$this->assertInstanceOf(User::class, $message->sender);
$this->assertInstanceOf(UserWithPhoto::class, $message->sender);
$this->assertSame($data['url'], $message->url);
$this->assertInstanceOf(MessageStat::class, $message->stat);
$this->assertSame(500, $message->stat->views);
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotAddedToChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(BotAddedToChatUpdate::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class BotAddedToChatUpdateTest extends TestCase
{
#[Test]
@@ -30,6 +30,9 @@ final class BotAddedToChatUpdateTest extends TestCase
'last_activity_time' => 1678000000,
'last_name' => null,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'is_channel' => false,
];
@@ -41,7 +44,7 @@ final class BotAddedToChatUpdateTest extends TestCase
$this->assertSame(1679000000, $update->timestamp);
$this->assertSame(987654321, $update->chatId);
$this->assertFalse($update->isChannel);
$this->assertInstanceOf(User::class, $update->user);
$this->assertInstanceOf(UserWithPhoto::class, $update->user);
$this->assertSame(101, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotRemovedFromChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(BotRemovedFromChatUpdate::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class BotRemovedFromChatUpdateTest extends TestCase
{
#[Test]
@@ -30,6 +30,9 @@ final class BotRemovedFromChatUpdateTest extends TestCase
'is_bot' => false,
'last_activity_time' => 1679000000,
'last_name' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'is_channel' => false,
];
@@ -41,7 +44,7 @@ final class BotRemovedFromChatUpdateTest extends TestCase
$this->assertSame(1679100000, $update->timestamp);
$this->assertSame(123456, $update->chatId);
$this->assertFalse($update->isChannel);
$this->assertInstanceOf(User::class, $update->user);
$this->assertInstanceOf(UserWithPhoto::class, $update->user);
$this->assertSame(555, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(BotStartedUpdate::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class BotStartedUpdateTest extends TestCase
{
#[Test]
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStoppedUpdate;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(BotStoppedUpdate::class)]
#[UsesClass(UserWithPhoto::class)]
final class BotStoppedUpdateTest extends TestCase
{
#[Test]
public function canBeCreatedFromArray(): void
{
$data = [
'update_type' => UpdateType::BotStopped->value,
'timestamp' => 1678886400000,
'chat_id' => 123,
'user' => [
'user_id' => 123,
'first_name' => 'John',
'last_name' => 'Doe',
'is_bot' => false,
'last_activity_time' => 1678886400000,
],
'user_locale' => 'ru-ru',
];
$update = BotStoppedUpdate::fromArray($data);
$this->assertInstanceOf(BotStoppedUpdate::class, $update);
$this->assertSame(UpdateType::BotStopped, $update->updateType);
$this->assertSame(123, $update->user->userId);
$this->assertSame('John', $update->user->firstName);
$this->assertSame('Doe', $update->user->lastName);
$this->assertSame('ru-ru', $update->userLocale);
}
}
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\ChatTitleChangedUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ChatTitleChangedUpdate::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class ChatTitleChangedUpdateTest extends TestCase
{
#[Test]
@@ -30,6 +30,9 @@ final class ChatTitleChangedUpdateTest extends TestCase
'last_activity_time' => 1679999999,
'last_name' => null,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'title' => 'New Awesome Chat Title',
];
@@ -41,7 +44,7 @@ final class ChatTitleChangedUpdateTest extends TestCase
$this->assertSame(1680000000, $update->timestamp);
$this->assertSame(12345, $update->chatId);
$this->assertSame('New Awesome Chat Title', $update->title);
$this->assertInstanceOf(User::class, $update->user);
$this->assertInstanceOf(UserWithPhoto::class, $update->user);
$this->assertSame(54321, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogClearedUpdate;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(DialogClearedUpdate::class)]
#[UsesClass(UserWithPhoto::class)]
final class DialogClearedUpdateTest extends TestCase
{
#[Test]
public function canBeCreatedFromArray(): void
{
$data = [
'update_type' => UpdateType::DialogCleared->value,
'timestamp' => 1678886400000,
'chat_id' => 123,
'user' => [
'user_id' => 123,
'first_name' => 'John',
'last_name' => 'Doe',
'is_bot' => false,
'last_activity_time' => 1678886400000,
],
'user_locale' => 'ru-ru',
];
$update = DialogClearedUpdate::fromArray($data);
$this->assertInstanceOf(DialogClearedUpdate::class, $update);
$this->assertSame(UpdateType::DialogCleared, $update->updateType);
$this->assertSame(123, $update->user->userId);
$this->assertSame('John', $update->user->firstName);
$this->assertSame('Doe', $update->user->lastName);
$this->assertSame('ru-ru', $update->userLocale);
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogMutedUpdate;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(DialogMutedUpdate::class)]
#[UsesClass(UserWithPhoto::class)]
final class DialogMutedUpdateTest extends TestCase
{
#[Test]
public function canBeCreatedFromArray(): void
{
$data = [
'update_type' => UpdateType::DialogMuted->value,
'timestamp' => 1678886400000,
'chat_id' => 123,
'user' => [
'user_id' => 123,
'first_name' => 'John',
'last_name' => 'Doe',
'is_bot' => false,
'last_activity_time' => 1678886400000,
],
'muted_until' => 1678886400000,
'user_locale' => 'ru-ru',
];
$update = DialogMutedUpdate::fromArray($data);
$this->assertInstanceOf(DialogMutedUpdate::class, $update);
$this->assertSame(UpdateType::DialogMuted, $update->updateType);
$this->assertSame(123, $update->user->userId);
$this->assertSame('John', $update->user->firstName);
$this->assertSame('Doe', $update->user->lastName);
$this->assertSame(1678886400000, $update->mutedUntil);
$this->assertSame('ru-ru', $update->userLocale);
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogRemovedUpdate;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(DialogRemovedUpdate::class)]
#[UsesClass(UserWithPhoto::class)]
final class DialogRemovedUpdateTest extends TestCase
{
#[Test]
public function canBeCreatedFromArray(): void
{
$data = [
'update_type' => UpdateType::DialogRemoved->value,
'timestamp' => 1678886400000,
'chat_id' => 123,
'user' => [
'user_id' => 123,
'first_name' => 'John',
'last_name' => 'Doe',
'is_bot' => false,
'last_activity_time' => 1678886400000,
],
'user_locale' => 'ru-ru',
];
$update = DialogRemovedUpdate::fromArray($data);
$this->assertInstanceOf(DialogRemovedUpdate::class, $update);
$this->assertSame(UpdateType::DialogRemoved, $update->updateType);
$this->assertSame(123, $update->user->userId);
$this->assertSame('John', $update->user->firstName);
$this->assertSame('Doe', $update->user->lastName);
$this->assertSame('ru-ru', $update->userLocale);
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\DialogUnmutedUpdate;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(DialogUnmutedUpdate::class)]
#[UsesClass(UserWithPhoto::class)]
final class DialogUnmutedUpdateTest extends TestCase
{
#[Test]
public function canBeCreatedFromArray(): void
{
$data = [
'update_type' => UpdateType::DialogUnmuted->value,
'timestamp' => 1678886400000,
'chat_id' => 123,
'user' => [
'user_id' => 123,
'first_name' => 'John',
'last_name' => 'Doe',
'is_bot' => false,
'last_activity_time' => 1678886400000,
],
'user_locale' => 'ru-ru',
];
$update = DialogUnmutedUpdate::fromArray($data);
$this->assertInstanceOf(DialogUnmutedUpdate::class, $update);
$this->assertSame(UpdateType::DialogUnmuted, $update->updateType);
$this->assertSame(123, $update->user->userId);
$this->assertSame('John', $update->user->firstName);
$this->assertSame('Doe', $update->user->lastName);
$this->assertSame('ru-ru', $update->userLocale);
}
}
@@ -10,7 +10,7 @@ use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\Recipient;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCallbackUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -21,7 +21,7 @@ use PHPUnit\Framework\TestCase;
#[UsesClass(Message::class)]
#[UsesClass(MessageBody::class)]
#[UsesClass(Recipient::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class MessageCallbackUpdateTest extends TestCase
{
#[Test]
@@ -8,7 +8,7 @@ use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\Recipient;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageEditedUpdate;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
@@ -19,7 +19,7 @@ use PHPUnit\Framework\TestCase;
#[UsesClass(Message::class)]
#[UsesClass(MessageBody::class)]
#[UsesClass(Recipient::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class MessageEditedUpdateTest extends TestCase
{
#[Test]
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\UserAddedToChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(UserAddedToChatUpdate::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class UserAddedToChatUpdateTest extends TestCase
{
#[Test]
@@ -30,6 +30,9 @@ final class UserAddedToChatUpdateTest extends TestCase
'is_bot' => false,
'last_activity_time' => 1680000000,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'inviter_id' => 202,
'is_channel' => false,
@@ -42,7 +45,7 @@ final class UserAddedToChatUpdateTest extends TestCase
$this->assertSame(12345, $update->chatId);
$this->assertSame(202, $update->inviterId);
$this->assertFalse($update->isChannel);
$this->assertInstanceOf(User::class, $update->user);
$this->assertInstanceOf(UserWithPhoto::class, $update->user);
$this->assertSame(101, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
@@ -61,6 +64,9 @@ final class UserAddedToChatUpdateTest extends TestCase
'is_bot' => false,
'last_activity_time' => 1680000001,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'inviter_id' => null,
'is_channel' => true,
@@ -6,14 +6,14 @@ namespace BushlanovDev\MaxMessengerBot\Tests\Models\Updates;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Models\Updates\UserRemovedFromChatUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(UserRemovedFromChatUpdate::class)]
#[UsesClass(User::class)]
#[UsesClass(UserWithPhoto::class)]
final class UserRemovedFromChatUpdateTest extends TestCase
{
#[Test]
@@ -30,6 +30,9 @@ final class UserRemovedFromChatUpdateTest extends TestCase
'is_bot' => false,
'last_activity_time' => 1681000000,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'admin_id' => 222,
'is_channel' => false,
@@ -42,7 +45,7 @@ final class UserRemovedFromChatUpdateTest extends TestCase
$this->assertSame(98765, $update->chatId);
$this->assertSame(222, $update->adminId);
$this->assertFalse($update->isChannel);
$this->assertInstanceOf(User::class, $update->user);
$this->assertInstanceOf(UserWithPhoto::class, $update->user);
$this->assertSame(111, $update->user->userId);
$this->assertEquals($data, $update->toArray());
}
@@ -61,6 +64,9 @@ final class UserRemovedFromChatUpdateTest extends TestCase
'is_bot' => false,
'last_activity_time' => 1681000001,
'username' => null,
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
],
'admin_id' => null,
'is_channel' => true,
+12 -6
View File
@@ -4,18 +4,21 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
#[CoversClass(User::class)]
#[CoversClass(UserWithPhoto::class)]
final class UserTest extends TestCase
{
#[Test]
public function canBeCreatedFromArray(): void
{
$data = [
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
'user_id' => 123,
'first_name' => 'John',
'last_name' => 'Doe',
@@ -24,9 +27,9 @@ final class UserTest extends TestCase
'last_activity_time' => 1678886400000,
];
$sender = User::fromArray($data);
$sender = UserWithPhoto::fromArray($data);
$this->assertInstanceOf(User::class, $sender);
$this->assertInstanceOf(UserWithPhoto::class, $sender);
$this->assertSame($data['user_id'], $sender->userId);
$this->assertSame($data['first_name'], $sender->firstName);
$this->assertSame($data['last_name'], $sender->lastName);
@@ -44,15 +47,18 @@ final class UserTest extends TestCase
public function canBeCreatedFromArrayWithOptionalDataNull(): void
{
$data = [
'description' => null,
'avatar_url' => null,
'full_avatar_url' => null,
'user_id' => 123,
'first_name' => 'John',
'is_bot' => false,
'last_activity_time' => 1678886400000,
];
$sender = User::fromArray($data);
$sender = UserWithPhoto::fromArray($data);
$this->assertInstanceOf(User::class, $sender);
$this->assertInstanceOf(UserWithPhoto::class, $sender);
$this->assertSame($data['user_id'], $sender->userId);
$this->assertSame($data['first_name'], $sender->firstName);
$this->assertNull($sender->lastName);
+3 -3
View File
@@ -16,15 +16,15 @@ final class UserWithPhotoTest extends TestCase
public function canBeCreatedFromArray(): void
{
$data = [
'description' => 'Description',
'avatar_url' => 'https://example.com/avatar.jpg',
'full_avatar_url' => 'https://example.com/full_avatar.jpg',
'user_id' => 123,
'first_name' => 'John',
'last_name' => 'Doe',
'username' => 'johndoe',
'is_bot' => false,
'last_activity_time' => 1678886400000,
'description' => 'Description',
'avatar_url' => 'https://example.com/avatar.jpg',
'full_avatar_url' => 'https://example.com/full_avatar.jpg',
];
$user = UserWithPhoto::fromArray($data);
+7 -5
View File
@@ -12,15 +12,17 @@ use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\Recipient;
use BushlanovDev\MaxMessengerBot\Models\Updates\BotStartedUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate;
use BushlanovDev\MaxMessengerBot\Models\User;
use BushlanovDev\MaxMessengerBot\Models\UserWithPhoto;
use BushlanovDev\MaxMessengerBot\UpdateDispatcher;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(UpdateDispatcher::class)]
#[UsesClass(User::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(UserWithPhoto::class)]
#[UsesClass(BotStartedUpdate::class)]
#[UsesClass(Message::class)]
#[UsesClass(MessageBody::class)]
@@ -42,7 +44,7 @@ final class UpdateDispatcherTest extends TestCase
{
$wasCalled = false;
$user = new User(100, 'Test', 'User', 'testuser', false, time());
$user = new UserWithPhoto(100, 'Test', 'User', 'testuser', false, time(), null, null, null);
$update = new BotStartedUpdate(time(), 12345, $user, null, 'ru-RU');
$this->dispatcher->addHandler(
@@ -66,7 +68,7 @@ final class UpdateDispatcherTest extends TestCase
$messageHandlerCalled = false;
$messageBody = new MessageBody('mid1', 1, '/start with args', null, null);
$sender = new User(101, 'Cmd', 'Sender', 'cmdsender', false, time());
$sender = new UserWithPhoto(101, 'Cmd', 'Sender', 'cmdsender', false, time(), null, null, null);
$recipient = new Recipient(ChatType::Dialog, 101, null);
$message = new Message(time(), $recipient, $messageBody, $sender, null, null, null);
$update = new MessageCreatedUpdate(time(), $message, 'ru-RU');
@@ -96,7 +98,7 @@ final class UpdateDispatcherTest extends TestCase
$messageHandlerCalled = false;
$messageBody = new MessageBody('mid2', 2, 'Hello world', null, null);
$sender = new User(102, 'Msg', 'Sender', 'msgsender', false, time());
$sender = new UserWithPhoto(102, 'Msg', 'Sender', 'msgsender', false, time(), null, null, null);
$recipient = new Recipient(ChatType::Dialog, 102, null);
$message = new Message(time(), $recipient, $messageBody, $sender, null, null, null);
$update = new MessageCreatedUpdate(time(), $message, 'en-US');
+41
View File
@@ -19,6 +19,7 @@ use BushlanovDev\MaxMessengerBot\UpdateDispatcher;
use BushlanovDev\MaxMessengerBot\WebhookHandler;
use LogicException;
use phpmock\phpunit\PHPMock;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\PreserveGlobalState;
@@ -32,6 +33,7 @@ use Psr\Http\Message\StreamInterface;
use Psr\Log\LoggerInterface;
#[CoversClass(WebhookHandler::class)]
#[AllowMockObjectsWithoutExpectations]
#[UsesClass(UpdateDispatcher::class)]
#[UsesClass(Message::class)]
#[UsesClass(MessageBody::class)]
@@ -185,4 +187,43 @@ final class WebhookHandlerTest extends TestCase
$handler = new WebhookHandler($this->dispatcher, $this->modelFactoryMock, $this->loggerMock, null);
$handler->handle(null);
}
#[Test]
public function handleCatchesAndLogsLogicExceptionFromModelFactory(): void
{
$payload = '{"update_type":"unknown_type","timestamp":123}';
$updateData = json_decode($payload, true);
$exception = new LogicException('Unknown or unsupported update type received: unknown_type');
$request = $this->createMockRequest($payload, self::SECRET);
$this->modelFactoryMock->expects($this->once())
->method('createUpdate')
->with($updateData)
->willThrowException($exception);
$callIndex = 0;
$this->loggerMock->expects($this->exactly(2))
->method('debug')
->willReturnCallback(
function (string $message, array $context = []) use (&$callIndex, $payload, $exception) {
if ($callIndex === 0) {
$this->assertSame('Received webhook payload', $message);
$this->assertArrayHasKey('body', $context);
$this->assertSame($payload, $context['body']);
} elseif ($callIndex === 1) {
$this->assertSame('Unknown or unsupported update type received: unknown_type', $message);
$this->assertArrayHasKey('payload', $context);
$this->assertArrayHasKey('exception', $context);
$this->assertSame($payload, $context['payload']);
$this->assertSame($exception, $context['exception']);
}
$callIndex++;
}
);
$handler = new WebhookHandler($this->dispatcher, $this->modelFactoryMock, $this->loggerMock, self::SECRET);
$handler->handle($request);
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
error_reporting(E_ALL & ~E_DEPRECATED);
require_once dirname(__DIR__) . '/vendor/autoload.php';