Aleksandr Trofimov 1 месяц назад
Родитель
Сommit
dfc9e7de81

+ 14 - 0
tests/bootstrap.php

@@ -91,6 +91,19 @@ class BxDolTestCase extends \PHPUnit\Framework\TestCase
         return $oModule;
     }
 
+    protected function bxRequirePersons()
+    {
+        bx_import('BxDolModule');
+        $oModule = BxDolModule::getInstance('bx_persons');
+        if (!$oModule)
+            $this->markTestSkipped('bx_persons module is not installed.');
+
+        if (!BxDolDb::getInstance()->isTableExists('bx_persons_data'))
+            $this->markTestSkipped('bx_persons module is not installed (table bx_persons_data is missing).');
+
+        return $oModule;
+    }
+
     protected function bxCallProtected($o, $sMethod, ...$aArgs)
     {
         $oMethod = new ReflectionMethod($o, $sMethod);
@@ -156,3 +169,4 @@ class BxDolTestCase extends \PHPUnit\Framework\TestCase
 }
 
 require_once dirname(__FILE__) . '/units/modules/boonex/posts/BxPostsTestCase.php';
+require_once dirname(__FILE__) . '/units/modules/boonex/persons/BxPersonsTestCase.php';

+ 97 - 0
tests/units/modules/boonex/persons/BxPersonsTestCase.php

@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * Shared helpers for Persons-backed profile-module unit tests.
+ */
+abstract class BxPersonsTestCase extends BxDolTestCase
+{
+    protected $_oModule;
+    protected $_aModuleInstancesBackup;
+    protected $_iProfileIdBackup;
+    protected $_iAccountIdBackup;
+
+    protected function setUp(): void
+    {
+        $this->_oModule = $this->bxRequirePersons();
+        $this->_oModule->_oDb;
+        $this->_aModuleInstancesBackup = $this->bxModuleInstances($this->_oModule);
+
+        foreach (['SearchResult', 'FormEntry', 'FormsEntryHelper'] as $sClass)
+            bx_import($sClass, $this->_oModule->_aModule);
+
+        $oProfileId = new ReflectionProperty(BxBaseModGeneralModule::class, '_iProfileId');
+        $oProfileId->setAccessible(true);
+        $this->_iProfileIdBackup = $oProfileId->getValue($this->_oModule);
+
+        $oAccountId = new ReflectionProperty(BxBaseModProfileModule::class, '_iAccountId');
+        $oAccountId->setAccessible(true);
+        $this->_iAccountIdBackup = $oAccountId->getValue($this->_oModule);
+    }
+
+    protected function bxTemplate()
+    {
+        $oTemplate = $this->_oModule->_oTemplate;
+        if ($oTemplate instanceof BxDolModuleProxy) {
+            $oProperty = new ReflectionProperty(BxDolModuleProxy::class, '_oProxifiedObject');
+            $oProperty->setAccessible(true);
+            $oTemplate = $oProperty->getValue($oTemplate);
+        }
+
+        return $oTemplate;
+    }
+
+    protected function tearDown(): void
+    {
+        if ($this->_oModule && $this->_aModuleInstancesBackup !== null)
+            $this->bxModuleInstances($this->_oModule, $this->_aModuleInstancesBackup);
+
+        if ($this->_oModule && $this->_iProfileIdBackup !== null)
+            $this->bxSetProtected($this->_oModule, '_iProfileId', $this->_iProfileIdBackup, BxBaseModGeneralModule::class);
+
+        if ($this->_oModule && $this->_iAccountIdBackup !== null)
+            $this->bxSetProtected($this->_oModule, '_iAccountId', $this->_iAccountIdBackup, BxBaseModProfileModule::class);
+    }
+
+    protected function bxReplaceDb($oDb): void
+    {
+        $a = $this->_aModuleInstancesBackup;
+        $a['_oDb'] = $oDb;
+        $this->bxModuleInstances($this->_oModule, $a);
+    }
+
+    protected function bxSamplePerson(array $aOverride = []): array
+    {
+        return array_merge([
+            'id' => 15,
+            'author' => 10,
+            'account_id' => 10,
+            'profile_id' => 20,
+            'added' => 1700000000,
+            'changed' => 1700000100,
+            'fullname' => 'Ada',
+            'last_name' => 'Lovelace',
+            'description' => 'Bio',
+            'picture' => 0,
+            'cover' => 0,
+            'cover_data' => '',
+            'badge' => 0,
+            'badge_link' => '',
+            'allow_view_to' => 3,
+            'allow_post_to' => 5,
+            'allow_contact_to' => 3,
+            'status' => 'active',
+            'profile_status' => 'active',
+            'featured' => 0,
+            'views' => 0,
+            'votes' => 0,
+            'rvotes' => 0,
+            'score' => 0,
+            'reports' => 0,
+            'comments' => 0,
+            'labels' => '',
+            'location' => '',
+            'birthday' => '1815-12-10',
+            'settings' => '',
+        ], $aOverride);
+    }
+}

+ 120 - 0
tests/units/modules/boonex/persons/classes/BxPersonsConfigTest.php

