feature #37830 [Notifier] Add LinkedIn provider (ismail1432)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[Notifier] Add LinkedIn provider

| Q             | A
| ------------- | ---
| Branch?       | master for features 5.2
| Bug fix?      | no
| New feature?  | yes/no <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | yes/no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets       | Fix #34563 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT
| Doc PR        | symfony/symfony-docs#... <!-- required for new features -->

### Add the LinkedIn provider for the Notifier

Few months ago I created a bridge to send message to LinkedIn, following [the discussion here](https://github.com/symfony/symfony/issues/34563) I create the PR.

If you're curious [I wrote an article where I use the bridge](https://medium.com/@smaine.milianni/aws-lambda-and-symfony-6d3e9831c3cd)

[Edit] test are green ~missing improvement body construction and integration test with update changes + framework integration~

[Notification sent with this PR](https://www.linkedin.com/posts/smainemilianni_hello-linkedin-from-symfony-bridge-see-pull-activity-6700328970665177088-31tT)
<!--
Replace this notice by a short README for your feature/bugfix. This will help people
understand your PR and can be used as a start for the documentation.

Additionally (see https://symfony.com/releases):
 - Always add tests and ensure they pass.
 - Never break backward compatibility (see https://symfony.com/bc).
 - Bug fixes must be submitted against the lowest maintained branch where they apply
   (lowest branches are regularly merged to upper ones so they get the fixes too.)
 - Features and deprecations must be submitted against branch master.
-->

Commits
-------

0064cae9a0 add Linkedin transport and option
This commit is contained in:
Fabien Potencier 2020-08-18 11:12:14 +02:00
commit 14c9d05dc8
15 changed files with 927 additions and 0 deletions

View File

@ -15,6 +15,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\Infobip\InfobipTransportFactory;
use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory;
use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory;
use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory;
use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory;
@ -38,6 +39,10 @@ return static function (ContainerConfigurator $container) {
->parent('notifier.transport_factory.abstract')
->tag('chatter.transport_factory')
->set('notifier.transport_factory.linkedin', LinkedInTransportFactory::class)
->parent('notifier.transport_factory.abstract')
->tag('chatter.transport_factory')
->set('notifier.transport_factory.telegram', TelegramTransportFactory::class)
->parent('notifier.transport_factory.abstract')
->tag('chatter.transport_factory')

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
5.2.0
-----
* Added the bridge

View File

@ -0,0 +1,127 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Notifier\Bridge\LinkedIn;
use Symfony\Component\Notifier\Bridge\LinkedIn\Share\AuthorShare;
use Symfony\Component\Notifier\Bridge\LinkedIn\Share\LifecycleStateShare;
use Symfony\Component\Notifier\Bridge\LinkedIn\Share\ShareContentShare;
use Symfony\Component\Notifier\Bridge\LinkedIn\Share\VisibilityShare;
use Symfony\Component\Notifier\Message\MessageOptionsInterface;
use Symfony\Component\Notifier\Notification\Notification;
/**
* @author Smaïne Milianni <smaine.milianni@gmail.com>
*
* @experimental in 5.2
*/
final class LinkedInOptions implements MessageOptionsInterface
{
private $options = [];
public function __construct(array $options = [])
{
$this->options = $options;
}
public function toArray(): array
{
return $this->options;
}
public function getRecipientId(): ?string
{
return null;
}
public static function fromNotification(Notification $notification): self
{
$options = new self();
$options->specificContent(new ShareContentShare($notification->getSubject()));
if ($notification->getContent()) {
$options->specificContent(new ShareContentShare($notification->getContent()));
}
$options->visibility(new VisibilityShare());
$options->lifecycleState(new LifecycleStateShare());
return $options;
}
public function contentCertificationRecord(string $contentCertificationRecord): self
{
$this->options['contentCertificationRecord'] = $contentCertificationRecord;
return $this;
}
public function firstPublishedAt(int $firstPublishedAt): self
{
$this->options['firstPublishedAt'] = $firstPublishedAt;
return $this;
}
public function lifecycleState(LifecycleStateShare $lifecycleStateOption): self
{
$this->options['lifecycleState'] = $lifecycleStateOption->lifecycleState();
return $this;
}
public function origin(string $origin): self
{
$this->options['origin'] = $origin;
return $this;
}
public function ugcOrigin(string $ugcOrigin): self
{
$this->options['ugcOrigin'] = $ugcOrigin;
return $this;
}
public function versionTag(string $versionTag): self
{
$this->options['versionTag'] = $versionTag;
return $this;
}
public function specificContent(ShareContentShare $specificContent): self
{
$this->options['specificContent']['com.linkedin.ugc.ShareContent'] = $specificContent->toArray();
return $this;
}
public function author(AuthorShare $authorOption): self
{
$this->options['author'] = $authorOption->author();
return $this;
}
public function visibility(VisibilityShare $visibilityOption): self
{
$this->options['visibility'] = $visibilityOption->toArray();
return $this;
}
public function getAuthor(): ?string
{
return $this->options['author'] ?? null;
}
}

View File

@ -0,0 +1,115 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Notifier\Bridge\LinkedIn;
use Symfony\Component\Notifier\Bridge\LinkedIn\Share\AuthorShare;
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 Smaïne Milianni <smaine.milianni@gmail.com>
*
* @experimental in 5.2
*
* @see https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api#sharecontent
*/
final class LinkedInTransport extends AbstractTransport
{
protected const PROTOCOL_VERSION = '2.0.0';
protected const PROTOCOL_HEADER = 'X-Restli-Protocol-Version';
protected const HOST = 'api.linkedin.com';
private $authToken;
private $accountId;
public function __construct(string $authToken, string $accountId, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)
{
$this->authToken = $authToken;
$this->accountId = $accountId;
parent::__construct($client, $dispatcher);
}
public function __toString(): string
{
return sprintf('linkedin://%s', $this->getEndpoint());
}
public function supports(MessageInterface $message): bool
{
return $message instanceof ChatMessage && (null === $message->getOptions() || $message->getOptions() instanceof LinkedInOptions);
}
/**
* @see https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api
*/
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 LinkedInOptions) {
throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" for options.', __CLASS__, LinkedInOptions::class));
}
if (!($opts = $message->getOptions()) && $notification = $message->getNotification()) {
$opts = LinkedInOptions::fromNotification($notification);
$opts->author(new AuthorShare($this->accountId));
}
$endpoint = sprintf('https://%s/v2/ugcPosts', $this->getEndpoint());
$response = $this->client->request('POST', $endpoint, [
'auth_bearer' => $this->authToken,
'headers' => [self::PROTOCOL_HEADER => self::PROTOCOL_VERSION],
'json' => array_filter($opts ? $opts->toArray() : $this->bodyFromMessageWithNoOption($message)),
]);
if (201 !== $response->getStatusCode()) {
throw new TransportException(sprintf('Unable to post the Linkedin message: "%s".', $response->getContent(false)), $response);
}
$result = $response->toArray(false);
if (!$result['id']) {
throw new TransportException(sprintf('Unable to post the Linkedin message : "%s".', $result['error']), $response);
}
return new SentMessage($message, (string) $this);
}
private function bodyFromMessageWithNoOption(MessageInterface $message): array
{
return [
'specificContent' => [
'com.linkedin.ugc.ShareContent' => [
'shareCommentary' => [
'attributes' => [],
'text' => $message->getSubject(),
],
'shareMediaCategory' => 'NONE',
],
],
'visibility' => [
'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC',
],
'lifecycleState' => 'PUBLISHED',
'author' => sprintf('urn:li:person:%s', $this->accountId),
];
}
}

View File

@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Notifier\Bridge\LinkedIn;
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 Smaïne Milianni <smaine.milianni@gmail.com>
*
* @experimental in 5.2
*/
class LinkedInTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
$scheme = $dsn->getScheme();
$authToken = $this->getUser($dsn);
$accountId = $this->getPassword($dsn);
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
$port = $dsn->getPort();
if ('linkedin' === $scheme) {
return (new LinkedInTransport($authToken, $accountId, $this->client, $this->dispatcher))->setHost($host)->setPort($port);
}
throw new UnsupportedSchemeException($dsn, 'linkedin', $this->getSupportedSchemes());
}
protected function getSupportedSchemes(): array
{
return ['linkedin'];
}
}

View File

@ -0,0 +1,20 @@
LinkedIn Notifier
=================
Provides LinkedIn integration for Symfony Notifier.
DSN example
-----------
```
// .env file
LINKEDIN_DSN='linkedin://ACCESS_TOKEN:USER_ID@default'
```
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)

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
/**
* @author Smaïne Milianni <smaine.milianni@gmail.com>
*
* @experimental in 5.2
*/
abstract class AbstractLinkedInShare
{
protected $options = [];
public function toArray(): array
{
return $this->options;
}
}

