db constraint with unique remoteId if set, handle sync with tests

This commit is contained in:
Julien Fastré 2022-06-10 00:26:16 +02:00
parent f149b24802
commit c92077926e
14 changed files with 957 additions and 332 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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');
}
}
}

View File

@ -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';

View File

@ -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']);
}
}
}

View File

@ -1,13 +1,27 @@
<?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\Invite;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use LogicException;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use function in_array;
class CalendarSyncer
{
@ -15,14 +29,13 @@ class CalendarSyncer
private HttpClientInterface $machineHttpClient;
/**
* @param LoggerInterface $logger
* @param HttpClientInterface $machineHttpClient
*/
public function __construct(LoggerInterface $logger, HttpClientInterface $machineHttpClient)
private UserRepositoryInterface $userRepository;
public function __construct(LoggerInterface $logger, HttpClientInterface $machineHttpClient, UserRepositoryInterface $userRepository)
{
$this->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);
}
}
}
}

View File

@ -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);

View File

@ -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());
}

View File

@ -1,11 +1,21 @@
<?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\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 DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
@ -14,189 +24,128 @@ use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
class CalendarRangeSyncerTest extends TestCase
/**
* @internal
* @coversNothing
*/
final 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"
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"
}
]
}
},
"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;
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"
"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;
JSON;
private const NOTIF_DELETE = <<<'JSON'
{
"value": [
private const REMOTE_CALENDAR_RANGE = <<<'JSON'
{
"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="
"@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"
},
"clientState": "uds18apRCByqWIodFCHKeM0kJqhfr+qXL/rJWYn7xmtdQ4t03W2OHEOdGJ0Ceo52NAzOYVDpbfRM3TdrZDUiE09OxZkPX/vkpdcnipoiVnPPMFBQn05p8KhklOM=",
"tenantId": "421bf216-3f48-47bd-a7cf-8b1995cb24bd"
"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;
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);
}
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);
}
}

View File