@@ -0,0 +1,120 @@
+<?php
+
+use PHPUnit\Framework\Attributes\DataProvider;
+
+/**
+ * Profile-module config as implemented by Persons (CNF, roles, friends, connections).
+ */
+class BxPersonsConfigTest extends BxPersonsTestCase
+{
+    public function testCnfKeysForProfileModule()
+    {
+        $CNF = $this->_oModule->_oConfig->CNF;
+
+        $this->assertSame('bx_persons_data', $CNF['TABLE_ENTRIES']);
+        $this->assertSame('id', $CNF['FIELD_ID']);
+        $this->assertSame('author', $CNF['FIELD_AUTHOR']);
+        $this->assertSame('fullname', $CNF['FIELD_NAME']);
+        $this->assertSame('last_name', $CNF['FIELD_LAST_NAME']);
+        $this->assertSame('fullname', $CNF['FIELD_TITLE']);
+        $this->assertSame('description', $CNF['FIELD_TEXT']);
+        $this->assertSame('picture', $CNF['FIELD_PICTURE']);
+        $this->assertSame('cover', $CNF['FIELD_COVER']);
+        $this->assertSame('cover_data', $CNF['FIELD_COVER_POSITION']);
+        $this->assertSame('badge', $CNF['FIELD_BADGE']);
+        $this->assertSame('badge_link', $CNF['FIELD_BADGE_LINK']);
+        $this->assertSame('allow_view_to', $CNF['FIELD_ALLOW_VIEW_TO']);
+        $this->assertSame('allow_post_to', $CNF['FIELD_ALLOW_POST_TO']);
+        $this->assertSame('allow_contact_to', $CNF['FIELD_ALLOW_CONTACT_TO']);
+        $this->assertSame(['fullname', 'last_name'], $CNF['FIELDS_QUICK_SEARCH']);
+        $this->assertSame('view-persons-profile', $CNF['URI_VIEW_ENTRY']);
+        $this->assertSame('create-persons-profile', $CNF['URI_ADD_ENTRY']);
+        $this->assertSame('edit-persons-profile', $CNF['URI_EDIT_ENTRY']);
+        $this->assertSame('persons-profile-friends', $CNF['URI_VIEW_FRIENDS']);
+        $this->assertTrue($CNF['BADGES_AVALIABLE']);
+    }
+
+    public function testPrefixesAndJsObjects()
+    {
+        $oConfig = $this->_oModule->_oConfig;
+
+        $this->assertSame('BxPersonsManageTools', $oConfig->getJsClass('manage_tools'));
+        $this->assertSame('oBxPersonsManageTools', $oConfig->getJsObject('manage_tools'));
+        $this->assertSame('bx_persons_common', $oConfig->getGridObject('common'));
+        $this->assertSame('bx_persons_administration', $oConfig->getGridObject('administration'));
+        $this->assertSame('', $oConfig->getGridObject('missing'));
+    }
+
+    public function testFriendsFlagMatchesParam()
+    {
+        $sKey = $this->_oModule->_oConfig->CNF['PARAM_FRIENDS'];
+        $this->assertSame(getParam($sKey) == 'on', $this->_oModule->_oConfig->isFriends());
+        $this->assertSame($this->_oModule->_oConfig->isFriends(), $this->_oModule->serviceIsEnableFriends());
+    }
+
+    public function testRolesDisabledWithoutPreList()
+    {
+        $oConfig = $this->_oModule->_oConfig;
+
+        $this->assertArrayNotHasKey('OBJECT_PRE_LIST_ROLES', $oConfig->CNF);
+        $this->assertFalse($oConfig->isRoles());
+        $this->assertFalse($oConfig->isMultiRoles());
+        $this->assertFalse($oConfig->getRoles());
+    }
+
+    #[DataProvider('providerForRoleId')]
+    public function testRoleIdConversion($iId, $sId)
+    {
+        $oConfig = $this->_oModule->_oConfig;
+
+        $this->assertSame($sId, $oConfig->roleIdI2S($iId));
+        $this->assertSame($iId, $oConfig->roleIdS2I($sId));
+    }
+
+    static public function providerForRoleId()
+    {
+        return [
+            [1, 'r1'],
+            [12, 'r12'],
+            [0, 'r0'],
+        ];
+    }
+
+    public function testConnectionToFunctionCheck()
+    {
+        $a = $this->_oModule->_oConfig->getConnectionToFunctionCheck();
+
+        $this->assertSame('checkAllowedFriends', $a['sys_profiles_friends']['friends']);
+        $this->assertSame('checkAllowedFriendAdd', $a['sys_profiles_friends']['add']);
+        $this->assertSame('checkAllowedFriendRemove', $a['sys_profiles_friends']['remove']);
+        $this->assertSame('checkAllowedSubscriptions', $a['sys_profiles_subscriptions']['subscriptions']);
+        $this->assertSame('checkAllowedSubscribeAdd', $a['sys_profiles_subscriptions']['add']);
+        $this->assertSame('checkAllowedSubscribeRemove', $a['sys_profiles_subscriptions']['remove']);
+    }
+
+    public function testMenuItemToMethodIncludesProfileActions()
+    {
+        $CNF = $this->_oModule->_oConfig->CNF;
+
+        $this->assertSame('checkAllowedView', $CNF['MENU_ITEM_TO_METHOD']['bx_persons_view_actions']['view-persons-profile']);
+        $this->assertSame('checkAllowedEdit', $CNF['MENU_ITEM_TO_METHOD']['bx_persons_view_actions']['edit-persons-profile']);
+        $this->assertSame('checkAllowedChangeCover', $CNF['MENU_ITEM_TO_METHOD']['bx_persons_view_actions']['edit-persons-cover']);
+        $this->assertSame('checkAllowedFriendsView', $CNF['MENU_ITEM_TO_METHOD']['bx_persons_view_submenu']['persons-profile-friends']);
+    }
+
+    #[DataProvider('providerForEntryUri')]
+    public function testGetEntryUri($sAction, $sUri)
+    {
+        $this->assertSame($sUri, $this->_oModule->_oConfig->getEntryUri($sAction));
+    }
+
+    static public function providerForEntryUri()
+    {
+        return [
+            ['view', 'view-persons-profile'],
+            ['add', 'create-persons-profile'],
+            ['edit', 'edit-persons-profile'],
+            ['missing', ''],
+        ];
+    }
+}

+ 76 - 0
tests/units/modules/boonex/persons/classes/BxPersonsDbTest.php

@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * Profile-module DB helpers via Persons (no row writes).
+ */
+class BxPersonsDbTest extends BxPersonsTestCase
+{
+    public function testSearchByTermWithoutQuickSearchFields()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $aBackup = $CNF['FIELDS_QUICK_SEARCH'];
+        $CNF['FIELDS_QUICK_SEARCH'] = [];
+        try {
+            $this->assertSame([], $this->_oModule->_oDb->searchByTerm('ada', 5));
+        } finally {
+            $CNF['FIELDS_QUICK_SEARCH'] = $aBackup;
+        }
+    }
+
+    public function testSearchByIdsJoinsProfilesAndRequiresActive()
+    {
+        $oDb = $this->_oModule->_oDb;
+        $aParams = [
+            'search_params' => [
+                'fullname' => ['operator' => 'like', 'value' => 'ada'],
+            ],
+            'per_page' => 5,
+            'show_all_content' => true,
+        ];
+        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query', 1 => []]];
+        $sSelect = $sJoin = $sWhere = $sOrder = $sLimit = '';
+        $aArgs = [$aParams, &$aMethod, &$sSelect, &$sJoin, &$sWhere, &$sOrder, &$sLimit];
+        $this->bxCallProtectedArgs($oDb, '_getEntriesBySearchIds', $aArgs);
+
+        $this->assertSame('getColumn', $aMethod['name']);
+        $this->assertSame('bx_persons', $aMethod['params'][1]['profile_type']);
+        $this->assertStringContainsString('LEFT JOIN `sys_profiles` AS `tp`', $sJoin);
+        $this->assertStringContainsString("`tp`.`status`='active'", $sWhere);
+        $this->assertStringContainsString('LIKE', $sWhere);
+        $this->assertStringNotContainsString('sys_sessions', $sJoin);
+    }
+
+    public function testSearchByIdsOnlineJoinsSessions()
+    {
+        $oDb = $this->_oModule->_oDb;
+        $aParams = [
+            'search_params' => [
+                'online' => ['operator' => '=', 'value' => 1],
+            ],
+            'show_all_content' => true,
+        ];
+        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query', 1 => []]];
+        $sSelect = $sJoin = $sWhere = $sOrder = $sLimit = '';
+        $aArgs = [$aParams, &$aMethod, &$sSelect, &$sJoin, &$sWhere, &$sOrder, &$sLimit];
+        $this->bxCallProtectedArgs($oDb, '_getEntriesBySearchIds', $aArgs);
+
+        $this->assertArrayHasKey('online_time', $aMethod['params'][1]);
+        $this->assertSame((int)getParam('sys_account_online_time'), $aMethod['params'][1]['online_time']);
+        $this->assertStringContainsString('INNER JOIN `sys_accounts` AS `ta`', $sJoin);
+        $this->assertStringContainsString('INNER JOIN `sys_sessions` AS `ts`', $sJoin);
+        $this->assertStringContainsString('`ts`.`date` > (UNIX_TIMESTAMP() - 60 * :online_time)', $sWhere);
+        $this->assertStringNotContainsString('`online`', $sWhere);
+    }
+
+    public function testGetEntriesNumByParamsEmptyAndInvalidOperator()
+    {
+        $i = $this->_oModule->_oDb->getEntriesNumByParams();
+        $this->assertIsNumeric($i);
+        $this->assertGreaterThanOrEqual(0, (int)$i);
+
+        $this->expectException(Exception::class);
+        $this->_oModule->_oDb->getEntriesNumByParams([
+            ['key' => 'status', 'operator' => 'OR 1=1', 'value' => 'active'],
+        ]);
+    }
+}

