diff --git a/composer.json b/composer.json index f593633..7fc3cc9 100644 --- a/composer.json +++ b/composer.json @@ -24,12 +24,11 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.77", - "illuminate/console": "^12.22", - "illuminate/http": "^12.22", - "illuminate/testing": "^11.0||^12.0", "jaschilz/php-coverage-badger": "^2.0", + "laravel/framework": "^11.0", "mikey179/vfsstream": "^1.6", "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0", "php-mock/php-mock-phpunit": "^2.13", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^12.0", diff --git a/src/Laravel/MaxBotServiceProvider.php b/src/Laravel/MaxBotServiceProvider.php index 8ae6c86..84e789a 100644 --- a/src/Laravel/MaxBotServiceProvider.php +++ b/src/Laravel/MaxBotServiceProvider.php @@ -86,10 +86,6 @@ class MaxBotServiceProvider extends ServiceProvider return new ModelFactory(); }); - $this->app->singleton(UpdateDispatcher::class, function (Application $app) { - return new UpdateDispatcher($app->make(Api::class)); - }); - $this->app->singleton(Api::class, function (Application $app) { /** @var Config $config */ $config = $app->make(Config::class); @@ -106,10 +102,14 @@ class MaxBotServiceProvider extends ServiceProvider $app->make(ClientApiInterface::class), $app->make(ModelFactory::class), $app->make(LoggerInterface::class), - $app->make(UpdateDispatcher::class), + null, ); }); + $this->app->singleton(UpdateDispatcher::class, function (Application $app) { + return $app->make(Api::class)->getUpdateDispatcher(); + }); + $this->app->bind(WebhookHandler::class, function (Application $app) { /** @var Config $config */ $config = $app->make(Config::class); @@ -171,6 +171,7 @@ class MaxBotServiceProvider extends ServiceProvider * Get the services provided by the provider. * * @return array + * @codeCoverageIgnore */ public function provides(): array { diff --git a/tests/Laravel/MaxBotServiceProviderTest.php b/tests/Laravel/MaxBotServiceProviderTest.php new file mode 100644 index 0000000..d05b064 --- /dev/null +++ b/tests/Laravel/MaxBotServiceProviderTest.php @@ -0,0 +1,266 @@ +set('maxbot.access_token', 'test-token'); + $app['config']->set('maxbot.webhook_secret', 'test-secret'); + $app['config']->set('maxbot.base_url', 'https://test.max.ru'); + $app['config']->set('maxbot.api_version', 'test-version'); + } + + protected function getPackageProviders($app): array + { + return [MaxBotServiceProvider::class]; + } + + #[Test] + public function serviceProviderIsLoaded(): void + { + $this->assertInstanceOf(MaxBotServiceProvider::class, $this->app->getProvider(MaxBotServiceProvider::class)); + } + + #[Test] + public function itThrowsExceptionWhenAccessTokenIsMissingForClient(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Max Bot access token is not configured. Please set MAXBOT_ACCESS_TOKEN in your .env file.' + ); + + $this->app['config']->set('maxbot.access_token', null); + + $this->app->make(ClientApiInterface::class); + } + + #[Test] + public function itThrowsExceptionWhenAccessTokenIsMissingForApi(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Max Bot access token is not configured. Please set MAXBOT_ACCESS_TOKEN in your .env file.' + ); + + $this->app['config']->set('maxbot.access_token', null); + + $this->app->make(Api::class); + } + + #[Test] + #[RunInSeparateProcess] + #[PreserveGlobalState(false)] + public function itThrowsExceptionWhenGuzzleIsMissing(): void + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage( + 'Guzzle HTTP client is required. Please run "composer require guzzlehttp/guzzle".' + ); + + $classExistsMock = $this->getFunctionMock('BushlanovDev\\MaxMessengerBot\\Laravel', 'class_exists'); + $classExistsMock->expects($this->once())->with(\GuzzleHttp\Client::class)->willReturn(false); + + (new MaxBotServiceProvider($this->app))->register(); + + $this->app->make(ClientApiInterface::class); + } + + + /** + * @return array + */ + public static function servicesProvider(): array + { + return [ + 'Api::class' => [Api::class, Api::class], + 'maxbot alias' => ['maxbot', Api::class], + 'maxbot.api alias' => ['maxbot.api', Api::class], + 'ClientApiInterface::class' => [ClientApiInterface::class, Client::class], + 'maxbot.client alias' => ['maxbot.client', Client::class], + 'ModelFactory::class' => [ModelFactory::class, ModelFactory::class], + 'UpdateDispatcher::class' => [UpdateDispatcher::class, UpdateDispatcher::class], + 'maxbot.dispatcher alias' => ['maxbot.dispatcher', UpdateDispatcher::class], + 'WebhookHandler::class' => [WebhookHandler::class, WebhookHandler::class], + 'maxbot.webhook alias' => ['maxbot.webhook', WebhookHandler::class], + 'LongPollingHandler::class' => [LongPollingHandler::class, LongPollingHandler::class], + 'maxbot.polling alias' => ['maxbot.polling', LongPollingHandler::class], + 'MaxBotManager::class' => [MaxBotManager::class, MaxBotManager::class], + 'maxbot.manager alias' => ['maxbot.manager', MaxBotManager::class], + ]; + } + + #[Test] + #[DataProvider('servicesProvider')] + public function allServicesAreRegisteredCorrectly(string $service, string $expectedClass): void + { + $this->assertInstanceOf($expectedClass, $this->app->make($service)); + } + + /** + * @return array + */ + public static function singletonsProvider(): array + { + return [ + 'Api' => [Api::class], + 'ClientApiInterface' => [ClientApiInterface::class], + 'ModelFactory' => [ModelFactory::class], + 'UpdateDispatcher' => [UpdateDispatcher::class], + 'MaxBotManager' => [MaxBotManager::class], + ]; + } + + #[Test] + #[DataProvider('singletonsProvider')] + public function servicesAreRegisteredAsSingletons(string $service): void + { + $instance1 = $this->app->make($service); + $instance2 = $this->app->make($service); + + $this->assertSame($instance1, $instance2); + } + + #[Test] + public function clientIsConfiguredCorrectlyFromConfig(): void + { + /** @var Client $client */ + $client = $this->app->make(ClientApiInterface::class); + $this->assertInstanceOf(Client::class, $client); + + $reflection = new \ReflectionClass($client); + + $accessTokenProp = $reflection->getProperty('accessToken'); + $baseUrlProp = $reflection->getProperty('baseUrl'); + $apiVersionProp = $reflection->getProperty('apiVersion'); + + $this->assertSame('test-token', $accessTokenProp->getValue($client)); + $this->assertSame('https://test.max.ru', $baseUrlProp->getValue($client)); + $this->assertSame('test-version', $apiVersionProp->getValue($client)); + } + + #[Test] + public function clientIsConfiguredWithApplicationLoggerWhenLoggingIsEnabled(): void + { + $this->app['config']->set('maxbot.logging.enabled', true); + + $mockLogger = $this->createMock(LoggerInterface::class); + $this->app->instance(LoggerInterface::class, $mockLogger); + + /** @var Client $client */ + $client = $this->app->make(ClientApiInterface::class); + + $reflection = new \ReflectionClass($client); + $loggerProp = $reflection->getProperty('logger'); + $actualLogger = $loggerProp->getValue($client); + + $this->assertSame($mockLogger, $actualLogger); + } + + #[Test] + public function clientIsConfiguredWithNullLoggerWhenLoggingIsDisabled(): void + { + $this->app['config']->set('maxbot.logging.enabled', false); + + /** @var Client $client */ + $client = $this->app->make(ClientApiInterface::class); + + $reflection = new \ReflectionClass($client); + $loggerProp = $reflection->getProperty('logger'); + $actualLogger = $loggerProp->getValue($client); + + $this->assertInstanceOf(NullLogger::class, $actualLogger); + } + + #[Test] + public function webhookHandlerIsConfiguredWithSecretFromConfig(): void + { + /** @var WebhookHandler $handler */ + $handler = $this->app->make(WebhookHandler::class); + $this->assertInstanceOf(WebhookHandler::class, $handler); + + $reflection = new \ReflectionClass($handler); + $secretProp = $reflection->getProperty('secret'); + + $this->assertSame('test-secret', $secretProp->getValue($handler)); + } + + #[Test] + public function apiIsCreatedWithAllDependenciesFromContainer(): void + { + /** @var Api $api */ + $api = $this->app->make(Api::class); + + $reflection = new \ReflectionClass($api); + + $clientProp = $reflection->getProperty('client'); + $factoryProp = $reflection->getProperty('modelFactory'); + $loggerProp = $reflection->getProperty('logger'); + $dispatcherProp = $reflection->getProperty('updateDispatcher'); + + $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->assertSame($this->app->make(UpdateDispatcher::class), $dispatcherProp->getValue($api)); + } + + /** + * @return array + */ + public static function commandsProvider(): array + { + return [ + 'WebhookSubscribeCommand' => [WebhookSubscribeCommand::class, 'maxbot:webhook:subscribe'], + 'WebhookUnsubscribeCommand' => [WebhookUnsubscribeCommand::class, 'maxbot:webhook:unsubscribe'], + 'WebhookListCommand' => [WebhookListCommand::class, 'maxbot:webhook:list'], + 'PollingStartCommand' => [PollingStartCommand::class, 'maxbot:polling:start'], + ]; + } + + #[Test] + #[DataProvider('commandsProvider')] + public function bootMethodRegistersCommandsInConsole(string $class, string $signature): void + { + $commands = Artisan::all(); + $this->assertArrayHasKey($signature, $commands); + $this->assertInstanceOf($class, $commands[$signature]); + } +}