From c4cda758b598f9f7cd2129d2f1f7ca79169441de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20TAMARELLE?= Date: Sat, 18 Apr 2020 03:36:51 +0200 Subject: [PATCH] =?UTF-8?q?[Notifier]=C2=A0Add=20Google=20Chat=20bridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FrameworkExtension.php | 2 + .../Resources/config/notifier_transports.php | 5 + .../Notifier/Bridge/GoogleChat/.gitattributes | 3 + .../Notifier/Bridge/GoogleChat/CHANGELOG.md | 7 + .../Bridge/GoogleChat/GoogleChatOptions.php | 96 ++++++++ .../Bridge/GoogleChat/GoogleChatTransport.php | 144 ++++++++++++ .../GoogleChat/GoogleChatTransportFactory.php | 56 +++++ .../Notifier/Bridge/GoogleChat/LICENSE | 19 ++ .../Notifier/Bridge/GoogleChat/README.md | 14 ++ .../Tests/GoogleChatOptionsTest.php | 46 ++++ .../Tests/GoogleChatTransportFactoryTest.php | 67 ++++++ .../Tests/GoogleChatTransportTest.php | 211 ++++++++++++++++++ .../Notifier/Bridge/GoogleChat/composer.json | 35 +++ .../Bridge/GoogleChat/phpunit.xml.dist | 31 +++ .../Exception/UnsupportedSchemeException.php | 4 + 15 files changed, 740 insertions(+) create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/.gitattributes create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatOptions.php create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatOptionsTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportFactoryTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/GoogleChat/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 6c30dd8ff1a14..e2380f09b6f1f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -96,6 +96,7 @@ use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; +use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory; use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; @@ -2076,6 +2077,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ SlackTransportFactory::class => 'notifier.transport_factory.slack', TelegramTransportFactory::class => 'notifier.transport_factory.telegram', MattermostTransportFactory::class => 'notifier.transport_factory.mattermost', + GoogleChatTransportFactory::class => 'notifier.transport_factory.googlechat', NexmoTransportFactory::class => 'notifier.transport_factory.nexmo', RocketChatTransportFactory::class => 'notifier.transport_factory.rocketchat', TwilioTransportFactory::class => 'notifier.transport_factory.twilio', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 20a72019da5b7..65af5e03a76ac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -13,6 +13,7 @@ use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; +use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory; use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; @@ -51,6 +52,10 @@ ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') + ->set('notifier.transport_factory.googlechat', GoogleChatTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + ->set('notifier.transport_factory.twilio', TwilioTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/.gitattributes b/src/Symfony/Component/Notifier/Bridge/GoogleChat/.gitattributes new file mode 100644 index 0000000000000..ebb9287043dc4 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md new file mode 100644 index 0000000000000..0d994e934e55a --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.2.0 +----- + + * Added the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatOptions.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatOptions.php new file mode 100644 index 0000000000000..a7b2b817716f7 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatOptions.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\GoogleChat; + +use Symfony\Component\Notifier\Bridge\GoogleChat\Component\Card; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageOptionsInterface; +use Symfony\Component\Notifier\Notification\Notification; + +/** + * @author Jérôme Tamarelle + * + * @experimental in 5.2 + */ +final class GoogleChatOptions implements MessageOptionsInterface +{ + private $threadKey; + private $options = []; + + public function __construct(array $options = []) + { + $this->options = $options; + } + + public static function fromNotification(Notification $notification): self + { + $options = new self(); + + $text = $notification->getEmoji().' *'.$notification->getSubject().'* '; + + if ($notification->getContent()) { + $text .= "\r\n".$notification->getContent(); + } + if ($exception = $notification->getExceptionAsString()) { + $text .= "\r\n".'```'.$notification->getExceptionAsString().'```'; + } + + $options->text($text); + + return $options; + } + + public static function fromMessage(ChatMessage $message): self + { + $options = new self(); + + $options->text($message->getSubject()); + + return $options; + } + + public function toArray(): array + { + return $this->options; + } + + public function card(array $card): self + { + $this->options['cards'][] = $card; + + return $this; + } + + public function text(string $text): self + { + $this->options['text'] = $text; + + return $this; + } + + public function setThreadKey(?string $threadKey): self + { + $this->threadKey = $threadKey; + + return $this; + } + + public function getThreadKey(): ?string + { + return $this->threadKey; + } + + public function getRecipientId(): ?string + { + return null; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php new file mode 100644 index 0000000000000..ab191a469959e --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransport.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\GoogleChat; + +use Symfony\Component\HttpClient\Exception\JsonException; +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Jérôme Tamarelle + * + * @internal + * + * @experimental in 5.2 + */ +final class GoogleChatTransport extends AbstractTransport +{ + protected const HOST = 'chat.googleapis.com'; + + private $space; + private $accessKey; + private $accessToken; + + /** + * @var ?string + */ + private $threadKey; + + /** + * @param string $space The space name the the webhook url "/v1/spaces//messages" + * @param string $accessKey The "key" parameter of the webhook url + * @param string $accessToken The "token" parameter of the webhook url + */ + public function __construct(string $space, string $accessKey, string $accessToken, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->space = $space; + $this->accessKey = $accessKey; + $this->accessToken = $accessToken; + + parent::__construct($client, $dispatcher); + } + + /** + * Opaque thread identifier string that can be specified to group messages into a single thread. + * If this is the first message with a given thread identifier, a new thread is created. + * Subsequent messages with the same thread identifier will be posted into the same thread. + * + * @see https://developers.google.com/hangouts/chat/reference/rest/v1/spaces.messages/create#query-parameters + */ + public function setThreadKey(?string $threadKey): self + { + $this->threadKey = $threadKey; + + return $this; + } + + public function __toString(): string + { + return sprintf('googlechat://%s/%s%s', + $this->getEndpoint(), + $this->space, + $this->threadKey ? '?threadKey='.urlencode($this->threadKey) : '' + ); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof ChatMessage && (null === $message->getOptions() || $message->getOptions() instanceof GoogleChatOptions); + } + + /** + * @see https://developers.google.com/hangouts/chat/how-tos/webhooks + */ + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof ChatMessage) { + throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" (instance of "%s" given).', __CLASS__, ChatMessage::class, \get_class($message))); + } + if ($message->getOptions() && !$message->getOptions() instanceof GoogleChatOptions) { + throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" for options.', __CLASS__, GoogleChatOptions::class)); + } + + $opts = $message->getOptions(); + if (!$opts) { + if ($notification = $message->getNotification()) { + $opts = GoogleChatOptions::fromNotification($notification); + } else { + $opts = GoogleChatOptions::fromMessage($message); + } + } + + if (null !== $this->threadKey && null === $opts->getThreadKey()) { + $opts->setThreadKey($this->threadKey); + } + + $threadKey = $opts->getThreadKey() ?: $this->threadKey; + + $options = $opts->toArray(); + $url = sprintf('https://%s/v1/spaces/%s/messages?key=%s&token=%s%s', + $this->getEndpoint(), + $this->space, + urlencode($this->accessKey), + urlencode($this->accessToken), + $threadKey ? '&threadKey='.urlencode($threadKey) : '' + ); + $response = $this->client->request('POST', $url, [ + 'json' => array_filter($options), + ]); + + try { + $result = $response->toArray(false); + } catch (JsonException $jsonException) { + throw new TransportException(sprintf('Unable to post the Google Chat message: Invalid response.'), $response, $response->getStatusCode(), $jsonException); + } + + if (200 !== $response->getStatusCode()) { + throw new TransportException(sprintf('Unable to post the Google Chat message: "%s".', $result['error']['message'] ?? $response->getContent(false)), $response, $result['error']['code'] ?? $response->getStatusCode()); + } + + if (!\array_key_exists('name', $result)) { + throw new TransportException(sprintf('Unable to post the Google Chat message: "%s".', $response->getContent(false)), $response); + } + + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($result['name']); + + return $sentMessage; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransportFactory.php new file mode 100644 index 0000000000000..039b3f5e7a4ff --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/GoogleChatTransportFactory.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\GoogleChat; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; +use Symfony\Component\Notifier\Transport\TransportInterface; + +/** + * @author Jérôme Tamarelle + * + * @experimental in 5.2 + */ +final class GoogleChatTransportFactory extends AbstractTransportFactory +{ + /** + * @param Dsn $dsn Format: googlechat://:@default/?threadKey= + * + * @return GoogleChatTransport + */ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + + if ('googlechat' === $scheme) { + $space = explode('/', $dsn->getPath())[1]; + $accessKey = $this->getUser($dsn); + $accessToken = $this->getPassword($dsn); + $threadKey = $dsn->getOption('threadKey'); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new GoogleChatTransport($space, $accessKey, $accessToken, $this->client, $this->dispatcher)) + ->setThreadKey($threadKey) + ->setHost($host) + ->setPort($port); + } + + throw new UnsupportedSchemeException($dsn, 'googlechat', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['googlechat']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/LICENSE b/src/Symfony/Component/Notifier/Bridge/GoogleChat/LICENSE new file mode 100644 index 0000000000000..1a1869751d250 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/README.md b/src/Symfony/Component/Notifier/Bridge/GoogleChat/README.md new file mode 100644 index 0000000000000..09860d21e2869 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/README.md @@ -0,0 +1,14 @@ +Google Chat Notifier +==================== + +Provides Google Chat integration for Symfony Notifier. + + googlechat://ACCESS_KEY:ACCESS_TOKEN@default/SPACE?threadKey=THREAD_KEY + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatOptionsTest.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatOptionsTest.php new file mode 100644 index 0000000000000..aa763bcd0f2cd --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatOptionsTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\GoogleChat\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatOptions; + +class GoogleChatOptionsTest extends TestCase +{ + public function testToArray() + { + $options = new GoogleChatOptions(); + + $options + ->text('Pizza Bot') + ->card(['header' => ['Pizza Bot Customer Support']]); + + $expected = [ + 'text' => 'Pizza Bot', + 'cards' => [ + ['header' => ['Pizza Bot Customer Support']], + ], + ]; + + $this->assertSame($expected, $options->toArray()); + } + + public function testOptionsWithThread() + { + $thread = 'fgh.ijk'; + $options = new GoogleChatOptions(); + $options->setThreadKey($thread); + $this->assertSame($thread, $options->getThreadKey()); + $options->setThreadKey(null); + $this->assertNull($options->getThreadKey()); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportFactoryTest.php new file mode 100644 index 0000000000000..81e0424036d01 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportFactoryTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\GoogleChat\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory; +use Symfony\Component\Notifier\Exception\IncompleteDsnException; +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\Dsn; + +final class GoogleChatTransportFactoryTest extends TestCase +{ + public function testCreateWithDsn(): void + { + $factory = new GoogleChatTransportFactory(); + + $dsn = 'googlechat://abcde-fghij:kl_mnopqrstwxyz%3D@chat.googleapis.com/AAAAA_YYYYY'; + $transport = $factory->create(Dsn::fromString($dsn)); + + $this->assertSame('googlechat://chat.googleapis.com/AAAAA_YYYYY', (string) $transport); + } + + public function testCreateWithThreadKeyInDsn(): void + { + $factory = new GoogleChatTransportFactory(); + + $dsn = 'googlechat://abcde-fghij:kl_mnopqrstwxyz%3D@chat.googleapis.com/AAAAA_YYYYY?threadKey=abcdefg'; + $transport = $factory->create(Dsn::fromString($dsn)); + + $this->assertSame('googlechat://chat.googleapis.com/AAAAA_YYYYY?threadKey=abcdefg', (string) $transport); + } + + public function testCreateRequiresCredentials(): void + { + $this->expectException(IncompleteDsnException::class); + $factory = new GoogleChatTransportFactory(); + + $dsn = 'googlechat://chat.googleapis.com/v1/spaces/AAAAA_YYYYY/messages'; + $factory->create(Dsn::fromString($dsn)); + } + + public function testSupportsGoogleChatScheme(): void + { + $factory = new GoogleChatTransportFactory(); + + $this->assertTrue($factory->supports(Dsn::fromString('googlechat://host/path'))); + $this->assertFalse($factory->supports(Dsn::fromString('somethingElse://host/path'))); + } + + public function testNonGoogleChatSchemeThrows(): void + { + $factory = new GoogleChatTransportFactory(); + + $this->expectException(UnsupportedSchemeException::class); + + $factory->create(Dsn::fromString('somethingElse://host/path')); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php b/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php new file mode 100644 index 0000000000000..cf6e65d19077f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/Tests/GoogleChatTransportTest.php @@ -0,0 +1,211 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\GoogleChat\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatOptions; +use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransport; +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\MessageOptionsInterface; +use Symfony\Component\Notifier\Notification\Notification; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class GoogleChatTransportTest extends TestCase +{ + public function testToStringContainsProperties(): void + { + $transport = new GoogleChatTransport('My-Space', 'theAccessKey', 'theAccessToken=', $this->createMock(HttpClientInterface::class)); + $transport->setHost(null); + + $this->assertSame('googlechat://chat.googleapis.com/My-Space', (string) $transport); + } + + public function testSupportsChatMessage(): void + { + $transport = new GoogleChatTransport('My-Space', 'theAccessKey', 'theAccessToken=', $this->createMock(HttpClientInterface::class)); + + $this->assertTrue($transport->supports(new ChatMessage('testChatMessage'))); + $this->assertFalse($transport->supports($this->createMock(MessageInterface::class))); + } + + public function testSendNonChatMessageThrows(): void + { + $this->expectException(LogicException::class); + + $transport = new GoogleChatTransport('My-Space', 'theAccessKey', 'theAccessToken=', $this->createMock(HttpClientInterface::class)); + + $transport->send($this->createMock(MessageInterface::class)); + } + + public function testSendWithEmptyArrayResponseThrows(): void + { + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Unable to post the Google Chat message: "[]"'); + $this->expectExceptionCode(500); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(500); + $response->expects($this->once()) + ->method('getContent') + ->willReturn('[]'); + + $client = new MockHttpClient(function () use ($response): ResponseInterface { + return $response; + }); + + $transport = new GoogleChatTransport('My-Space', 'theAccessKey', 'theAccessToken=', $client); + + $sentMessage = $transport->send(new ChatMessage('testMessage')); + + $this->assertSame('spaces/My-Space/messages/abcdefg.hijklmno', $sentMessage->getMessageId()); + } + + public function testSendWithErrorResponseThrows(): void + { + $this->expectException(TransportException::class); + $this->expectExceptionMessage('API key not valid. Please pass a valid API key.'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(400); + $response->expects($this->once()) + ->method('getContent') + ->willReturn('{"error":{"code":400,"message":"API key not valid. Please pass a valid API key.","status":"INVALID_ARGUMENT"}}'); + + $client = new MockHttpClient(function () use ($response): ResponseInterface { + return $response; + }); + + $transport = new GoogleChatTransport('My-Space', 'theAccessKey', 'theAccessToken=', $client); + + $sentMessage = $transport->send(new ChatMessage('testMessage')); + + $this->assertSame('spaces/My-Space/messages/abcdefg.hijklmno', $sentMessage->getMessageId()); + } + + public function testSendWithOptions(): void + { + $message = 'testMessage'; + + $response = $this->createMock(ResponseInterface::class); + + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects($this->once()) + ->method('getContent') + ->willReturn('{"name":"spaces/My-Space/messages/abcdefg.hijklmno"}'); + + $expectedBody = json_encode(['text' => $message]); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://chat.googleapis.com/v1/spaces/My-Space/messages?key=theAccessKey&token=theAccessToken%3D&threadKey=My-Thread', $url); + $this->assertSame($expectedBody, $options['body']); + + return $response; + }); + + $transport = new GoogleChatTransport('My-Space', 'theAccessKey', 'theAccessToken=', $client); + $transport->setThreadKey('My-Thread'); + + $sentMessage = $transport->send(new ChatMessage('testMessage')); + + $this->assertSame('spaces/My-Space/messages/abcdefg.hijklmno', $sentMessage->getMessageId()); + } + + public function testSendWithNotification(): void + { + $response = $this->createMock(ResponseInterface::class); + + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects($this->once()) + ->method('getContent') + ->willReturn('{"name":"spaces/My-Space/messages/abcdefg.hijklmno","thread":{"name":"spaces/My-Space/threads/abcdefg.hijklmno"}}'); + + $notification = new Notification('testMessage'); + $chatMessage = ChatMessage::fromNotification($notification); + + $expectedBody = json_encode([ + 'text' => ' *testMessage* ', + ]); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { + $this->assertSame($expectedBody, $options['body']); + + return $response; + }); + + $transport = new GoogleChatTransport('My-Space', 'theAccessKey', 'theAccessToken=', $client); + + $sentMessage = $transport->send($chatMessage); + + $this->assertSame('spaces/My-Space/messages/abcdefg.hijklmno', $sentMessage->getMessageId()); + } + + public function testSendWithInvalidOptions(): void + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The "'.GoogleChatTransport::class.'" transport only supports instances of "'.GoogleChatOptions::class.'" for options.'); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []): ResponseInterface { + return $this->createMock(ResponseInterface::class); + }); + + $transport = new GoogleChatTransport('My-Space', 'theAccessKey', 'theAccessToken=', $client); + + $transport->send(new ChatMessage('testMessage', $this->createMock(MessageOptionsInterface::class))); + } + + public function testSendWith200ResponseButNotOk(): void + { + $message = 'testMessage'; + + $this->expectException(TransportException::class); + + $response = $this->createMock(ResponseInterface::class); + + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects($this->once()) + ->method('getContent') + ->willReturn('testErrorCode'); + + $expectedBody = json_encode(['text' => $message]); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { + $this->assertSame($expectedBody, $options['body']); + + return $response; + }); + + $transport = new GoogleChatTransport('My-Space', 'theAccessKey', 'theAccessToken=', $client); + + $sentMessage = $transport->send(new ChatMessage('testMessage')); + + $this->assertSame('spaces/My-Space/messages/abcdefg.hijklmno', $sentMessage->getMessageId()); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/composer.json b/src/Symfony/Component/Notifier/Bridge/GoogleChat/composer.json new file mode 100644 index 0000000000000..34f10a8dd29d9 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/googlechat-notifier", + "type": "symfony-bridge", + "description": "Symfony Google Chat Notifier Bridge", + "keywords": ["google", "chat", "google chat", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/http-client": "^4.3|^5.0", + "symfony/notifier": "^5.2" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\GoogleChat\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/GoogleChat/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/GoogleChat/phpunit.xml.dist new file mode 100644 index 0000000000000..e5808073cbcd1 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/GoogleChat/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index 5836b6607f2b8..84f7806d238ba 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -34,6 +34,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Mattermost\MattermostTransportFactory::class, 'package' => 'symfony/mattermost-notifier', ], + 'googlechat' => [ + 'class' => Bridge\GoogleChat\GoogleChatTransportFactory::class, + 'package' => 'symfony/googlechat-notifier', + ], 'nexmo' => [ 'class' => Bridge\Nexmo\NexmoTransportFactory::class, 'package' => 'symfony/nexmo-notifier',