mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-03 18:58:24 +00:00 
			
		
		
		
	first bootstrap for handling calendar range sync frommsgraph
This commit is contained in:
		@@ -47,7 +47,7 @@ class RemoteCalendarMSGraphSyncController
 | 
			
		||||
            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);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,7 @@ class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
     */
 | 
			
		||||
    private ?User $user = null;
 | 
			
		||||
 | 
			
		||||
    public function getCalendar(): Calendar
 | 
			
		||||
    public function getCalendar(): ?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
 | 
			
		||||
{
 | 
			
		||||
    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->userId = $userId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getContent(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->content;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getUserId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->userId;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,10 @@ class MachineHttpClient implements HttpClientInterface
 | 
			
		||||
        $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
 | 
			
		||||
    {
 | 
			
		||||
        $options['headers'] = array_merge(
 | 
			
		||||
 
 | 
			
		||||
@@ -73,6 +73,19 @@ class MapCalendarToUser
 | 
			
		||||
        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
 | 
			
		||||
    {
 | 
			
		||||
        $value = $this->machineHttpClient->request('GET', 'users', [
 | 
			
		||||
@@ -105,6 +118,15 @@ class MapCalendarToUser
 | 
			
		||||
            >= (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
 | 
			
		||||
    {
 | 
			
		||||
        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 DateTimeImmutable;
 | 
			
		||||
use DateTimeZone;
 | 
			
		||||
use RuntimeException;
 | 
			
		||||
use Symfony\Component\Templating\EngineInterface;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
 | 
			
		||||
@@ -28,10 +29,16 @@ use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
 */
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
    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 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
 | 
			
		||||
    {
 | 
			
		||||
        $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\MapAndSubscribeUserCalendarCommand;
 | 
			
		||||
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\NullRemoteCalendarConnector;
 | 
			
		||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
 | 
			
		||||
use RuntimeException;
 | 
			
		||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
 | 
			
		||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
 | 
			
		||||
use TheNetworg\OAuth2\Client\Provider\Azure;
 | 
			
		||||
 | 
			
		||||
class RemoteCalendarCompilerPass implements CompilerPassInterface
 | 
			
		||||
@@ -30,16 +33,21 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
 | 
			
		||||
        $connector = null;
 | 
			
		||||
 | 
			
		||||
        if (!$config['remote_calendars_sync']['enabled']) {
 | 
			
		||||
            $connector = MSGraphRemoteCalendarConnector::class;
 | 
			
		||||
            $connector = NullRemoteCalendarConnector::class;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) {
 | 
			
		||||
            $connector = MSGraphRemoteCalendarConnector::class;
 | 
			
		||||
 | 
			
		||||
            $container->setAlias(HttpClientInterface::class.' $machineHttpClient', MachineHttpClient::class);
 | 
			
		||||
        } else {
 | 
			
		||||
            // remove services which cannot be loaded
 | 
			
		||||
            $container->removeDefinition(MapAndSubscribeUserCalendarCommand::class);
 | 
			
		||||
            $container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::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')) {
 | 
			
		||||
@@ -58,7 +66,9 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
 | 
			
		||||
                    ->setDecoratedService(RemoteCalendarConnectorInterface::class);
 | 
			
		||||
            } else {
 | 
			
		||||
                // 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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 Submodule tests/app updated: 8694ad7c4d...5b35e7ccd0
									
								
							
		Reference in New Issue
	
	Block a user