From 6b28c214205d631b4bcdc9906a8ca1aafff103b8 Mon Sep 17 00:00:00 2001 From: tmedvedevv Date: Mon, 15 Sep 2025 04:21:10 +0300 Subject: [PATCH] feat: add metadata oauth button and some fixes --- README.md | 37 ++- examples/oauth_button_actions.php | 52 ++++ ...moCRMOAuthButtonConfigurationException.php | 10 + src/AmoCRM/OAuth/AmoCRMOAuth.php | 99 +++++++- .../AmoCRM/OAuth/AmoCRMOAuthButtonTest.php | 239 ++++++++++++++++++ .../{AmoCRMOAuth.php => AmoCRMOAuthTest.php} | 2 +- 6 files changed, 419 insertions(+), 20 deletions(-) create mode 100644 examples/oauth_button_actions.php create mode 100644 src/AmoCRM/Exceptions/AmoCRMOAuthButtonConfigurationException.php create mode 100644 tests/Cases/AmoCRM/OAuth/AmoCRMOAuthButtonTest.php rename tests/Cases/AmoCRM/OAuth/{AmoCRMOAuth.php => AmoCRMOAuthTest.php} (98%) diff --git a/README.md b/README.md index e97fe183..2b9fb476 100644 --- a/README.md +++ b/README.md @@ -72,22 +72,33 @@ $apiClient->setAccessToken($accessToken) }); ``` -Отправить пользователя на страницу авторизации можно 2мя способами: -1. Отрисовав кнопку на сайт: +Доступные методы отправки на страницу авторизации пользователя +1. Кнопка установки существующей интеграции по client_id ```php -$apiClient->getOAuthClient()->getOAuthButton( - [ - 'title' => 'Установить интеграцию', - 'compact' => true, - 'class_name' => 'className', - 'color' => 'default', - 'error_callback' => 'handleOauthError', - 'state' => $state, - ] - ); +// data-client-id будет взят из API клиента +$apiClient->getOAuthClient()->getOAuthButton([ + 'title' => 'Установить интеграцию', + 'compact' => true, + 'class_name' => 'className', + 'color' => 'default', + 'error_callback' => 'handleOauthError', + 'state' => $state, +]); ``` -2. Отправив пользователя на страницу авторизации +2. Кнопка с метаданными для создания внешней интеграции. Для каждой установки создается отдельная интеграция + +```php +// redirect_uri будет взят из API клиента +$apiClient->getOAuthClient()->getOAuthButton([ + 'is_metadata' => true, // указываем, что передаем метаданные в кнопку. По умолчанию - false + 'secrets_uri' => 'https://your-domain.com/secrets' + 'title' => 'Создать интеграцию', + // ... другие параметры +]); +``` + +3. Прямой редирект на страницу авторизации ```php $authorizationUrl = $apiClient->getOAuthClient()->getAuthorizeUrl([ 'state' => $state, diff --git a/examples/oauth_button_actions.php b/examples/oauth_button_actions.php new file mode 100644 index 00000000..f5bdfb74 --- /dev/null +++ b/examples/oauth_button_actions.php @@ -0,0 +1,52 @@ +getOAuthClient()->getOAuthButton( + [ + 'title' => 'title_tmedvedevv', + 'compact' => false, + 'is_kommo' => true, + 'class_name' => 'classNameTmedvedevv', + 'color' => 'red', + 'mode' => 'popup', + 'error_callback' => 'handleOauthError' + ] +); + +var_dump($buttonByClientId); + +echo "\n=== Генерация кнопки с метаданными, необходимые для создания внешней интеграции ===\n"; + +// Генерируем кнопку с метаданными для внешней интеграции +// client_id из API клиента будет проигнорирован т.к создается новая интеграция в аккаунте +// После установки приходит 2 хука. + +$buttonMetadata = $apiClient->getOAuthClient()->getOAuthButton([ + 'title' => 'title_tmedvedevv', + 'compact' => true, + 'is_kommo' => false, + 'class_name' => 'classNameTmedvedevv', + 'color' => 'red', + 'mode' => 'popup', + 'error_callback' => 'handleOauthError', + 'scopes' => ['crm'], + 'is_metadata' => true, // Указываем, поскольку нужна кнопка с передачей метаданных. По умолчанию false + 'secrets_uri' => $secretsUri // url куда будут отправлены данные по интеграции +]); + +var_dump($buttonMetadata); diff --git a/src/AmoCRM/Exceptions/AmoCRMOAuthButtonConfigurationException.php b/src/AmoCRM/Exceptions/AmoCRMOAuthButtonConfigurationException.php new file mode 100644 index 00000000..e2dac98c --- /dev/null +++ b/src/AmoCRM/Exceptions/AmoCRMOAuthButtonConfigurationException.php @@ -0,0 +1,10 @@ + '#D84315', ]; + /** + * Доступные scopes для кнопки с метаданными, необходимые для создания внешней интеграции. + */ + public const METADATA_BUTTON_AVAILABLE_SCOPES = ['crm', 'notifications']; + protected const REQUEST_TIMEOUT = 15; /** @@ -333,24 +338,38 @@ public function getResourceOwner(AccessTokenInterface $accessToken): ResourceOwn /** * Доступные значения для options: + * string class_name * string title * bool compact - * string class_name * string color * string state * string error_callback + * string mode + * bool is_kommo - Использовать Kommo вместо amoCRM + * bool is_metadata - Создать кнопку с передачей метаданных для установки внешней интеграции + * + * Для кнопки с метаданными: + * string name - Название интеграции + * string secrets_uri - Адрес, куда будет отправлен webhook с client_id, state и секретным ключом интеграции + * string description - Описание интеграции + * array scopes - Запрашиваемые права + * string logo - URL логотипа * * @param array $options * - * @return string - * @throws BadTypeException + * @return string HTML код кнопки + * @throws AmoCRMOAuthButtonConfigurationException */ public function getOAuthButton(array $options = []): string { if (isset($options['color']) && !array_key_exists($options['color'], self::BUTTON_COLORS)) { - throw new BadTypeException('Invalid color selected'); + throw new AmoCRMOAuthButtonConfigurationException( + 'Cannot create OAuth button: Invalid color selected' + ); } + $isMetadata = $options['is_metadata'] ?? false; + $clientId = $this->oauthProvider->getClientId(); $title = $options['title'] ?? 'Установить интеграцию'; $compact = isset($options['compact']) && $options['compact'] ? 'true' : 'false'; $className = $options['class_name'] ?? 'className'; @@ -371,7 +390,8 @@ public function getOAuthButton(array $options = []): string ? 'https://www.kommo.com/auth/button.min.js' : 'https://www.amocrm.ru/auth/button.min.js'; - return '
+ if ($isMetadata === false) { + return '
'; + } + + $secretsUri = $options['secrets_uri'] ?? ''; + $redirectUri = $this->redirectUri; + + // Должны быть заполнены т.к при передаче метаданных после установки приходит два хука + // с данными по самой интеграции и с данными для получения токенов. + if (empty($secretsUri) || empty($redirectUri)) { + throw new AmoCRMOAuthButtonConfigurationException( + 'Cannot create metadata OAuth button: For metadata OAuth button, + both secrets_uri and redirect_uri must be configured' + ); + } + + $description = $options['description'] ?? 'Описание интеграции'; + $name = $options['name'] ?? 'Название интеграции'; + $logo = $options['logo'] ?? 'https://example.com/amocrm_logo.png'; + + if (!isset($options['scopes'])) { + $scopes = self::METADATA_BUTTON_AVAILABLE_SCOPES; + } else { + if (!is_array($options['scopes'])) { + throw new AmoCRMOAuthButtonConfigurationException( + 'scopes parameter must be an array.' + ); + } + + $scopes = $options['scopes']; + } + + $scopes = array_filter($scopes); + + if (empty($scopes)) { + $scopes = self::METADATA_BUTTON_AVAILABLE_SCOPES; + } + + $invalidScopes = array_diff($scopes, self::METADATA_BUTTON_AVAILABLE_SCOPES); + if (!empty($invalidScopes)) { + throw new AmoCRMOAuthButtonConfigurationException(sprintf( + 'Invalid scopes: %s. Available scopes: %s', + implode(', ', $invalidScopes), + implode(', ', self::METADATA_BUTTON_AVAILABLE_SCOPES) + )); + } + + $scopes = implode(',', $scopes); + + return '
+ +
'; } /** diff --git a/tests/Cases/AmoCRM/OAuth/AmoCRMOAuthButtonTest.php b/tests/Cases/AmoCRM/OAuth/AmoCRMOAuthButtonTest.php new file mode 100644 index 00000000..f4bdbbf9 --- /dev/null +++ b/tests/Cases/AmoCRM/OAuth/AmoCRMOAuthButtonTest.php @@ -0,0 +1,239 @@ +oauthClient = new AmoCRMOAuth( + 'test_client_id', + 'test_secret', + 'https://example.com/redirect' + ); + } + + /** + * Проверяем генерацию обычной кнопки с client_id + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testRegularButtonWithClientId(): void + { + $result = $this->oauthClient->getOAuthButton([]); + $this->assertStringContainsString('data-client-id="test_client_id"', $result); + $this->assertStringContainsString('data-title="Установить интеграцию"', $result); + $this->assertStringContainsString('data-class-name="className"', $result); + } + + + /** + * Проверяем кастомные параметры для обычной кнопки + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testRegularButtonWithCustomOptions(): void + { + $result = $this->oauthClient->getOAuthButton([ + 'title' => 'Custom Title', + 'class_name' => 'custom-class', + 'color' => 'red', + 'compact' => false, + 'mode' => 'popup' + ]); + + $this->assertStringContainsString('data-title="Custom Title"', $result); + $this->assertStringContainsString('data-class-name="custom-class"', $result); + $this->assertStringContainsString('data-color="red"', $result); + $this->assertStringContainsString('data-compact="false"', $result); + $this->assertStringContainsString('data-mode="popup"', $result); + } + + /** + * Проверяем ошибку при невалидном цвете + * @return void + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testInvalidColorThrowsException(): void + { + $this->expectException(AmoCRMOAuthButtonConfigurationException::class); + $this->expectExceptionMessage('Invalid color selected'); + $this->oauthClient->getOAuthButton(['color' => 'invalid_color']); + } + + + /** + * Тест 4: Проверяем генерацию metadata кнопки + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testMetadataButtonGeneration(): void + { + $result = $this->oauthClient->getOAuthButton([ + 'is_metadata' => true, + 'name' => 'Test Integration', + 'description' => 'Test Description', + 'scopes' => ['crm', 'notifications'], + 'secrets_uri' => 'https://secrets.com' + ]); + + $this->assertStringNotContainsString('data-client-id', $result); + $this->assertStringContainsString('data-name="Test Integration"', $result); + $this->assertStringContainsString('data-description="Test Description"', $result); + $this->assertStringContainsString('data-scopes="crm,notifications"', $result); + $this->assertStringContainsString('data-secrets_uri="https://secrets.com"', $result); + } + + /** + * Должно упасть, если не передали redirect_uri + * @return void + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testMetadataButtonWithoutNameThrowsException(): void + { + $oauthClient = new AmoCRMOAuth( + 'test_client_id', + 'test_secret', + null + ); + + $this->expectException(AmoCRMOAuthButtonConfigurationException::class); + + + $oauthClient->getOAuthButton([ + 'is_metadata' => true, + 'description' => 'Test', + 'scopes' => ['crm'], + 'secrets_uri' => 'https://secrets.com' + ]); + } + + /** + * Проверяем ошибку при невалидных scopes + * @return void + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testMetadataButtonWithInvalidScopesThrowsException(): void + { + $this->expectException(AmoCRMOAuthButtonConfigurationException::class); + $this->expectExceptionMessage('Invalid scopes'); + + $this->oauthClient->getOAuthButton([ + 'is_metadata' => true, + 'name' => 'Test', + 'description' => 'Test', + 'scopes' => ['invalid_scope', 'another_invalid'], + 'secrets_uri' => 'https://secrets.com' + ]); + } + + /** + * Проверяем ошибку когда scopes не массив + * @return void + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testMetadataButtonWithStringScopesThrowsException(): void + { + $this->expectException(AmoCRMOAuthButtonConfigurationException::class); + $this->expectExceptionMessage('scopes parameter must be an array'); + + $this->oauthClient->getOAuthButton([ + 'is_metadata' => true, + 'name' => 'Test', + 'description' => 'Test', + 'scopes' => 'crm,notifications', + 'secrets_uri' => 'https://secrets.com' + ]); + } + + /** + * Проверяем использование дефолтных scopes когда не переданы + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testMetadataButtonUsesDefaultScopes(): void + { + $result = $this->oauthClient->getOAuthButton([ + 'is_metadata' => true, + 'name' => 'Test', + 'description' => 'Test', + 'secrets_uri' => 'https://secrets.com' + // должны использоваться дефолтные scopes - crm, notifications + ]); + + $defaultScopes = implode(',', AmoCRMOAuth::METADATA_BUTTON_AVAILABLE_SCOPES); + $this->assertStringContainsString('data-scopes="' . $defaultScopes . '"', $result); + } + + /** + * Проверяем поддержку Kommo + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testKommoButtonGeneration(): void + { + $result = $this->oauthClient->getOAuthButton([ + 'is_kommo' => true, + 'is_metadata' => true, + 'name' => 'Test', + 'description' => 'Test', + 'scopes' => ['crm'], + 'secrets_uri' => 'https://secrets.com' + ]); + + $this->assertStringContainsString('class="kommo_oauth"', $result); + $this->assertStringContainsString('kommo.com', $result); + } + + /** + * Проверяем ошибку при отсутствии secrets_uri для metadata кнопки + * @return void + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testMetadataButtonWithoutSecretsUriThrowsException(): void + { + $this->expectException(AmoCRMOAuthButtonConfigurationException::class); + $this->expectExceptionMessage('secrets_uri'); + + // Создаем клиент без secrets_uri + $clientWithoutSecrets = new AmoCRMOAuth( + 'test_client_id', + 'test_secret', + 'https://redirect.com' + ); + + $clientWithoutSecrets->getOAuthButton([ + 'is_metadata' => true, + 'name' => 'Test', + 'description' => 'Test', + 'scopes' => ['crm'] + ]); + } + + /** + * Проверяем работу с пустым массивом scopes + * Должны подставиться дефолтные scopes + * @throws AmoCRMOAuthButtonConfigurationException + */ + public function testMetadataButtonWithEmptyScopesArrayUsesDefaults(): void + { + $result = $this->oauthClient->getOAuthButton([ + 'is_metadata' => true, + 'name' => 'Test', + 'description' => 'Test', + 'scopes' => [], // Пустой массив + 'secrets_uri' => 'https://secrets.com' + ]); + + $defaultScopes = implode(',', AmoCRMOAuth::METADATA_BUTTON_AVAILABLE_SCOPES); + $this->assertStringContainsString('data-scopes="' . $defaultScopes . '"', $result); + } +} diff --git a/tests/Cases/AmoCRM/OAuth/AmoCRMOAuth.php b/tests/Cases/AmoCRM/OAuth/AmoCRMOAuthTest.php similarity index 98% rename from tests/Cases/AmoCRM/OAuth/AmoCRMOAuth.php rename to tests/Cases/AmoCRM/OAuth/AmoCRMOAuthTest.php index 2d9f205a..37a1102d 100644 --- a/tests/Cases/AmoCRM/OAuth/AmoCRMOAuth.php +++ b/tests/Cases/AmoCRM/OAuth/AmoCRMOAuthTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\TestCase; use ReflectionClass; -class AmoCRMOAuth extends TestCase +class AmoCRMOAuthTest extends TestCase { public function testCreateValidAtConstraintReturnsConstraintInstance() {