+ 148 - 0
tests/units/modules/boonex/persons/classes/BxPersonsFormsTest.php

@@ -0,0 +1,148 @@
+<?php
+
+use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
+use PHPUnit\Framework\Attributes\DataProvider;
+
+/**
+ * Profile-module form helper and entry-form methods via Persons.
+ */
+#[AllowMockObjectsWithoutExpectations]
+class BxPersonsFormsTest extends BxPersonsTestCase
+{
+    #[DataProvider('providerForAutoApproval')]
+    public function testAutoApproval($sStored, $sAction, $bOut)
+    {
+        $o = new BxPersonsFormsEntryHelper($this->_oModule);
+        $o->setAutoApproval($sStored);
+        $this->assertSame($bOut, $o->isAutoApproval($sAction));
+    }
+
+    static public function providerForAutoApproval()
+    {
+        return [
+            ['on', 'add', true],
+            ['on', 'edit', true],
+            [true, 'edit', true],
+            ['add', 'add', true],
+            ['add', 'edit', false],
+            ['add', 'on', false],
+            ['off', 'add', false],
+        ];
+    }
+
+    public function testSetAutoApprovalTrueBecomesAlways()
+    {
+        $o = new BxPersonsFormsEntryHelper($this->_oModule);
+        $this->assertSame(BX_DOL_PROFILE_ACTIVATE_ALWAYS, $o->setAutoApproval(true));
+    }
+
+    public function testProfileAndContentDataMissing()
+    {
+        $oDb = $this->createMock(BxPersonsDb::class);
+        $oDb->method('getContentInfoById')->willReturn(false);
+        $this->bxReplaceDb($oDb);
+
+        $o = new BxPersonsFormsEntryHelper($this->_oModule);
+        [$oProfile, $aContent] = $this->bxCallProtected($o, '_getProfileAndContentData', 15);
+        $this->assertFalse($oProfile);
+        $this->assertFalse($aContent);
+    }
+
+    public function testRedirectAfterAddApiDefault()
+    {
+        $o = new BxPersonsFormsEntryHelper($this->_oModule);
+        $this->bxSetProtected($o, '_bIsApi', true, BxBaseModGeneralFormsEntryHelper::class);
+
+        $a = $o->redirectAfterAdd($this->bxSamplePerson());
+        $this->assertIsArray($a);
+        $this->assertNotEmpty($a);
+    }
+
+    public function testPhotoGhostTemplateVars()
+    {
+        $oForm = $this->bxFormDouble();
+        $oForm->aInputs = [
+            'picture' => [
+                'name' => 'picture',
+                'content_id' => 15,
+            ],
+        ];
+
+        $a = $this->bxCallProtected($oForm, '_getProfilePhotoGhostTmplVars', 'picture', $this->bxSamplePerson());
+        $this->assertSame('picture', $a['name']);
+        $this->assertSame(15, $a['content_id']);
+        $this->assertFalse($a['bx_if:set_thumb']['condition']);
+        $this->assertSame([], $a['bx_if:set_thumb']['content']);
+    }
+
+    public function testPrivacyFields()
+    {
+        $oForm = $this->bxFormDouble();
+        $oForm->aInputs = [
+            'allow_view_to' => ['name' => 'allow_view_to'],
+            'allow_post_to' => ['name' => 'allow_post_to'],
+            'allow_contact_to' => ['name' => 'allow_contact_to'],
+        ];
+
+        $a = $this->bxCallProtected($oForm, '_getPrivacyFields');
+        $this->assertSame('bx_persons_allow_view_to', $a['allow_view_to']);
+        $this->assertSame('bx_persons_allow_post_to', $a['allow_post_to']);
+        $this->assertSame('bx_persons_allow_contact_to', $a['allow_contact_to']);
+    }
+
+    public function testProcessFilesFlagsApiSkipsMissingAndMultiple()
+    {
+        $oForm = $this->bxFormDouble();
+        $oForm->aInputs = [];
+        $this->assertFalse($oForm->processFilesFlagsApi('picture', 15));
+
+        $oForm->aInputs = ['picture' => ['name' => 'picture']];
+        $this->assertFalse($oForm->processFilesFlagsApi('picture', 15));
+    }
+
+    public function testCustomViewRowEmptyValues()
+    {
+        $oForm = $this->bxFormDouble();
+        $sPrevId = bx_get('id');
+        $sPrevProfile = bx_get('profile_id');
+        unset($_GET['id'], $_GET['profile_id'], $_POST['id'], $_POST['profile_id']);
+        try {
+            $this->assertSame('', $this->bxCallProtected($oForm, 'genCustomViewRowValueProfileEmail', ['value' => '']));
+            $this->assertSame('', $this->bxCallProtected($oForm, 'genCustomViewRowValueProfileIp', ['value' => '']));
+            $this->assertSame('', $this->bxCallProtected($oForm, 'genCustomViewRowValueFriendsCount', ['value' => 1]));
+            $this->assertSame('', $this->bxCallProtected($oForm, 'genCustomViewRowValueFollowersCount', ['value' => 1]));
+            $this->assertSame('', $this->bxCallProtected($oForm, 'genCustomViewRowValueProfileStatus', ['value' => '']));
+            $this->assertSame('', $this->bxCallProtected($oForm, 'genCustomViewRowValueProfileLastActive', ['value' => 0]));
+        } finally {
+            if ($sPrevId === false)
+                unset($_GET['id']);
+            else
+                $_GET['id'] = $sPrevId;
+            if ($sPrevProfile === false)
+                unset($_GET['profile_id']);
+            else
+                $_GET['profile_id'] = $sPrevProfile;
+        }
+    }
+
+    public function testBirthdayMaxYearOnAddDisplay()
+    {
+        $oForm = BxDolForm::getObjectInstance('bx_person', 'bx_person_add', $this->_oModule->_oTemplate);
+        if (!$oForm || empty($oForm->aInputs['birthday']))
+            $this->markTestSkipped('Persons add form or birthday field is missing.');
+
+        $this->assertSame((string)date('Y'), $oForm->aInputs['birthday']['attrs']['max']);
+    }
+
+    protected function bxFormDouble(): BxPersonsFormEntry
+    {
+        return new class($this->_oModule) extends BxPersonsFormEntry {
+            public function __construct($oModule)
+            {
+                $this->MODULE = 'bx_persons';
+                $this->_oModule = $oModule;
+                $this->aInputs = [];
+            }
+        };
+    }
+}

