Skip to content

Commit

Permalink
Merge pull request #1 from Jeroeny/init
Browse files Browse the repository at this point in the history
create Mattermost bridge for symfony notifier
  • Loading branch information
Jeroeny authored Oct 12, 2019
2 parents a214493 + 955535b commit fab0842
Show file tree
Hide file tree
Showing 12 changed files with 435 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/Tests export-ignore
/phpunit.xml.dist export-ignore
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/.idea
/vendor
composer.lock

.phpunit.result.cache
15 changes: 15 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
language: php

php:
- 7.2
- 7.3
- 7.4snapshot

install:
- composer install --no-progress

script:
- ./vendor/bin/phpunit --coverage-clover=coverage.xml

after_success:
- bash <(curl -s https://codecov.io/bash)
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CHANGELOG
=========

1.0.0
-----

* Created the bridge
38 changes: 38 additions & 0 deletions MattermostMessageOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Notifier\Bridge\Mattermost;

use Symfony\Component\Notifier\Message\MessageOptionsInterface;

final class MattermostMessageOptions implements MessageOptionsInterface
{
/** @var string the channel to send the message to. in case of a person, prefixed with '@' */
private $channel;

/** @var string the username to display above the sent message */
private $username;

/** @var string the avatar to display with the sent message */
private $icon_url;

public function __construct(string $channel, string $username, string $icon_url)
{
$this->channel = $channel;
$this->username = $username;
$this->icon_url = $icon_url;
}

public function toArray(): array
{
return [
'channel' => $this->channel,
'username' => $this->username,
'icon_url' => $this->icon_url,
];
}

public function getRecipientId(): ?string
{
return $this->channel;
}
}
75 changes: 75 additions & 0 deletions MattermostTransport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace Notifier\Bridge\Mattermost;

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\Transport\AbstractTransport;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use function array_filter;
use function get_class;
use function sprintf;

/**
* MattermostTransport.
*
* @author Jeroen Spee <[email protected]>
*
* @internal
*/
final class MattermostTransport extends AbstractTransport
{
/** @var string webhook token */
private $token;

/** @var string channel to send a message to by default. prefix with '@' for user */
private $chatChannel;

public function __construct(string $token, string $chatChannel, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)
{
$this->token = $token;
$this->chatChannel = $chatChannel;
$this->client = $client;

parent::__construct($client, $dispatcher);
}

public function __toString(): string
{
return sprintf('mattermost://%s@%s?channel=%s', $this->token, $this->getEndpoint(), $this->chatChannel);
}

public function supports(MessageInterface $message): bool
{
return $message instanceof ChatMessage;
}

/**
* @see https://docs.mattermost.com/developer/webhooks-incoming.html
*/
protected function doSend(MessageInterface $message): void
{
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)));
}

$endpoint = sprintf('https://%s/hooks/%s', $this->getEndpoint(), $this->token);
$options = ($opts = $message->getOptions()) ? $opts->toArray() : [];
if (!isset($options['channel'])) {
$options['channel'] = $message->getRecipientId() ?: $this->chatChannel;
}
$options['text'] = $message->getSubject();
$response = $this->client->request('POST', $endpoint, [
'json' => array_filter($options),
]);

if (200 !== $response->getStatusCode()) {
$result = $response->toArray(false);

throw new TransportException(sprintf('Unable to post the Mattermost message: %s (%s: %s).', $result['message'], $result['status_code'], $result['id']), $response);
}
}
}
36 changes: 36 additions & 0 deletions MattermostTransportFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Notifier\Bridge\Mattermost;

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 Jeroen Spee <[email protected]>
*/
final class MattermostTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
$scheme = $dsn->getScheme();
$token = $this->getUser($dsn);
$channel = $dsn->getOption('channel');
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
$port = $dsn->getPort();

if ('mattermost' === $scheme) {
return (new MattermostTransport($token, $channel, $this->client, $this->dispatcher))
->setHost($host)
->setPort($port);
}

throw new UnsupportedSchemeException($dsn, 'mattermost', $this->getSupportedSchemes());
}

protected function getSupportedSchemes(): array
{
return ['mattermost'];
}
}
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# mattermost-bridge
# Mattermost bridge