View File

@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
/**
* @author Smaïne Milianni <smaine.milianni@gmail.com>
*
* @experimental in 5.2
*/
final class AuthorShare extends AbstractLinkedInShare
{
public const PERSON = 'person';
private $author;
public function __construct(string $value, string $organisation = self::PERSON)
{
$this->author = "urn:li:$organisation:$value";
}
public function author(): string
{
return $this->author;
}
}

View File

@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
use Symfony\Component\Notifier\Exception\LogicException;
/**
* @author Smaïne Milianni <smaine.milianni@gmail.com>
*
* @see https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api#schema lifecycleState section
*
* @experimental in 5.2
*/
final class LifecycleStateShare extends AbstractLinkedInShare
{
public const DRAFT = 'DRAFT';
public const PUBLISHED = 'PUBLISHED';
public const PROCESSING = 'PROCESSING';
public const PROCESSING_FAILED = 'PROCESSING_FAILED';
public const DELETED = 'DELETED';
public const PUBLISHED_EDITED = 'PUBLISHED_EDITED';
private const AVAILABLE_LIFECYCLE = [
self::DRAFT,
self::PUBLISHED,
self::PROCESSING_FAILED,
self::DELETED,
self::PROCESSING_FAILED,
self::PUBLISHED_EDITED,
];
private $lifecycleState;
public function __construct(string $lifecycleState = self::PUBLISHED)
{
if (!\in_array($lifecycleState, self::AVAILABLE_LIFECYCLE)) {
throw new LogicException(sprintf('"%s" is not a valid value, available lifecycle are "%s".', $lifecycleState, implode(', ', self::AVAILABLE_LIFECYCLE)));
}
$this->lifecycleState = $lifecycleState;
}
public function lifecycleState(): string
{
return $this->lifecycleState;
}
}

