diff --git a/app/ModelSerializers/Summit/Presentation/AdminPresentationCSVSerializer.php b/app/ModelSerializers/Summit/Presentation/AdminPresentationCSVSerializer.php index c21c481f5..a98740341 100644 --- a/app/ModelSerializers/Summit/Presentation/AdminPresentationCSVSerializer.php +++ b/app/ModelSerializers/Summit/Presentation/AdminPresentationCSVSerializer.php @@ -88,7 +88,7 @@ public function serialize($expand = null, array $fields = [], array $relations = if(in_array("moderator_id",$fields)) $values['moderator_id'] = $presentation->getModerator()->getId(); if(in_array("moderator_full_name",$fields)) - $values['moderator_full_name'] = $presentation->getModerator()->getFullName(); + $values['moderator_full_name'] = $presentation->getModerator()->getFullName(true); if(in_array("moderator_email",$fields)) $values['moderator_email'] = $presentation->getModerator()->getEmail(); if(in_array("moderator_title",$fields)) @@ -122,7 +122,7 @@ public function serialize($expand = null, array $fields = [], array $relations = foreach ($presentation->getSpeakers() as $speaker) { $speaker_ids[] = $speaker->getId(); - $speaker_fullnames[] = $speaker->getFullName(); + $speaker_fullnames[] = $speaker->getFullName(true); $speaker_emails[] = $speaker->getEmail(); $speaker_titles[] = trim($speaker->getTitle()); $speaker_companies[] = trim($speaker->getCompany()); @@ -164,7 +164,7 @@ public function serialize($expand = null, array $fields = [], array $relations = if(in_array("submitter_id",$fields)) $values['submitter_id'] = $submitter->getId(); if(in_array("submitter_full_name",$fields)) - $values['submitter_full_name'] = $submitter->getFullName(); + $values['submitter_full_name'] = $submitter->getFullName(true); if(in_array("submitter_email",$fields)) $values['submitter_email'] = $submitter->getEmail(); if(in_array("submitter_title",$fields)) diff --git a/app/ModelSerializers/Summit/Presentation/TrackChairPresentationCSVSerializer.php b/app/ModelSerializers/Summit/Presentation/TrackChairPresentationCSVSerializer.php index 0aac8fb23..9801bb144 100644 --- a/app/ModelSerializers/Summit/Presentation/TrackChairPresentationCSVSerializer.php +++ b/app/ModelSerializers/Summit/Presentation/TrackChairPresentationCSVSerializer.php @@ -95,7 +95,7 @@ public function serialize($expand = null, array $fields = [], array $relations = if ($presentation->hasModerator()) { $values['moderator_id'] = $presentation->getModerator()->getId(); - $values['moderator_full_name'] = $presentation->getModerator()->getFullName(); + $values['moderator_full_name'] = $presentation->getModerator()->getFullName(true); $values['moderator_email'] = $presentation->getModerator()->getEmail(); $values['moderator_title'] = trim($presentation->getModerator()->getTitle()); $values['moderator_company'] = trim($presentation->getModerator()->getCompany()); @@ -119,7 +119,7 @@ public function serialize($expand = null, array $fields = [], array $relations = foreach ($presentation->getSpeakers() as $speaker) { $speaker_ids[] = $speaker->getId(); - $speaker_fullnames[] = $speaker->getFullName(); + $speaker_fullnames[] = $speaker->getFullName(true); $speaker_emails[] = $speaker->getEmail(); $speaker_titles[] = trim($speaker->getTitle()); $speaker_companies[] = trim($speaker->getCompany()); @@ -145,7 +145,7 @@ public function serialize($expand = null, array $fields = [], array $relations = if ($creator->hasSpeaker()) { $submitter = $creator->getSpeaker(); $values['submitter_id'] = $submitter->getId(); - $values['submitter_full_name'] = $submitter->getFullName(); + $values['submitter_full_name'] = $submitter->getFullName(true); $values['submitter_email'] = $submitter->getEmail(); $values['submitter_title'] = $submitter->getTitle(); $values['submitter_company'] = $submitter->getCompany(); diff --git a/app/ModelSerializers/Summit/Registration/PromoCodes/SpeakersRegistrationDiscountCodeCSVSerializer.php b/app/ModelSerializers/Summit/Registration/PromoCodes/SpeakersRegistrationDiscountCodeCSVSerializer.php index 6f7d4a32d..64bf8cde5 100644 --- a/app/ModelSerializers/Summit/Registration/PromoCodes/SpeakersRegistrationDiscountCodeCSVSerializer.php +++ b/app/ModelSerializers/Summit/Registration/PromoCodes/SpeakersRegistrationDiscountCodeCSVSerializer.php @@ -44,7 +44,7 @@ public function serialize($expand = null, array $fields = [], array $relations = $owner_name = []; $owner_email = []; foreach($code->getOwners() as $owner){ - $owner_name[] = $owner->getSpeaker()->getFullName(); + $owner_name[] = $owner->getSpeaker()->getFullName(true); $owner_email[] = $owner->getSpeaker()->getEmail(); } diff --git a/app/ModelSerializers/Summit/Registration/PromoCodes/SpeakersSummitRegistrationPromoCodeCSVSerializer.php b/app/ModelSerializers/Summit/Registration/PromoCodes/SpeakersSummitRegistrationPromoCodeCSVSerializer.php index 9bfab8fe5..e2d162159 100644 --- a/app/ModelSerializers/Summit/Registration/PromoCodes/SpeakersSummitRegistrationPromoCodeCSVSerializer.php +++ b/app/ModelSerializers/Summit/Registration/PromoCodes/SpeakersSummitRegistrationPromoCodeCSVSerializer.php @@ -43,7 +43,7 @@ public function serialize($expand = null, array $fields = [], array $relations = $owner_name = []; $owner_email = []; foreach($code->getOwners() as $owner){ - $owner_name[] = $owner->getSpeaker()->getFullName(); + $owner_name[] = $owner->getSpeaker()->getFullName(true); $owner_email[] = $owner->getSpeaker()->getEmail(); } diff --git a/app/ModelSerializers/Summit/Speakers/AdminPresentationSpeakerCSVSerializer.php b/app/ModelSerializers/Summit/Speakers/AdminPresentationSpeakerCSVSerializer.php index 9593b2b5f..9eea4586c 100644 --- a/app/ModelSerializers/Summit/Speakers/AdminPresentationSpeakerCSVSerializer.php +++ b/app/ModelSerializers/Summit/Speakers/AdminPresentationSpeakerCSVSerializer.php @@ -48,6 +48,15 @@ public function serialize($expand = null, array $fields = [], array $relations = if(!$speaker instanceof PresentationSpeaker) return []; $values = parent::serialize($expand, $fields, $relations, $params); + + if(in_array('first_name', $fields)) { + $values['first_name'] = $speaker->getFirstName(true); + } + + if(in_array('last_name', $fields)) { + $values['last_name'] = $speaker->getLastName(true); + } + if(in_array("email", $fields)) $values['email'] = JsonUtils::toJsonString($speaker->getEmail()); @@ -92,4 +101,4 @@ public function serialize($expand = null, array $fields = [], array $relations = return $values; } -} \ No newline at end of file +} diff --git a/app/ModelSerializers/Summit/Speakers/AdminPresentationSpeakerSerializer.php b/app/ModelSerializers/Summit/Speakers/AdminPresentationSpeakerSerializer.php index 9fa4c52f4..314d4a4ec 100644 --- a/app/ModelSerializers/Summit/Speakers/AdminPresentationSpeakerSerializer.php +++ b/app/ModelSerializers/Summit/Speakers/AdminPresentationSpeakerSerializer.php @@ -39,7 +39,7 @@ final class AdminPresentationSpeakerSerializer extends PresentationSpeakerSerial 'summit_assistance', 'registration_code', ]; - + protected function checkDataPermissions(PresentationSpeaker $speaker, array $values):array{ return $values; } @@ -64,6 +64,15 @@ public function serialize($expand = null, array $fields = [], array $relations = $values = parent::serialize($expand, $fields, $relations, $params); $summit = isset($params['summit'])? $params['summit']:null; + + if(in_array('first_name', $fields)) { + $values['first_name'] = $speaker->getFirstName(true); + } + + if(in_array('last_name', $fields)) { + $values['last_name'] = $speaker->getLastName(true); + } + if(in_array("email", $fields)) { $application_type = $this->resource_server_context->getApplicationType(); // choose email serializer depending on user permissions @@ -214,4 +223,4 @@ public function serialize($expand = null, array $fields = [], array $relations = } return $values; } -} \ No newline at end of file +} diff --git a/app/ModelSerializers/Summit/Speakers/PresentationSpeakerBaseSerializer.php b/app/ModelSerializers/Summit/Speakers/PresentationSpeakerBaseSerializer.php index c9d91b91e..11341c4d6 100644 --- a/app/ModelSerializers/Summit/Speakers/PresentationSpeakerBaseSerializer.php +++ b/app/ModelSerializers/Summit/Speakers/PresentationSpeakerBaseSerializer.php @@ -83,23 +83,6 @@ public function serialize($expand = null, array $fields = [], array $relations = $values = parent::serialize($expand, $fields, $relations, $params); - if ( - (empty($values['first_name']) || empty($values['last_name'])) - && in_array('first_name', $fields) && in_array('last_name', $fields) - ) { - - $first_name = ''; - $last_name = ''; - if ($speaker->hasMember()) { - $member = $speaker->getMember(); - $first_name = $member->getFirstName(); - $last_name = $member->getLastName(); - } - $values['first_name'] = $first_name; - $values['last_name'] = $last_name; - } - - if(in_array("email", $fields)) { $application_type = $this->resource_server_context->getApplicationType(); // choose email serializer depending on user permissions @@ -111,4 +94,4 @@ public function serialize($expand = null, array $fields = [], array $relations = return $values; } -} \ No newline at end of file +} diff --git a/app/ModelSerializers/Summit/Speakers/PresentationSpeakerSerializer.php b/app/ModelSerializers/Summit/Speakers/PresentationSpeakerSerializer.php index fdc018f3a..2d4975a1a 100644 --- a/app/ModelSerializers/Summit/Speakers/PresentationSpeakerSerializer.php +++ b/app/ModelSerializers/Summit/Speakers/PresentationSpeakerSerializer.php @@ -350,54 +350,15 @@ public function serialize($expand = null, array $fields = [], array $relations = protected function checkDataPermissions(PresentationSpeaker $speaker, array $values):array{ // permissions check - if(!$speaker->isPublicProfileShowBio()) - { - if(isset($values['bio'])) $values['bio'] = ''; - if(isset($values['gender'])) $values['gender'] = ''; - if(isset($values['company'])) $values['company'] = ''; - if(isset($values['state'])) $values['state'] = ''; - if(isset($values['country'])) $values['country'] = ''; - if(isset($values['title'])) $values['title'] = ''; - - if(isset($values['affiliations'])) $values['affiliations'] = []; - if(isset($values['languages'])) $values['languages'] = []; - if(isset($values['other_presentation_links'])) $values['other_presentation_links'] = []; - if(isset($values['areas_of_expertise'])) $values['areas_of_expertise'] = []; - if(isset($values['travel_preferences'])) $values['travel_preferences'] = []; - if(isset($values['active_involvements'])) $values['active_involvements'] = []; - if(isset($values['organizational_roles'])) $values['organizational_roles'] = []; - if(isset($values['badge_features'])) $values['badge_features'] = []; - } - if(!$speaker->isPublicProfileShowEmail()) { if(isset($values['email'])) $values['email'] = ''; } - if(!$speaker->isPublicProfileShowSocialMediaInfo()) - { - if(isset($values['irc'])) $values['irc'] = ''; - if(isset($values['twitter'])) $values['twitter'] = ''; - } - - if(!$speaker->isPublicProfileShowPhoto()) - { - if(isset($values['pic'])) $values['pic'] = Config::get("app.default_profile_image", null); - if(isset($values['big_pic'])) $values['big_pic'] = Config::get("app.default_profile_image", null); - } - - /* - * relax full name restriction for signage - if(!$speaker->isPublicProfileShowFullname()) - { - if(isset($values['last_name'])) $values['last_name'] = ''; - } - */ - // phone_number is never public regardless of the target speaker's own account // visibility toggle - see policy/profile-data-handling.md Rule 4. if(isset($values['phone_number'])) $values['phone_number'] = ''; return $values; } -} \ No newline at end of file +} diff --git a/app/Models/Foundation/Summit/Speakers/PresentationSpeaker.php b/app/Models/Foundation/Summit/Speakers/PresentationSpeaker.php index cdcae06d5..f7b5a18ee 100644 --- a/app/Models/Foundation/Summit/Speakers/PresentationSpeaker.php +++ b/app/Models/Foundation/Summit/Speakers/PresentationSpeaker.php @@ -207,10 +207,10 @@ class PresentationSpeaker extends SilverstripeBaseModel /** * @return string|null */ - public function getFirstName():?string + public function getFirstName(bool $override_permission=false):?string { $res = $this->first_name; - if(empty($res) && $this->hasMember()){ + if(empty($res) && ($this->isPublicProfileShowFullname() || $override_permission) && $this->hasMember()){ $res = $this->member->getFirstName(); } return $res; @@ -225,12 +225,13 @@ public function setFirstName(string $first_name):void } /** + * @param bool $override_permission * @return string|null */ - public function getLastName():?string + public function getLastName(bool $override_permission=false):?string { $res = $this->last_name; - if(empty($res) && $this->hasMember()){ + if(empty($res) && ($this->isPublicProfileShowFullname() || $override_permission) && $this->hasMember()){ $res = $this->member->getLastName(); } return $res; @@ -1792,16 +1793,17 @@ public function setRegistrationRequest($registration_request) } /** - * @return string + * @param bool $override_permission + * @return string|null */ - public function getFullName(): ?string + public function getFullName(bool $override_permission=false): ?string { $fullname = $this->first_name; if (!empty($this->last_name)) { if (!empty($fullname)) $fullname .= ' '; $fullname .= $this->last_name; } - if (empty($fullname) && $this->hasMember()) { + if (empty($fullname) && ($this->isPublicProfileShowFullname() || $override_permission) && $this->hasMember()) { $fullname = $this->member->getFullName(); } @@ -2342,7 +2344,7 @@ public function getBigProfilePhotoUrl(): ?string if ($this->hasBigPhoto() && $photo = $this->getBigPhoto()) { $photoUrl = $photo->getUrl(); } - if (empty($photoUrl) && $this->hasMember() && $this->member->hasPhoto() && $photo = $this->member->getPhoto()) { + if (empty($photoUrl) && $this->isPublicProfileShowPhoto() && $this->hasMember() && $this->member->hasPhoto() && $photo = $this->member->getPhoto()) { $photoUrl = $photo->getUrl(); } @@ -2367,7 +2369,7 @@ public function getProfilePhotoUrl(): ?string if ($this->hasPhoto() && $photo = $this->getPhoto()) { $photoUrl = $photo->getUrl(); } - if (empty($photoUrl) && $this->hasMember() && $this->member->hasPhoto() && $photo = $this->member->getPhoto()) { + if (empty($photoUrl) && $this->isPublicProfileShowPhoto() && $this->hasMember() && $this->member->hasPhoto() && $photo = $this->member->getPhoto()) { $photoUrl = $photo->getUrl(); } } catch (\Exception $ex) { diff --git a/tests/AdminCsvSpeakerFullNameFallbackTest.php b/tests/AdminCsvSpeakerFullNameFallbackTest.php new file mode 100644 index 000000000..5ba5f268c --- /dev/null +++ b/tests/AdminCsvSpeakerFullNameFallbackTest.php @@ -0,0 +1,178 @@ +shouldReceive('getApplicationType')->andReturn('JS_CLIENT'); + $context->shouldReceive('getCurrentUser')->andReturn($current_user); + return $context; + } + + private function buildSpeaker(string $full_name): PresentationSpeaker + { + $speaker = Mockery::mock(PresentationSpeaker::class); + $speaker->shouldReceive('getFullName')->once()->with(true)->andReturn($full_name); + $speaker->shouldReceive('getId')->andReturn(1); + $speaker->shouldReceive('getEmail')->andReturn('speaker@example.com'); + $speaker->shouldReceive('getTitle')->andReturn(''); + $speaker->shouldReceive('getCompany')->andReturn(''); + $speaker->shouldReceive('getCountry')->andReturn(''); + return $speaker; + } + + private function buildPresentation + ( + int $id, + PresentationSpeaker $moderator, + PresentationSpeaker $speaker, + PresentationSpeaker $submitter + ): Presentation + { + $creator = Mockery::mock(Member::class); + $creator->shouldReceive('hasSpeaker')->andReturn(true); + $creator->shouldReceive('getSpeaker')->andReturn($submitter); + + $summit = Mockery::mock(Summit::class); + $summit->shouldReceive('getTrackChairByMember')->andReturn(null); + + $presentation = Mockery::mock(Presentation::class); + $presentation->shouldReceive('getId')->andReturn($id); + $presentation->shouldReceive('getLastEditedUTC')->andReturn(null); + $presentation->shouldReceive('hasModerator')->andReturn(true); + $presentation->shouldReceive('getModerator')->andReturn($moderator); + $presentation->shouldReceive('getSpeakers')->andReturn(new ArrayCollection([$speaker])); + $presentation->shouldReceive('hasCreatedBy')->andReturn(true); + $presentation->shouldReceive('getCreatedBy')->andReturn($creator); + $presentation->shouldReceive('getMediaUploads')->andReturn(new ArrayCollection([])); + $presentation->shouldReceive('getSummit')->andReturn($summit); + $presentation->shouldReceive('getExtraQuestionAnswers')->andReturn(new ArrayCollection([])); + $presentation->shouldReceive('hasCategory')->andReturn(false); + $presentation->shouldReceive('getPresentationActions')->andReturn(new ArrayCollection([])); + + return $presentation; + } + + public function testAdminPresentationCSVSerializerBypassesToggleForModeratorSpeakerAndSubmitter() + { + $moderator = $this->buildSpeaker('Moderator Real Name'); + $speaker = $this->buildSpeaker('Co-Speaker Real Name'); + $submitter = $this->buildSpeaker('Submitter Real Name'); + $presentation = $this->buildPresentation(80101, $moderator, $speaker, $submitter); + + $serializer = new AdminPresentationCSVSerializer($presentation, $this->buildResourceServerContext()); + $values = $serializer->serialize( + null, + ['moderator_full_name', 'speaker_fullnames', 'submitter_full_name'], + ['none'] + ); + + $this->assertSame('Moderator Real Name', $values['moderator_full_name']); + $this->assertSame('Co-Speaker Real Name', $values['speaker_fullnames']); + $this->assertSame('Submitter Real Name', $values['submitter_full_name']); + } + + public function testTrackChairPresentationCSVSerializerBypassesToggleForModeratorSpeakerAndSubmitter() + { + $moderator = $this->buildSpeaker('Moderator Real Name'); + $speaker = $this->buildSpeaker('Co-Speaker Real Name'); + $submitter = $this->buildSpeaker('Submitter Real Name'); + $presentation = $this->buildPresentation(80102, $moderator, $speaker, $submitter); + + $serializer = new TrackChairPresentationCSVSerializer($presentation, $this->buildResourceServerContext()); + $values = $serializer->serialize(null, ['id'], ['none']); + + $this->assertSame('Moderator Real Name', $values['moderator_full_name']); + $this->assertSame('Co-Speaker Real Name', $values['speaker_fullnames']); + $this->assertSame('Submitter Real Name', $values['submitter_full_name']); + } + + public function testSpeakersRegistrationDiscountCodeCSVSerializerBypassesToggleForOwner() + { + $owner_speaker = $this->buildSpeaker('Owner Real Name'); + $owner = Mockery::mock(AssignedPromoCodeSpeaker::class); + $owner->shouldReceive('getSpeaker')->andReturn($owner_speaker); + + $code = Mockery::mock(SpeakersRegistrationDiscountCode::class); + $code->shouldReceive('getOwners')->andReturn(new ArrayCollection([$owner])); + $code->shouldReceive('getBadgeFeatures')->andReturn(new ArrayCollection([])); + $code->shouldReceive('getTicketTypesRules')->andReturn(new ArrayCollection([])); + $code->shouldReceive('getTags')->andReturn(new ArrayCollection([])); + $code->shouldReceive('isInfinite')->andReturn(false); + + $serializer = new SpeakersRegistrationDiscountCodeCSVSerializer($code, $this->buildResourceServerContext()); + $values = $serializer->serialize(null, ['owner_name'], ['none']); + + $this->assertSame('Owner Real Name', $values['owner_name']); + } + + public function testSpeakersSummitRegistrationPromoCodeCSVSerializerBypassesToggleForOwner() + { + $owner_speaker = $this->buildSpeaker('Owner Real Name'); + $owner = Mockery::mock(AssignedPromoCodeSpeaker::class); + $owner->shouldReceive('getSpeaker')->andReturn($owner_speaker); + + $code = Mockery::mock(SpeakersSummitRegistrationPromoCode::class); + $code->shouldReceive('getOwners')->andReturn(new ArrayCollection([$owner])); + $code->shouldReceive('getBadgeFeatures')->andReturn(new ArrayCollection([])); + $code->shouldReceive('getAllowedTicketTypes')->andReturn(new ArrayCollection([])); + $code->shouldReceive('getTags')->andReturn(new ArrayCollection([])); + $code->shouldReceive('isInfinite')->andReturn(false); + + $serializer = new SpeakersSummitRegistrationPromoCodeCSVSerializer($code, $this->buildResourceServerContext()); + $values = $serializer->serialize(null, ['owner_name'], ['none']); + + $this->assertSame('Owner Real Name', $values['owner_name']); + } +} diff --git a/tests/PresentationSpeakerSerializerTest.php b/tests/PresentationSpeakerSerializerTest.php index 192a02184..587250b63 100644 --- a/tests/PresentationSpeakerSerializerTest.php +++ b/tests/PresentationSpeakerSerializerTest.php @@ -12,6 +12,7 @@ * limitations under the License. **/ +use models\main\Member; use models\oauth2\IResourceServerContext; use models\summit\PresentationSpeaker; use ModelSerializers\PresentationSpeakerSerializer; @@ -49,4 +50,77 @@ public function testPhoneNumberIsMaskedInPublicContextEvenWhenSpeakerToggleIsOn( $this->assertSame('', $values['phone_number']); } + + public function testBioGatedSpeakerFieldsAreNotMaskedWhenAccountBioToggleIsOff() + { + $member = Mockery::mock(Member::class); + $member->shouldReceive('getGender')->andReturn('Female'); + + $speaker = Mockery::mock(PresentationSpeaker::class)->makePartial(); + $speaker->shouldReceive('hasMember')->andReturn(true); + $speaker->shouldReceive('getMember')->andReturn($member); + $speaker->shouldReceive('getBio')->andReturn('A speaker bio'); + $speaker->shouldReceive('getCompany')->andReturn('Acme Corp'); + $speaker->shouldReceive('getCountry')->andReturn('AR'); + $speaker->shouldReceive('getTitle')->andReturn('CTO'); + // the target speaker's own account-level "show bio" toggle is OFF - populated + // speaker-profile fields (policy Rule 2) must stay public regardless. + $speaker->shouldReceive('isPublicProfileShowBio')->andReturn(false); + $speaker->shouldReceive('isPublicProfileShowEmail')->andReturn(true); + $speaker->shouldReceive('isPublicProfileShowPhoto')->andReturn(true); + + $resource_server_context = Mockery::mock(IResourceServerContext::class); + $serializer = new PresentationSpeakerSerializer($speaker, $resource_server_context); + + $values = $serializer->serialize(null, ['bio', 'gender', 'company', 'country', 'title'], ['none']); + + $this->assertSame('A speaker bio', $values['bio']); + $this->assertSame('Female', $values['gender']); + $this->assertSame('Acme Corp', $values['company']); + $this->assertSame('AR', $values['country']); + $this->assertSame('CTO', $values['title']); + } + + public function testSocialMediaFieldsAreNotMaskedWhenAccountSocialToggleIsOff() + { + $speaker = Mockery::mock(PresentationSpeaker::class)->makePartial(); + $speaker->shouldReceive('hasMember')->andReturn(false); + $speaker->shouldReceive('getIRCHandle')->andReturn('speaker_nick'); + $speaker->shouldReceive('getTwitterName')->andReturn('@speaker_nick'); + // the target speaker's own account-level "show social media" toggle is OFF - irc/twitter + // are populated speaker-profile fields (policy Rule 2) and must stay public regardless. + $speaker->shouldReceive('isPublicProfileShowSocialMediaInfo')->andReturn(false); + $speaker->shouldReceive('isPublicProfileShowEmail')->andReturn(true); + $speaker->shouldReceive('isPublicProfileShowPhoto')->andReturn(true); + + $resource_server_context = Mockery::mock(IResourceServerContext::class); + $serializer = new PresentationSpeakerSerializer($speaker, $resource_server_context); + + $values = $serializer->serialize(null, ['irc', 'twitter'], ['none']); + + $this->assertSame('speaker_nick', $values['irc']); + $this->assertSame('@speaker_nick', $values['twitter']); + } + + public function testPhotoUrlsPassThroughSerializerRegardlessOfAccountPhotoToggle() + { + $speaker = Mockery::mock(PresentationSpeaker::class)->makePartial(); + $speaker->shouldReceive('hasMember')->andReturn(false); + $speaker->shouldReceive('getProfilePhotoUrl')->andReturn('https://example.com/pic.jpg'); + $speaker->shouldReceive('getBigProfilePhotoUrl')->andReturn('https://example.com/big_pic.jpg'); + // policy Rule 9: the account-toggle gate on the borrowed-from-account photo fallback now + // lives inside getProfilePhotoUrl()/getBigProfilePhotoUrl() themselves (ClickUp 86bbmbm0f), + // not in this serializer - so the serializer must pass their result through unmasked even + // when the toggle is off, instead of re-applying its own masking on top. + $speaker->shouldReceive('isPublicProfileShowPhoto')->andReturn(false); + $speaker->shouldReceive('isPublicProfileShowEmail')->andReturn(true); + + $resource_server_context = Mockery::mock(IResourceServerContext::class); + $serializer = new PresentationSpeakerSerializer($speaker, $resource_server_context); + + $values = $serializer->serialize(null, ['pic', 'big_pic'], ['none']); + + $this->assertSame('https://example.com/pic.jpg', $values['pic']); + $this->assertSame('https://example.com/big_pic.jpg', $values['big_pic']); + } } diff --git a/tests/Unit/Entities/PresentationSpeakerTest.php b/tests/Unit/Entities/PresentationSpeakerTest.php index 04ca3eb08..f1706edbe 100644 --- a/tests/Unit/Entities/PresentationSpeakerTest.php +++ b/tests/Unit/Entities/PresentationSpeakerTest.php @@ -16,7 +16,10 @@ **/ use App\Models\Foundation\Main\Language; +use Illuminate\Support\Facades\Config; +use Mockery; use models\main\File; +use models\main\Member; use models\summit\Presentation; use models\summit\PresentationSpeaker; use models\summit\SpeakerExpertise; @@ -42,6 +45,7 @@ protected function setUp():void public function tearDown():void { + Mockery::close(); self::clearSummitTestData(); parent::tearDown(); } @@ -142,4 +146,100 @@ public function testPersistPresentationSpeaker(){ $this->assertEmpty($found_speaker->getLanguages()->toArray()); } + + /** + * Policy Rule 9: the name fallback to the linked Member must skip that Member's value + * (leaving the speaker's own field blank) when the Member's own visibility toggle is off. + */ + public function testNameFallbackSkipsMemberWhenAccountFullnameToggleIsOff() + { + $member = Mockery::mock(Member::class); + $member->shouldReceive('getId')->andReturn(42); + $member->shouldReceive('setSpeaker')->andReturnNull(); + $member->shouldReceive('isPublicProfileShowFullname')->andReturn(false); + // stubbed (not just omitted) so a missing gate surfaces this value instead of an + // uncaught Mockery exception, which would falsely look like the gate held. + $member->shouldReceive('getFirstName')->andReturn('Ada'); + $member->shouldReceive('getLastName')->andReturn('Lovelace'); + $member->shouldReceive('getFullName')->andReturn('Ada Lovelace'); + + $speaker = new PresentationSpeaker(); + $speaker->setMember($member); + + $this->assertEmpty($speaker->getFirstName()); + $this->assertEmpty($speaker->getLastName()); + $this->assertEmpty($speaker->getFullName()); + } + + /** + * Policy Rule 9: the name fallback still applies the Member's own value when that + * Member's visibility toggle is on. + */ + public function testNameFallbackUsesMemberWhenAccountFullnameToggleIsOn() + { + $member = Mockery::mock(Member::class); + $member->shouldReceive('getId')->andReturn(42); + $member->shouldReceive('setSpeaker')->andReturnNull(); + $member->shouldReceive('isPublicProfileShowFullname')->andReturn(true); + $member->shouldReceive('getFirstName')->andReturn('Ada'); + $member->shouldReceive('getLastName')->andReturn('Lovelace'); + $member->shouldReceive('getFullName')->andReturn('Ada Lovelace'); + + $speaker = new PresentationSpeaker(); + $speaker->setMember($member); + + $this->assertSame('Ada', $speaker->getFirstName()); + $this->assertSame('Lovelace', $speaker->getLastName()); + $this->assertSame('Ada Lovelace', $speaker->getFullName()); + } + + /** + * Policy Rule 9: the photo fallback to the linked Member must skip that Member's photo + * (continuing to the configured default image) when the Member's own visibility toggle is off. + */ + public function testPhotoFallbackSkipsMemberWhenAccountPhotoToggleIsOff() + { + $photo = Mockery::mock(File::class); + $photo->shouldReceive('getUrl')->andReturn('https://example.com/member-photo.jpg'); + + $member = Mockery::mock(Member::class); + $member->shouldReceive('getId')->andReturn(42); + $member->shouldReceive('setSpeaker')->andReturnNull(); + $member->shouldReceive('isPublicProfileShowPhoto')->andReturn(false); + // stubbed (not just omitted) so a missing gate surfaces this photo instead of silently + // passing: both getProfilePhotoUrl()/getBigProfilePhotoUrl() wrap this branch in a + // try/catch that would swallow an unstubbed-call exception and mask a missing gate. + $member->shouldReceive('hasPhoto')->andReturn(true); + $member->shouldReceive('getPhoto')->andReturn($photo); + + $speaker = new PresentationSpeaker(); + $speaker->setMember($member); + + $default_pic = Config::get("app.default_profile_image", null); + $this->assertSame($default_pic, $speaker->getProfilePhotoUrl()); + $this->assertSame($default_pic, $speaker->getBigProfilePhotoUrl()); + } + + /** + * Policy Rule 9: the photo fallback still applies the Member's own photo when that + * Member's visibility toggle is on. + */ + public function testPhotoFallbackUsesMemberWhenAccountPhotoToggleIsOn() + { + $photo = Mockery::mock(File::class); + $photo->shouldReceive('getUrl')->andReturn('https://example.com/member-photo.jpg'); + + $member = Mockery::mock(Member::class); + $member->shouldReceive('getId')->andReturn(42); + $member->shouldReceive('setSpeaker')->andReturnNull(); + $member->shouldReceive('isPublicProfileShowPhoto')->andReturn(true); + $member->shouldReceive('hasPhoto')->andReturn(true); + $member->shouldReceive('getPhoto')->andReturn($photo); + + $speaker = new PresentationSpeaker(); + $speaker->setMember($member); + + $this->assertSame('https://example.com/member-photo.jpg', $speaker->getProfilePhotoUrl()); + $this->assertSame('https://example.com/member-photo.jpg', $speaker->getBigProfilePhotoUrl()); + } } \ No newline at end of file