From 6feea3b2d7afd373a5a8323bd7436b650c31f7b1 Mon Sep 17 00:00:00 2001 From: lucs7 Date: Wed, 10 Jun 2026 19:13:08 -0700 Subject: [PATCH 1/2] fix(schedule): allow explicit label visibility bypass Add an explicit skipVisibilityChecks argument to SlotLabelFactory::Format for callers that have already authorized reservation details. Keep the default path unchanged and keep NullSlotLabelFactory fail-closed. Assisted-by: Codex:GPT-5 --- lib/Application/Schedule/SlotLabelFactory.php | 47 +++++++++----- .../CalendarExportPresenterTest.php | 64 +++++++++++++++++++ 2 files changed, 95 insertions(+), 16 deletions(-) diff --git a/lib/Application/Schedule/SlotLabelFactory.php b/lib/Application/Schedule/SlotLabelFactory.php index 13265be411..00fa1463ce 100644 --- a/lib/Application/Schedule/SlotLabelFactory.php +++ b/lib/Application/Schedule/SlotLabelFactory.php @@ -48,10 +48,15 @@ public static function Create(ReservationItemView $reservation) /** * @param ReservationItemView $reservation - * @param string $format + * @param string|null $format + * @param bool $skipVisibilityChecks When true, the privacy and permission gates + * that normally suppress the label are not applied. The caller takes full + * responsibility for having verified the viewer may see this reservation + * (e.g. an ICS feed authorized by the user's secret subscription URL). + * User-detail redaction from privacy.hide.user.details still applies. * @return string */ - public function Format(ReservationItemView $reservation, $format = null) + public function Format(ReservationItemView $reservation, ?string $format = null, bool $skipVisibilityChecks = false) { $shouldHideUser = Configuration::Instance()->GetKey(ConfigKeys::PRIVACY_HIDE_USER_DETAILS, new BooleanConverter()); $shouldHideDetails = ReservationDetailsFilter::HideReservationDetails($reservation->StartDate, $reservation->EndDate); @@ -64,16 +69,18 @@ public function Format(ReservationItemView $reservation, $format = null) $shouldHideDetails = $shouldHideDetails && !$canEditResource && !$canSeeUserDetails; } - if ($shouldHideDetails) { - return ''; - } + if (!$skipVisibilityChecks) { + if ($shouldHideDetails) { + return ''; + } - if (!$shouldHideReservations && !$this->user->IsLoggedIn()) { - return ''; - } + if (!$shouldHideReservations && !$this->user->IsLoggedIn()) { + return ''; + } - if (!in_array($reservation->ResourceId, $this->UserResourcePermissions($this->user->UserId)) && !$reservation->IsUserOwner($this->user->UserId) && !$reservation->IsUserInvited($this->user->UserId) && !$reservation->IsUserParticipating($this->user->UserId)) { - return ''; + if (!in_array($reservation->ResourceId, $this->UserResourcePermissions($this->user->UserId)) && !$reservation->IsUserOwner($this->user->UserId) && !$reservation->IsUserInvited($this->user->UserId) && !$reservation->IsUserParticipating($this->user->UserId)) { + return ''; + } } if (empty($format)) { @@ -84,7 +91,12 @@ public function Format(ReservationItemView $reservation, $format = null) return ''; } - $name = $shouldHideUser ? Resources::GetInstance()->GetString('Private') : $this->GetFullName($reservation); + $privateNotice = Resources::GetInstance()->GetString('Private'); + $name = $shouldHideUser ? $privateNotice : $this->GetFullName($reservation); + $email = $shouldHideUser ? $privateNotice : ($reservation->OwnerEmailAddress ?? ''); + $organization = $shouldHideUser ? $privateNotice : ($reservation->OwnerOrganization ?? ''); + $phone = $shouldHideUser ? $privateNotice : ($reservation->OwnerPhone ?? ''); + $position = $shouldHideUser ? $privateNotice : ($reservation->OwnerPosition ?? ''); $timezone = 'UTC'; $dateFormat = Resources::GetInstance()->GetDateFormat('res_popup'); @@ -95,16 +107,19 @@ public function Format(ReservationItemView $reservation, $format = null) $label = str_replace('{name}', $name ?? '', $label); $label = str_replace('{title}', $reservation->Title ?? '', $label); $label = str_replace('{description}', $reservation->Description ?? '', $label); - $label = str_replace('{email}', $reservation->OwnerEmailAddress, $label); - $label = str_replace('{organization}', $reservation->OwnerOrganization ?? '', $label); - $label = str_replace('{phone}', $reservation->OwnerPhone ?? '', $label); - $label = str_replace('{position}', $reservation->OwnerPosition ?? '', $label); + $label = str_replace('{email}', $email, $label); + $label = str_replace('{organization}', $organization, $label); + $label = str_replace('{phone}', $phone, $label); + $label = str_replace('{position}', $position, $label); $label = str_replace('{startdate}', $reservation->StartDate->ToTimezone($timezone)->Format($dateFormat), $label); $label = str_replace('{enddate}', $reservation->EndDate->ToTimezone($timezone)->Format($dateFormat), $label); $label = str_replace('{resourcename}', implode(', ', $reservation->ResourceNames), $label); if (!$shouldHideUser) { $label = str_replace('{participants}', trim(implode(', ', $reservation->ParticipantNames)), $label); $label = str_replace('{invitees}', trim(implode(', ', $reservation->InviteeNames)), $label); + } else { + $label = str_replace('{participants}', '', $label); + $label = str_replace('{invitees}', '', $label); } $matches = []; @@ -171,7 +186,7 @@ private function UserResourcePermissions($userId) class NullSlotLabelFactory extends SlotLabelFactory { - public function Format(ReservationItemView $reservation, $format = null) + public function Format(ReservationItemView $reservation, ?string $format = null, bool $skipVisibilityChecks = false) { return ''; } diff --git a/tests/Presenters/CalendarExportPresenterTest.php b/tests/Presenters/CalendarExportPresenterTest.php index a348918877..b04a0abcc1 100644 --- a/tests/Presenters/CalendarExportPresenterTest.php +++ b/tests/Presenters/CalendarExportPresenterTest.php @@ -247,6 +247,70 @@ public function testViewEscapesBackslashSemicolonAndCommaInTextPropertiesForICal $this->assertEquals('a\\\\b\\;c\\,d', $reservationView->Description); } + public function testSlotLabelFormatCanExplicitlySkipVisibilityChecks() + { + $user = new NullUserSession(); + $res = new ReservationItemView(); + $res->Title = 'Public Meeting'; + $res->StartDate = Date::Now(); + $res->EndDate = Date::Now()->AddHours(1); + + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, false); + + $factory = new SlotLabelFactory($user, new FakeAuthorizationService()); + + $this->assertEquals('', $factory->Format($res, '{title}')); + $this->assertEquals('Public Meeting', $factory->Format($res, '{title}', skipVisibilityChecks: true)); + } + + public function testSlotLabelFormatStillRedactsUserTokensWhenSkippingVisibilityChecks() + { + $user = new FakeUserSession(false, 'America/New_York', 7); + $auth = new FakeAuthorizationService(); + $auth->_CanEditForResource = false; + + $res = new ReservationItemView(); + $res->OwnerId = 42; + $res->FirstName = 'Alice'; + $res->LastName = 'Smith'; + $res->OwnerEmailAddress = 'alice@example.com'; + $res->OwnerPhone = '555-1234'; + $res->OwnerOrganization = 'Engineering'; + $res->OwnerPosition = 'Manager'; + $res->ParticipantNames = ['Participant One']; + $res->InviteeNames = ['Invitee One']; + $res->StartDate = Date::Now(); + $res->EndDate = Date::Now()->AddHours(1); + + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_HIDE_USER_DETAILS, true); + + $factory = new SlotLabelFactory($user, $auth); + $label = $factory->Format( + $res, + '{name} {email} {phone} {organization} {position} {participants} {invitees}', + skipVisibilityChecks: true + ); + + $this->assertStringContainsString('Private', $label); + $this->assertStringNotContainsString('Alice', $label); + $this->assertStringNotContainsString('alice@example.com', $label); + $this->assertStringNotContainsString('555-1234', $label); + $this->assertStringNotContainsString('Engineering', $label); + $this->assertStringNotContainsString('Manager', $label); + $this->assertStringNotContainsString('Participant One', $label); + $this->assertStringNotContainsString('Invitee One', $label); + } + + public function testNullSlotLabelFactoryRemainsFailClosedWhenSkippingVisibilityChecks() + { + $res = new ReservationItemView(); + $res->Title = 'Public Meeting'; + + $factory = new NullSlotLabelFactory(); + + $this->assertEquals('', $factory->Format($res, '{title}', skipVisibilityChecks: true)); + } + public function testCalendarExportProdIdUsesApplicationVersionInsteadOfConfigValue() { $this->fakeConfig->SetKey('version', '9.9.9-user-config'); From 7d6502485c121fb600a3de3c6c90c646311ac6ea Mon Sep 17 00:00:00 2001 From: lucs7 Date: Wed, 10 Jun 2026 22:34:56 -0700 Subject: [PATCH 2/2] fix(ics): handle subscription feed visibility explicitly Add explicit iCalendar view options for anonymous public subscriptions and user-scoped subscription URLs. Keep anonymous requests anonymous while allowing user subscription URLs to format with the subscribed user context. Assisted-by: Codex:GPT-5 --- Presenters/CalendarSubscriptionPresenter.php | 24 +++- .../Schedule/iCalendarReservationView.php | 103 ++++++++++--- .../CalendarExportPresenterTest.php | 135 ++++++++++++++++++ .../CalendarSubscriptionPresenterTest.php | 48 +++++++ 4 files changed, 293 insertions(+), 17 deletions(-) diff --git a/Presenters/CalendarSubscriptionPresenter.php b/Presenters/CalendarSubscriptionPresenter.php index 612d1356ab..57d82db461 100644 --- a/Presenters/CalendarSubscriptionPresenter.php +++ b/Presenters/CalendarSubscriptionPresenter.php @@ -76,6 +76,7 @@ public function PageLoad(): bool $rid = null; $uid = null; $aid = null; + $user = null; $resourceIds = []; $reservations = []; @@ -123,6 +124,14 @@ public function PageLoad(): bool ); $session = ServiceLocator::GetServer()->GetUserSession(); + $viewOptions = iCalendarReservationViewOptions::Default(); + if ($uid !== null) { + // The full icskey+uid URL is bearer authorization for this user's feed. + // It identifies the subscribed user, but it is not a logged-in session. + $viewOptions = iCalendarReservationViewOptions::ForUserSubscription($this->CreateSubscriptionUserSession($user)); + } elseif (!$session->IsLoggedIn()) { + $viewOptions = iCalendarReservationViewOptions::ForAnonymousSubscription(); + } foreach ($res as $r) { if (empty($resourceIds) || in_array($r->ResourceId, $resourceIds)) { @@ -130,7 +139,8 @@ public function PageLoad(): bool $r, $session, $this->privacyFilter, - $summaryFormat + $summaryFormat, + $viewOptions ); } } @@ -139,4 +149,16 @@ public function PageLoad(): bool return true; } + + private function CreateSubscriptionUserSession(User $user): UserSession + { + $session = new UserSession($user->Id()); + $session->FirstName = $user->FirstName(); + $session->LastName = $user->LastName(); + $session->Email = $user->EmailAddress(); + $session->Timezone = $user->Timezone(); + $session->PublicId = $user->GetPublicId(); + + return $session; + } } diff --git a/lib/Application/Schedule/iCalendarReservationView.php b/lib/Application/Schedule/iCalendarReservationView.php index 682fb4072b..8ad1cdcc8b 100644 --- a/lib/Application/Schedule/iCalendarReservationView.php +++ b/lib/Application/Schedule/iCalendarReservationView.php @@ -1,5 +1,70 @@ formatWithoutVisibilityChecks = true; + return $options; + } + + public static function ForUserSubscription(UserSession $subscribedUser): static + { + $options = new iCalendarReservationViewOptions(); + $options->canViewUser = true; + $options->canViewDetails = true; + $options->formattingUser = $subscribedUser; + $options->formatWithoutVisibilityChecks = true; + $options->respectPublicReservationVisibility = false; + return $options; + } + + public function CanViewUser(UserSession $currentUser, IPrivacyFilter $privacyFilter, ReservationItemView $reservation): bool + { + if ($this->canViewUser !== null) { + return $this->canViewUser; + } + + return $privacyFilter->CanViewUser($currentUser, $reservation, $reservation->OwnerId); + } + + public function CanViewDetails(UserSession $currentUser, IPrivacyFilter $privacyFilter, ReservationItemView $reservation): bool + { + if ($this->canViewDetails !== null) { + return $this->canViewDetails; + } + + return $privacyFilter->CanViewDetails($currentUser, $reservation, $reservation->OwnerId); + } + + public function FormattingUser(UserSession $currentUser): UserSession + { + return $this->formattingUser ?? $currentUser; + } + + public function FormatWithoutVisibilityChecks(): bool + { + return $this->formatWithoutVisibilityChecks; + } + + public function RespectPublicReservationVisibility(): bool + { + return $this->respectPublicReservationVisibility; + } +} + class iCalendarReservationView { public $Classification; @@ -35,26 +100,23 @@ class iCalendarReservationView * @param UserSession $currentUser * @param IPrivacyFilter $privacyFilter * @param string|null $summaryFormat + * @param iCalendarReservationViewOptions|null $options */ - public function __construct($res, UserSession $currentUser, IPrivacyFilter $privacyFilter, $summaryFormat = null) + public function __construct($res, UserSession $currentUser, IPrivacyFilter $privacyFilter, $summaryFormat = null, $options = null) { if ($summaryFormat == null) { $summaryFormat = Configuration::Instance()->GetKey(ConfigKeys::RESERVATION_LABELS_ICS_SUMMARY); } - $factory = new SlotLabelFactory($currentUser); + $options = $options ?? iCalendarReservationViewOptions::Default(); + $formattingUser = $options->FormattingUser($currentUser); + $factory = new SlotLabelFactory($formattingUser); $this->ReservationItemView = $res; - $canViewUser = $privacyFilter->CanViewUser($currentUser, $res, $res->OwnerId); - $canViewDetails = $privacyFilter->CanViewDetails($currentUser, $res, $res->OwnerId); - - // PrivacyFilter only gates on privacy.hide.reservation.details and privacy.hide.user.details. - // For anonymous (not logged-in) callers, also enforce privacy.view.reservations, which is the - // site-wide switch that controls whether unauthenticated visitors may see reservation details at all. - if (!$currentUser->IsLoggedIn()) { - $publicViewAllowed = Configuration::Instance()->GetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, new BooleanConverter()); - if (!$publicViewAllowed) { - $canViewUser = false; - $canViewDetails = false; - } + $canViewUser = $options->CanViewUser($currentUser, $privacyFilter, $res); + $canViewDetails = $options->CanViewDetails($currentUser, $privacyFilter, $res); + + if ($options->RespectPublicReservationVisibility() && !$this->CanViewPublicReservations($currentUser)) { + $canViewUser = false; + $canViewDetails = false; } $this->ExportFactory = PluginManager::Instance()->LoadExport(); @@ -70,7 +132,7 @@ public function __construct($res, UserSession $currentUser, IPrivacyFilter $priv $this->DateEnd = $res->EndDate; $this->DateStart = $res->StartDate; - $this->Summary = $canViewDetails ? self::toRfc5545Text($factory->Format($res, $summaryFormat)) : $privateNotice; + $this->Summary = $canViewDetails ? self::toRfc5545Text($factory->Format($res, $summaryFormat, skipVisibilityChecks: $options->FormatWithoutVisibilityChecks())) : $privateNotice; $this->Description = $canViewDetails ? self::toRfc5545Text($res->Description ?? '') : $privateNotice; $fullName = new FullName($res->OwnerFirstName, $res->OwnerLastName); $this->Organizer = $canViewUser ? $fullName->__toString() : $privateNotice; @@ -91,13 +153,22 @@ public function __construct($res, UserSession $currentUser, IPrivacyFilter $priv $this->LastModified = empty($res->ModifiedDate) || $res->ModifiedDate->ToString() == '' ? $this->DateCreated : $res->ModifiedDate; $this->IsPending = $res->RequiresApproval; - if ($canViewUser && $res->OwnerId == $currentUser->UserId) { + if ($canViewUser && !empty($res->OwnerId) && $res->OwnerId == $currentUser->UserId) { $this->OrganizerEmail = str_replace('@', '-noreply@', $res->OwnerEmailAddress); } $this->ExtraIcalLines = method_exists($this->ExportFactory, 'GetIcalendarExtraLines') ? $this->ExportFactory->GetIcalendarExtraLines($res) : null; } + private function CanViewPublicReservations(UserSession $currentUser) + { + if ($currentUser->IsLoggedIn()) { + return true; + } + + return Configuration::Instance()->GetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, new BooleanConverter()); + } + /** * Escapes a plain-text value for use in an iCalendar TEXT property (RFC 5545 ยง3.3.11). * Backslashes, semicolons, and commas are escaped first; then newline sequences diff --git a/tests/Presenters/CalendarExportPresenterTest.php b/tests/Presenters/CalendarExportPresenterTest.php index b04a0abcc1..1c1d4eeea7 100644 --- a/tests/Presenters/CalendarExportPresenterTest.php +++ b/tests/Presenters/CalendarExportPresenterTest.php @@ -209,6 +209,141 @@ public function testAnonymousUserSeesPrivateWhenPublicReservationViewingIsDisabl $this->assertEquals('Private', $reservationView->OrganizerEmail); } + public function testUserSubscriptionViewShowsDetailsWithAnonymousRequest() + { + $session = new NullUserSession(); + $subscribedUser = new UserSession(999); + $subscribedUser->Timezone = 'America/Chicago'; + $res = new ReservationItemView(); + $res->UserId = 999; + $res->UserLevelId = ReservationUserLevel::OWNER; + $res->Title = 'Team Meeting'; + $res->Description = 'Planning notes'; + $res->StartDate = Date::Now(); + $res->EndDate = Date::Now()->AddHours(1); + $res->OwnerFirstName = 'Alice'; + $res->OwnerLastName = 'Smith'; + $res->OwnerEmailAddress = 'alice@example.com'; + + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, false); + $this->privacyFilter->_CanViewDetails = false; + $this->privacyFilter->_CanViewUser = false; + + $view = new iCalendarReservationView( + $res, + $session, + $this->privacyFilter, + '{title}', + iCalendarReservationViewOptions::ForUserSubscription($subscribedUser) + ); + + $this->assertEquals('Team Meeting', $view->Summary); + $this->assertEquals('Planning notes', $view->Description); + $this->assertEquals('Alice Smith', $view->Organizer); + $this->assertEquals('alice@example.com', $view->OrganizerEmail); + } + + public function testAnonymousSubscriptionViewHidesDetailsWhenPublicReservationVisibilityDisabled() + { + $session = new NullUserSession(); + $res = new ReservationItemView(); + $res->Title = 'Public Meeting'; + $res->Description = 'Public notes'; + $res->StartDate = Date::Now(); + $res->EndDate = Date::Now()->AddHours(1); + $res->OwnerFirstName = 'Alice'; + $res->OwnerLastName = 'Smith'; + $res->OwnerEmailAddress = 'alice@example.com'; + + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, false); + $this->privacyFilter->_CanViewDetails = true; + $this->privacyFilter->_CanViewUser = true; + + $view = new iCalendarReservationView( + $res, + $session, + $this->privacyFilter, + '{title}', + iCalendarReservationViewOptions::ForAnonymousSubscription() + ); + + $this->assertEquals('Private', $view->Summary); + $this->assertEquals('Private', $view->Description); + $this->assertEquals('Private', $view->Organizer); + $this->assertEquals('Private', $view->OrganizerEmail); + } + + public function testAnonymousSubscriptionViewShowsDetailsWhenPublicReservationVisibilityEnabled() + { + $session = new NullUserSession(); + $res = new ReservationItemView(); + $res->Title = 'Public Meeting'; + $res->Description = 'Public notes'; + $res->StartDate = Date::Now(); + $res->EndDate = Date::Now()->AddHours(1); + $res->OwnerFirstName = 'Alice'; + $res->OwnerLastName = 'Smith'; + $res->OwnerEmailAddress = 'alice@example.com'; + + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, true); + $this->privacyFilter->_CanViewDetails = true; + $this->privacyFilter->_CanViewUser = true; + + $view = new iCalendarReservationView( + $res, + $session, + $this->privacyFilter, + '{title}', + iCalendarReservationViewOptions::ForAnonymousSubscription() + ); + + $this->assertEquals('Public Meeting', $view->Summary); + $this->assertEquals('Public notes', $view->Description); + } + + public function testAnonymousSubscriptionSummaryHidesUserTokensWhenUserPrivacyDenied() + { + $session = new NullUserSession(); + $res = new ReservationItemView(); + $res->OwnerId = 42; + $res->Title = 'Public Meeting'; + $res->Description = 'Public notes'; + $res->StartDate = Date::Now(); + $res->EndDate = Date::Now()->AddHours(1); + $res->OwnerFirstName = 'Alice'; + $res->OwnerLastName = 'Smith'; + $res->OwnerEmailAddress = 'alice@example.com'; + $res->OwnerPhone = '555-1234'; + $res->OwnerOrganization = 'Engineering'; + $res->OwnerPosition = 'Manager'; + $res->ParticipantNames = ['Participant One']; + $res->InviteeNames = ['Invitee One']; + + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, true); + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_HIDE_USER_DETAILS, true); + $this->privacyFilter->_CanViewDetails = true; + $this->privacyFilter->_CanViewUser = false; + + $view = new iCalendarReservationView( + $res, + $session, + $this->privacyFilter, + '{name} {email} {phone} {organization} {position} {participants} {invitees}', + iCalendarReservationViewOptions::ForAnonymousSubscription() + ); + + $this->assertStringContainsString('Private', $view->Summary); + $this->assertStringNotContainsString('Alice', $view->Summary); + $this->assertStringNotContainsString('alice@example.com', $view->Summary); + $this->assertStringNotContainsString('555-1234', $view->Summary); + $this->assertStringNotContainsString('Engineering', $view->Summary); + $this->assertStringNotContainsString('Manager', $view->Summary); + $this->assertStringNotContainsString('Participant One', $view->Summary); + $this->assertStringNotContainsString('Invitee One', $view->Summary); + $this->assertEquals('Private', $view->Organizer); + $this->assertEquals('Private', $view->OrganizerEmail); + } + public function testViewEscapesNewlinesInTextPropertiesForICalCompliance() { $user = new FakeUserSession(); diff --git a/tests/Presenters/CalendarSubscriptionPresenterTest.php b/tests/Presenters/CalendarSubscriptionPresenterTest.php index a6a788c9bf..602f767326 100644 --- a/tests/Presenters/CalendarSubscriptionPresenterTest.php +++ b/tests/Presenters/CalendarSubscriptionPresenterTest.php @@ -144,6 +144,54 @@ public function testGetsUserReservationsForTheNextYearByResourceId() $this->assertCount(1, $this->page->Reservations); } + public function testAnonymousUserSubscriptionUsesBearerUrlVisibility() + { + $publicId = 'user-public-id'; + $userId = 999; + $user = new FakeUser($userId); + $user->SetTimezone('America/Chicago'); + $user->WithPublicId($publicId); + + $reservation = new TestReservationItemView(1, Date::Now(), Date::Now()->AddHours(1)); + $reservation->UserId = $userId; + $reservation->UserLevelId = ReservationUserLevel::OWNER; + $reservation->OwnerId = $userId; + $reservation->Title = 'Team Meeting'; + $reservation->Description = 'Planning notes'; + $reservation->OwnerFirstName = 'Alice'; + $reservation->OwnerLastName = 'Smith'; + $reservation->OwnerEmailAddress = 'alice@example.com'; + + $this->fakeServer->SetUserSession(new NullUserSession()); + $this->fakeConfig->SetKey(ConfigKeys::PRIVACY_VIEW_RESERVATIONS, false); + $this->fakeConfig->SetKey(ConfigKeys::RESERVATION_LABELS_ICS_MY_SUMMARY, '{title}'); + $this->privacyFilter->_CanViewDetails = false; + $this->privacyFilter->_CanViewUser = false; + + $weekAgo = Date::Now()->AddDays(0); + $nextYear = Date::Now()->AddDays(30); + + $this->page->UserId = $publicId; + + $this->service->expects($this->once()) + ->method('GetUser') + ->with($this->equalTo($publicId)) + ->willReturn($user); + + $this->repo->expects($this->once()) + ->method('GetReservations') + ->with($this->equalTo($weekAgo), $this->equalTo($nextYear), $this->equalTo($userId), ReservationUserLevel::ALL, $this->isNull(), $this->isNull()) + ->willReturn([$reservation]); + + $this->presenter->PageLoad(); + + $this->assertFalse($this->fakeServer->GetUserSession()->IsLoggedIn()); + $this->assertCount(1, $this->page->Reservations); + $this->assertEquals('Team Meeting', $this->page->Reservations[0]->Summary); + $this->assertEquals('Planning notes', $this->page->Reservations[0]->Description); + $this->assertEquals('Alice Smith', $this->page->Reservations[0]->Organizer); + } + public function testGetsResourceGroupReservationsForTheNextYearByGroupId() { $publicId = '1';