Laravel support tests

This commit is contained in:
Alex
2025-08-11 20:48:04 +03:00
parent 3b242c1186
commit b37e7b6a73
9 changed files with 935 additions and 11 deletions
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
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\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Tester\CommandTester;
#[CoversClass(PollingStartCommand::class)]
#[UsesClass(MaxBotManager::class)]
final class PollingStartCommandTest extends TestCase
{
private MockObject&MaxBotManager $botManagerMock;
private PollingStartCommand $command;
protected function setUp(): void
{
parent::setUp();
$this->botManagerMock = $this->createMock(MaxBotManager::class);
$this->container->instance(MaxBotManager::class, $this->botManagerMock);
$this->container->alias(MaxBotManager::class, 'maxbot.manager');
$this->command = new PollingStartCommand();
$this->command->setLaravel($this->container);
$application = new ConsoleApplication();
$application->add($this->command);
$commandInApp = $application->find('maxbot:polling:start');
$this->tester = new CommandTester($commandInApp);
}
#[Test]
public function handleSuccessfullyCallsManagerWithCustomTimeout(): void
{
$timeout = 60;
$this->botManagerMock
->expects($this->once())
->method('startLongPolling')
->with($timeout);
$this->tester->execute(['--timeout' => $timeout]);
$this->tester->assertCommandIsSuccessful();
$output = $this->tester->getDisplay();
$this->assertStringContainsString("Starting long polling with a timeout of $timeout seconds...", $output);
}
#[Test]
public function handleSuccessfullyUsesDefaultTimeout(): void
{
$defaultTimeout = 90;
$this->botManagerMock
->expects($this->once())
->method('startLongPolling')
->with($defaultTimeout);
$this->tester->execute([]);
$this->tester->assertCommandIsSuccessful();
$output = $this->tester->getDisplay();
$this->assertStringContainsString(
"Starting long polling with a timeout of $defaultTimeout seconds...",
$output
);
}
#[Test]
public function handleCatchesExceptionAndLogsError(): void
{
$exceptionMessage = 'Something went wrong';
$exception = new \RuntimeException($exceptionMessage);
$this->botManagerMock
->expects($this->once())
->method('startLongPolling')
->willThrowException($exception);
Log::shouldReceive('error')
->once()
->with(
"Long polling failed to start or crashed: $exceptionMessage",
['exception' => $exception],
);
$statusCode = $this->tester->execute([]);
$this->assertSame(1, $statusCode, 'Command should return a failure exit code.');
$output = $this->tester->getDisplay();
$this->assertStringContainsString("❌ Long polling failed: $exceptionMessage", $output);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Laravel\Commands;
use Illuminate\Container\Container;
use Illuminate\Support\Facades\Facade;
use PHPUnit\Framework\TestCase as TestCaseOriginal;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Tester\CommandTester;
abstract class TestCase extends TestCaseOriginal
{
protected Container $container;
protected CommandTester $tester;
protected function setUp(): void
{
parent::setUp();
$this->container = new TestApplicationContainer();
Container::setInstance($this->container);
Facade::setFacadeApplication($this->container);
$loggerMock = $this->createMock(LoggerInterface::class);
$this->container->instance('log', $loggerMock);
}
protected function tearDown(): void
{
Container::setInstance(null);
Facade::clearResolvedInstances();
if (class_exists(\Mockery::class)) {
\Mockery::close();
}
parent::tearDown();
}
}
class TestApplicationContainer extends Container
{
public function runningUnitTests(): bool
{
return true;
}
}
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Laravel\Commands;
use BushlanovDev\MaxMessengerBot\Api;
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\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Tester\CommandTester;
#[CoversClass(WebhookListCommand::class)]
#[UsesClass(Api::class)]
#[UsesClass(Subscription::class)]
final class WebhookListCommandTest extends TestCase
{
private MockObject&Api $apiMock;
private WebhookListCommand $command;
protected function setUp(): void
{
parent::setUp();
$this->apiMock = $this->createMock(Api::class);
$this->container->instance(Api::class, $this->apiMock);
$this->command = new WebhookListCommand();
$this->command->setLaravel($this->container);
$application = new ConsoleApplication();
$application->add($this->command);
$commandInApp = $application->find('maxbot:webhook:list');
$this->tester = new CommandTester($commandInApp);
}
#[Test]
public function handleDisplaysTableWithActiveSubscriptions(): void
{
$timestamp = 1678886400; // 2023-03-15 13:20:00 UTC
$subscriptions = [
new Subscription(
'https://example.com/hook1',
$timestamp,
[UpdateType::MessageCreated, UpdateType::BotStarted],
'0.0.6'
),
new Subscription(
'https://example.com/hook2',
$timestamp + 3600,
null, // Should be rendered as 'all'
'0.0.6'
),
];
$this->apiMock
->expects($this->once())
->method('getSubscriptions')
->willReturn($subscriptions);
$this->tester->execute([]);
$this->tester->assertCommandIsSuccessful();
$output = $this->tester->getDisplay();
$this->assertStringContainsString('https://example.com/hook1', $output);
$this->assertStringContainsString('https://example.com/hook2', $output);
$this->assertStringContainsString('message_created, bot_started', $output);
$this->assertStringContainsString('all', $output);
$this->assertStringContainsString(date('Y-m-d H:i:s', $timestamp), $output);
$this->assertStringContainsString(date('Y-m-d H:i:s', $timestamp + 3600), $output);
}
#[Test]
public function handleDisplaysMessageWhenNoSubscriptionsExist(): void
{
$this->apiMock
->expects($this->once())
->method('getSubscriptions')
->willReturn([]);
$this->tester->execute([]);
$this->tester->assertCommandIsSuccessful();
$output = $this->tester->getDisplay();
$this->assertStringContainsString('No active webhook subscriptions found.', $output);
$this->assertStringNotContainsString('URL', $output, 'Table headers should not be displayed.');
}
#[Test]
public function handleCatchesExceptionAndLogsError(): void
{
$exceptionMessage = 'API is down';
$exception = new \RuntimeException($exceptionMessage);
$this->apiMock
->expects($this->once())
->method('getSubscriptions')
->willThrowException($exception);
Log::shouldReceive('error')
->once()
->with("Webhook list error: $exceptionMessage", ['exception' => $exception]);
$statusCode = $this->tester->execute([]);
$this->assertSame(1, $statusCode, 'Command should return a failure exit code.');
$output = $this->tester->getDisplay();
$this->assertStringContainsString("❌ Webhook list error: $exceptionMessage", $output);
}
}
@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Laravel\Commands;
use BushlanovDev\MaxMessengerBot\Api;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
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\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Tester\CommandTester;
#[CoversClass(WebhookSubscribeCommand::class)]
#[UsesClass(Api::class)]
#[UsesClass(Result::class)]
final class WebhookSubscribeCommandTest extends TestCase
{
private MockObject&Api $apiMock;
private MockObject&Config $configMock;
private WebhookSubscribeCommand $command;
protected function setUp(): void
{
parent::setUp();
$this->apiMock = $this->createMock(Api::class);
$this->configMock = $this->createMock(Config::class);
$this->container->instance(Api::class, $this->apiMock);
$this->container->instance(Config::class, $this->configMock);
$this->command = new WebhookSubscribeCommand();
$this->command->setLaravel($this->container);
$application = new ConsoleApplication();
$application->add($this->command);
$commandInApp = $application->find('maxbot:webhook:subscribe');
$this->tester = new CommandTester($commandInApp);
}
#[Test]
public function handleSuccessfullySubscribesWithAllOptions(): void
{
$url = 'https://example.com/webhook';
$secret = 'my-super-secret';
$types = ['message_created', 'bot_started'];
$expectedUpdateTypes = [UpdateType::MessageCreated, UpdateType::BotStarted];
$this->apiMock
->expects($this->once())
->method('subscribe')
->with($url, $secret, $expectedUpdateTypes)
->willReturn(new Result(true, null));
$this->tester->execute([
'url' => $url,
'--secret' => $secret,
'--types' => $types,
]);
$this->tester->assertCommandIsSuccessful();
$output = $this->tester->getDisplay();
$this->assertStringContainsString('✅ Successfully subscribed to webhook!', $output);
$this->assertStringContainsString("URL: $url", $output);
$this->assertStringContainsString("Secret: ***************", $output);
$this->assertStringContainsString("Update types: message_created, bot_started", $output);
}
#[Test]
public function handleUsesSecretFromConfigWhenOptionIsNotProvided(): void
{
$url = 'https://example.com/webhook';
$configSecret = 'secret-from-config';
$this->configMock
->expects($this->once())
->method('get')
->with('maxbot.webhook_secret')
->willReturn($configSecret);
$this->apiMock
->expects($this->once())
->method('subscribe')
->with($url, $configSecret, null)
->willReturn(new Result(true, null));
$this->tester->execute(['url' => $url]);
$this->tester->assertCommandIsSuccessful();
}
#[Test]
public function handleFailsForInvalidUrl(): void
{
$this->apiMock->expects($this->never())->method('subscribe');
$statusCode = $this->tester->execute(['url' => 'not-a-valid-url']);
$this->assertSame(1, $statusCode);
$this->assertStringContainsString('Invalid URL provided.', $this->tester->getDisplay());
}
#[Test]
public function handleFailsForInvalidUpdateType(): void
{
$this->apiMock->expects($this->never())->method('subscribe');
$statusCode = $this->tester->execute([
'url' => 'https://example.com',
'--types' => ['message_created', 'invalid_type'],
]);
$this->assertSame(1, $statusCode);
$this->assertStringContainsString('Invalid update type: invalid_type', $this->tester->getDisplay());
}
#[Test]
public function handleDisplaysApiErrorMessageOnFailure(): void
{
$url = 'https://example.com/webhook';
$apiErrorMessage = 'URL is already subscribed';
$this->apiMock
->expects($this->once())
->method('subscribe')
->willReturn(new Result(false, $apiErrorMessage));
$statusCode = $this->tester->execute(['url' => $url]);
$this->assertSame(1, $statusCode);
$output = $this->tester->getDisplay();
$this->assertStringContainsString('❌ Failed to subscribe to webhook.', $output);
$this->assertStringContainsString("Response: $apiErrorMessage", $output);
}
#[Test]
public function handleCatchesExceptionAndLogsError(): void
{
$url = 'https://example.com/webhook';
$exceptionMessage = 'Network error';
$exception = new \RuntimeException($exceptionMessage);
$this->apiMock
->expects($this->once())
->method('subscribe')
->willThrowException($exception);
Log::shouldReceive('error')
->once()
->with("Webhook subscription error: $exceptionMessage", ['exception' => $exception]);
$statusCode = $this->tester->execute(['url' => $url]);
$this->assertSame(1, $statusCode);
$output = $this->tester->getDisplay();
$this->assertStringContainsString("❌ Webhook subscription error: $exceptionMessage", $output);
}
}
@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Laravel\Commands;
use BushlanovDev\MaxMessengerBot\Api;
use BushlanovDev\MaxMessengerBot\Laravel\Commands\WebhookUnsubscribeCommand;
use BushlanovDev\MaxMessengerBot\Models\Result;
use Illuminate\Support\Facades\Log;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Tester\CommandTester;
#[CoversClass(WebhookUnsubscribeCommand::class)]
#[UsesClass(Api::class)]
#[UsesClass(Result::class)]
final class WebhookUnsubscribeCommandTest extends TestCase
{
private MockObject&Api $apiMock;
private WebhookUnsubscribeCommand $command;
protected function setUp(): void
{
parent::setUp();
$this->apiMock = $this->createMock(Api::class);
$this->container->instance(Api::class, $this->apiMock);
$this->command = new WebhookUnsubscribeCommand();
$this->command->setLaravel($this->container);
$application = new ConsoleApplication();
$application->add($this->command);
$commandInApp = $application->find('maxbot:webhook:unsubscribe');
$this->tester = new CommandTester($commandInApp);
}
#[Test]
public function handleSuccessfullyUnsubscribesWithConfirmationFlag(): void
{
$url = 'https://example.com/webhook';
$this->apiMock
->expects($this->once())
->method('unsubscribe')
->with($url)
->willReturn(new Result(true, null));
$this->tester->execute([
'url' => $url,
'--confirm' => true,
]);
$this->tester->assertCommandIsSuccessful();
$output = $this->tester->getDisplay();
$this->assertStringContainsString('✅ Successfully unsubscribed from webhook!', $output);
}
#[Test]
public function handleCancelsWhenNotConfirmed(): void
{
$url = 'https://example.com/webhook';
$this->apiMock->expects($this->never())->method('unsubscribe');
$this->tester->setInputs(['no']);
$this->tester->execute(['url' => $url]);
$this->tester->assertCommandIsSuccessful();
$output = $this->tester->getDisplay();
$this->assertStringContainsString('Are you sure you want to unsubscribe', $output);
$this->assertStringContainsString('Operation cancelled.', $output);
}
#[Test]
public function handleFailsForInvalidUrl(): void
{
$this->apiMock->expects($this->never())->method('unsubscribe');
$statusCode = $this->tester->execute(['url' => 'not-a-valid-url']);
$this->assertSame(1, $statusCode);
$this->assertStringContainsString('Invalid URL provided.', $this->tester->getDisplay());
}
#[Test]
public function handleDisplaysApiErrorMessageOnFailure(): void
{
$url = 'https://example.com/webhook';
$apiErrorMessage = 'Subscription not found';
$this->apiMock
->expects($this->once())
->method('unsubscribe')
->willReturn(new Result(false, $apiErrorMessage));
$statusCode = $this->tester->execute(['url' => $url, '--confirm' => true]);
$this->assertSame(1, $statusCode);
$output = $this->tester->getDisplay();
$this->assertStringContainsString('❌ Failed to unsubscribe from webhook.', $output);
$this->assertStringContainsString("Response: $apiErrorMessage", $output);
}
#[Test]
public function handleCatchesExceptionAndLogsError(): void
{
$url = 'https://example.com/webhook';
$exceptionMessage = 'API connection refused';
$exception = new \RuntimeException($exceptionMessage);
$this->apiMock
->expects($this->once())
->method('unsubscribe')
->willThrowException($exception);
Log::shouldReceive('error')
->once()
->with("Webhook unsubscribe error: $exceptionMessage", ['exception' => $exception]);
$statusCode = $this->tester->execute(['url' => $url, '--confirm' => true]);
$this->assertSame(1, $statusCode);
$output = $this->tester->getDisplay();
$this->assertStringContainsString("❌ Webhook unsubscribe error: $exceptionMessage", $output);
}
}
+340
View File
@@ -0,0 +1,340 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Laravel;
use BushlanovDev\MaxMessengerBot\Api;
use BushlanovDev\MaxMessengerBot\Enums\ChatType;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Laravel\MaxBotManager;
use BushlanovDev\MaxMessengerBot\ModelFactory;
use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\Recipient;
use BushlanovDev\MaxMessengerBot\Models\Updates\AbstractUpdate;
use BushlanovDev\MaxMessengerBot\Models\Updates\MessageCreatedUpdate;
use BushlanovDev\MaxMessengerBot\UpdateDispatcher;
use BushlanovDev\MaxMessengerBot\WebhookHandler;
use Illuminate\Container\Container;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Facade;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
#[CoversClass(MaxBotManager::class)]
#[UsesClass(Message::class)]
#[UsesClass(MessageBody::class)]
#[UsesClass(Recipient::class)]
#[UsesClass(AbstractUpdate::class)]
#[UsesClass(MessageCreatedUpdate::class)]
#[UsesClass(UpdateDispatcher::class)]
#[UsesClass(WebhookHandler::class)]
final class MaxBotManagerTest extends TestCase
{
private Container $container;
private MockObject&Api $apiMock;
private MockObject&ModelFactory $modelFactoryMock;
private UpdateDispatcher $updateDispatcher;
private MaxBotManager $manager;
protected function setUp(): void
{
parent::setUp();
$this->container = new Container();
Container::setInstance($this->container);
Facade::setFacadeApplication($this->container);
$this->container->singleton('config', function ($app) {
return $app->make(Repository::class);
});
$loggerMock = $this->createMock(LoggerInterface::class);
$this->container->instance(LoggerInterface::class, $loggerMock);
$this->container->instance('log', $loggerMock);
$this->apiMock = $this->createMock(Api::class);
$this->modelFactoryMock = $this->createMock(ModelFactory::class);
$this->updateDispatcher = new UpdateDispatcher($this->apiMock);
$this->container->instance(Api::class, $this->apiMock);
$this->container->instance('maxbot', $this->apiMock);
$this->container->instance(ModelFactory::class, $this->modelFactoryMock);
$this->container->instance(UpdateDispatcher::class, $this->updateDispatcher);
$this->container->singleton(MaxBotManager::class, function ($app) {
return new MaxBotManager(
$app,
$app->make(Api::class),
$app->make(UpdateDispatcher::class),
);
});
$this->manager = $this->container->make(MaxBotManager::class);
Handlers::reset();
}
protected function tearDown(): void
{
Container::setInstance(null);
Facade::clearResolvedInstances();
parent::tearDown();
}
#[Test]
public function handleWebhookReturns200OnSuccess(): void
{
$webhookHandler = new WebhookHandler(
$this->updateDispatcher,
$this->modelFactoryMock,
$this->container->make(\Psr\Log\LoggerInterface::class),
null,
);
$this->container->instance(WebhookHandler::class, $webhookHandler);
$request = Request::create('/webhook', 'POST', content: '{"update_type":"message_created"}');
$realUpdate = new MessageCreatedUpdate(
time(),
$this->createMinimalMessage(),
'ru-RU',
);
$this->modelFactoryMock->method('createUpdate')->willReturn($realUpdate);
$wasDispatched = false;
$this->updateDispatcher->addHandler(UpdateType::MessageCreated, function () use (&$wasDispatched) {
$wasDispatched = true;
});
$response = $this->manager->handleWebhook($request);
$this->assertSame(200, $response->getStatusCode());
$this->assertTrue($wasDispatched, 'The update was not dispatched correctly.');
}
#[Test]
public function handleWebhookReturns403OnSecurityException(): void
{
$webhookHandler = new WebhookHandler(
$this->updateDispatcher,
$this->modelFactoryMock,
$this->container->make(\Psr\Log\LoggerInterface::class),
'real-secret',
);
$this->container->instance(WebhookHandler::class, $webhookHandler);
$request = Request::create('/webhook', 'POST', content: '{}');
$request->headers->set('X-Max-Bot-Api-Secret', 'wrong-secret');
$response = $this->manager->handleWebhook($request);
$this->assertSame(403, $response->getStatusCode());
$this->assertJsonStringEqualsJsonString('{"status":"error","message":"Forbidden"}', $response->getContent());
}
#[Test]
public function handleWebhookReturns400OnSerializationException(): void
{
$webhookHandler = new WebhookHandler(
$this->updateDispatcher,
$this->modelFactoryMock,
$this->container->make(\Psr\Log\LoggerInterface::class),
null
);
$this->container->instance(WebhookHandler::class, $webhookHandler);
$request = Request::create('/webhook', 'POST', content: '{invalid-json');
$response = $this->manager->handleWebhook($request);
$this->assertSame(400, $response->getStatusCode());
$this->assertJsonStringEqualsJsonString('{"status":"error","message":"Bad Request"}', $response->getContent());
}
#[Test]
public function handleWebhookReturns500OnGenericException(): void
{
$webhookHandler = new WebhookHandler(
$this->updateDispatcher,
$this->modelFactoryMock,
$this->container->make(\Psr\Log\LoggerInterface::class),
null
);
$this->container->instance(WebhookHandler::class, $webhookHandler);
$this->modelFactoryMock->method('createUpdate')->willThrowException(new \Exception('DB error'));
$request = Request::create('/webhook', 'POST', content: '{"update_type":"message_created"}');
$response = $this->manager->handleWebhook($request);
$this->assertSame(500, $response->getStatusCode());
$this->assertJsonStringEqualsJsonString(
'{"status":"error","message":"Internal Server Error"}',
$response->getContent(),
);
}
#[Test]
public function resolveHandlerCanResolveCallable(): void
{
$wasCalled = false;
$callable = function () use (&$wasCalled) {
$wasCalled = true;
};
$this->manager->onCommand('test', $callable);
$dispatcher = $this->manager->getDispatcher();
$messageBody = new MessageBody('mid.cmd', 1, 'test', [], []);
$message = new Message(time(), new Recipient(ChatType::Dialog, 1, null), $messageBody, null, null, null, null);
$update = new MessageCreatedUpdate(time(), $message, null);
$dispatcher->dispatch($update);
$this->assertTrue($wasCalled, 'The resolved callable handler was not called.');
}
#[Test]
public function resolveHandlerResolvesClassWithHandleMethod(): void
{
$this->manager->onCommand('test', TestHandlerWithHandleMethod::class);
$this->dispatchCommand('test');
$this->assertTrue(Handlers::$wasCalled, 'Handler with handle() method was not resolved and called.');
}
#[Test]
public function resolveHandlerResolvesInvokableClassFromContainer(): void
{
$this->container->bind(TestHandlerWithInvokeMethod::class);
$this->manager->onCommand('test', TestHandlerWithInvokeMethod::class);
$this->dispatchCommand('test');
$this->assertTrue(Handlers::$wasCalled, 'Invokable handler was not resolved and called.');
}
#[Test]
public function resolveHandlerResolvesClassAtMethodString(): void
{
$this->manager->onCommand('test', TestHandlerWithCustomMethod::class . '@custom');
$this->dispatchCommand('test');
$this->assertTrue(Handlers::$wasCalled, 'Handler with "Class@method" string was not resolved and called.');
}
#[Test]
public function resolveHandlerResolvesBoundClassWithHandleMethod(): void
{
// Шаг 1: Явно регистрируем класс обработчика в контейнере.
// Это гарантирует, что будет выбрана ветка `if ($this->container->bound($handler))`.
$this->container->bind(TestHandlerWithHandleMethod::class);
// Шаг 2: Регистрируем обработчик, используя его имя класса (строку).
$this->manager->onCommand('test', TestHandlerWithHandleMethod::class);
// Шаг 3: Диспетчеризуем команду, которая вызовет обработчик.
$this->dispatchCommand('test');
// Шаг 4: Убеждаемся, что метод `handle` был вызван.
$this->assertTrue(
Handlers::$wasCalled,
'Bound handler with handle() method was not resolved and called via the bound path.'
);
}
#[Test]
public function resolveHandlerThrowsExceptionForUnresolvableString(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Unable to resolve handler: NonExistentClass');
$this->manager->onCommand('test', 'NonExistentClass');
}
#[Test]
public function resolveHandlerThrowsExceptionForClassWithoutHandleOrInvoke(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage(
"Handler class '" . Handlers::class . "' is not callable and doesn't have a handle method."
);
$this->container->bind(Handlers::class);
$this->manager->onCommand('test', Handlers::class);
}
/**
* Helper to dispatch a command to the real UpdateDispatcher.
*/
private function dispatchCommand(string $commandText): void
{
$dispatcher = $this->manager->getDispatcher();
$messageBody = new MessageBody('mid.cmd', 1, $commandText, [], []);
$message = new Message(time(), new Recipient(ChatType::Dialog, 1, null), $messageBody, null, null, null, null);
$update = new MessageCreatedUpdate(time(), $message, null);
$dispatcher->dispatch($update);
}
private function createMinimalMessage(): Message
{
return new Message(
time(),
new Recipient(ChatType::Dialog, 1, null),
new MessageBody('mid.1', 1, 'test', [], []),
null,
null,
null,
null,
);
}
}
class Handlers
{
public static bool $wasCalled = false;
public static function reset(): void
{
self::$wasCalled = false;
}
}
class TestHandlerWithHandleMethod
{
public function handle(MessageCreatedUpdate $update, Api $api): void
{
Handlers::$wasCalled = true;
}
}
class TestHandlerWithInvokeMethod
{
public function __invoke(MessageCreatedUpdate $update, Api $api): void
{
Handlers::$wasCalled = true;
}
}
class TestHandlerWithCustomMethod
{
public function custom(MessageCreatedUpdate $update, Api $api): void
{
Handlers::$wasCalled = true;
}
}