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; } }