mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
first bootstrap for handling calendar range sync frommsgraph
This commit is contained in:
parent
d95d97f8fe
commit
64e07c54fa
@ -47,7 +47,7 @@ class RemoteCalendarMSGraphSyncController
|
|||||||
throw new BadRequestHttpException('could not decode json', $e);
|
throw new BadRequestHttpException('could not decode json', $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->messageBus->dispatch(new MSGraphChangeNotificationMessage($body));
|
$this->messageBus->dispatch(new MSGraphChangeNotificationMessage($body, $userId));
|
||||||
|
|
||||||
return new Response('', Response::HTTP_ACCEPTED);
|
return new Response('', Response::HTTP_ACCEPTED);
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
*/
|
*/
|
||||||
private ?User $user = null;
|
private ?User $user = null;
|
||||||
|
|
||||||
public function getCalendar(): Calendar
|
public function getCalendar(): ?Calendar
|
||||||
{
|
{
|
||||||
return $this->calendar;
|
return $this->calendar;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\CalendarBundle\Messenger\Handler;
|
||||||
|
|
||||||
|
use Chill\CalendarBundle\Messenger\Message\MSGraphChangeNotificationMessage;
|
||||||
|
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
|
||||||
|
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync\CalendarRangeSyncer;
|
||||||
|
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||||
|
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||||
|
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||||
|
use Chill\MainBundle\Repository\UserRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @AsMessageHandler
|
||||||
|
*/
|
||||||
|
class MSGraphChangeNotificationHandler implements MessageHandlerInterface
|
||||||
|
{
|
||||||
|
private CalendarRangeRepository $calendarRangeRepository;
|
||||||
|
|
||||||
|
private CalendarRangeSyncer $calendarRangeSyncer;
|
||||||
|
|
||||||
|
private CalendarRepository $calendarRepository;
|
||||||
|
|
||||||
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
|
private InviteRepository $inviteRepository;
|
||||||
|
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
|
private MapCalendarToUser $mapCalendarToUser;
|
||||||
|
|
||||||
|
private UserRepository $userRepository;
|
||||||
|
|
||||||
|
public function __invoke(MSGraphChangeNotificationMessage $changeNotificationMessage): void
|
||||||
|
{
|
||||||
|
$user = $this->userRepository->find($changeNotificationMessage->getUserId());
|
||||||
|
|
||||||
|
if (null === $user) {
|
||||||
|
$this->logger->warning(__CLASS__ . ' notification concern non-existent user, skipping');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($changeNotificationMessage->getContent()['value'] as $notification) {
|
||||||
|
$secret = $this->mapCalendarToUser->getSubscriptionSecret($user);
|
||||||
|
|
||||||
|
if ($secret !== ($notification['clientState'] ?? -1)) {
|
||||||
|
$this->logger->warning(__CLASS__ . ' could not validate secret, skipping');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$remoteId = $notification['resourceData']['id'];
|
||||||
|
|
||||||
|
// is this a calendar range ?
|
||||||
|
if (null !== $calendarRange = $this->calendarRangeRepository->findOneBy(['remoteId' => $remoteId])) {
|
||||||
|
$this->calendarRangeSyncer->handleCalendarRangeSync($calendarRange, $notification, $user);
|
||||||
|
$this->em->flush();
|
||||||
|
} elseif (null !== $calendar = $this->calendarRepository->findOneBy(['remoteId' => $remoteId])) {
|
||||||
|
$this->remoteToLocalSyncer->handleCalendarSync($calendar, $notification, $user);
|
||||||
|
$this->em->flush();
|
||||||
|
} elseif (null !== $invite = $this->inviteRepository->findOneBy(['remoteId' => $remoteId])) {
|
||||||
|
$this->remoteToLocalSyncer->handleInviteSync($invite, $notification, $user);
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,15 +13,23 @@ namespace Chill\CalendarBundle\Messenger\Message;
|
|||||||
|
|
||||||
class MSGraphChangeNotificationMessage
|
class MSGraphChangeNotificationMessage
|
||||||
{
|
{
|
||||||
private array $content = [];
|
private array $content;
|
||||||
|
|
||||||
public function __construct(array $content)
|
private int $userId;
|
||||||
|
|
||||||
|
public function __construct(array $content, int $userId)
|
||||||
{
|
{
|
||||||
$this->content = $content;
|
$this->content = $content;
|
||||||
|
$this->userId = $userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getContent(): array
|
public function getContent(): array
|
||||||
{
|
{
|
||||||
return $this->content;
|
return $this->content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUserId(): int
|
||||||
|
{
|
||||||
|
return $this->userId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,10 @@ class MachineHttpClient implements HttpClientInterface
|
|||||||
$this->machineTokenStorage = $machineTokenStorage;
|
$this->machineTokenStorage = $machineTokenStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||||
|
* @throws LogicException if method is not supported
|
||||||
|
*/
|
||||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||||
{
|
{
|
||||||
$options['headers'] = array_merge(
|
$options['headers'] = array_merge(
|
||||||
|
@ -73,6 +73,19 @@ class MapCalendarToUser
|
|||||||
return $value[0] ?? null;
|
return $value[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSubscriptionSecret(User $user): string
|
||||||
|
{
|
||||||
|
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||||
|
throw new LogicException('do not contains msgraph metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists(self::SECRET_SUBSCRIPTION_EVENT, $user->getAttributes()[self::METADATA_KEY])) {
|
||||||
|
throw new LogicException('do not contains secret in msgraph');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user->getAttributes()[self::METADATA_KEY][self::SECRET_SUBSCRIPTION_EVENT];
|
||||||
|
}
|
||||||
|
|
||||||
public function getUserByEmail(string $email): ?array
|
public function getUserByEmail(string $email): ?array
|
||||||
{
|
{
|
||||||
$value = $this->machineHttpClient->request('GET', 'users', [
|
$value = $this->machineHttpClient->request('GET', 'users', [
|
||||||
@ -105,6 +118,15 @@ class MapCalendarToUser
|
|||||||
>= (new DateTimeImmutable('now'))->getTimestamp();
|
>= (new DateTimeImmutable('now'))->getTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasSubscriptionSecret(User $user): bool
|
||||||
|
{
|
||||||
|
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_key_exists(self::SECRET_SUBSCRIPTION_EVENT, $user->getAttributes()[self::METADATA_KEY]);
|
||||||
|
}
|
||||||
|
|
||||||
public function hasUserId(User $user): bool
|
public function hasUserId(User $user): bool
|
||||||
{
|
{
|
||||||
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
if (!array_key_exists(self::METADATA_KEY, $user->getAttributes())) {
|
||||||
|
@ -19,6 +19,7 @@ use Chill\PersonBundle\Entity\Person;
|
|||||||
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use DateTimeZone;
|
use DateTimeZone;
|
||||||
|
use RuntimeException;
|
||||||
use Symfony\Component\Templating\EngineInterface;
|
use Symfony\Component\Templating\EngineInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
@ -28,10 +29,16 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||||||
*/
|
*/
|
||||||
class RemoteEventConverter
|
class RemoteEventConverter
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* valid when the remote string contains also a timezone, like in
|
||||||
|
* lastModifiedDate
|
||||||
|
*/
|
||||||
public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P';
|
public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P';
|
||||||
|
|
||||||
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';
|
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';
|
||||||
|
|
||||||
|
private const REMOTE_DATETIME_WITHOUT_TZ_FORMAT = 'Y-m-d\TH:i:s.u?';
|
||||||
|
|
||||||
private DateTimeZone $defaultDateTimeZone;
|
private DateTimeZone $defaultDateTimeZone;
|
||||||
|
|
||||||
private EngineInterface $engine;
|
private EngineInterface $engine;
|
||||||
@ -147,6 +154,34 @@ class RemoteEventConverter
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function convertStringDateWithoutTimezone(string $date): DateTimeImmutable
|
||||||
|
{
|
||||||
|
$d = DateTimeImmutable::createFromFormat(
|
||||||
|
self::REMOTE_DATETIME_WITHOUT_TZ_FORMAT,
|
||||||
|
$date,
|
||||||
|
self::getRemoteTimeZone()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (false === $d) {
|
||||||
|
throw new RuntimeException("could not convert string date to datetime: {$date}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $d->setTimezone((new DateTimeImmutable())->getTimezone());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function convertStringDateWithTimezone(string $date): DateTimeImmutable
|
||||||
|
{
|
||||||
|
$d = DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT, $date);
|
||||||
|
|
||||||
|
if (false === $d) {
|
||||||
|
throw new RuntimeException("could not convert string date to datetime: {$date}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$d->setTimezone((new DateTimeImmutable())->getTimezone());
|
||||||
|
|
||||||
|
return $d;
|
||||||
|
}
|
||||||
|
|
||||||
public function convertToRemote(array $event): RemoteEvent
|
public function convertToRemote(array $event): RemoteEvent
|
||||||
{
|
{
|
||||||
$startDate =
|
$startDate =
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync;
|
||||||
|
|
||||||
|
use Chill\CalendarBundle\Entity\Calendar;
|
||||||
|
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||||
|
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient;
|
||||||
|
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
class CalendarRangeSyncer
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
|
private HttpClientInterface $machineHttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param EntityManagerInterface $em
|
||||||
|
* @param LoggerInterface $logger
|
||||||
|
* @param MachineHttpClient $machineHttpClient
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
EntityManagerInterface $em,
|
||||||
|
LoggerInterface $logger,
|
||||||
|
HttpClientInterface $machineHttpClient
|
||||||
|
) {
|
||||||
|
$this->em = $em;
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->machineHttpClient = $machineHttpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function handleCalendarRangeSync(CalendarRange $calendarRange, array $notification, User $user): void
|
||||||
|
{
|
||||||
|
switch ($notification['changeType']) {
|
||||||
|
case 'deleted':
|
||||||
|
// test if the notification is not linked to a Calendar
|
||||||
|
if (null !== $calendarRange->getCalendar()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info(__CLASS__ . ' remove a calendar range because deleted on remote calendar');
|
||||||
|
$this->em->remove($calendarRange);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'updated':
|
||||||
|
try {
|
||||||
|
$new = $this->machineHttpClient->request(
|
||||||
|
'GET',
|
||||||
|
'v1.0/' . $notification['resource']
|
||||||
|
)->toArray();
|
||||||
|
} catch (ClientExceptionInterface $clientException) {
|
||||||
|
$this->logger->warning(__CLASS__ . ' could not retrieve event from ms graph. Already deleted ?', [
|
||||||
|
'calendarRangeId' => $calendarRange->getId(),
|
||||||
|
'remoteEventId' => $notification['resource'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastModified = RemoteEventConverter::convertStringDateWithTimezone($new['lastModifiedDateTime']);
|
||||||
|
|
||||||
|
if ($calendarRange->getRemoteAttributes()['lastModifiedDateTime'] === $lastModified->getTimestamp()) {
|
||||||
|
$this->logger->info(__CLASS__ . ' change key is equals. Source is probably a local update', [
|
||||||
|
'calendarRangeId' => $calendarRange->getId(),
|
||||||
|
'remoteEventId' => $notification['resource'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['start']['dateTime']);
|
||||||
|
$endDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['end']['dateTime']);
|
||||||
|
|
||||||
|
$calendarRange
|
||||||
|
->setStartDate($startDate)->setEndDate($endDate)
|
||||||
|
->addRemoteAttributes([
|
||||||
|
'lastModifiedDateTime' => $lastModified->getTimestamp(),
|
||||||
|
'changeKey' => $new['changeKey'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,12 +14,15 @@ namespace Chill\CalendarBundle\RemoteCalendar\DependencyInjection;
|
|||||||
use Chill\CalendarBundle\Command\AzureGrantAdminConsentAndAcquireToken;
|
use Chill\CalendarBundle\Command\AzureGrantAdminConsentAndAcquireToken;
|
||||||
use Chill\CalendarBundle\Command\MapAndSubscribeUserCalendarCommand;
|
use Chill\CalendarBundle\Command\MapAndSubscribeUserCalendarCommand;
|
||||||
use Chill\CalendarBundle\Controller\RemoteCalendarConnectAzureController;
|
use Chill\CalendarBundle\Controller\RemoteCalendarConnectAzureController;
|
||||||
|
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient;
|
||||||
|
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineTokenStorage;
|
||||||
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector;
|
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector;
|
||||||
use Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector;
|
use Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector;
|
||||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
use TheNetworg\OAuth2\Client\Provider\Azure;
|
use TheNetworg\OAuth2\Client\Provider\Azure;
|
||||||
|
|
||||||
class RemoteCalendarCompilerPass implements CompilerPassInterface
|
class RemoteCalendarCompilerPass implements CompilerPassInterface
|
||||||
@ -30,16 +33,21 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
|
|||||||
$connector = null;
|
$connector = null;
|
||||||
|
|
||||||
if (!$config['remote_calendars_sync']['enabled']) {
|
if (!$config['remote_calendars_sync']['enabled']) {
|
||||||
$connector = MSGraphRemoteCalendarConnector::class;
|
$connector = NullRemoteCalendarConnector::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) {
|
if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) {
|
||||||
$connector = MSGraphRemoteCalendarConnector::class;
|
$connector = MSGraphRemoteCalendarConnector::class;
|
||||||
|
|
||||||
|
$container->setAlias(HttpClientInterface::class.' $machineHttpClient', MachineHttpClient::class);
|
||||||
} else {
|
} else {
|
||||||
// remove services which cannot be loaded
|
// remove services which cannot be loaded
|
||||||
$container->removeDefinition(MapAndSubscribeUserCalendarCommand::class);
|
$container->removeDefinition(MapAndSubscribeUserCalendarCommand::class);
|
||||||
$container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class);
|
$container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class);
|
||||||
$container->removeDefinition(RemoteCalendarConnectAzureController::class);
|
$container->removeDefinition(RemoteCalendarConnectAzureController::class);
|
||||||
|
$container->removeDefinition(MachineTokenStorage::class);
|
||||||
|
$container->removeDefinition(MachineHttpClient::class);
|
||||||
|
$container->removeDefinition(MSGraphRemoteCalendarConnector::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) {
|
if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) {
|
||||||
@ -58,7 +66,9 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
|
|||||||
->setDecoratedService(RemoteCalendarConnectorInterface::class);
|
->setDecoratedService(RemoteCalendarConnectorInterface::class);
|
||||||
} else {
|
} else {
|
||||||
// keep the container lighter by removing definitions
|
// keep the container lighter by removing definitions
|
||||||
$container->removeDefinition($serviceId);
|
if ($container->hasDefinition($serviceId)) {
|
||||||
|
$container->removeDefinition($serviceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\CalendarBundle\Tests\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Symfony\Component\Messenger\Transport\InMemoryTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class RemoteCalendarMSGraphSyncControllerTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private const SAMPLE_BODY = <<<'JSON'
|
||||||
|
{
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"subscriptionId": "739703eb-80c4-4c03-b15a-ca370f19624b",
|
||||||
|
"subscriptionExpirationDateTime": "2022-06-09T02:40:28-07:00",
|
||||||
|
"changeType": "updated",
|
||||||
|
"resource": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||||
|
"resourceData": {
|
||||||
|
"@odata.type": "#Microsoft.Graph.Event",
|
||||||
|
"@odata.id": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||||
|
"@odata.etag": "W/\"DwAAABYAAAAHduaxajFfTpv0kchk+m1FAAAlyzAU\"",
|
||||||
|
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA="
|
||||||
|
},
|
||||||
|
"clientState": "2k05qlr3ds2KzvUP3Ps4A+642fYaI8ThxHGIGbNr2p0MnNkmzxLTNEMxpMc/UEuDkBHfID7OYWj4DQc94vlEkPBdsh9sGTTkHxIE68hqkKkDKhwvfvdj6lS6Dus=",
|
||||||
|
"tenantId": "421bf216-3f48-47bd-a7cf-8b1995cb24bd"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendNotification(): void
|
||||||
|
{
|
||||||
|
$client = self::createClient();
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
'/public/incoming-hook/calendar/msgraph/events/23',
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
self::SAMPLE_BODY
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertResponseStatusCodeSame(202);
|
||||||
|
|
||||||
|
/* @var InMemoryTransport $transport */
|
||||||
|
$transport = self::$container->get('messenger.transport.async');
|
||||||
|
$this->assertCount(1, $transport->getSent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateSubscription(): void
|
||||||
|
{
|
||||||
|
$client = self::createClient();
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
'/public/incoming-hook/calendar/msgraph/events/23?validationToken=something%20to%20decode'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
|
||||||
|
$response = $client->getResponse();
|
||||||
|
|
||||||
|
$this->assertResponseHasHeader('Content-Type');
|
||||||
|
$this->assertStringContainsString('text/plain', $response->headers->get('Content-Type'));
|
||||||
|
$this->assertEquals('something to decode', $response->getContent());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,228 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\CalendarBundle\Tests\RemoteCalendar\Connector\MSGraph;
|
||||||
|
|
||||||
|
use Chill\CalendarBundle\Entity\Calendar;
|
||||||
|
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||||
|
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync\CalendarRangeSyncer;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
|
||||||
|
class CalendarRangeSyncerTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private const REMOTE_CALENDAR_RANGE = <<<'JSON'
|
||||||
|
{
|
||||||
|
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/events/$entity",
|
||||||
|
"@odata.etag": "W/\"B3bmsWoxX06b9JHIZPptRQAAJcswFA==\"",
|
||||||
|
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||||
|
"createdDateTime": "2022-06-08T15:22:24.0096697Z",
|
||||||
|
"lastModifiedDateTime": "2022-06-09T09:27:09.9223729Z",
|
||||||
|
"changeKey": "B3bmsWoxX06b9JHIZPptRQAAJcswFA==",
|
||||||
|
"categories": [],
|
||||||
|
"transactionId": "90c23105-a6b1-b594-1811-e4ffa612092a",
|
||||||
|
"originalStartTimeZone": "Romance Standard Time",
|
||||||
|
"originalEndTimeZone": "Romance Standard Time",
|
||||||
|
"iCalUId": "040000008200E00074C5B7101A82E00800000000A971DA8D4B7BD801000000000000000010000000BE3F4A21C9008E4FB35A4DE1F80E0118",
|
||||||
|
"reminderMinutesBeforeStart": 15,
|
||||||
|
"isReminderOn": true,
|
||||||
|
"hasAttachments": false,
|
||||||
|
"subject": "test notif",
|
||||||
|
"bodyPreview": "",
|
||||||
|
"importance": "normal",
|
||||||
|
"sensitivity": "normal",
|
||||||
|
"isAllDay": false,
|
||||||
|
"isCancelled": false,
|
||||||
|
"isOrganizer": true,
|
||||||
|
"responseRequested": true,
|
||||||
|
"seriesMasterId": null,
|
||||||
|
"showAs": "busy",
|
||||||
|
"type": "singleInstance",
|
||||||
|
"webLink": "https://outlook.office365.com/owa/?itemid=AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk%2Bm1FAAAAAAENAAAHduaxajFfTpv0kchk%2Bm1FAAAl1BupAAA%3D&exvsurl=1&path=/calendar/item",
|
||||||
|
"onlineMeetingUrl": null,
|
||||||
|
"isOnlineMeeting": false,
|
||||||
|
"onlineMeetingProvider": "unknown",
|
||||||
|
"allowNewTimeProposals": true,
|
||||||
|
"occurrenceId": null,
|
||||||
|
"isDraft": false,
|
||||||
|
"hideAttendees": false,
|
||||||
|
"responseStatus": {
|
||||||
|
"response": "organizer",
|
||||||
|
"time": "0001-01-01T00:00:00Z"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"contentType": "html",
|
||||||
|
"content": ""
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"dateTime": "2022-06-10T13:30:00.0000000",
|
||||||
|
"timeZone": "UTC"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"dateTime": "2022-06-10T15:30:00.0000000",
|
||||||
|
"timeZone": "UTC"
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"displayName": "",
|
||||||
|
"locationType": "default",
|
||||||
|
"uniqueIdType": "unknown",
|
||||||
|
"address": {},
|
||||||
|
"coordinates": {}
|
||||||
|
},
|
||||||
|
"locations": [],
|
||||||
|
"recurrence": null,
|
||||||
|
"attendees": [],
|
||||||
|
"organizer": {
|
||||||
|
"emailAddress": {
|
||||||
|
"name": "Diego Siciliani",
|
||||||
|
"address": "DiegoS@2zy74l.onmicrosoft.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"onlineMeeting": null,
|
||||||
|
"calendar@odata.associationLink": "https://graph.microsoft.com/v1.0/Users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/calendar/$ref",
|
||||||
|
"calendar@odata.navigationLink": "https://graph.microsoft.com/v1.0/Users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/calendar"
|
||||||
|
}
|
||||||
|
JSON;
|
||||||
|
|
||||||
|
private const NOTIF_UPDATE = <<<'JSON'
|
||||||
|
{
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"subscriptionId": "739703eb-80c4-4c03-b15a-ca370f19624b",
|
||||||
|
"subscriptionExpirationDateTime": "2022-06-09T02:40:28-07:00",
|
||||||
|
"changeType": "updated",
|
||||||
|
"resource": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||||
|
"resourceData": {
|
||||||
|
"@odata.type": "#Microsoft.Graph.Event",
|
||||||
|
"@odata.id": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||||
|
"@odata.etag": "W/\"DwAAABYAAAAHduaxajFfTpv0kchk+m1FAAAlyzAU\"",
|
||||||
|
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA="
|
||||||
|
},
|
||||||
|
"clientState": "2k05qlr3ds2KzvUP3Ps4A+642fYaI8ThxHGIGbNr2p0MnNkmzxLTNEMxpMc/UEuDkBHfID7OYWj4DQc94vlEkPBdsh9sGTTkHxIE68hqkKkDKhwvfvdj6lS6Dus=",
|
||||||
|
"tenantId": "421bf216-3f48-47bd-a7cf-8b1995cb24bd"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON;
|
||||||
|
|
||||||
|
private const NOTIF_DELETE = <<<'JSON'
|
||||||
|
{
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"subscriptionId": "077e8d19-68b3-4d8e-9b1e-8b4ba6733799",
|
||||||
|
"subscriptionExpirationDateTime": "2022-06-09T06:22:02-07:00",
|
||||||
|
"changeType": "deleted",
|
||||||
|
"resource": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||||
|
"resourceData": {
|
||||||
|
"@odata.type": "#Microsoft.Graph.Event",
|
||||||
|
"@odata.id": "Users/4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4/Events/AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA=",
|
||||||
|
"@odata.etag": "W/\"CQAAAA==\"",
|
||||||
|
"id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BupAAA="
|
||||||
|
},
|
||||||
|
"clientState": "uds18apRCByqWIodFCHKeM0kJqhfr+qXL/rJWYn7xmtdQ4t03W2OHEOdGJ0Ceo52NAzOYVDpbfRM3TdrZDUiE09OxZkPX/vkpdcnipoiVnPPMFBQn05p8KhklOM=",
|
||||||
|
"tenantId": "421bf216-3f48-47bd-a7cf-8b1995cb24bd"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON;
|
||||||
|
|
||||||
|
|
||||||
|
public function testUpdateCalendarRange(): void
|
||||||
|
{
|
||||||
|
$em = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$machineHttpClient = new MockHttpClient([
|
||||||
|
new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200])
|
||||||
|
]);
|
||||||
|
|
||||||
|
$calendarRangeSyncer = new CalendarRangeSyncer(
|
||||||
|
$em->reveal(),
|
||||||
|
new NullLogger(),
|
||||||
|
$machineHttpClient
|
||||||
|
);
|
||||||
|
|
||||||
|
$calendarRange = new CalendarRange();
|
||||||
|
$calendarRange
|
||||||
|
->setUser($user = new User())
|
||||||
|
->setStartDate(new \DateTimeImmutable('2020-01-01 15:00:00'))
|
||||||
|
->setEndDate(new \DateTimeImmutable('2020-01-01 15:30:00'))
|
||||||
|
->addRemoteAttributes([
|
||||||
|
'lastModifiedDateTime' => 0,
|
||||||
|
'changeKey' => 'abc'
|
||||||
|
]);
|
||||||
|
$notification = json_decode(self::NOTIF_UPDATE, true);
|
||||||
|
|
||||||
|
$calendarRangeSyncer->handleCalendarRangeSync(
|
||||||
|
$calendarRange,
|
||||||
|
$notification['value'][0],
|
||||||
|
$user);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('2022-06-10T15:30:00',
|
||||||
|
$calendarRange->getStartDate()->format(\DateTimeImmutable::ATOM));
|
||||||
|
$this->assertStringContainsString('2022-06-10T17:30:00',
|
||||||
|
$calendarRange->getEndDate()->format(\DateTimeImmutable::ATOM));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteCalendarRangeWithoutAssociation(): void
|
||||||
|
{
|
||||||
|
$em = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$em->remove(Argument::type(CalendarRange::class))->shouldBeCalled();
|
||||||
|
|
||||||
|
$machineHttpClient = new MockHttpClient([
|
||||||
|
new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200])
|
||||||
|
]);
|
||||||
|
|
||||||
|
$calendarRangeSyncer = new CalendarRangeSyncer(
|
||||||
|
$em->reveal(),
|
||||||
|
new NullLogger(),
|
||||||
|
$machineHttpClient
|
||||||
|
);
|
||||||
|
|
||||||
|
$calendarRange = new CalendarRange();
|
||||||
|
$calendarRange
|
||||||
|
->setUser($user = new User())
|
||||||
|
;
|
||||||
|
$notification = json_decode(self::NOTIF_DELETE, true);
|
||||||
|
|
||||||
|
$calendarRangeSyncer->handleCalendarRangeSync(
|
||||||
|
$calendarRange,
|
||||||
|
$notification['value'][0],
|
||||||
|
$user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteCalendarRangeWithAssociation(): void
|
||||||
|
{
|
||||||
|
$em = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$em->remove(Argument::type(CalendarRange::class))->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$machineHttpClient = new MockHttpClient([
|
||||||
|
new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200])
|
||||||
|
]);
|
||||||
|
|
||||||
|
$calendarRangeSyncer = new CalendarRangeSyncer(
|
||||||
|
$em->reveal(),
|
||||||
|
new NullLogger(),
|
||||||
|
$machineHttpClient
|
||||||
|
);
|
||||||
|
|
||||||
|
$calendarRange = new CalendarRange();
|
||||||
|
$calendarRange
|
||||||
|
->setUser($user = new User())
|
||||||
|
;
|
||||||
|
$calendar = new Calendar();
|
||||||
|
$calendar->setCalendarRange($calendarRange);
|
||||||
|
|
||||||
|
$notification = json_decode(self::NOTIF_DELETE, true);
|
||||||
|
|
||||||
|
$calendarRangeSyncer->handleCalendarRangeSync(
|
||||||
|
$calendarRange,
|
||||||
|
$notification['value'][0],
|
||||||
|
$user);
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
Subproject commit 8694ad7c4de306f9d5e35af966d24fb2e3e1c2ff
|
Subproject commit 5b35e7ccd0735e5593835e28acbf82386c18e1b6
|
Loading…
x
Reference in New Issue
Block a user