[![build](https://travis-ci.org/Jeroeny/mattermost-bridge.svg?branch=master)](https://travis-ci.org/Jeroeny/mattermost-bridge)
[![codecov](https://codecov.io/gh/Jeroeny/mattermost-bridge/branch/master/graph/badge.svg)](https://codecov.io/gh/Jeroeny/mattermost-bridge)

Provides Mattermost integration for Symfony Notifier.
60 changes: 60 additions & 0 deletions Tests/MattermostTransportFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Notifier\Bridge\Mattermost\Tests;

use Notifier\Bridge\Mattermost\MattermostMessageOptions;
use Notifier\Bridge\Mattermost\MattermostTransport;
use Notifier\Bridge\Mattermost\MattermostTransportFactory;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Notifier\Exception\LogicException;
use Symfony\Component\Notifier\Exception\TransportException;
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Message\MessageInterface;
use Symfony\Component\Notifier\Transport\Dsn;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

final class MattermostTransportFactoryTest extends TestCase
{
/** @var string */
private $token;
/** @var string */
private $channel;

/** @var MockObject|HttpClientInterface */
private $httpClient;

/** @var MattermostTransportFactory */
private $transportFactory;

public function setUp(): void
{
$this->token = 'testToken';
$this->channel = 'testChannel';
$this->httpClient = $this->createMock(HttpClientInterface::class);
$this->transportFactory = new MattermostTransportFactory(null, $this->httpClient);
}

public function test_create_from_string(): void
{
$dsn = 'mattermost://token@localhost?channel=test';
$transport = $this->transportFactory->create(Dsn::fromString($dsn));
$this->assertInstanceOf(MattermostTransport::class, $transport);
$this->assertSame($dsn, $transport->__toString());
}

public function test_create_unsupported(): void
{
$this->expectException(UnsupportedSchemeException::class);
$dsn = 'test://token@localhost?channel=test';
$this->transportFactory->create(Dsn::fromString($dsn));
}

public function test_supports(): void
{
$this->assertTrue($this->transportFactory->supports(Dsn::fromString('mattermost://token@localhost?channel=test')));
$this->assertFalse($this->transportFactory->supports(Dsn::fromString('test://user@localhost?channel=test')));
}
}
112 changes: 112 additions & 0 deletions Tests/MattermostTransportTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace Notifier\Bridge\Mattermost\Tests;

use Notifier\Bridge\Mattermost\MattermostMessageOptions;
use Notifier\Bridge\Mattermost\MattermostTransport;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
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\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

final class MattermostTransportTest extends TestCase
{
/** @var string */
private $token;
/** @var string */
private $channel;

/** @var MockObject|HttpClientInterface */
private $httpClient;

/** @var MattermostTransport */
private $transport;

public function setUp(): void
{
$this->token = 'testToken';
$this->channel = 'testChannel';
$this->httpClient = $this->createMock(HttpClientInterface::class);
$this->transport = new MattermostTransport($this->token, $this->channel, $this->httpClient);
}

public function test_transport(): void
{
$response = $this->createMock(ResponseInterface::class);
$this->httpClient
->expects($this->once())
->method('request')
->willReturn($response);

$response->expects($this->once())->method('getStatusCode')->willReturn(200);

$channel = 'testChannel';
$messageOptions = new MattermostMessageOptions(
$channel, 'webhookTest', 'iconUrl'
);
$this->assertSame($channel, $messageOptions->getRecipientId());
$this->transport->send(new ChatMessage('testMessage', $messageOptions));
}

public function test_transport_fallback_channel(): void
{
$response = $this->createMock(ResponseInterface::class);
$this->httpClient
->expects($this->once())
->method('request')
->willReturn($response);

$response->expects($this->once())->method('getStatusCode')->willReturn(200);

$this->transport->send(new ChatMessage('testMessage'));
}

public function test_to_string(): void
{
$this->assertSame('mattermost://' . $this->token . '@localhost?channel=' . $this->channel, $this->transport->__toString());
}

public function test_supports(): void
{
$this->assertTrue($this->transport->supports(new ChatMessage('test')));
$this->assertFalse($this->transport->supports($this->createMock(MessageInterface::class)));
}

public function test_send_fail(): void
{
$this->expectException(LogicException::class);
$this->transport->send($this->createMock(MessageInterface::class));
}

public function test_transport_fail(): void
{
$this->expectException(TransportException::class);
$this->expectExceptionMessage('Unable to post the Mattermost message: Invalid webhook (400: web.incoming_webhook.invalid.app_error).');
$response = $this->createMock(ResponseInterface::class);
$this->httpClient
->expects($this->once())
->method('request')
->willReturn($response);

$response->expects($this->once())->method('getStatusCode')->willReturn(400);
$response->expects($this->once())->method('toArray')->willReturn([
'id' => 'web.incoming_webhook.invalid.app_error',
'message' => 'Invalid webhook',
'detailed_error' => '',
'request_id' => 'ging73md9qmadtest',
'status_code' => 400
]);

$this->transport->send(new ChatMessage(
'testMessage',
new MattermostMessageOptions(
'testChannel', 'webhookTest', 'iconUrl'
)
)
);
}
}
Loading

0 comments on commit fab0842

Please sign in to comment.