View File

@ -0,0 +1,84 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
use Symfony\Component\Notifier\Exception\LogicException;
/**
* @author Smaïne Milianni <smaine.milianni@gmail.com>
*
* @see https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api#sharecontent
*
* @experimental in 5.2
*/
final class ShareContentShare extends AbstractLinkedInShare
{
public const ARTICLE = 'ARTICLE';
public const IMAGE = 'IMAGE';
public const NONE = 'NONE';
public const RICH = 'RICH';
public const VIDEO = 'VIDEO';
public const LEARNING_COURSE = 'LEARNING_COURSE';
public const JOB = 'JOB';
public const QUESTION = 'QUESTION';
public const ANSWER = 'ANSWER';
public const CAROUSEL = 'CAROUSEL';
public const TOPIC = 'TOPIC';
public const NATIVE_DOCUMENT = 'NATIVE_DOCUMENT';
public const URN_REFERENCE = 'URN_REFERENCE';
public const LIVE_VIDEO = 'LIVE_VIDEO';
public const ALL = [
self::ARTICLE,
self::IMAGE,
self::NONE,
self::RICH,
self::VIDEO,
self::LEARNING_COURSE,
self::JOB,
self::QUESTION,
self::ANSWER,
self::CAROUSEL,
self::TOPIC,
self::NATIVE_DOCUMENT,
self::URN_REFERENCE,
self::LIVE_VIDEO,
];
public function __construct(string $text, array $attributes = [], string $inferredLocale = null, ShareMediaShare $media = null, string $primaryLandingPageUrl = null, string $shareMediaCategory = self::NONE)
{
$this->options['shareCommentary'] = [
'attributes' => $attributes,
'text' => $text,
];
if (null !== $inferredLocale) {
$this->options['shareCommentary']['inferredLocale'] = $inferredLocale;
}
if (null !== $media) {
$this->options['media'] = $media->toArray();
}
if (null !== $primaryLandingPageUrl) {
$this->options['primaryLandingPageUrl'] = $primaryLandingPageUrl;
}
if ($shareMediaCategory) {
if (!\in_array($shareMediaCategory, self::ALL)) {
throw new LogicException(sprintf('"%s" is not valid option, available options are "%s".', $shareMediaCategory, implode(', ', self::ALL)));
}
$this->options['shareMediaCategory'] = $shareMediaCategory;
}
}
}

View File