+ 162 - 0
tests/units/modules/boonex/persons/classes/BxPersonsModulePermissionsTest.php

@@ -0,0 +1,162 @@
+<?php
+
+use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
+
+/**
+ * Profile-module permission checks via Persons.
+ */
+#[AllowMockObjectsWithoutExpectations]
+class BxPersonsModulePermissionsTest extends BxPersonsTestCase
+{
+    public function testCheckAllowedViewEmpty()
+    {
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedView([]));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->serviceCheckAllowedViewForProfile([]));
+    }
+
+    public function testCheckAllowedEditEmptyAndMissingProfile()
+    {
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedEdit([]));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedEdit($this->bxSamplePerson()));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedChangeBadge([]));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedChangeCover($this->bxSamplePerson()));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedChangeSettings($this->bxSamplePerson()));
+    }
+
+    public function testCheckAllowedFriendsView()
+    {
+        $aPerson = $this->bxSamplePerson();
+        $aEmpty = [];
+        if ($this->_oModule->_oConfig->isFriends())
+            $this->assertSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedFriendsView($aPerson));
+        else
+            $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedFriendsView($aPerson));
+
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedFriendsView($aEmpty));
+    }
+
+    public function testCheckAllowedFriendsWithoutViewerProfile()
+    {
+        $aPerson = $this->bxSamplePerson();
+        $this->bxSetProtected($this->_oModule, '_iProfileId', 0, BxBaseModGeneralModule::class);
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedFriends($aPerson));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedFriendAdd($aPerson));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedFriendRemove($aPerson));
+    }
+
+    public function testCheckAllowedRelationsWhenDisabled()
+    {
+        if (BxDolConnectionRelation::isEnabled())
+            $this->markTestSkipped('Relations are enabled on this install.');
+
+        $aPerson = $this->bxSamplePerson();
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedRelations($aPerson));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedRelationAdd($aPerson));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedRelationRemove($aPerson));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedRelationsView($aPerson));
+        $this->assertFalse($this->_oModule->serviceIsEnableRelations());
+    }
+
+    public function testCheckAllowedSubscriptionsViewEmpty()
+    {
+        $aEmpty = [];
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->checkAllowedSubscriptionsView($aEmpty));
+    }
+
+    public function testCheckMyselfMissingContent()
+    {
+        $this->assertFalse($this->_oModule->checkMyself(0));
+        $this->assertFalse($this->_oModule->checkMyself(99999999));
+    }
+
+    public function testCheckAllowedConnectWithoutViewer()
+    {
+        $this->bxSetProtected($this->_oModule, '_iProfileId', 0, BxBaseModGeneralModule::class);
+        $aPerson = $this->bxSamplePerson();
+        $aArgs = [&$aPerson, false, 'sys_profiles_friends', false, false];
+        $this->assertNotSame(
+            CHECK_ACTION_RESULT_ALLOWED,
+            $this->bxCallProtectedArgs($this->_oModule, '_checkAllowedConnect', $aArgs)
+        );
+    }
+
+    public function testServiceCheckAllowedModuleActionInProfileWithoutRoles()
+    {
+        $this->assertNotSame(
+            CHECK_ACTION_RESULT_ALLOWED,
+            $this->_oModule->serviceCheckAllowedModuleActionInProfile(15, 'bx_posts', 'post')
+        );
+    }
+
+    public function testServiceCheckAllowedWithContentMissing()
+    {
+        $oDb = $this->createMock(BxPersonsDb::class);
+        $oDb->method('getContentInfoById')->willReturn(false);
+        $this->bxReplaceDb($oDb);
+
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->serviceCheckAllowedProfileView(15));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->serviceCheckAllowedProfileContact(15));
+        $this->assertNotSame(CHECK_ACTION_RESULT_ALLOWED, $this->_oModule->serviceCheckSpacePrivacy(15));
+    }
+
+    public function testGetProfileObjectMissingIsUndefined()
+    {
+        $o = $this->_oModule->getProfileObject(99999999);
+        $this->assertInstanceOf(BxDolProfileUndefined::class, $o);
+    }
+
+    public function testIsAllowDeleteOrDisableSameModeratorPair()
+    {
+        $this->assertTrue($this->_oModule->isAllowDeleteOrDisable(0, 0));
+    }
+
+    public function testDecodeDataApiPrivateContent()
+    {
+        $o = new class($this->_oModule->_aModule) extends BxPersonsModule {
+            public $mixedViewResult = 'denied';
+
+            public function serviceCheckAllowedViewForProfile($aDataEntry, $isPerformAction = false, $iProfileId = false)
+            {
+                return $this->mixedViewResult;
+            }
+        };
+
+        $a = $o->decodeDataAPI($this->bxSamplePerson());
+        $this->assertSame(15, $a['id']);
+        $this->assertSame('sys_private', $a['module']);
+        $this->assertSame('bx_persons', $a['module_src']);
+        $this->assertArrayNotHasKey('description', $a);
+    }
+
+    public function testPrivacyPartiallyVisible()
+    {
+        $o = BxDolPrivacy::getObjectInstance('bx_persons_allow_view_to');
+        if (!$o)
+            $this->markTestSkipped('bx_persons_allow_view_to privacy object is missing.');
+
+        $this->assertTrue($o->isPartiallyVisible(BX_DOL_PG_FRIENDS));
+        $this->assertFalse($o->isPartiallyVisible(BX_DOL_PG_ALL));
+        $this->assertContains(BX_DOL_PG_FRIENDS, $o->getPartiallyVisiblePrivacyGroups());
+    }
+
+    public function testCustomPrivacyItemsRequireContentWhenActingAsProfile()
+    {
+        $o = BxDolPrivacy::getObjectInstance('bx_persons_allow_view_to');
+        if (!$o)
+            $this->markTestSkipped('bx_persons_allow_view_to privacy object is missing.');
+
+        $mixedEmpty = $this->bxCallProtected($o, '_isSelectGroupCustomItems', ['content_id' => 0]);
+        $this->assertNotTrue($mixedEmpty);
+
+        $this->assertTrue($this->bxCallProtected($o, '_isSelectGroupCustomItems', ['content_id' => 15]));
+    }
+
+    public function testCheckAllowedViewCoverDelegatesToProfileImage()
+    {
+        $aPerson = ['id' => 15, 'allow_view_to' => BX_DOL_PG_ALL];
+        $this->assertSame(
+            $this->_oModule->checkAllowedViewProfileImage($aPerson),
+            $this->_oModule->checkAllowedViewCoverImage($aPerson)
+        );
+    }
+}