@ -1,154 +1,456 @@
<?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\RemoteCalendar\Connector\MSGraph;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync\CalendarRangeSyncer;
use Chill\CalendarBundle\Entity\Invite;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteToLocalSync\CalendarSyncer;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use DateTimeImmutable;
use DateTimeZone;
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 CalendarSyncerTest extends TestCase
/**
* @internal
* @coversNothing
*/
final class CalendarSyncerTest extends TestCase
{
private const REMOTE_CALENDAR_NO_ATTENDEES = <<<'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;
use ProphecyTrait;
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"
"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;
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 REMOTE_CALENDAR_NO_ATTENDEES = <<<'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 REMOTE_CALENDAR_NOT_ORGANIZER = <<<'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": false,
"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": "<html>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n</head>\r\n<body>\r\n<div></div>\r\n<br>\r\n<div style=\"width:100%; height:20px\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n<div class=\"me-email-text\" lang=\"fr-FR\" style=\"color:#252424; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n<div style=\"margin-top:24px; margin-bottom:20px\"><span style=\"font-size:24px; color:#252424\">Réunion Microsoft Teams</span>\r\n</div>\r\n<div style=\"margin-bottom:20px\">\r\n<div style=\"margin-top:0px; margin-bottom:0px; font-weight:bold\"><span style=\"font-size:14px; color:#252424\">Rejoindre sur votre ordinateur ou application mobile</span>\r\n</div>\r\n<a href=\"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\" class=\"me-email-headline\" style=\"font-size:14px; font-family:'Segoe UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif; text-decoration:underline; color:#6264a7\">Cliquez\r\n ici pour participer à la réunion</a> </div>\r\n<div style=\"margin-bottom:24px; margin-top:20px\"><a href=\"https://aka.ms/JoinTeamsMeeting\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Pour en savoir\r\n plus</a> | <a href=\"https://teams.microsoft.com/meetingOptions/?organizerId=4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4&amp;tenantId=421bf216-3f48-47bd-a7cf-8b1995cb24bd&amp;threadId=19_meeting_NTE3ODUxY2ItNGJhNi00Y2UwLTljN2QtMmQ3YjAxNWY1Nzk2@thread.v2&amp;messageId=0&amp;language=fr-FR\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\nOptions de réunion</a> </div>\r\n</div>\r\n<div style=\"font-size:14px; margin-bottom:4px; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n</div>\r\n<div style=\"font-size:12px\"></div>\r\n<div></div>\r\n<div style=\"width:100%; height:20px\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n</body>\r\n</html>\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": "<html>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n</head>\r\n<body>\r\n<div></div>\r\n<br>\r\n<div style=\"width:100%; height:20px\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n<div class=\"me-email-text\" lang=\"fr-FR\" style=\"color:#252424; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n<div style=\"margin-top:24px; margin-bottom:20px\"><span style=\"font-size:24px; color:#252424\">Réunion Microsoft Teams</span>\r\n</div>\r\n<div style=\"margin-bottom:20px\">\r\n<div style=\"margin-top:0px; margin-bottom:0px; font-weight:bold\"><span style=\"font-size:14px; color:#252424\">Rejoindre sur votre ordinateur ou application mobile</span>\r\n</div>\r\n<a href=\"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\" class=\"me-email-headline\" style=\"font-size:14px; font-family:'Segoe UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif; text-decoration:underline; color:#6264a7\">Cliquez\r\n ici pour participer à la réunion</a> </div>\r\n<div style=\"margin-bottom:24px; margin-top:20px\"><a href=\"https://aka.ms/JoinTeamsMeeting\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">Pour en savoir\r\n plus</a> | <a href=\"https://teams.microsoft.com/meetingOptions/?organizerId=4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4&amp;tenantId=421bf216-3f48-47bd-a7cf-8b1995cb24bd&amp;threadId=19_meeting_NTE3ODUxY2ItNGJhNi00Y2UwLTljN2QtMmQ3YjAxNWY1Nzk2@thread.v2&amp;messageId=0&amp;language=fr-FR\" class=\"me-email-link\" style=\"font-size:14px; text-decoration:underline; color:#6264a7; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\nOptions de réunion</a> </div>\r\n</div>\r\n<div style=\"font-size:14px; margin-bottom:4px; font-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif\">\r\n</div>\r\n<div style=\"font-size:12px\"></div>\r\n<div></div>\r\n<div style=\"width:100%; height:20px\"><span style=\"white-space:nowrap; color:#5F5F5F; opacity:.36\">________________________________________________________________________________</span>\r\n</div>\r\n</body>\r\n</html>\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());
}
}

View File

@ -0,0 +1,43 @@
<?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\Migrations\Calendar;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220609200857 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->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 <> \'\'');
}
}

View File

@ -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;
}

View File

@ -0,0 +1,73 @@
<?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\MainBundle\Repository;
use Chill\MainBundle\Entity\User;
use Doctrine\Persistence\ObjectRepository;
interface UserRepositoryInterface extends ObjectRepository
{
public function countBy(array $criteria): int;
public function countByActive(): int;
public function countByNotHavingAttribute(string $key): int;
public function countByUsernameOrEmail(string $pattern): int;
public function find($id, $lockMode = null, $lockVersion = null): ?User;
/**
* @return User[]
*/
public function findAll(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return User[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
/**
* @return array|User[]
*/
public function findByActive(?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
/**
* Find users which does not have a key on attribute column.
*
* @return array|User[]
*/
public function findByNotHavingAttribute(string $key, ?int $limit = null, ?int $offset = null): array;
public function findByUsernameOrEmail(string $pattern, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?User;
public function findOneByUsernameOrEmail(string $pattern);
/**
* Get the users having a specific flags.
*
* If provided, only the users amongst "filtered users" are searched. This
* allows to make a first search amongst users based on role and center
* and, then filter those users having some flags.
*
* @param \Chill\MainBundle\Entity\User[] $amongstUsers
* @param mixed $flag
*/
public function findUsersHavingFlags($flag, array $amongstUsers = []): array;
public function getClassName(): string;
}