From d039ce7e63014e0a082b0546c94f00e77d0f5c22 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Thu, 8 Apr 2021 13:04:26 +0200 Subject: [PATCH] [Notifier] Add options to Microsoft Teams notifier --- .../MicrosoftTeams/Action/ActionCard.php | 51 ++++ .../ActionCardCompatibleActionInterface.php | 21 ++ .../MicrosoftTeams/Action/ActionInterface.php | 21 ++ .../MicrosoftTeams/Action/Element/Header.php | 42 +++ .../MicrosoftTeams/Action/HttpPostAction.php | 65 ++++ .../Action/Input/AbstractInput.php | 54 ++++ .../MicrosoftTeams/Action/Input/DateInput.php | 35 +++ .../Action/Input/InputInterface.php | 21 ++ .../Action/Input/MultiChoiceInput.php | 60 ++++ .../MicrosoftTeams/Action/Input/TextInput.php | 42 +++ .../Action/InvokeAddInCommandAction.php | 56 ++++ .../MicrosoftTeams/Action/OpenUriAction.php | 55 ++++ .../Bridge/MicrosoftTeams/CHANGELOG.md | 1 + .../MicrosoftTeams/MicrosoftTeamsOptions.php | 160 ++++++++++ .../MicrosoftTeamsTransport.php | 13 +- .../MicrosoftTeams/Section/Field/Activity.php | 56 ++++ .../MicrosoftTeams/Section/Field/Fact.php | 39 +++ .../MicrosoftTeams/Section/Field/Image.php | 42 +++ .../Bridge/MicrosoftTeams/Section/Section.php | 84 ++++++ .../Section/SectionInterface.php | 21 ++ .../Action/Input/AbstractInputTestCase.php | 68 +++++ .../Tests/Action/ActionCardTest.php | 71 +++++ .../Tests/Action/Element/HeaderTest.php | 25 ++ .../Tests/Action/HttpPostActionTest.php | 70 +++++ .../Tests/Action/Input/DateInputTest.php | 44 +++ .../Action/Input/MultiChoiceInputTest.php | 88 ++++++ .../Tests/Action/Input/TextInputTest.php | 52 ++++ .../Action/InvokeAddInCommandActionTest.php | 55 ++++ .../Tests/Action/OpenUriActionTest.php | 75 +++++ .../Tests/MicrosoftTeamsOptionsTest.php | 285 ++++++++++++++++++ .../Tests/MicrosoftTeamsTransportTest.php | 75 ++++- .../Tests/Section/Field/ActivityTest.php | 41 +++ .../Tests/Section/Field/FactTest.php | 25 ++ .../Tests/Section/Field/ImageTest.php | 25 ++ .../Tests/Section/SectionTest.php | 127 ++++++++ 35 files changed, 2061 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionCard.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionCardCompatibleActionInterface.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionInterface.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Element/Header.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/HttpPostAction.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/AbstractInput.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/DateInput.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/InputInterface.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/MultiChoiceInput.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/TextInput.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/InvokeAddInCommandAction.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/OpenUriAction.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/MicrosoftTeamsOptions.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Activity.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Fact.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Image.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Section.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/SectionInterface.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Test/Action/Input/AbstractInputTestCase.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/ActionCardTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Element/HeaderTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/HttpPostActionTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/DateInputTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/MultiChoiceInputTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/TextInputTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/InvokeAddInCommandActionTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/OpenUriActionTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/MicrosoftTeamsOptionsTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/ActivityTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/FactTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/ImageTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/SectionTest.php diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionCard.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionCard.php new file mode 100644 index 0000000000000..f036ea98bc3f3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionCard.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action; + +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\Input\InputInterface; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#actioncard-action + */ +final class ActionCard implements ActionInterface +{ + private $options = []; + + public function name(string $name): self + { + $this->options['name'] = $name; + + return $this; + } + + public function input(InputInterface $inputAction): self + { + $this->options['inputs'][] = $inputAction->toArray(); + + return $this; + } + + public function action(ActionCardCompatibleActionInterface $action): self + { + $this->options['actions'][] = $action->toArray(); + + return $this; + } + + public function toArray(): array + { + return $this->options + ['@type' => 'ActionCard']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionCardCompatibleActionInterface.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionCardCompatibleActionInterface.php new file mode 100644 index 0000000000000..85d9c52545d95 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionCardCompatibleActionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action; + +/** + * An Action which can be used inside an ActionCard. + * + * @author Oskar Stark + */ +interface ActionCardCompatibleActionInterface extends ActionInterface +{ +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionInterface.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionInterface.php new file mode 100644 index 0000000000000..d37b13b9faf07 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/ActionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action; + +/** + * @author Edouard Lescot + * @author Oskar Stark + */ +interface ActionInterface +{ + public function toArray(): array; +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Element/Header.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Element/Header.php new file mode 100644 index 0000000000000..cc636a44cb1f3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Element/Header.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\Element; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#header + */ +final class Header +{ + private $options = []; + + public function name(string $name): self + { + $this->options['name'] = $name; + + return $this; + } + + public function value(string $value): self + { + $this->options['value'] = $value; + + return $this; + } + + public function toArray(): array + { + return $this->options; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/HttpPostAction.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/HttpPostAction.php new file mode 100644 index 0000000000000..99b943c86b7a8 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/HttpPostAction.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action; + +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\Element\Header; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#httppost-action + */ +final class HttpPostAction implements ActionCardCompatibleActionInterface +{ + private $options = ['@type' => 'HttpPOST']; + + public function name(string $name): self + { + $this->options['name'] = $name; + + return $this; + } + + public function target(string $url): self + { + $this->options['target'] = $url; + + return $this; + } + + public function header(Header $header): self + { + $this->options['headers'][] = $header->toArray(); + + return $this; + } + + public function body(string $body): self + { + $this->options['body'] = $body; + + return $this; + } + + public function bodyContentType(string $contentType): self + { + $this->options['bodyContentType'] = $contentType; + + return $this; + } + + public function toArray(): array + { + return $this->options; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/AbstractInput.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/AbstractInput.php new file mode 100644 index 0000000000000..9c5f22a39929e --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/AbstractInput.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\Input; + +/** + * @author Edouard Lescot + * @author Oskar Stark + */ +abstract class AbstractInput implements InputInterface +{ + private $options = []; + + public function id(string $id): self + { + $this->options['id'] = $id; + + return $this; + } + + public function isRequired(bool $required): self + { + $this->options['isRequired'] = $required; + + return $this; + } + + public function title(string $title): self + { + $this->options['title'] = $title; + + return $this; + } + + public function value(string $value): self + { + $this->options['value'] = $value; + + return $this; + } + + public function toArray(): array + { + return $this->options; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/DateInput.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/DateInput.php new file mode 100644 index 0000000000000..1794dad3e131b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/DateInput.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\Input; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#dateinput + */ +final class DateInput extends AbstractInput +{ + private $options = []; + + public function includeTime(bool $includeTime): self + { + $this->options['includeTime'] = $includeTime; + + return $this; + } + + public function toArray(): array + { + return parent::toArray() + $this->options + ['@type' => 'DateInput']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/InputInterface.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/InputInterface.php new file mode 100644 index 0000000000000..8a1c4761346fd --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/InputInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\Input; + +/** + * @author Edouard Lescot + * @author Oskar Stark + */ +interface InputInterface +{ + public function toArray(): array; +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/MultiChoiceInput.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/MultiChoiceInput.php new file mode 100644 index 0000000000000..85403f6378adc --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/MultiChoiceInput.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\Input; + +use Symfony\Component\Notifier\Exception\InvalidArgumentException; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#multichoiceinput + */ +final class MultiChoiceInput extends AbstractInput +{ + private const STYLES = [ + 'expanded', + 'normal', + ]; + + private $options = []; + + public function choice(string $display, string $value): self + { + $this->options['choices'][] = ['display' => $display, 'value' => $value]; + + return $this; + } + + public function isMultiSelect(bool $multiSelect): self + { + $this->options['isMultiSelect'] = $multiSelect; + + return $this; + } + + public function style(string $style): self + { + if (!\in_array($style, self::STYLES)) { + throw new InvalidArgumentException(sprintf('Supported styles for "%s" method are: "%s".', __METHOD__, implode('", "', self::STYLES))); + } + + $this->options['style'] = $style; + + return $this; + } + + public function toArray(): array + { + return parent::toArray() + $this->options + ['@type' => 'MultichoiceInput']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/TextInput.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/TextInput.php new file mode 100644 index 0000000000000..a62edddcd58b1 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/Input/TextInput.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\Input; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#textinput + */ +final class TextInput extends AbstractInput +{ + private $options = []; + + public function isMultiline(bool $multiline): self + { + $this->options['isMultiline'] = $multiline; + + return $this; + } + + public function maxLength(int $maxLength): self + { + $this->options['maxLength'] = $maxLength; + + return $this; + } + + public function toArray(): array + { + return parent::toArray() + $this->options + ['@type' => 'TextInput']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/InvokeAddInCommandAction.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/InvokeAddInCommandAction.php new file mode 100644 index 0000000000000..e305f4fd84526 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/InvokeAddInCommandAction.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\MicrosoftTeams\Action; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#invokeaddincommand-action + */ +final class InvokeAddInCommandAction implements ActionInterface +{ + private $options = []; + + public function name(string $name): self + { + $this->options['name'] = $name; + + return $this; + } + + public function addInId(string $addInId): self + { + $this->options['addInId'] = $addInId; + + return $this; + } + + public function desktopCommandId(string $desktopCommandId): self + { + $this->options['desktopCommandId'] = $desktopCommandId; + + return $this; + } + + public function initializationContext(array $context): self + { + $this->options['initializationContext'] = $context; + + return $this; + } + + public function toArray(): array + { + return $this->options + ['@type' => 'InvokeAddInCommand']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/OpenUriAction.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/OpenUriAction.php new file mode 100644 index 0000000000000..39c28054da552 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Action/OpenUriAction.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action; + +use Symfony\Component\Notifier\Exception\InvalidArgumentException; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#openuri-action + */ +final class OpenUriAction implements ActionCardCompatibleActionInterface +{ + private const OPERATING_SYSTEMS = [ + 'android', + 'default', + 'iOS', + 'windows', + ]; + + private $options = []; + + public function name(string $name): self + { + $this->options['name'] = $name; + + return $this; + } + + public function target(string $uri, string $os = 'default'): self + { + if (!\in_array($os, self::OPERATING_SYSTEMS)) { + throw new InvalidArgumentException(sprintf('Supported operating systems for "%s" method are: "%s".', __METHOD__, implode('", "', self::OPERATING_SYSTEMS))); + } + + $this->options['targets'][] = ['os' => $os, 'uri' => $uri]; + + return $this; + } + + public function toArray(): array + { + return $this->options + ['@type' => 'OpenUri']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/CHANGELOG.md index 1f2b652ac20ea..f24fc06e6faf3 100644 --- a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/CHANGELOG.md @@ -5,3 +5,4 @@ CHANGELOG --- * Add the bridge + * Add options support diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/MicrosoftTeamsOptions.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/MicrosoftTeamsOptions.php new file mode 100644 index 0000000000000..bd3d01393f17e --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/MicrosoftTeamsOptions.php @@ -0,0 +1,160 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams; + +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\ActionInterface; +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\Section\Section; +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\Section\SectionInterface; +use Symfony\Component\Notifier\Exception\InvalidArgumentException; +use Symfony\Component\Notifier\Message\MessageOptionsInterface; +use Symfony\Component\Notifier\Notification\Notification; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference + */ +final class MicrosoftTeamsOptions implements MessageOptionsInterface +{ + private const MAX_ACTIONS = 4; + + private $options = []; + + public function __construct(array $options = []) + { + if (\array_key_exists('themeColor', $options)) { + $this->validateThemeColor($options['themeColor']); + } + + $this->options = $options; + + $this->validateNumberOfActions(); + } + + public static function fromNotification(Notification $notification): self + { + $options = (new self()) + ->title($notification->getSubject()) + ->text($notification->getContent()); + + if ($exception = $notification->getExceptionAsString()) { + $options->section((new Section())->text($exception)); + } + + return $options; + } + + public function toArray(): array + { + $options = $this->options; + + // Send a text, not a message card + if (1 === \count($options) && isset($options['text'])) { + return $options; + } + + $options['@type'] = 'MessageCard'; + $options['@context'] = 'https://schema.org/extensions'; + + return $options; + } + + public function getRecipientId(): ?string + { + return $this->options['recipient_id'] ?? null; + } + + /** + * @param string $path The hook path (anything after https://outlook.office.com) + */ + public function recipient(string $path): self + { + if (!preg_match('/^\/webhook\//', $path)) { + throw new InvalidArgumentException(sprintf('"%s" require recipient id format to be "/webhook/{uuid}@{uuid}/IncomingWebhook/{id}/{uuid}", "%s" given.', __CLASS__, $path)); + } + + $this->options['recipient_id'] = $path; + + return $this; + } + + /** + * @param string $summary Markdown string + */ + public function summary(string $summary): self + { + $this->options['summary'] = $summary; + + return $this; + } + + public function title(string $title): self + { + $this->options['title'] = $title; + + return $this; + } + + public function text(string $text): self + { + $this->options['text'] = $text; + + return $this; + } + + public function themeColor(string $themeColor): self + { + $this->validateThemeColor($themeColor); + + $this->options['themeColor'] = $themeColor; + + return $this; + } + + public function section(SectionInterface $section): self + { + $this->options['sections'][] = $section->toArray(); + + return $this; + } + + public function action(ActionInterface $action): self + { + $this->validateNumberOfActions(); + + $this->options['potentialAction'][] = $action->toArray(); + + return $this; + } + + public function expectedActor(string $actor): self + { + $this->options['expectedActors'][] = $actor; + + return $this; + } + + private function validateNumberOfActions(): void + { + if (\count($this->options['potentialAction'] ?? []) >= self::MAX_ACTIONS) { + throw new InvalidArgumentException(sprintf('MessageCard maximum number of "potentialAction" has been reached (%d).', self::MAX_ACTIONS)); + } + } + + private function validateThemeColor(string $themeColor): void + { + if (!preg_match('/^#([0-9a-f]{6}|[0-9a-f]{3})$/i', $themeColor)) { + throw new InvalidArgumentException('MessageCard themeColor must have a valid hex color format.'); + } + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/MicrosoftTeamsTransport.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/MicrosoftTeamsTransport.php index ccd0f4cdf770a..6f7322c758a46 100644 --- a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/MicrosoftTeamsTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/MicrosoftTeamsTransport.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams; +use Symfony\Component\Notifier\Exception\LogicException; use Symfony\Component\Notifier\Exception\TransportException; use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; use Symfony\Component\Notifier\Message\ChatMessage; @@ -56,12 +57,18 @@ protected function doSend(MessageInterface $message): SentMessage throw new UnsupportedMessageTypeException(__CLASS__, ChatMessage::class, $message); } + if ($message->getOptions() && !$message->getOptions() instanceof MicrosoftTeamsOptions) { + throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" for options.', __CLASS__, MicrosoftTeamsOptions::class)); + } + + $options = ($opts = $message->getOptions()) ? $opts->toArray() : []; + + $options['text'] = $options['text'] ?? $message->getSubject(); + $path = $message->getRecipientId() ?? $this->path; $endpoint = sprintf('https://%s%s', $this->getEndpoint(), $path); $response = $this->client->request('POST', $endpoint, [ - 'json' => [ - 'text' => $message->getSubject(), - ], + 'json' => $options, ]); $requestId = $response->getHeaders(false)['request-id'][0] ?? null; diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Activity.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Activity.php new file mode 100644 index 0000000000000..33e0bdeb11725 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Activity.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\MicrosoftTeams\Section\Field; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#section-fields + */ +final class Activity +{ + private $options = []; + + public function image(string $imageUrl): self + { + $this->options['activityImage'] = $imageUrl; + + return $this; + } + + public function title(string $title): self + { + $this->options['activityTitle'] = $title; + + return $this; + } + + public function subtitle(string $subtitle): self + { + $this->options['activitySubtitle'] = $subtitle; + + return $this; + } + + public function text(string $text): self + { + $this->options['activityText'] = $text; + + return $this; + } + + public function toArray(): array + { + return $this->options; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Fact.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Fact.php new file mode 100644 index 0000000000000..37fa4bb627140 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Fact.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Section\Field; + +/** + * @author Oskar Stark + */ +final class Fact +{ + private $options = []; + + public function name(string $name): self + { + $this->options['name'] = $name; + + return $this; + } + + public function value(string $value): self + { + $this->options['value'] = $value; + + return $this; + } + + public function toArray(): array + { + return $this->options; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Image.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Image.php new file mode 100644 index 0000000000000..49f65bae9d8f0 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Field/Image.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Section\Field; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#image-object + */ +final class Image +{ + private $options = []; + + public function image(string $imageUrl): self + { + $this->options['image'] = $imageUrl; + + return $this; + } + + public function title(string $title): self + { + $this->options['title'] = $title; + + return $this; + } + + public function toArray(): array + { + return $this->options; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Section.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Section.php new file mode 100644 index 0000000000000..eb0856f05bf97 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/Section.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Section; + +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\ActionInterface; +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\Section\Field\Activity; +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\Section\Field\Fact; +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\Section\Field\Image; + +/** + * @author Edouard Lescot + * @author Oskar Stark + * + * @see https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#section-fields + */ +final class Section implements SectionInterface +{ + private $options = []; + + public function title(string $title): self + { + $this->options['title'] = $title; + + return $this; + } + + public function text(string $text): self + { + $this->options['text'] = $text; + + return $this; + } + + public function action(ActionInterface $action): self + { + $this->options['potentialAction'][] = $action->toArray(); + + return $this; + } + + public function activity(Activity $activity): self + { + foreach ($activity->toArray() as $key => $element) { + $this->options[$key] = $element; + } + + return $this; + } + + public function image(Image $image): self + { + $this->options['images'][] = $image->toArray(); + + return $this; + } + + public function fact(Fact $fact): self + { + $this->options['facts'][] = $fact->toArray(); + + return $this; + } + + public function markdown(bool $markdown): self + { + $this->options['markdown'] = $markdown; + + return $this; + } + + public function toArray(): array + { + return $this->options; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/SectionInterface.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/SectionInterface.php new file mode 100644 index 0000000000000..0ca5e22e75ff9 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Section/SectionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Section; + +/** + * @author Edouard Lescot + * @author Oskar Stark + */ +interface SectionInterface +{ + public function toArray(): array; +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Test/Action/Input/AbstractInputTestCase.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Test/Action/Input/AbstractInputTestCase.php new file mode 100644 index 0000000000000..fc26c37bebbd6 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Test/Action/Input/AbstractInputTestCase.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MicrosoftTeams\Test\Action\Input; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\Action\Input\AbstractInput; + +/** + * @author Oskar Stark + */ +abstract class AbstractInputTestCase extends TestCase +{ + abstract public function createInput(): AbstractInput; + + public function testId() + { + $input = $this->createInput(); + + $input->id($value = '1234'); + + $this->assertSame($value, $input->toArray()['id']); + } + + public function testIsRequiredWithFalse() + { + $input = $this->createInput(); + + $input->isRequired(false); + + $this->assertFalse($input->toArray()['isRequired']); + } + + public function testIsRequiredWithTrue() + { + $input = $this->createInput(); + + $input->isRequired(true); + + $this->assertTrue($input->toArray()['isRequired']); + } + + public function testTitle() + { + $input = $this->createInput(); + + $input->title($value = 'Hey Symfony!'); + + $this->assertSame($value, $input->toArray()['title']); + } + + public function testValue() + { + $input = $this->createInput(); + + $input->value($value = 'Community power!'); + + $this->assertSame($value, $input->toArray()['value']); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/ActionCardTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/ActionCardTest.php new file mode 100644 index 0000000000000..27496f3266fa2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/ActionCardTest.php @@ -0,0 +1,71 @@ +name($value = 'My name'); + + $this->assertSame($value, $action->toArray()['name']); + } + + /** + * @dataProvider availableInputs + */ + public function testInput(array $expected, InputInterface $input) + { + $action = (new ActionCard()) + ->input($input); + + $this->assertCount(1, $action->toArray()['inputs']); + $this->assertSame($expected, $action->toArray()['inputs']); + } + + public function availableInputs(): \Generator + { + yield [[['@type' => 'DateInput']], new DateInput()]; + yield [[['@type' => 'TextInput']], new TextInput()]; + yield [[['@type' => 'MultichoiceInput']], new MultiChoiceInput()]; + } + + /** + * @dataProvider compatibleActions + */ + public function testAction(array $expected, ActionCardCompatibleActionInterface $action) + { + $section = (new ActionCard()) + ->action($action); + + $this->assertCount(1, $section->toArray()['actions']); + $this->assertSame($expected, $section->toArray()['actions']); + } + + public function compatibleActions(): \Generator + { + yield [[['@type' => 'HttpPOST']], new HttpPostAction()]; + yield [[['@type' => 'OpenUri']], new OpenUriAction()]; + } + + public function testToArray() + { + $this->assertSame( + [ + '@type' => 'ActionCard', + ], + (new ActionCard())->toArray() + ); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Element/HeaderTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Element/HeaderTest.php new file mode 100644 index 0000000000000..e6677e6acaca2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Element/HeaderTest.php @@ -0,0 +1,25 @@ +name($value = 'My name'); + + $this->assertSame($value, $action->toArray()['name']); + } + + public function testValue() + { + $action = (new Header()) + ->value($value = 'The value...'); + + $this->assertSame($value, $action->toArray()['value']); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/HttpPostActionTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/HttpPostActionTest.php new file mode 100644 index 0000000000000..cd1794b1662a1 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/HttpPostActionTest.php @@ -0,0 +1,70 @@ +name($value = 'My name'); + + $this->assertSame($value, $action->toArray()['name']); + } + + public function testTarget() + { + $action = (new HttpPostAction()) + ->target($value = 'https://symfony.com'); + + $this->assertSame($value, $action->toArray()['target']); + } + + public function testHeader() + { + $header = (new Header()) + ->name($name = 'Header-Name') + ->value($value = 'Header-Value'); + + $action = (new HttpPostAction()) + ->header($header); + + $this->assertCount(1, $action->toArray()['headers']); + $this->assertSame( + [ + ['name' => $name, 'value' => $value], + ], + $action->toArray()['headers'] + ); + } + + public function testBody() + { + $action = (new HttpPostAction()) + ->body($value = 'content'); + + $this->assertSame($value, $action->toArray()['body']); + } + + public function testBodyContentType() + { + $action = (new HttpPostAction()) + ->bodyContentType($value = 'application/json'); + + $this->assertSame($value, $action->toArray()['bodyContentType']); + } + + public function testToArray() + { + $this->assertSame( + [ + '@type' => 'HttpPOST', + ], + (new HttpPostAction())->toArray() + ); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/DateInputTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/DateInputTest.php new file mode 100644 index 0000000000000..6cb843177cf99 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/DateInputTest.php @@ -0,0 +1,44 @@ +createInput() + ->includeTime(true); + + $this->assertTrue($input->toArray()['includeTime']); + } + + public function testIncludeTimeWithFalse() + { + $input = $this->createInput() + ->includeTime(false); + + $this->assertFalse($input->toArray()['includeTime']); + } + + public function testToArray() + { + $this->assertSame( + [ + '@type' => 'DateInput', + ], + $this->createInput()->toArray() + ); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/MultiChoiceInputTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/MultiChoiceInputTest.php new file mode 100644 index 0000000000000..6cdded3eaa58b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/MultiChoiceInputTest.php @@ -0,0 +1,88 @@ +createInput() + ->choice($display = 'DISPLAY', $value = 'VALUE'); + + $this->assertSame( + [ + ['display' => $display, 'value' => $value], + ], + $input->toArray()['choices'] + ); + } + + public function testIsMultiSelectWithTrue() + { + $input = $this->createInput() + ->isMultiSelect(true); + + $this->assertTrue($input->toArray()['isMultiSelect']); + } + + public function testIsMultiSelectWithFalse() + { + $input = $this->createInput() + ->isMultiSelect(false); + + $this->assertFalse($input->toArray()['isMultiSelect']); + } + + /** + * @dataProvider styles + */ + public function testStyle(string $value) + { + $input = $this->createInput() + ->style($value); + + $this->assertSame($value, $input->toArray()['style']); + } + + /** + * @return \Generator + */ + public function styles(): \Generator + { + yield 'style-expanded' => ['expanded']; + yield 'style-normal' => ['normal']; + } + + /** + * @dataProvider styles + */ + public function testStyleThrowsWithUnknownStyle() + { + $this->expectException(InvalidArgumentException::class); + + $this->createInput()->style('red'); + } + + public function testToArray() + { + $this->assertSame( + [ + '@type' => 'MultichoiceInput', + ], + $this->createInput()->toArray() + ); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/TextInputTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/TextInputTest.php new file mode 100644 index 0000000000000..fa958ec298cd3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/Input/TextInputTest.php @@ -0,0 +1,52 @@ +createInput() + ->isMultiline(true); + + $this->assertTrue($input->toArray()['isMultiline']); + } + + public function testIsMultilineWithFalse() + { + $input = $this->createInput() + ->isMultiline(false); + + $this->assertFalse($input->toArray()['isMultiline']); + } + + public function testMaxLength() + { + $input = $this->createInput() + ->maxLength($value = 10); + + $this->assertSame($value, $input->toArray()['maxLength']); + } + + public function testToArray() + { + $this->assertSame( + [ + '@type' => 'TextInput', + ], + $this->createInput()->toArray() + ); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/InvokeAddInCommandActionTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/InvokeAddInCommandActionTest.php new file mode 100644 index 0000000000000..b877793f57cca --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/InvokeAddInCommandActionTest.php @@ -0,0 +1,55 @@ +name($value = 'My name'); + + $this->assertSame($value, $action->toArray()['name']); + } + + public function testAddInId() + { + $action = (new InvokeAddInCommandAction()) + ->addInId($value = '1234'); + + $this->assertSame($value, $action->toArray()['addInId']); + } + + public function testDesktopCommandId() + { + $action = (new InvokeAddInCommandAction()) + ->desktopCommandId($value = '324'); + + $this->assertSame($value, $action->toArray()['desktopCommandId']); + } + + public function testInitializationContext() + { + $value = [ + 'foo' => 'bar', + ]; + + $action = (new InvokeAddInCommandAction()) + ->initializationContext($value); + + $this->assertSame($value, $action->toArray()['initializationContext']); + } + + public function testToArray() + { + $this->assertSame( + [ + '@type' => 'InvokeAddInCommand', + ], + (new InvokeAddInCommandAction())->toArray() + ); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/OpenUriActionTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/OpenUriActionTest.php new file mode 100644 index 0000000000000..458f25b6e20a0 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Action/OpenUriActionTest.php @@ -0,0 +1,75 @@ +name($value = 'My name'); + + $this->assertSame($value, $action->toArray()['name']); + } + + public function testTargetWithDefaultValue() + { + $action = (new OpenUriAction()) + ->target($uri = 'URI'); + + $this->assertSame( + [ + ['os' => 'default', 'uri' => $uri], + ], + $action->toArray()['targets'] + ); + } + + /** + * @dataProvider operatingSystems + */ + public function testTarget(string $os) + { + $action = (new OpenUriAction()) + ->target($uri = 'URI', $os); + + $this->assertSame( + [ + ['os' => $os, 'uri' => $uri], + ], + $action->toArray()['targets'] + ); + } + + /** + * @return \Generator + */ + public function operatingSystems(): \Generator + { + yield 'os-android' => ['android']; + yield 'os-default' => ['default']; + yield 'os-ios' => ['iOS']; + yield 'os-windows' => ['windows']; + } + + public function testTargetThrowsWithUnknownOperatingSystem() + { + $this->expectException(InvalidArgumentException::class); + + (new OpenUriAction())->target('URI', 'FOO'); + } + + public function testToArray() + { + $this->assertSame( + [ + '@type' => 'OpenUri', + ], + (new OpenUriAction())->toArray() + ); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/MicrosoftTeamsOptionsTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/MicrosoftTeamsOptionsTest.php new file mode 100644 index 0000000000000..3c766ffdb4592 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/MicrosoftTeamsOptionsTest.php @@ -0,0 +1,285 @@ +content($content = 'Content'); + + $this->assertSame( + [ + 'title' => $subject, + 'text' => $content, + '@type' => 'MessageCard', + '@context' => 'https://schema.org/extensions', + ], + (MicrosoftTeamsOptions::fromNotification($notification))->toArray() + ); + } + + public function testGetRecipientIdReturnsRecipientWhenSetViaConstructor() + { + $options = new MicrosoftTeamsOptions([ + 'recipient_id' => $recipient = '/webhook/foo', + ]); + + $this->assertSame($recipient, $options->getRecipientId()); + } + + public function testGetRecipientIdReturnsRecipientWhenSetSetter() + { + $options = (new MicrosoftTeamsOptions()) + ->recipient($recipient = '/webhook/foo'); + + $this->assertSame($recipient, $options->getRecipientId()); + } + + public function testGetRecipientIdReturnsNullIfNotSetViaConstructorAndSetter() + { + $options = new MicrosoftTeamsOptions(); + + $this->assertNull($options->getRecipientId()); + } + + public function testRecipientMethodThrowsIfValueDoesNotMatchRegex() + { + $options = new MicrosoftTeamsOptions(); + + $recipient = 'foo'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('"%s" require recipient id format to be "/webhook/{uuid}@{uuid}/IncomingWebhook/{id}/{uuid}", "%s" given.', MicrosoftTeamsOptions::class, $recipient)); + + $options->recipient($recipient); + } + + public function testSummaryViaConstructor() + { + $options = new MicrosoftTeamsOptions([ + 'summary' => $summary = 'My summary', + ]); + + $this->assertSame($summary, $options->toArray()['summary']); + } + + public function testSummaryViaSetter() + { + $options = (new MicrosoftTeamsOptions()) + ->summary($summary = 'My summary'); + + $this->assertSame($summary, $options->toArray()['summary']); + } + + public function testTitleViaConstructor() + { + $options = new MicrosoftTeamsOptions([ + 'title' => $title = 'My title', + ]); + + $this->assertSame($title, $options->toArray()['title']); + } + + public function testTitleViaSetter() + { + $options = (new MicrosoftTeamsOptions()) + ->title($title = 'My title'); + + $this->assertSame($title, $options->toArray()['title']); + } + + public function testTextViaConstructor() + { + $options = new MicrosoftTeamsOptions([ + 'text' => $text = 'My text', + ]); + + $this->assertSame($text, $options->toArray()['text']); + } + + public function testTextViaSetter() + { + $options = (new MicrosoftTeamsOptions()) + ->text($text = 'My text'); + + $this->assertSame($text, $options->toArray()['text']); + } + + /** + * @dataProvider validThemeColors + */ + public function testThemeColorViaConstructor(string $themeColor) + { + $options = new MicrosoftTeamsOptions([ + 'themeColor' => $themeColor, + ]); + + $this->assertSame($themeColor, $options->toArray()['themeColor']); + } + + /** + * @dataProvider validThemeColors + */ + public function testThemeColorViaSetter(string $themeColor) + { + $options = (new MicrosoftTeamsOptions()) + ->themeColor($themeColor); + + $this->assertSame($themeColor, $options->toArray()['themeColor']); + } + + public function validThemeColors(): \Generator + { + yield ['#333']; + yield ['#333333']; + yield ['#fff']; + yield ['#ff0000']; + yield ['#FFF']; + yield ['#FF0000']; + } + + /** + * @dataProvider invalidThemeColors + */ + public function testThemeColorViaConstructorThrowsInvalidArgumentException(string $themeColor) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('MessageCard themeColor must have a valid hex color format.'); + + new MicrosoftTeamsOptions([ + 'themeColor' => $themeColor, + ]); + } + + /** + * @dataProvider invalidThemeColors + */ + public function testThemeColorViaSetterThrowsInvalidArgumentException(string $themeColor) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('MessageCard themeColor must have a valid hex color format.'); + + (new MicrosoftTeamsOptions()) + ->themeColor($themeColor); + } + + public function invalidThemeColors(): \Generator + { + yield ['']; + yield [' ']; + yield ['red']; + yield ['#1']; + yield ['#22']; + yield ['#4444']; + yield ['#55555']; + } + + public function testSectionViaConstructor() + { + $options = new MicrosoftTeamsOptions([ + 'sections' => $sections = [(new Section())->toArray()], + ]); + + $this->assertSame($sections, $options->toArray()['sections']); + } + + public function testSectionViaSetter() + { + $options = (new MicrosoftTeamsOptions()) + ->section($section = new Section()); + + $this->assertSame([$section->toArray()], $options->toArray()['sections']); + } + + public function testActionViaConstructor() + { + $options = new MicrosoftTeamsOptions([ + 'potentialAction' => $actions = [(new OpenUriAction())->toArray()], + ]); + + $this->assertSame($actions, $options->toArray()['potentialAction']); + } + + public function testActionViaSetter() + { + $options = (new MicrosoftTeamsOptions()) + ->action($action = new OpenUriAction()); + + $this->assertSame([$action->toArray()], $options->toArray()['potentialAction']); + } + + public function testActionViaConstructorThrowsIfMaxNumberOfActionsIsReached() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('MessageCard maximum number of "potentialAction" has been reached (4).'); + + new MicrosoftTeamsOptions([ + 'potentialAction' => [ + new OpenUriAction(), + new OpenUriAction(), + new OpenUriAction(), + new OpenUriAction(), + new OpenUriAction(), + ], + ]); + } + + public function testActionViaSetterThrowsIfMaxNumberOfActionsIsReached() + { + $options = (new MicrosoftTeamsOptions()) + ->action(new OpenUriAction()) + ->action(new OpenUriAction()) + ->action(new OpenUriAction()) + ->action(new OpenUriAction()); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('MessageCard maximum number of "potentialAction" has been reached (4).'); + + $options->action(new OpenUriAction()); + } + + public function testExpectedActorViaConstructor() + { + $options = new MicrosoftTeamsOptions([ + 'expectedActors' => $expectedActors = ['Oskar'], + ]); + + $this->assertSame($expectedActors, $options->toArray()['expectedActors']); + } + + public function testExpectedActorViaSetter() + { + $options = (new MicrosoftTeamsOptions()) + ->expectedActor($expectedActor = 'Oskar'); + + $this->assertSame([$expectedActor], $options->toArray()['expectedActors']); + } + + public function testExpectedActorsViaConstructor() + { + $options = new MicrosoftTeamsOptions([ + 'expectedActors' => $expectedActors = ['Oskar', 'Fabien'], + ]); + + $this->assertSame($expectedActors, $options->toArray()['expectedActors']); + } + + public function testExpectedActorsViaSetter() + { + $options = (new MicrosoftTeamsOptions()) + ->expectedActor($expectedActor1 = 'Oskar') + ->expectedActor($expectedActor2 = 'Fabien') + ; + + $this->assertSame([$expectedActor1, $expectedActor2], $options->toArray()['expectedActors']); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/MicrosoftTeamsTransportTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/MicrosoftTeamsTransportTest.php index a583bed135440..5c4517bd85b42 100644 --- a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/MicrosoftTeamsTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/MicrosoftTeamsTransportTest.php @@ -13,11 +13,13 @@ use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsOptions; use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransport; use Symfony\Component\Notifier\Exception\TransportException; use Symfony\Component\Notifier\Message\ChatMessage; use Symfony\Component\Notifier\Message\MessageInterface; use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Notification\Notification; use Symfony\Component\Notifier\Test\TransportTestCase; use Symfony\Component\Notifier\Transport\TransportInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -79,7 +81,9 @@ public function testSend() { $message = 'testMessage'; - $expectedBody = json_encode(['text' => $message]); + $expectedBody = json_encode([ + 'text' => $message, + ]); $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($expectedBody): ResponseInterface { $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); @@ -91,4 +95,73 @@ public function testSend() $transport->send(new ChatMessage($message)); } + + public function testSendWithOptionsAndTextOverwritesChatMessage() + { + $message = 'testMessage'; + $options = new MicrosoftTeamsOptions([ + 'text' => $optionsTextMessage = 'optionsTestMessage', + ]); + + $expectedBody = json_encode([ + 'text' => $optionsTextMessage, + ]); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($expectedBody): ResponseInterface { + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return new MockResponse('1', ['response_headers' => ['request-id' => ['testRequestId']], 'http_code' => 200]); + }); + + $transport = $this->createTransport($client); + + $transport->send(new ChatMessage($message, $options)); + } + + public function testSendWithOptionsAsMessageCard() + { + $title = 'title'; + $message = 'testMessage'; + + $options = new MicrosoftTeamsOptions([ + 'title' => $title, + ]); + + $expectedBody = json_encode([ + '@context' => 'https://schema.org/extensions', + '@type' => 'MessageCard', + 'text' => $message, + 'title' => $title, + ]); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($expectedBody): ResponseInterface { + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return new MockResponse('1', ['response_headers' => ['request-id' => ['testRequestId']], 'http_code' => 200]); + }); + + $transport = $this->createTransport($client); + + $transport->send(new ChatMessage($message, $options)); + } + + public function testSendFromNotification() + { + $notification = new Notification($message = 'testMessage'); + $chatMessage = ChatMessage::fromNotification($notification); + + $expectedBody = json_encode([ + 'text' => $message, + ]); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($expectedBody): ResponseInterface { + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return new MockResponse('1', ['response_headers' => ['request-id' => ['testRequestId']], 'http_code' => 200]); + }); + + $transport = $this->createTransport($client); + + $transport->send($chatMessage); + } } diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/ActivityTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/ActivityTest.php new file mode 100644 index 0000000000000..3acd868cf87c3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/ActivityTest.php @@ -0,0 +1,41 @@ +image($value = 'https://symfony.com/logo.png'); + + $this->assertSame($value, $field->toArray()['activityImage']); + } + + public function testTitle() + { + $field = (new Activity()) + ->title($value = 'Symfony is great!'); + + $this->assertSame($value, $field->toArray()['activityTitle']); + } + + public function testSubtitle() + { + $field = (new Activity()) + ->subtitle($value = 'I am a subtitle!'); + + $this->assertSame($value, $field->toArray()['activitySubtitle']); + } + + public function testText() + { + $field = (new Activity()) + ->text($value = 'Text goes here'); + + $this->assertSame($value, $field->toArray()['activityText']); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/FactTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/FactTest.php new file mode 100644 index 0000000000000..17667c5c942e3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/FactTest.php @@ -0,0 +1,25 @@ +name($value = 'Current version'); + + $this->assertSame($value, $field->toArray()['name']); + } + + public function testTitle() + { + $field = (new Fact()) + ->value($value = '5.3'); + + $this->assertSame($value, $field->toArray()['value']); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/ImageTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/ImageTest.php new file mode 100644 index 0000000000000..bed326e6fbf85 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/Field/ImageTest.php @@ -0,0 +1,25 @@ +image($value = 'https://symfony.com/logo.png'); + + $this->assertSame($value, $field->toArray()['image']); + } + + public function testTitle() + { + $field = (new Image()) + ->title($value = 'Symfony is great!'); + + $this->assertSame($value, $field->toArray()['title']); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/SectionTest.php b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/SectionTest.php new file mode 100644 index 0000000000000..eb4cef597a9f9 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MicrosoftTeams/Tests/Section/SectionTest.php @@ -0,0 +1,127 @@ +title($value = 'Symfony is great!'); + + $this->assertSame($value, $section->toArray()['title']); + } + + public function testText() + { + $section = (new Section()) + ->text($value = 'Community power is awesome!'); + + $this->assertSame($value, $section->toArray()['text']); + } + + /** + * @dataProvider allowedActions + */ + public function testAction(array $expected, ActionInterface $action) + { + $section = (new Section()) + ->action($action); + + $this->assertCount(1, $section->toArray()['potentialAction']); + $this->assertSame($expected, $section->toArray()['potentialAction']); + } + + public function allowedActions(): \Generator + { + yield [[['@type' => 'ActionCard']], new ActionCard()]; + yield [[['@type' => 'HttpPOST']], new HttpPostAction()]; + yield [[['@type' => 'InvokeAddInCommand']], new InvokeAddInCommandAction()]; + yield [[['@type' => 'OpenUri']], new OpenUriAction()]; + } + + public function testActivity() + { + $activity = (new Activity()) + ->image($imageUrl = 'https://symfony.com/logo.png') + ->title($title = 'Activities') + ->subtitle($subtitle = 'for Admins only') + ->text($text = 'Hey Symfony!'); + + $section = (new Section()) + ->activity($activity); + + $this->assertSame( + [ + 'activityImage' => $imageUrl, + 'activityTitle' => $title, + 'activitySubtitle' => $subtitle, + 'activityText' => $text, + ], + $section->toArray() + ); + } + + public function testImage() + { + $image = (new Image()) + ->image($imageUrl = 'https://symfony.com/logo.png') + ->title($title = 'Symfony logo'); + + $section = (new Section()) + ->image($image); + + $this->assertCount(1, $section->toArray()['images']); + $this->assertSame( + [ + ['image' => $imageUrl, 'title' => $title], + ], + $section->toArray()['images'] + ); + } + + public function testFact() + { + $fact = (new Fact()) + ->name($name = 'Current version') + ->value($value = '5.3'); + + $section = (new Section()) + ->fact($fact); + + $this->assertCount(1, $section->toArray()['facts']); + $this->assertSame( + [ + ['name' => $name, 'value' => $value], + ], + $section->toArray()['facts'] + ); + } + + public function testMarkdownWithTrue() + { + $action = (new Section()) + ->markdown(true); + + $this->assertTrue($action->toArray()['markdown']); + } + + public function testMarkdownWithFalse() + { + $action = (new Section()) + ->markdown(false); + + $this->assertFalse($action->toArray()['markdown']); + } +}