+ 271 - 0
tests/units/modules/boonex/persons/classes/BxPersonsModuleServicesTest.php

@@ -0,0 +1,271 @@
+<?php
+
+use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
+
+/**
+ * Profile-module services and helpers via Persons.
+ */
+#[AllowMockObjectsWithoutExpectations]
+class BxPersonsModuleServicesTest extends BxPersonsTestCase
+{
+    public function testSafeServicesIncludeProfileEndpoints()
+    {
+        $a = $this->_oModule->serviceGetSafeServices();
+
+        foreach ([
+            'ProfileUnitSafe',
+            'ProfileUrl',
+            'EntityCreate',
+            'BrowseRecommended',
+            'BrowseRecentProfiles',
+            'BrowseActiveProfiles',
+            'BrowseTopProfiles',
+            'BrowseOnlineProfiles',
+            'BrowseConnections',
+            'BrowseByAcl',
+        ] as $sName)
+            $this->assertArrayHasKey($sName, $a);
+    }
+
+    public function testModuleSampleAndActAsProfile()
+    {
+        $this->assertNotEmpty($this->_oModule->serviceModuleSample());
+        $this->assertTrue($this->_oModule->serviceActAsProfile());
+        $this->assertSame(pow(2, BX_DOL_MODULE_SUBTYPE_PROFILE), $this->_oModule->getSubtypes());
+        $this->assertSame(_t('_sys_ps_space_title_friend'), $this->_oModule->serviceGetSpaceTitle());
+    }
+
+    public function testGetOptionsRedirectAndActivation()
+    {
+        $aRedirect = $this->_oModule->serviceGetOptionsRedirectAfterAdd();
+        $aKeys = array_column($aRedirect, 'key');
+        $this->assertSame(
+            [BX_DOL_PROFILE_REDIRECT_PROFILE, BX_DOL_PROFILE_REDIRECT_LAST, BX_DOL_PROFILE_REDIRECT_CUSTOM, BX_DOL_PROFILE_REDIRECT_HOMEPAGE],
+            $aKeys
+        );
+
+        $aActivation = $this->_oModule->serviceGetOptionsActivation();
+        $aActivationKeys = array_column($aActivation, 'key');
+        $this->assertSame(
+            [BX_DOL_PROFILE_ACTIVATE_ALWAYS, BX_DOL_PROFILE_ACTIVATE_NEVER, BX_DOL_PROFILE_ACTIVATE_ADD, BX_DOL_PROFILE_ACTIVATE_EDIT],
+            $aActivationKeys
+        );
+    }
+
+    public function testProfileNameCombinesLastName()
+    {
+        $this->assertSame('Ada Lovelace', $this->_oModule->getProfileName($this->bxSamplePerson()));
+        $this->assertSame('Ada', $this->_oModule->getProfileName($this->bxSamplePerson(['last_name' => ''])));
+    }
+
+    public function testServiceProfileNameAndUrlMissing()
+    {
+        $this->assertFalse($this->_oModule->serviceProfileName(0));
+        $this->assertFalse($this->_oModule->serviceProfileUrl(0));
+        $this->assertFalse($this->_oModule->serviceProfileSettings(0));
+        $this->assertFalse($this->_oModule->serviceHasImage(0));
+
+        $oDb = $this->createMock(BxPersonsDb::class);
+        $oDb->method('getContentInfoById')->willReturn(false);
+        $this->bxReplaceDb($oDb);
+
+        $this->assertFalse($this->_oModule->serviceProfileName(15));
+        $this->assertFalse($this->_oModule->serviceProfileUrl(15));
+        $this->assertFalse($this->_oModule->serviceHasImage(15));
+        $this->assertFalse($this->_oModule->serviceProfileSettings(15));
+    }
+
+    public function testServiceHasImageFromPictureField()
+    {
+        $oDb = $this->createMock(BxPersonsDb::class);
+        $oDb->method('getContentInfoById')->willReturnMap([
+            [15, $this->bxSamplePerson(['picture' => 9])],
+            [16, $this->bxSamplePerson(['id' => 16, 'picture' => 0])],
+        ]);
+        $this->bxReplaceDb($oDb);
+
+        $this->assertTrue($this->_oModule->serviceHasImage(15));
+        $this->assertFalse($this->_oModule->serviceHasImage(16));
+    }
+
+    public function testServiceProfileCreateAndEditUrl()
+    {
+        $sCreate = $this->_oModule->serviceProfileCreateUrl(false);
+        $this->assertSame('page.php?i=create-persons-profile', $sCreate);
+
+        $sCreateAbs = $this->_oModule->serviceProfileCreateUrl(true);
+        $this->assertNotSame('', $sCreateAbs);
+        $this->assertStringContainsString('create-persons-profile', $sCreateAbs);
+
+        $sEdit = $this->_oModule->serviceProfileEditUrl(15);
+        $this->assertNotSame('', $sEdit);
+        $this->assertStringContainsString('edit-persons-profile', $sEdit);
+    }
+
+    public function testServicePrepareFieldsCopiesName()
+    {
+        $a = $this->_oModule->servicePrepareFields(['name' => 'Ada', 'description' => 'Bio']);
+
+        $this->assertSame('Ada', $a['fullname']);
+        $this->assertSame('Ada', $a['name']);
+        $this->assertSame(BX_DOL_PG_ALL, $a['allow_view_to']);
+        $this->assertSame(BX_DOL_PG_ALL, $a['allow_contact_to']);
+        $this->assertSame(BX_DOL_PG_FRIENDS, $a['allow_post_to']);
+    }
+
+    public function testServicePrepareFieldsMap()
+    {
+        $a = $this->bxCallProtected(
+            $this->_oModule,
+            '_servicePrepareFields',
+            ['name' => 'Ada', 'description' => 'Bio'],
+            ['gender' => 'female'],
+            ['fullname' => 'name', 'desc' => 'description']
+        );
+
+        $this->assertSame('Ada', $a['fullname']);
+        $this->assertSame('Bio', $a['desc']);
+        $this->assertSame('female', $a['gender']);
+        $this->assertArrayNotHasKey('name', $a);
+        $this->assertArrayNotHasKey('description', $a);
+        $this->assertSame(BX_DOL_PG_FRIENDS, $a['allow_post_to']);
+    }
+
+    public function testFormsHelper()
+    {
+        $this->assertInstanceOf(BxPersonsFormsEntryHelper::class, $this->_oModule->getFormsHelper());
+        $this->assertInstanceOf(BxPersonsFormsEntryHelper::class, $this->_oModule->serviceFormsHelper());
+    }
+
+    public function testBuildRssParamsConnections()
+    {
+        $this->assertSame([
+            'object' => 'sys_profiles_friends',
+            'type' => 'content',
+            'profile' => 7,
+            'mutual' => 1,
+            'profile2' => 8,
+        ], $this->bxCallProtected($this->_oModule, '_buildRssParams', 'connections', ['sys_profiles_friends', 'content', 7, 1, 8]));
+
+        $this->assertSame([
+            'object' => '',
+            'type' => '',
+            'profile' => 0,
+            'mutual' => 0,
+            'profile2' => 0,
+        ], $this->bxCallProtected($this->_oModule, '_buildRssParams', 'connections', []));
+
+        $this->assertSame([], $this->bxCallProtected($this->_oModule, '_buildRssParams', 'recent', [1]));
+    }
+
+    public function testNotificationsAndTimelineData()
+    {
+        $aNtfs = $this->_oModule->serviceGetNotificationsData();
+        $this->assertContains('bx_persons_timeline_post_common', array_column($aNtfs['handlers'], 'group'));
+        $this->assertContains('timeline_post_common', array_column($aNtfs['alerts'], 'action'));
+
+        $aTl = $this->_oModule->serviceGetTimelineData();
+        $aGroups = array_column($aTl['handlers'], 'group');
+        $this->assertContains('bx_persons_profile_picture', $aGroups);
+        $this->assertContains('bx_persons_profile_cover', $aGroups);
+        $this->assertContains('profile_picture_changed', array_column($aTl['alerts'], 'action'));
+        $this->assertContains('profile_cover_changed', array_column($aTl['alerts'], 'action'));
+    }
+
+    public function testNotificationsTimelinePostCommonMissingProfile()
+    {
+        $this->assertSame([], $this->_oModule->serviceGetNotificationsTimelinePostCommon(['object_id' => 99999999, 'subobject_id' => 1]));
+    }
+
+    public function testGetProfileByCurrentUrl()
+    {
+        $sPrevId = bx_get('id');
+        $sPrevProfile = bx_get('profile_id');
+        unset($_GET['id'], $_GET['profile_id'], $_POST['id'], $_POST['profile_id']);
+        try {
+            $this->assertFalse($this->_oModule->getProfileByCurrentUrl());
+        } finally {
+            if ($sPrevId === false)
+                unset($_GET['id']);
+            else
+                $_GET['id'] = $sPrevId;
+            if ($sPrevProfile === false)
+                unset($_GET['profile_id']);
+            else
+                $_GET['profile_id'] = $sPrevProfile;
+        }
+    }
+
+    public function testMenuItemTitleByConnection()
+    {
+        $this->assertFalse($this->_oModule->getMenuItemTitleByConnection('not_a_connection', 'add', 1, 2));
+
+        $aFriends = $this->_oModule->getMenuItemTitleByConnection('sys_profiles_friends', '', 999999, 888888);
+        $this->assertIsArray($aFriends);
+        $this->assertArrayHasKey('add', $aFriends);
+        $this->assertArrayHasKey('remove', $aFriends);
+        $this->assertNotSame('', $aFriends['add']);
+        $this->assertSame('', $aFriends['remove']);
+
+        $sAdd = $this->_oModule->serviceGetMenuItemTitleByConnection('sys_profiles_friends', 'add', 999999, 888888);
+        $this->assertIsString($sAdd);
+        $this->assertNotSame('', $sAdd);
+
+        $aSubs = $this->_oModule->getMenuItemTitleByConnection('sys_profiles_subscriptions', '', 999999, 888888);
+        $this->assertIsArray($aSubs);
+        $this->assertNotSame('', $aSubs['add']);
+        $this->assertSame('', $aSubs['remove']);
+    }
+
+    public function testSearchableFieldsExtendedAddsOnlineAndPicture()
+    {
+        $a = $this->_oModule->serviceGetSearchableFieldsExtended();
+
+        $this->assertArrayHasKey('online', $a);
+        $this->assertSame('checkbox', $a['online']['type']);
+        $this->assertArrayHasKey('picture', $a);
+        $this->assertSame('>=', $a['picture']['search_operator']);
+        $this->assertArrayNotHasKey('author', $a);
+    }
+
+    public function testGetSearchOptionsUnknownField()
+    {
+        $this->assertFalse($this->_oModule->serviceGetSearchOptions('not_a_field', 'text', 'like'));
+    }
+
+    public function testServiceGetMenuAddonManageTools()
+    {
+        $a = $this->_oModule->serviceGetMenuAddonManageTools();
+
+        $this->assertArrayHasKey('counter1_value', $a);
+        $this->assertArrayHasKey('counter2_value', $a);
+        $this->assertArrayHasKey('counter3_value', $a);
+        $this->assertIsNumeric($a['counter1_value']);
+        $this->assertIsNumeric($a['counter3_value']);
+    }
+
+    public function testGetContentWithoutIds()
+    {
+        $sPrevId = bx_get('id');
+        $sPrevProfile = bx_get('profile_id');
+        unset($_GET['id'], $_GET['profile_id'], $_POST['id'], $_POST['profile_id']);
+        try {
+            $this->assertFalse($this->bxCallProtected($this->_oModule, '_getContent', 0));
+        } finally {
+            if ($sPrevId === false)
+                unset($_GET['id']);
+            else
+                $_GET['id'] = $sPrevId;
+            if ($sPrevProfile === false)
+                unset($_GET['profile_id']);
+            else
+                $_GET['profile_id'] = $sPrevProfile;
+        }
+    }
+
+    public function testAlertParamsIncludePrivacyView()
+    {
+        $a = $this->bxCallProtected($this->_oModule, '_alertParams', $this->bxSamplePerson(['allow_view_to' => 5]));
+        $this->assertSame(5, $a['privacy_view']);
+    }
+}