@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
use Symfony\Component\Notifier\Exception\LogicException;
/**
* @author Smaïne Milianni <smaine.milianni@gmail.com>
*
* @see https://docs.microsoft.com/en-us/linkedin/marketing/integrations/community-management/shares/ugc-post-api#sharemedia
*
* @experimental in 5.2
*/
class ShareMediaShare extends AbstractLinkedInShare
{
public const LEARN_MORE = 'LEARN_MORE';
public const APPLY_NOW = 'APPLY_NOW ';
public const DOWNLOAD = 'DOWNLOAD';
public const GET_QUOTE = 'GET_QUOTE';
public const SIGN_UP = 'SIGN_UP';
public const SUBSCRIBE = 'SUBSCRIBE ';
public const REGISTER = 'REGISTER';
public const ALL = [
self::LEARN_MORE,
self::APPLY_NOW,
self::DOWNLOAD,
self::GET_QUOTE,
self::SIGN_UP,
self::SUBSCRIBE,
self::REGISTER,
];
public function __construct(string $text, array $attributes = [], string $inferredLocale = null, bool $landingPage = false, string $landingPageTitle = null, string $landingPageUrl = null)
{
$this->options['description'] = [
'text' => $text,
'attributes' => $attributes,
];
if ($inferredLocale) {
$this->options['description']['inferredLocale'] = $inferredLocale;
}
if ($landingPage || $landingPageUrl) {
$this->options['landingPage']['landingPageUrl'] = $landingPageUrl;
}
if (null !== $landingPageTitle) {
if (!\in_array($landingPageTitle, self::ALL)) {
throw new LogicException(sprintf('"%s" is not valid option, available options are "%s".', $landingPageTitle, implode(', ', self::ALL)));
}
$this->options['landingPage']['landingPageTitle'] = $landingPageTitle;
}
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Share;
use Symfony\Component\Notifier\Exception\LogicException;
/**
* @author Smaïne Milianni <smaine.milianni@gmail.com>
*
* @experimental in 5.2
*/
final class VisibilityShare extends AbstractLinkedInShare
{
public const MEMBER_NETWORK_VISIBILITY = 'MemberNetworkVisibility';
public const SPONSORED_CONTENT_VISIBILITY = 'SponsoredContentVisibility';
public const CONNECTIONS = 'CONNECTIONS';
public const PUBLIC = 'PUBLIC';
public const LOGGED_IN = 'LOGGED_IN';
public const DARK = 'DARK';
private const MEMBER_NETWORK = [
self::CONNECTIONS,
self::PUBLIC,
self::LOGGED_IN,
];
private const AVAILABLE_VISIBILITY = [
self::MEMBER_NETWORK_VISIBILITY,
self::SPONSORED_CONTENT_VISIBILITY,
];
public function __construct(string $visibility = self::MEMBER_NETWORK_VISIBILITY, string $value = 'PUBLIC')
{
if (!\in_array($visibility, self::AVAILABLE_VISIBILITY)) {
throw new LogicException(sprintf('"%s" is not a valid visibility, available visibility are "%s".', $visibility, implode(', ', self::AVAILABLE_VISIBILITY)));
}
if (self::MEMBER_NETWORK_VISIBILITY === $visibility && !\in_array($value, self::MEMBER_NETWORK)) {
throw new LogicException(sprintf('"%s" is not a valid value, available value for visibility "%s" are "%s".', $value, $visibility, implode(', ', self::MEMBER_NETWORK)));
}
if (self::SPONSORED_CONTENT_VISIBILITY === $visibility && self::DARK !== $value) {
throw new LogicException(sprintf('"%s" is not a valid value, available value for visibility "%s" is "%s".', $value, $visibility, self::DARK));
}
$this->options['com.linkedin.ugc.'.$visibility] = $value;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory;
use Symfony\Component\Notifier\Exception\IncompleteDsnException;
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
use Symfony\Component\Notifier\Transport\Dsn;
final class LinkedInTransportFactoryTest extends TestCase
{
public function testCreateWithDsn(): void
{
$factory = new LinkedInTransportFactory();
$dsn = 'linkedin://login:pass@default';
$transport = $factory->create(Dsn::fromString($dsn));
$transport->setHost('testHost');
$this->assertSame('linkedin://testHost', (string) $transport);
}
public function testSupportsLinkedinScheme(): void
{
$factory = new LinkedInTransportFactory();
$this->assertTrue($factory->supports(Dsn::fromString('linkedin://host/path')));
$this->assertFalse($factory->supports(Dsn::fromString('somethingElse://host/path')));
}
public function testNonLinkedinSchemeThrows(): void
{
$factory = new LinkedInTransportFactory();
$this->expectException(UnsupportedSchemeException::class);
$dsn = 'foo://login:pass@default';
$factory->create(Dsn::fromString($dsn));
}
public function testIncompleteDsnMissingUserThrows(): void
{
$factory = new LinkedInTransportFactory();
$this->expectException(IncompleteDsnException::class);
$factory->create(Dsn::fromString('somethingElse://host/path'));
}
}

View File

@ -0,0 +1,198 @@
<?php
namespace Symfony\Component\Notifier\Bridge\LinkedIn\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransport;
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;
final class LinkedInTransportTest extends TestCase
{
public function testToString(): void
{
$this->assertSame(sprintf('linkedin://host.test'), (string) $this->getTransport());
}
public function testSupportsChatMessage(): void
{
$transport = $this->getTransport();
$this->assertTrue($transport->supports(new ChatMessage('testChatMessage')));
$this->assertFalse($transport->supports($this->createMock(MessageInterface::class)));
}
public function testSendNonChatMessageThrows(): void
{
$this->expectException(LogicException::class);
$transport = $this->getTransport();
$transport->send($this->createMock(MessageInterface::class));
}
public function testSendWithEmptyArrayResponseThrows(): void
{
$this->expectException(TransportException::class);
$response = $this->createMock(ResponseInterface::class);
$response->expects($this->exactly(2))
->method('getStatusCode')
->willReturn(500);
$response->expects($this->once())
->method('getContent')
->willReturn('[]');
$client = new MockHttpClient(static function () use ($response): ResponseInterface {
return $response;
});
$transport = $this->getTransport($client);
$transport->send(new ChatMessage('testMessage'));
}
public function testSendWithErrorResponseThrows(): void
{
$this->expectException(TransportException::class);
$this->expectExceptionMessage('testErrorCode');
$response = $this->createMock(ResponseInterface::class);
$response->expects($this->exactly(2))
->method('getStatusCode')
->willReturn(400);
$response->expects($this->once())
->method('getContent')
->willReturn('testErrorCode');
$client = new MockHttpClient(static function () use ($response): ResponseInterface {
return $response;
});
$transport = $this->getTransport($client);
$transport->send(new ChatMessage('testMessage'));
}
public function testSendWithOptions(): void
{
$message = 'testMessage';
$response = $this->createMock(ResponseInterface::class);
$response->expects($this->exactly(2))
->method('getStatusCode')
->willReturn(201);
$response->expects($this->once())
->method('getContent')
->willReturn(json_encode(['id' => '42']));
$expectedBody = json_encode([
'specificContent' => [
'com.linkedin.ugc.ShareContent' => [
'shareCommentary' => [
'attributes' => [],
'text' => 'testMessage',
],
'shareMediaCategory' => 'NONE',
],
],
'visibility' => [
'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC',
],
'lifecycleState' => 'PUBLISHED',
'author' => 'urn:li:person:MyLogin',
]);
$client = new MockHttpClient(function (string $method, string $url, array $options = []) use (
$response,
$expectedBody
): ResponseInterface {
$this->assertSame($expectedBody, $options['body']);
return $response;
});
$transport = $this->getTransport($client);
$transport->send(new ChatMessage($message));
}
public function testSendWithNotification(): void
{
$message = 'testMessage';
$response = $this->createMock(ResponseInterface::class);
$response->expects($this->exactly(2))
->method('getStatusCode')
->willReturn(201);
$response->expects($this->once())
->method('getContent')
->willReturn(json_encode(['id' => '42']));
$notification = new Notification($message);
$chatMessage = ChatMessage::fromNotification($notification);
$expectedBody = json_encode([
'specificContent' => [
'com.linkedin.ugc.ShareContent' => [
'shareCommentary' => [
'attributes' => [],
'text' => 'testMessage',
],
'shareMediaCategory' => 'NONE',
],
],
'visibility' => [
'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC',
],
'lifecycleState' => 'PUBLISHED',
'author' => 'urn:li:person:MyLogin',
]);
$client = new MockHttpClient(function (string $method, string $url, array $options = []) use (
$response,
$expectedBody
): ResponseInterface {
$this->assertSame($expectedBody, $options['body']);
return $response;
});
$transport = $this->getTransport($client);
$transport->send($chatMessage);
}
public function testSendWithInvalidOptions(): void
{
$this->expectException(LogicException::class);
$client = new MockHttpClient(function (string $method, string $url, array $options = []): ResponseInterface {
return $this->createMock(ResponseInterface::class);
});
$transport = $this->getTransport($client);
$transport->send(new ChatMessage('testMessage', $this->createMock(MessageOptionsInterface::class)));
}
public function getTransport($client = null)
{
return (new LinkedInTransport(
'MyToken',
'MyLogin',
$client ?? $this->createMock(HttpClientInterface::class)
))->setHost('host.test');
}
}

View File

@ -0,0 +1,35 @@
{
"name": "symfony/linkedin-notifier",
"type": "symfony-bridge",
"description": "Symfony LinkedIn Notifier Bridge",
"keywords": ["linkedin", "notifier"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Smaïne Milianni",
"email": "smaine.milianni@gmail.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\\LinkedIn\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "5.2-dev"
}
}
}