From c92077926e3db6487819118afd4c6d18731ad3ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 10 Jun 2022 00:26:16 +0200 Subject: [PATCH] db constraint with unique remoteId if set, handle sync with tests --- .../ChillCalendarBundle/Entity/Calendar.php | 5 +- .../Entity/CalendarRange.php | 5 +- .../ChillCalendarBundle/Entity/Invite.php | 7 +- .../MSGraphChangeNotificationHandler.php | 4 +- .../MSGraph/RemoteEventConverter.php | 2 +- .../RemoteToLocalSync/CalendarRangeSyncer.php | 11 +- .../RemoteToLocalSync/CalendarSyncer.php | 116 +++- .../RemoteCalendarCompilerPass.php | 2 +- ...emoteCalendarMSGraphSyncControllerTest.php | 2 +- .../MSGraph/CalendarRangeSyncerTest.php | 367 +++++----- .../Connector/MSGraph/CalendarSyncerTest.php | 647 ++++++++++++++---- .../migrations/Version20220609200857.php | 43 ++ .../Repository/UserRepository.php | 5 +- .../Repository/UserRepositoryInterface.php | 73 ++ 14 files changed, 957 insertions(+), 332 deletions(-) create mode 100644 src/Bundle/ChillCalendarBundle/migrations/Version20220609200857.php create mode 100644 src/Bundle/ChillMainBundle/Repository/UserRepositoryInterface.php diff --git a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php index c0d1fbb9a..25486459f 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php @@ -38,7 +38,10 @@ use Symfony\Component\Validator\Mapping\ClassMetadata; use function in_array; /** - * @ORM\Table(name="chill_calendar.calendar", indexes={@ORM\Index(name="idx_calendar_remote", columns={"remoteId"})})) + * @ORM\Table( + * name="chill_calendar.calendar", + * uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})} + * ) * @ORM\Entity */ class Calendar implements TrackCreationInterface, TrackUpdateInterface diff --git a/src/Bundle/ChillCalendarBundle/Entity/CalendarRange.php b/src/Bundle/ChillCalendarBundle/Entity/CalendarRange.php index 5bda987fc..76596347c 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/CalendarRange.php +++ b/src/Bundle/ChillCalendarBundle/Entity/CalendarRange.php @@ -21,7 +21,10 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; /** - * @ORM\Table(name="chill_calendar.calendar_range", indexes={@ORM\Index(name="idx_calendar_range_remote", columns={"remoteId"})}) + * @ORM\Table( + * name="chill_calendar.calendar_range", + * uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_range_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})} + * ) * @ORM\Entity */ class CalendarRange implements TrackCreationInterface, TrackUpdateInterface diff --git a/src/Bundle/ChillCalendarBundle/Entity/Invite.php b/src/Bundle/ChillCalendarBundle/Entity/Invite.php index e7f25d932..e53ca1ca1 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Invite.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Invite.php @@ -26,9 +26,10 @@ use Symfony\Component\Serializer\Annotation as Serializer; * The event/calendar in the user may have a different id than the mainUser. We add then fields to store the * remote id of this event in the remote calendar. * - * @ORM\Table(name="chill_calendar.invite", indexes={ - * @ORM\Index(name="idx_calendar_invite_remote", columns={"remoteId"}) - * }) + * @ORM\Table( + * name="chill_calendar.invite", + * uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_invite_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})} + * ) * @ORM\Entity */ class Invite implements TrackUpdateInterface, TrackCreationInterface diff --git a/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php b/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php index 410c5f9a2..4e201af8e 100644 --- a/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php +++ b/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php @@ -23,7 +23,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; /** - * Handle notification of chagnes from MSGraph + * Handle notification of chagnes from MSGraph. * * @AsMessageHandler */ @@ -97,7 +97,7 @@ class MSGraphChangeNotificationHandler implements MessageHandlerInterface $this->remoteToLocalSyncer->handleInviteSync($invite, $notification, $user); $this->em->flush(); } else { - $this->logger->info(__CLASS__." id not found in any calendar, calendar range nor invite"); + $this->logger->info(__CLASS__ . ' id not found in any calendar, calendar range nor invite'); } } } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php index e87952308..4ff42cd6f 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php @@ -31,7 +31,7 @@ class RemoteEventConverter { /** * valid when the remote string contains also a timezone, like in - * lastModifiedDate + * lastModifiedDate. */ public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P'; diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarRangeSyncer.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarRangeSyncer.php index d92e263fa..298bbc7b5 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarRangeSyncer.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarRangeSyncer.php @@ -17,6 +17,7 @@ use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter; use Chill\MainBundle\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; +use RuntimeException; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -29,8 +30,6 @@ class CalendarRangeSyncer private HttpClientInterface $machineHttpClient; /** - * @param EntityManagerInterface $em - * @param LoggerInterface $logger * @param MachineHttpClient $machineHttpClient */ public function __construct( @@ -43,7 +42,6 @@ class CalendarRangeSyncer $this->machineHttpClient = $machineHttpClient; } - public function handleCalendarRangeSync(CalendarRange $calendarRange, array $notification, User $user): void { switch ($notification['changeType']) { @@ -94,11 +92,12 @@ class CalendarRangeSyncer 'lastModifiedDateTime' => $lastModified->getTimestamp(), 'changeKey' => $new['changeKey'], ]) - ->preventEnqueueChanges = true - ; + ->preventEnqueueChanges = true; + break; + default: - throw new \RuntimeException('This changeType is not suppored: '.$notification['changeType']); + throw new RuntimeException('This changeType is not suppored: ' . $notification['changeType']); } } } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarSyncer.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarSyncer.php index ace9515ea..d04dc7005 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarSyncer.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarSyncer.php @@ -1,13 +1,27 @@ logger = $logger; $this->machineHttpClient = $machineHttpClient; + $this->userRepository = $userRepository; } public function handleCalendarSync(Calendar $calendar, array $notification, User $user): void @@ -30,14 +43,16 @@ class CalendarSyncer switch ($notification['changeType']) { case 'deleted': $this->handleDeleteCalendar($calendar, $notification, $user); + break; case 'updated': $this->handleUpdateCalendar($calendar, $notification, $user); + break; default: - throw new \RuntimeException("this change type is not supported: ".$notification['changeType']); + throw new RuntimeException('this change type is not supported: ' . $notification['changeType']); } } @@ -45,8 +60,7 @@ class CalendarSyncer { $calendar ->setStatus(Calendar::STATUS_CANCELED) - ->setCalendarRange(null) - ; + ->setCalendarRange(null); $calendar->preventEnqueueChanges = true; } @@ -64,6 +78,10 @@ class CalendarSyncer ]); } + if (false === $new['isOrganizer']) { + return; + } + $lastModified = RemoteEventConverter::convertStringDateWithTimezone( $new['lastModifiedDateTime'] ); @@ -77,17 +95,91 @@ class CalendarSyncer return; } + $this->syncAttendees($calendar, $new['attendees']); + $startDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['start']['dateTime']); $endDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['end']['dateTime']); + if ($startDate->getTimestamp() !== $calendar->getStartDate()->getTimestamp()) { + $calendar->setStartDate($startDate)->setStatus(Calendar::STATUS_MOVED); + } + + if ($endDate->getTimestamp() !== $calendar->getEndDate()->getTimestamp()) { + $calendar->setEndDate($endDate)->setStatus(Calendar::STATUS_MOVED); + } + $calendar - ->setStartDate($startDate)->setEndDate($endDate) - ->setStatus(Calendar::STATUS_MOVED) ->addRemoteAttributes([ 'lastModifiedDateTime' => $lastModified->getTimestamp(), 'changeKey' => $new['changeKey'], ]) - ->preventEnqueueChanges = true - ; + ->preventEnqueueChanges = true; + } + + private function syncAttendees(Calendar $calendar, array $attendees): void + { + $emails = []; + + foreach ($attendees as $attendee) { + $status = $attendee['status']['response']; + + if ('organizer' === $status) { + continue; + } + + $email = $attendee['emailAddress']['address']; + $emails[] = strtolower($email); + $user = $this->userRepository->findOneByUsernameOrEmail($email); + + if (null === $user) { + continue; + } + + if (!$calendar->isInvited($user)) { + $calendar->addUser($user); + } + + $invite = $calendar->getInviteForUser($user); + + switch ($status) { + // none, organizer, tentativelyAccepted, accepted, declined, notResponded. + case 'none': + case 'notResponded': + $invite->setStatus(Invite::PENDING); + + break; + + case 'organizer': + throw new LogicException('should not happens'); + + break; + + case 'tentativelyAccepted': + $invite->setStatus(Invite::TENTATIVELY_ACCEPTED); + + break; + + case 'accepted': + $invite->setStatus(Invite::ACCEPTED); + + break; + + case 'declined': + $invite->setStatus(Invite::DECLINED); + + break; + + default: + throw new LogicException('should not happens, not implemented: ' . $status); + + break; + } + } + + foreach ($calendar->getUsers() as $user) { + if (!in_array(strtolower($user->getEmailCanonical()), $emails, true)) { + $calendar->removeUser($user); + } + } } } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php index df10751ac..f6631f0cc 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php @@ -39,7 +39,7 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) { $connector = MSGraphRemoteCalendarConnector::class; - $container->setAlias(HttpClientInterface::class.' $machineHttpClient', MachineHttpClient::class); + $container->setAlias(HttpClientInterface::class . ' $machineHttpClient', MachineHttpClient::class); } else { // remove services which cannot be loaded $container->removeDefinition(MapAndSubscribeUserCalendarCommand::class); diff --git a/src/Bundle/ChillCalendarBundle/Tests/Controller/RemoteCalendarMSGraphSyncControllerTest.php b/src/Bundle/ChillCalendarBundle/Tests/Controller/RemoteCalendarMSGraphSyncControllerTest.php index 39b414426..2be0cef24 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/Controller/RemoteCalendarMSGraphSyncControllerTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/Controller/RemoteCalendarMSGraphSyncControllerTest.php @@ -61,7 +61,7 @@ final class RemoteCalendarMSGraphSyncControllerTest extends WebTestCase $this->assertResponseIsSuccessful(); $this->assertResponseStatusCodeSame(202); - /* @var InMemoryTransport $transport */ + /** @var InMemoryTransport $transport */ $transport = self::$container->get('messenger.transport.async'); $this->assertCount(1, $transport->getSent()); } diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarRangeSyncerTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarRangeSyncerTest.php index af2009fad..6ff112956 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarRangeSyncerTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarRangeSyncerTest.php @@ -1,11 +1,21 @@ 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)); - $this->assertTrue($calendarRange->preventEnqueueChanges); - } - - 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); - $this->assertTrue($calendarRange->preventEnqueueChanges); - } + JSON; public function testDeleteCalendarRangeWithAssociation(): void { @@ -204,7 +153,7 @@ JSON; $em->remove(Argument::type(CalendarRange::class))->shouldNotBeCalled(); $machineHttpClient = new MockHttpClient([ - new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]) + new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]), ]); $calendarRangeSyncer = new CalendarRangeSyncer( @@ -215,8 +164,7 @@ JSON; $calendarRange = new CalendarRange(); $calendarRange - ->setUser($user = new User()) - ; + ->setUser($user = new User()); $calendar = new Calendar(); $calendar->setCalendarRange($calendarRange); @@ -225,7 +173,76 @@ JSON; $calendarRangeSyncer->handleCalendarRangeSync( $calendarRange, $notification['value'][0], - $user); + $user + ); + } + 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 + ); + $this->assertTrue($calendarRange->preventEnqueueChanges); + } + + 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) + ); + $this->assertTrue($calendarRange->preventEnqueueChanges); } } diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarSyncerTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarSyncerTest.php index fb3da1ce6..f7f658b92 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarSyncerTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarSyncerTest.php @@ -1,154 +1,456 @@ \r\n\r\n\r\n\r\n\r\n
\r\n
\r\n
________________________________________________________________________________\r\n
\r\n
\r\n
Réunion Microsoft Teams\r\n
\r\n
\r\n
Rejoindre sur votre ordinateur ou application mobile\r\n
\r\nCliquez\r\n ici pour participer à la réunion
\r\n
Pour en savoir\r\n plus | \r\nOptions de réunion
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
________________________________________________________________________________\r\n
\r\n\r\n\r\n" + }, + "start": { + "dateTime": "2022-06-11T12:30:00.0000000", + "timeZone": "UTC" + }, + "end": { + "dateTime": "2022-06-11T13:30:00.0000000", + "timeZone": "UTC" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "attendees": [ + { + "type": "required", + "status": { + "response": "accepted", + "time": "2022-06-08T16:22:15.1392583Z" + }, + "emailAddress": { + "name": "Alex Wilber", + "address": "AlexW@2zy74l.onmicrosoft.com" + } + }, + { + "type": "required", + "status": { + "response": "declined", + "time": "2022-06-08T16:22:15.1392583Z" + }, + "emailAddress": { + "name": "Alfred Nobel", + "address": "alfredN@2zy74l.onmicrosoft.com" + } + } + ], + "organizer": { + "emailAddress": { + "name": "Diego Siciliani", + "address": "DiegoS@2zy74l.onmicrosoft.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_NTE3ODUxY2ItNGJhNi00Y2UwLTljN2QtMmQ3YjAxNWY1Nzk2%40thread.v2/0?context=%7b%22Tid%22%3a%22421bf216-3f48-47bd-a7cf-8b1995cb24bd%22%2c%22Oid%22%3a%224feb0ae3-7ffb-48dd-891e-c86b2cdeefd4%22%7d" + } + } + JSON; + + private const REMOTE_CALENDAR_WITH_ATTENDEES = <<<'JSON' + { + "@odata.etag": "W/\"B3bmsWoxX06b9JHIZPptRQAAJcrv7A==\"", + "id": "AAMkADM1MTdlMGIzLTZhZWUtNDQ0ZC05Y2M4LWViMjhmOWJlMDhhMQBGAAAAAAA5e3965gkBSLcU1p00sMSyBwAHduaxajFfTpv0kchk_m1FAAAAAAENAAAHduaxajFfTpv0kchk_m1FAAAl1BuqAAA=", + "createdDateTime": "2022-06-08T16:19:18.997293Z", + "lastModifiedDateTime": "2022-06-08T16:22:18.7276485Z", + "changeKey": "B3bmsWoxX06b9JHIZPptRQAAJcrv7A==", + "categories": [], + "transactionId": "42ac0b77-313e-20ca-2cac-1a3f58b3452d", + "originalStartTimeZone": "Romance Standard Time", + "originalEndTimeZone": "Romance Standard Time", + "iCalUId": "040000008200E00074C5B7101A82E00800000000A8955A81537BD801000000000000000010000000007EB443987CD641B6794C4BA48EE356", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "test 2", + "bodyPreview": "________________________________________________________________________________\r\nRéunion Microsoft Teams\r\nRejoindre sur votre ordinateur ou application mobile\r\nCliquez ici pour participer à la réunion\r\nPour en savoir plus | Options de réunion\r\n________", + "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%2Bm1FAAAl1BuqAAA%3D&exvsurl=1&path=/calendar/item", + "onlineMeetingUrl": null, + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": null, + "isDraft": false, + "hideAttendees": false, + "responseStatus": { + "response": "organizer", + "time": "0001-01-01T00:00:00Z" + }, + "body": { + "contentType": "html", + "content": "\r\n\r\n\r\n\r\n\r\n
\r\n
\r\n
________________________________________________________________________________\r\n
\r\n
\r\n
Réunion Microsoft Teams\r\n
\r\n
\r\n
Rejoindre sur votre ordinateur ou application mobile\r\n
\r\nCliquez\r\n ici pour participer à la réunion
\r\n
Pour en savoir\r\n plus | \r\nOptions de réunion
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
________________________________________________________________________________\r\n
\r\n\r\n\r\n" + }, + "start": { + "dateTime": "2022-06-11T12:30:00.0000000", + "timeZone": "UTC" + }, + "end": { + "dateTime": "2022-06-11T13:30:00.0000000", + "timeZone": "UTC" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "attendees": [ + { + "type": "required", + "status": { + "response": "accepted", + "time": "2022-06-08T16:22:15.1392583Z" + }, + "emailAddress": { + "name": "Alex Wilber", + "address": "AlexW@2zy74l.onmicrosoft.com" + } + }, + { + "type": "required", + "status": { + "response": "accepted", + "time": "2022-06-08T16:22:15.1392583Z" + }, + "emailAddress": { + "name": "External User", + "address": "external@example.com" + } + }, + { + "type": "required", + "status": { + "response": "declined", + "time": "2022-06-08T16:22:15.1392583Z" + }, + "emailAddress": { + "name": "Alfred Nobel", + "address": "alfredN@2zy74l.onmicrosoft.com" + } + } + ], + "organizer": { + "emailAddress": { + "name": "Diego Siciliani", + "address": "DiegoS@2zy74l.onmicrosoft.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_NTE3ODUxY2ItNGJhNi00Y2UwLTljN2QtMmQ3YjAxNWY1Nzk2%40thread.v2/0?context=%7b%22Tid%22%3a%22421bf216-3f48-47bd-a7cf-8b1995cb24bd%22%2c%22Oid%22%3a%224feb0ae3-7ffb-48dd-891e-c86b2cdeefd4%22%7d" + } + } + JSON; + + protected function setUp(): void + { + parent::setUp(); + + // all tests should run when timezone = +02:00 + $brussels = new DateTimeZone('Europe/Brussels'); + + if (7200 === $brussels->getOffset(new DateTimeImmutable())) { + date_default_timezone_set('Europe/Brussels'); + } else { + date_default_timezone_set('Europe/Moscow'); + } + } + + public function testHandleAttendeesConfirmingCalendar(): void + { + $machineHttpClient = new MockHttpClient([ + new MockResponse(self::REMOTE_CALENDAR_WITH_ATTENDEES, ['http_code' => 200]), + ]); + + $userA = (new User())->setEmail('alexw@2zy74l.onmicrosoft.com') + ->setEmailCanonical('alexw@2zy74l.onmicrosoft.com'); + $userB = (new User())->setEmail('zzzzz@2zy74l.onmicrosoft.com') + ->setEmailCanonical('zzzzz@2zy74l.onmicrosoft.com'); + $userC = (new User())->setEmail('alfredN@2zy74l.onmicrosoft.com') + ->setEmailCanonical('alfredn@2zy74l.onmicrosoft.com'); + + $userRepository = $this->prophesize(UserRepositoryInterface::class); + $userRepository->findOneByUsernameOrEmail(Argument::exact('AlexW@2zy74l.onmicrosoft.com')) + ->willReturn($userA); + $userRepository->findOneByUsernameOrEmail(Argument::exact('zzzzz@2zy74l.onmicrosoft.com')) + ->willReturn($userB); + $userRepository->findOneByUsernameOrEmail(Argument::exact('alfredN@2zy74l.onmicrosoft.com')) + ->willReturn($userC); + $userRepository->findOneByUsernameOrEmail(Argument::exact('external@example.com')) + ->willReturn(null); + + $calendarSyncer = new CalendarSyncer( + new NullLogger(), + $machineHttpClient, + $userRepository->reveal() + ); + + $calendar = new Calendar(); + $calendar + ->setMainUser($user = new User()) + ->setStartDate(new DateTimeImmutable('2022-06-11 14:30:00')) + ->setEndDate(new DateTimeImmutable('2022-06-11 15:30:00')) + ->addUser($userA) + ->addUser($userB) + ->setCalendarRange(new CalendarRange()) + ->addRemoteAttributes([ + 'lastModifiedDateTime' => 0, + 'changeKey' => 'abcd', + ]); + + $notification = json_decode(self::NOTIF_UPDATE, true); + + $calendarSyncer->handleCalendarSync( + $calendar, + $notification['value'][0], + $user + ); + + $this->assertTrue($calendar->preventEnqueueChanges); + $this->assertEquals(Calendar::STATUS_VALID, $calendar->getStatus()); + // user A is invited, and accepted + $this->assertTrue($calendar->isInvited($userA)); + $this->assertEquals(Invite::ACCEPTED, $calendar->getInviteForUser($userA)->getStatus()); + $this->assertFalse($calendar->getInviteForUser($userA)->preventEnqueueChanges); + // user B is no more invited + $this->assertFalse($calendar->isInvited($userB)); + // user C is invited, but declined + $this->assertFalse($calendar->getInviteForUser($userC)->preventEnqueueChanges); + $this->assertTrue($calendar->isInvited($userC)); + $this->assertEquals(Invite::DECLINED, $calendar->getInviteForUser($userC)->getStatus()); + } public function testHandleDeleteCalendar(): void { $machineHttpClient = new MockHttpClient([]); + $userRepository = $this->prophesize(UserRepositoryInterface::class); $calendarSyncer = new CalendarSyncer( new NullLogger(), - $machineHttpClient + $machineHttpClient, + $userRepository->reveal() ); $calendar = new Calendar(); $calendar ->setMainUser($user = new User()) ->setCalendarRange($calendarRange = new CalendarRange()); - ; + $notification = json_decode(self::NOTIF_DELETE, true); $calendarSyncer->handleCalendarSync( $calendar, $notification['value'][0], - $user); + $user + ); $this->assertEquals(Calendar::STATUS_CANCELED, $calendar->getStatus()); $this->assertNull($calendar->getCalendarRange()); @@ -158,37 +460,130 @@ JSON; public function testHandleMoveCalendar(): void { $machineHttpClient = new MockHttpClient([ - new MockResponse(self::REMOTE_CALENDAR_NO_ATTENDEES, ['http_code' => 200]) + new MockResponse(self::REMOTE_CALENDAR_NO_ATTENDEES, ['http_code' => 200]), ]); + $userRepository = $this->prophesize(UserRepositoryInterface::class); $calendarSyncer = new CalendarSyncer( new NullLogger(), - $machineHttpClient + $machineHttpClient, + $userRepository->reveal() ); $calendar = new Calendar(); $calendar ->setMainUser($user = new User()) - ->setStartDate(new \DateTimeImmutable('2020-01-01 10:00:00')) - ->setEndDate(new \DateTimeImmutable('2020-01-01 12:00:00')) + ->setStartDate(new DateTimeImmutable('2020-01-01 10:00:00')) + ->setEndDate(new DateTimeImmutable('2020-01-01 12:00:00')) ->setCalendarRange(new CalendarRange()) ->addRemoteAttributes([ 'lastModifiedDateTime' => 0, 'changeKey' => 'abcd', - ]) - ; + ]); $notification = json_decode(self::NOTIF_UPDATE, true); $calendarSyncer->handleCalendarSync( $calendar, $notification['value'][0], - $user); + $user + ); - $this->assertStringContainsString('2022-06-10T15:30:00', - $calendar->getStartDate()->format(\DateTimeImmutable::ATOM)); - $this->assertStringContainsString('2022-06-10T17:30:00', - $calendar->getEndDate()->format(\DateTimeImmutable::ATOM)); + $this->assertStringContainsString( + '2022-06-10T15:30:00', + $calendar->getStartDate()->format(DateTimeImmutable::ATOM) + ); + $this->assertStringContainsString( + '2022-06-10T17:30:00', + $calendar->getEndDate()->format(DateTimeImmutable::ATOM) + ); $this->assertTrue($calendar->preventEnqueueChanges); $this->assertEquals(Calendar::STATUS_MOVED, $calendar->getStatus()); } + + public function testHandleNotMovedCalendar(): void + { + $machineHttpClient = new MockHttpClient([ + new MockResponse(self::REMOTE_CALENDAR_NO_ATTENDEES, ['http_code' => 200]), + ]); + $userRepository = $this->prophesize(UserRepositoryInterface::class); + + $calendarSyncer = new CalendarSyncer( + new NullLogger(), + $machineHttpClient, + $userRepository->reveal() + ); + + $calendar = new Calendar(); + $calendar + ->setMainUser($user = new User()) + ->setStartDate(new DateTimeImmutable('2022-06-10 15:30:00')) + ->setEndDate(new DateTimeImmutable('2022-06-10 17:30:00')) + ->setCalendarRange(new CalendarRange()) + ->addRemoteAttributes([ + 'lastModifiedDateTime' => 0, + 'changeKey' => 'abcd', + ]); + $notification = json_decode(self::NOTIF_UPDATE, true); + + $calendarSyncer->handleCalendarSync( + $calendar, + $notification['value'][0], + $user + ); + + $this->assertStringContainsString( + '2022-06-10T15:30:00', + $calendar->getStartDate()->format(DateTimeImmutable::ATOM) + ); + $this->assertStringContainsString( + '2022-06-10T17:30:00', + $calendar->getEndDate()->format(DateTimeImmutable::ATOM) + ); + $this->assertTrue($calendar->preventEnqueueChanges); + $this->assertEquals(Calendar::STATUS_VALID, $calendar->getStatus()); + } + + public function testHandleNotOrganizer(): void + { + // when isOrganiser === false, nothing should happens + $machineHttpClient = new MockHttpClient([ + new MockResponse(self::REMOTE_CALENDAR_NOT_ORGANIZER, ['http_code' => 200]), + ]); + $userRepository = $this->prophesize(UserRepositoryInterface::class); + + $calendarSyncer = new CalendarSyncer( + new NullLogger(), + $machineHttpClient, + $userRepository->reveal() + ); + + $calendar = new Calendar(); + $calendar + ->setMainUser($user = new User()) + ->setStartDate(new DateTimeImmutable('2020-01-01 10:00:00')) + ->setEndDate(new DateTimeImmutable('2020-01-01 12:00:00')) + ->setCalendarRange(new CalendarRange()) + ->addRemoteAttributes([ + 'lastModifiedDateTime' => 0, + 'changeKey' => 'abcd', + ]); + $notification = json_decode(self::NOTIF_UPDATE, true); + + $calendarSyncer->handleCalendarSync( + $calendar, + $notification['value'][0], + $user + ); + + $this->assertStringContainsString( + '2020-01-01T10:00:00', + $calendar->getStartDate()->format(DateTimeImmutable::ATOM) + ); + $this->assertStringContainsString( + '2020-01-01T12:00:00', + $calendar->getEndDate()->format(DateTimeImmutable::ATOM) + ); + + $this->assertEquals(Calendar::STATUS_VALID, $calendar->getStatus()); + } } diff --git a/src/Bundle/ChillCalendarBundle/migrations/Version20220609200857.php b/src/Bundle/ChillCalendarBundle/migrations/Version20220609200857.php new file mode 100644 index 000000000..8894da5d3 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/migrations/Version20220609200857.php @@ -0,0 +1,43 @@ +addSql('DROP INDEX chill_calendar.idx_calendar_range_remote'); + $this->addSql('CREATE INDEX idx_calendar_range_remote ON chill_calendar.calendar_range (remoteId)'); + $this->addSql('DROP INDEX chill_calendar.idx_calendar_remote'); + $this->addSql('CREATE INDEX idx_calendar_remote ON chill_calendar.calendar (remoteId)'); + $this->addSql('DROP INDEX chill_calendar.idx_calendar_invite_remote'); + $this->addSql('CREATE INDEX idx_calendar_invite_remote ON chill_calendar.invite (remoteId)'); + } + + public function getDescription(): string + { + return 'Set an unique contraint on remoteId on calendar object which are synced to a remote'; + } + + public function up(Schema $schema): void + { + $this->addSql('DROP INDEX chill_calendar.idx_calendar_range_remote'); + $this->addSql('CREATE UNIQUE INDEX idx_calendar_range_remote ON chill_calendar.calendar_range (remoteId) WHERE remoteId <> \'\''); + $this->addSql('DROP INDEX chill_calendar.idx_calendar_remote'); + $this->addSql('CREATE UNIQUE INDEX idx_calendar_remote ON chill_calendar.calendar (remoteId) WHERE remoteId <> \'\''); + $this->addSql('DROP INDEX chill_calendar.idx_calendar_invite_remote'); + $this->addSql('CREATE UNIQUE INDEX idx_calendar_invite_remote ON chill_calendar.invite (remoteId) WHERE remoteId <> \'\''); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/UserRepository.php b/src/Bundle/ChillMainBundle/Repository/UserRepository.php index d72443aa8..10b53b143 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserRepository.php @@ -18,11 +18,10 @@ use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\QueryBuilder; -use Doctrine\Persistence\ObjectRepository; use function count; -final class UserRepository implements ObjectRepository +final class UserRepository implements UserRepositoryInterface { private EntityManagerInterface $entityManager; @@ -206,7 +205,7 @@ final class UserRepository implements ObjectRepository return $qb->getQuery()->getResult(); } - public function getClassName() + public function getClassName(): string { return User::class; } diff --git a/src/Bundle/ChillMainBundle/Repository/UserRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/UserRepositoryInterface.php new file mode 100644 index 000000000..b4802264f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/UserRepositoryInterface.php @@ -0,0 +1,73 @@ +