+ 149 - 0
tests/units/modules/boonex/persons/classes/BxPersonsSearchResultTest.php

@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * Profile-module search/browse modes via Persons SearchResult.
+ */
+class BxPersonsSearchResultTest extends BxPersonsTestCase
+{
+    public function testRecentMode()
+    {
+        $o = new BxPersonsSearchResult('recent');
+
+        $this->assertFalse((bool)$o->isError);
+        $this->assertSame('bx_persons', $o->aCurrent['module_name']);
+        $this->assertSame('sys_profiles', $o->aCurrent['table']);
+        $this->assertSame('bx_persons_data', $o->aCurrent['tableSearch']);
+        $this->assertSame('last', $o->aCurrent['sorting']);
+        $this->assertSame('active', $o->aCurrent['restriction']['perofileStatus']['value']);
+        $this->assertSame('bx_persons', $o->aCurrent['restriction']['perofileType']['value']);
+        $this->assertSame('modules/?r=persons/rss/recent', $o->aCurrent['rss']['link']);
+        $this->assertSame('unit_with_cover.html', $this->bxUnitTemplate($o));
+    }
+
+    public function testFeaturedActiveOnlineModes()
+    {
+        $oFeatured = new BxPersonsSearchResult('featured');
+        $this->assertFalse((bool)$oFeatured->isError);
+        $this->assertSame('featured', $oFeatured->aCurrent['sorting']);
+        $this->assertSame('0', $oFeatured->aCurrent['restriction']['featured']['value']);
+
+        $oActive = new BxPersonsSearchResult('active');
+        $this->assertSame('active', $oActive->aCurrent['sorting']);
+        $this->assertSame('modules/?r=persons/rss/active', $oActive->aCurrent['rss']['link']);
+
+        $oOnline = new BxPersonsSearchResult('online');
+        $this->assertSame('online', $oOnline->aCurrent['sorting']);
+        $this->assertNotEmpty($oOnline->aCurrent['restriction']['online']['value']);
+        $this->assertArrayHasKey('session', $oOnline->aCurrent['join']);
+        $this->assertStringContainsString('`sys_accounts`.`profile_id`=`sys_profiles`.`id`', $oOnline->aCurrent['restriction_sql']);
+    }
+
+    public function testRecommendedMode()
+    {
+        $o = new BxPersonsSearchResult('recommended');
+
+        $this->assertFalse((bool)$o->isError);
+        $this->assertSame('recommended', $o->aCurrent['sorting']);
+        $this->assertTrue($this->bxRecommendedView($o));
+        $this->assertNotEmpty($o->aCurrent['restriction_sql']);
+        $this->assertStringContainsString('IS NULL', $o->aCurrent['restriction_sql']);
+        $this->assertArrayHasKey('recommended', $o->aCurrent['join']);
+    }
+
+    public function testSearchResultsModeDropsPaginationAndRss()
+    {
+        $o = new BxPersonsSearchResult('');
+
+        $this->assertFalse((bool)$o->isError);
+        $this->assertArrayNotHasKey('perPage', $o->aCurrent['paginate']);
+        $this->assertArrayNotHasKey('rss', $o->aCurrent);
+    }
+
+    public function testUnknownModeIsError()
+    {
+        $o = new BxPersonsSearchResult('not-a-mode');
+        $this->assertTrue((bool)$o->isError);
+    }
+
+    public function testFavoriteWithoutProfileIsError()
+    {
+        $o = new BxPersonsSearchResult('favorite', ['user' => 0]);
+        $this->assertTrue((bool)$o->isError);
+    }
+
+    public function testConnectionsWithoutProfileDoesNotConfigure()
+    {
+        $o = new BxPersonsSearchResult('connections', []);
+        $this->assertFalse((bool)$o->isError);
+        $this->assertSame('', $o->aCurrent['rss']['link']);
+    }
+
+    public function testAlterOrder()
+    {
+        $o = new BxPersonsSearchResult('recent');
+
+        $o->aCurrent['sorting'] = 'featured';
+        $this->assertStringContainsString('`bx_persons_data`.`featured` DESC', $o->getAlterOrder()['order']);
+
+        $o->aCurrent['sorting'] = 'recommended';
+        $this->assertStringContainsString('RAND()', $o->getAlterOrder()['order']);
+
+        $o->aCurrent['sorting'] = 'none';
+        $this->assertSame([], $o->getAlterOrder());
+
+        $o->aCurrent['sorting'] = 'active';
+        $this->assertStringContainsString('`sys_accounts`.`logged` DESC', $o->getAlterOrder()['order']);
+
+        $o->aCurrent['sorting'] = 'online';
+        $this->assertStringContainsString('`sys_sessions`.`date` DESC', $o->getAlterOrder()['order']);
+
+        $o->aCurrent['sorting'] = 'last';
+        $this->assertStringContainsString('`bx_persons_data`.`added` DESC', $o->getAlterOrder()['order']);
+    }
+
+    public function testUnitViewParams()
+    {
+        $oSimple = new BxPersonsSearchResult('recent', ['unit_view' => 'simple']);
+        $this->assertSame('unit_wo_links.html', $this->bxUnitTemplate($oSimple));
+
+        $oWoInfo = new BxPersonsSearchResult('recent', ['unit_view' => 'unit_wo_info']);
+        $this->assertSame('unit_wo_info.html', $this->bxUnitTemplate($oWoInfo));
+
+        $oShowcase = new BxPersonsSearchResult('recent', ['unit_view' => 'showcase']);
+        $this->assertSame('unit_with_cover_showcase.html', $this->bxUnitTemplate($oShowcase));
+        $oShowcaseView = new ReflectionProperty($oShowcase, 'bShowcaseView');
+        $oShowcaseView->setAccessible(true);
+        $this->assertTrue($oShowcaseView->getValue($oShowcase));
+    }
+
+    public function testGetRssUnitImageEmpty()
+    {
+        $o = new BxPersonsSearchResult('recent');
+        $a = ['picture' => 0];
+        $this->assertSame('', $o->getRssUnitImage($a, 'picture'));
+        $this->assertSame('', $o->getRssUnitImage($a, ''));
+    }
+
+    public function testGetPseud()
+    {
+        $o = new BxPersonsSearchResult('recent');
+        $a = $this->bxCallProtected($o, '_getPseud');
+        $this->assertSame('id', $a['id']);
+        $this->assertSame('fullname', $a['fullname']);
+        $this->assertSame('picture', $a['picture']);
+    }
+
+    protected function bxUnitTemplate($o): string
+    {
+        $oTemplate = new ReflectionProperty($o, 'sUnitTemplate');
+        $oTemplate->setAccessible(true);
+        return $oTemplate->getValue($o);
+    }
+
+    protected function bxRecommendedView($o): bool
+    {
+        $oProp = new ReflectionProperty($o, 'bRecommendedView');
+        $oProp->setAccessible(true);
+        return (bool)$oProp->getValue($o);
+    }
+}

+ 86 - 0
tests/units/modules/boonex/persons/classes/BxPersonsTemplateTest.php

@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * Profile-module template helpers via Persons.
+ */
+class BxPersonsTemplateTest extends BxPersonsTestCase
+{
+    public function testUnitClassAndSize()
+    {
+        $o = $this->bxTemplate();
+        $aPerson = $this->bxSamplePerson();
+
+        $this->assertSame('bx-base-pofile-unit', $this->bxCallProtected($o, '_getUnitClass', $aPerson, 'unit.html'));
+        $this->assertSame('bx-base-pofile-unit-with-cover', $this->bxCallProtected($o, '_getUnitClass', $aPerson, 'unit_with_cover.html'));
+        $this->assertSame('bx-base-pofile-unit-wo-info', $this->bxCallProtected($o, '_getUnitClass', $aPerson, 'unit_wo_info.html'));
+        $this->assertSame(
+            'bx-base-pofile-unit-with-cover bx-base-unit-showcase bx-base-pofile-unit-showcase',
+            $this->bxCallProtected($o, '_getUnitClass', $aPerson, 'unit_with_cover_showcase.html')
+        );
+        $this->assertSame(
+            'bx-base-pofile-unit-wo-info bx-base-unit-showcase bx-base-pofile-unit-wo-info-showcase',
+            $this->bxCallProtected($o, '_getUnitClass', $aPerson, 'unit_wo_info_showcase.html')
+        );
+
+        $this->assertSame('ava', $this->bxCallProtected($o, '_getUnitSize', $aPerson, 'unit_with_cover.html'));
+        $this->assertSame('thumb', $this->bxCallProtected($o, '_getUnitSize', $aPerson, 'unit.html'));
+        $this->assertTrue($this->bxCallProtected($o, '_isUnitThumb', $aPerson, 'unit.html'));
+        $this->assertTrue($this->bxCallProtected($o, '_isTemplateWithMeta', 'unit_with_cover.html'));
+        $this->assertFalse($this->bxCallProtected($o, '_isTemplateWithMeta', 'unit.html'));
+    }
+
+    public function testGetBadgeLink()
+    {
+        $o = $this->bxTemplate();
+
+        $this->assertSame('https://example.com/badge', $o->getBadgeLink($this->bxSamplePerson([
+            'badge_link' => 'https://example.com/badge',
+        ])));
+        $this->assertSame('', $o->getBadgeLink($this->bxSamplePerson(['badge_link' => ''])));
+    }
+
+    public function testUnitCoverImageSettingsEmpty()
+    {
+        $o = $this->bxTemplate();
+        $this->assertSame('', $this->bxCallProtected($o, '_getImageSettings', 'cover', '{"x":1}', 'unit_cover'));
+    }
+
+    public function testImageWithoutPicture()
+    {
+        $o = $this->bxTemplate();
+        $aPerson = $this->bxSamplePerson(['picture' => 0]);
+
+        $this->assertFalse($this->bxCallProtected($o, '_image', 'picture', 'bx_persons_thumb', 'no-picture-thumb.png', $aPerson, false));
+
+        $sSub = $this->bxCallProtected($o, '_image', 'picture', 'bx_persons_thumb', 'no-picture-thumb.png', $aPerson, true);
+        $this->assertNotSame('', $sSub);
+        $this->assertIsString($sSub);
+    }
+
+    public function testUrlCoverWithoutSubstitute()
+    {
+        $s = $this->bxTemplate()->urlCover($this->bxSamplePerson(['cover' => 0]), false);
+        $this->assertTrue($s === '' || $s === false);
+    }
+
+    public function testGetBadgeWithoutImage()
+    {
+        $this->assertSame('', $this->bxTemplate()->getBadge($this->bxSamplePerson(['badge' => 0])));
+        $this->assertSame('', $this->bxTemplate()->getBadge(0));
+    }
+
+    public function testGetUnitThumbUrl()
+    {
+        $o = $this->bxTemplate();
+        $aPerson = $this->bxSamplePerson(['picture' => 0]);
+
+        $this->assertSame(
+            $this->bxCallProtected($o, '_getUnitThumbUrl', 'thumb', $aPerson, false),
+            $o->urlThumb($aPerson, false)
+        );
+        $this->assertSame(
+            $this->bxCallProtected($o, '_getUnitThumbUrl', 'ava', $aPerson, false),
+            $o->urlAvatar($aPerson, false)
+        );
+    }
+}

+ 0 - 1
tests/units/modules/boonex/posts/classes/BxPostsConfigTest.php

@@ -21,7 +21,6 @@ class BxPostsConfigTest extends BxPostsTestCase
         $this->assertSame('view-post', $CNF['URI_VIEW_ENTRY']);
         $this->assertSame('create-post', $CNF['URI_ADD_ENTRY']);
         $this->assertSame('edit-post', $CNF['URI_EDIT_ENTRY']);
-        $this->assertSame('file-alt', $CNF['ICON']);
         $this->assertTrue($CNF['PARAM_LINKS_ENABLED']);
         $this->assertTrue($CNF['PARAM_MULTICAT_ENABLED']);
         $this->assertTrue($CNF['PARAM_POLL_ENABLED']);

+ 1 - 1
tests/units/modules/boonex/posts/classes/BxPostsDbTest.php

@@ -3,7 +3,7 @@
 use PHPUnit\Framework\Attributes\DataProvider;
 
 /**
- * Content-module DB helpers via Posts (SQL construction, no row writes).
+ * Content-module DB helpers via Posts (no row writes).
  */
 class BxPostsDbTest extends BxPostsTestCase
 {

+ 1 - 3
tests/units/modules/boonex/posts/classes/BxPostsModuleServicesTest.php

@@ -16,9 +16,8 @@ class BxPostsModuleServicesTest extends BxPostsTestCase
             $this->assertArrayHasKey($sName, $a);
     }
 
-    public function testModuleIconAndSample()
+    public function testModuleSampleAndContext()
     {
-        $this->assertSame('file-alt', $this->_oModule->serviceModuleIcon());
         $this->assertNotEmpty($this->_oModule->serviceModuleSample());
         $this->assertTrue($this->_oModule->serviceIsAllowedPostInContext());
     }
@@ -113,7 +112,6 @@ class BxPostsModuleServicesTest extends BxPostsTestCase
 
         $this->assertIsArray($aResult);
         $this->assertSame(250, $aResult['date']);
-        $this->assertSame('file-alt', $aResult['icon']);
     }
 
     public function testGetContentOwnerProfileIdWithoutContentUsesViewer()