mirror of
https://github.com/Combodo/iTop.git
synced 2026-05-18 14:58:43 +02:00
Merge branch 'develop' into odain
This commit is contained in:
@@ -26,14 +26,14 @@ class LoginWebPageTest extends ItopDataTestCase
|
||||
$this->BackupConfiguration();
|
||||
$sFolderPath = APPROOT.'env-production/extension-with-delegated-authentication-endpoints-list';
|
||||
if (file_exists($sFolderPath)) {
|
||||
throw new Exception("Folder $sFolderPath already exists, please remove it before running the test");
|
||||
$this->RecurseRmdir($sFolderPath);
|
||||
}
|
||||
mkdir($sFolderPath);
|
||||
$this->RecurseCopy(__DIR__.'/extension-with-delegated-authentication-endpoints-list', $sFolderPath);
|
||||
|
||||
$sFolderPath = APPROOT.'env-production/extension-without-delegated-authentication-endpoints-list';
|
||||
if (file_exists($sFolderPath)) {
|
||||
throw new Exception("Folder $sFolderPath already exists, please remove it before running the test");
|
||||
$this->RecurseRmdir($sFolderPath);
|
||||
}
|
||||
mkdir($sFolderPath);
|
||||
$this->RecurseCopy(__DIR__.'/extension-without-delegated-authentication-endpoints-list', $sFolderPath);
|
||||
@@ -81,8 +81,7 @@ class LoginWebPageTest extends ItopDataTestCase
|
||||
|
||||
public function testUserCanAccessAnyFile()
|
||||
{
|
||||
// generate random login
|
||||
$sUserLogin = 'user-'.date('YmdHis');
|
||||
$sUserLogin = 'user-'.uniqid();
|
||||
$this->CreateUser($sUserLogin, self::$aURP_Profiles['Service Desk Agent'], self::PASSWORD);
|
||||
$this->GivenConfigFileAllowedLoginTypes(explode('|', 'form'));
|
||||
|
||||
@@ -102,7 +101,7 @@ class LoginWebPageTest extends ItopDataTestCase
|
||||
public function testWithoutDelegatedAuthenticationEndpointsListWithForceLoginConf()
|
||||
{
|
||||
@chmod($this->oConfig->GetLoadedFile(), 0770);
|
||||
$this->oConfig->Set('security.force_login_when_no_delegated_authentication_endpoints_list', true, 'AnythingButEmptyOrUnknownValue'); // 3rd param to write file even if show_in_conf_sample is false
|
||||
$this->oConfig->Set('security.disable_exec_forced_login_for_all_enpoints', false, 'AnythingButEmptyOrUnknownValue'); // 3rd param to write file even if show_in_conf_sample is false
|
||||
$this->oConfig->WriteToFile();
|
||||
@chmod($this->oConfig->GetLoadedFile(), 0444);
|
||||
$sPageContent = $this->CallItopUri(
|
||||
|
||||
@@ -1556,6 +1556,23 @@ abstract class ItopDataTestCase extends ItopTestCase
|
||||
@unlink($this->sConfigTmpBackupFile);
|
||||
}
|
||||
|
||||
protected function AddLoginModeAndSaveConfiguration(string $sLoginMode): void
|
||||
{
|
||||
$aAllowedLoginTypes = $this->oiTopConfig->GetAllowedLoginTypes();
|
||||
if (!in_array($sLoginMode, $aAllowedLoginTypes)) {
|
||||
$aAllowedLoginTypes[] = $sLoginMode;
|
||||
$this->oiTopConfig->SetAllowedLoginTypes($aAllowedLoginTypes);
|
||||
$this->SaveItopConfFile();
|
||||
}
|
||||
}
|
||||
|
||||
protected function SaveItopConfFile(): void
|
||||
{
|
||||
@chmod($this->oiTopConfig->GetLoadedFile(), 0770);
|
||||
$this->oiTopConfig->WriteToFile();
|
||||
@chmod($this->oiTopConfig->GetLoadedFile(), 0440);
|
||||
}
|
||||
|
||||
public function AssertPreviousAndCurrentInstallationAreEquivalent()
|
||||
{
|
||||
$aPreviousInstallations = ModuleInstallationRepository::GetInstance()->GetPreviousModuleInstallationsByOffset(1);
|
||||
|
||||
@@ -687,7 +687,7 @@ abstract class ItopTestCase extends KernelTestCase
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $sUrl);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);// set post data to true
|
||||
curl_setopt($ch, CURLOPT_POST, $aCurlOptions[CURLOPT_POST] ?? 1);// set post data to true
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
// Force disable of certificate check as most of dev / test env have a self-signed certificate
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
@@ -727,6 +727,16 @@ abstract class ItopTestCase extends KernelTestCase
|
||||
{
|
||||
$sUrl = \MetaModel::GetConfig()->Get('app_root_url')."/$sUri";
|
||||
|
||||
// Add PHP version in header to be able to handle Docker dev environments with automatic PHP version detection (instead of hardcoding the PHP version in the app_root_url)
|
||||
$sPhpVersion = PHP_VERSION;
|
||||
$aPhpVersionParts = explode('.', $sPhpVersion);
|
||||
$sPhpVersionHeaderValue = ($aPhpVersionParts[0] ?? '0').($aPhpVersionParts[1] ?? '0');
|
||||
$aCurlOptions = $aCurlOptions ?? [];
|
||||
$aCurlOptions[CURLOPT_HTTPHEADER] = array_merge(
|
||||
$aCurlOptions[CURLOPT_HTTPHEADER] ?? [],
|
||||
['X-PHP-Version: '.$sPhpVersionHeaderValue]
|
||||
);
|
||||
|
||||
return $this->CallUrl($sUrl, $aPostFields, $aCurlOptions, $bXDebugEnabled);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2026 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Application\Helper;
|
||||
|
||||
use Combodo\iTop\Application\Helper\SearchHelper;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use DBSearch;
|
||||
use MetaModel;
|
||||
|
||||
class SearchHelperTest extends ItopDataTestCase
|
||||
{
|
||||
protected static array $aHighCardinalityClasses = [];
|
||||
protected static bool $bSearchManualSubmit = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::$aHighCardinalityClasses = MetaModel::GetConfig()->Get('high_cardinality_classes');
|
||||
self::$bSearchManualSubmit = MetaModel::GetConfig()->Get('search_manual_submit');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
MetaModel::GetConfig()->Set('high_cardinality_classes', static::$aHighCardinalityClasses);
|
||||
MetaModel::GetConfig()->Set('search_manual_submit', static::$bSearchManualSubmit);
|
||||
}
|
||||
|
||||
public function testDisplaySearchSetWithNoHighCardinalityClassesAddsResultSubBlock(): void
|
||||
{
|
||||
MetaModel::GetConfig()->Set('high_cardinality_classes', []);
|
||||
MetaModel::GetConfig()->Set('search_manual_submit', false);
|
||||
|
||||
$oP = new iTopWebPage('SearchHelperTest');
|
||||
$oFilter = DBSearch::FromOQL('SELECT UserRequest');
|
||||
SearchHelper::DisplaySearchSet($oP, $oFilter);
|
||||
$oContentLayout = $oP->GetContentLayout();
|
||||
$this->assertTrue($oContentLayout->HasSubBlock('search_1'));
|
||||
$oSearchBlock = $oContentLayout->getSubBlock('search_1');
|
||||
$this->assertTrue($oSearchBlock->HasSubBlock('result_1'));
|
||||
|
||||
if (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
public function testDisplaySearchSetWithHighCardinalityClassesDoesNotAddResultSubBlock(): void
|
||||
{
|
||||
MetaModel::GetConfig()->Set('high_cardinality_classes', ['UserRequest']);
|
||||
MetaModel::GetConfig()->Set('search_manual_submit', false);
|
||||
|
||||
$oP = new iTopWebPage('SearchHelperTest');
|
||||
$oFilter = DBSearch::FromOQL('SELECT UserRequest');
|
||||
SearchHelper::DisplaySearchSet($oP, $oFilter);
|
||||
$oContentLayout = $oP->GetContentLayout();
|
||||
$this->assertTrue($oContentLayout->HasSubBlock('search_1'));
|
||||
$oSearchBlock = $oContentLayout->getSubBlock('search_1');
|
||||
$this->assertFalse($oSearchBlock->HasSubBlock('result_1'));
|
||||
|
||||
if (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
public function testDisplaySearchSetWithSearchManualSubmitAndWithoutHighCardinalityClassesDoesNotAddResultSubBlock(): void
|
||||
{
|
||||
MetaModel::GetConfig()->Set('high_cardinality_classes', []);
|
||||
MetaModel::GetConfig()->Set('search_manual_submit', true);
|
||||
|
||||
$oP = new iTopWebPage('SearchHelperTest');
|
||||
$oFilter = DBSearch::FromOQL('SELECT UserRequest');
|
||||
SearchHelper::DisplaySearchSet($oP, $oFilter);
|
||||
$oContentLayout = $oP->GetContentLayout();
|
||||
$this->assertTrue($oContentLayout->HasSubBlock('search_1'));
|
||||
$oSearchBlock = $oContentLayout->getSubBlock('search_1');
|
||||
$this->assertFalse($oSearchBlock->HasSubBlock('result_1'));
|
||||
|
||||
if (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2010-2024 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Application;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use utils;
|
||||
|
||||
class LoginExternalTest extends ItopDataTestCase
|
||||
{
|
||||
private $oConfig;
|
||||
private $sOriginalExtAuthVariable;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
require_once APPROOT.'application/loginexternal.class.inc.php';
|
||||
$this->oConfig = utils::GetConfig();
|
||||
$this->sOriginalExtAuthVariable = $this->oConfig->Get('ext_auth_variable');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->oConfig->SetExternalAuthenticationVariable($this->sOriginalExtAuthVariable);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function CallGetAuthUser()
|
||||
{
|
||||
$oLoginExternal = new \LoginExternal();
|
||||
$oMethod = new \ReflectionMethod(\LoginExternal::class, 'GetAuthUser');
|
||||
$oMethod->setAccessible(true);
|
||||
return $oMethod->invoke($oLoginExternal);
|
||||
}
|
||||
|
||||
public function testGetAuthUserFromServerVariable()
|
||||
{
|
||||
$_SERVER['REMOTE_USER'] = 'alice';
|
||||
$this->oConfig->SetExternalAuthenticationVariable('$_SERVER[\'REMOTE_USER\']');
|
||||
|
||||
$this->assertSame('alice', $this->CallGetAuthUser());
|
||||
}
|
||||
|
||||
public function testGetAuthUserFromCookie()
|
||||
{
|
||||
$_COOKIE['auth_user'] = 'bob';
|
||||
$this->oConfig->SetExternalAuthenticationVariable('$_COOKIE[\'auth_user\']');
|
||||
|
||||
$this->assertSame('bob', $this->CallGetAuthUser());
|
||||
}
|
||||
|
||||
public function testGetAuthUserFromRequest()
|
||||
{
|
||||
$_REQUEST['auth_user'] = 'carol';
|
||||
$this->oConfig->SetExternalAuthenticationVariable('$_REQUEST[\'auth_user\']');
|
||||
|
||||
$this->assertSame('carol', $this->CallGetAuthUser());
|
||||
}
|
||||
|
||||
public function testInvalidExpressionReturnsFalse()
|
||||
{
|
||||
$this->oConfig->SetExternalAuthenticationVariable('$_SERVER[\'HTTP_X_CMD\']) ? print(\'x\') : false; //');
|
||||
|
||||
$this->assertFalse($this->CallGetAuthUser());
|
||||
}
|
||||
|
||||
public function testGetAuthUserFromHeaderWithoutAllowlist()
|
||||
{
|
||||
if (!function_exists('getallheaders')) {
|
||||
$this->markTestSkipped('getallheaders() not available');
|
||||
}
|
||||
$_SERVER['HTTP_X_REMOTE_USER'] = 'CN=header-test';
|
||||
$this->oConfig->SetExternalAuthenticationVariable('getallheaders()[\'X-Remote-User\']');
|
||||
|
||||
$this->assertSame('CN=header-test', $this->CallGetAuthUser());
|
||||
}
|
||||
}
|
||||
@@ -645,4 +645,54 @@ SCSS;
|
||||
[ '/var/www/html/iTop/css/ui-lightness/images/ui-icons_222222_256x240.png', '/var/www/html/iTop/env-production//branding/themes/light-grey//../../../../css/ui-lightness/images/ui-icons_222222_256x240.png' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $aThemeParameters
|
||||
* @param $bSetupCompilationTimestamp
|
||||
* @param $aExpectedClonedParameters
|
||||
* @dataProvider CloneParameterParameterOverloadProvider
|
||||
*/
|
||||
public function testCloneParameterParameterOverload($aThemeParameters, $bSetupCompilationTimestamp, $aExpectedClonedParameters)
|
||||
{
|
||||
$aClonedParameters = ThemeHandler::CloneThemeParameterAndIncludeVersion($aThemeParameters, $bSetupCompilationTimestamp, [APPROOT.'tests/php-unit-tests/unitary-tests/application/theme-handler/imports/']);
|
||||
$this->assertEquals($aExpectedClonedParameters, $aClonedParameters);
|
||||
}
|
||||
|
||||
public function CloneParameterParameterOverloadProvider()
|
||||
{
|
||||
return [
|
||||
"empty parameters" => [
|
||||
'parameters' => [],
|
||||
'timestamp' => '1',
|
||||
'expected' => [
|
||||
'$version' => '1',
|
||||
],
|
||||
],
|
||||
"parameters without variables" => [
|
||||
'parameters' => [
|
||||
'variable_imports' => ['file1' => 'variable_imports.scss'],
|
||||
'utility_imports' => ['util1' => 'path2'],
|
||||
'stylesheets' => ['style1' => 'path3'],
|
||||
],
|
||||
'timestamp' => '2',
|
||||
'expected' => [
|
||||
'var1' => 'value1',
|
||||
'var2' => 'value2',
|
||||
'$version' => '2',
|
||||
],
|
||||
],
|
||||
"parameters with variables overload" => [
|
||||
'parameters' => [
|
||||
'variables' => ['var1' => 'value2'],
|
||||
'variable_imports' => ['file1' => 'variable_imports.scss'],
|
||||
],
|
||||
'timestamp' => '3',
|
||||
'expected' => [
|
||||
'var1' => 'value2',
|
||||
'var2' => 'value2',
|
||||
'$version' => '3',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
$var1: value1;
|
||||
$var2: value2;
|
||||
@@ -774,6 +774,11 @@ class utilsTest extends ItopTestCase
|
||||
'good element_identifier' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, 'AD05nb', 'AD05nb'],
|
||||
'bad element_identifier' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, 'AD05nb+', 'AD05nb'],
|
||||
'array' => [utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER, ['AD05nb+','apply_modify'], ['AD05nb','apply_modify']],
|
||||
'good module code' => [utils::ENUM_SANITIZATION_FILTER_MODULE_CODE, 'some-module-code', 'some-module-code'],
|
||||
'good module code with capitalized letters' => [utils::ENUM_SANITIZATION_FILTER_MODULE_CODE, 'SOME-module-code', 'SOME-module-code'],
|
||||
'good module code with dot' => [utils::ENUM_SANITIZATION_FILTER_MODULE_CODE, 'some-module-code-for-3.2-version', 'some-module-code-for-3.2-version'],
|
||||
'bad module code with underscores' => [utils::ENUM_SANITIZATION_FILTER_MODULE_CODE, 'some_module_code', null],
|
||||
'bad module code with slashes' => [utils::ENUM_SANITIZATION_FILTER_MODULE_CODE, 'some-module/code', null],
|
||||
'good url' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https://www.w3schools.com', 'https://www.w3schools.com'],
|
||||
'bad url' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https//www.w3schools.com', null],
|
||||
'url with injection' => [utils::ENUM_SANITIZATION_FILTER_URL, 'https://demo.combodo.com/simple/pages/UI.php?operation=full_text&text=<img zzz src=x onerror=alert(1) //>', 'https://demo.combodo.com/simple/pages/UI.php?operation=full_text&text=<imgzzzsrc=xonerror=alert(1)//>'],
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core;
|
||||
|
||||
use AttributeDateTime;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use DateTime;
|
||||
use MetaModel;
|
||||
|
||||
class AttributeSubItemTest extends ItopDataTestCase
|
||||
{
|
||||
public const CREATE_TEST_ORG = true;
|
||||
|
||||
/**
|
||||
* @param string $sAttCode
|
||||
* @param string $sVerb
|
||||
* @param string $sExpectedValue
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testGetForTemplate()
|
||||
{
|
||||
$aUserRequestCustomParams = [
|
||||
'title' => "Test DisplayStopwatch",
|
||||
];
|
||||
$oUserRequest = $this->CreateUserRequest(456, $aUserRequestCustomParams);
|
||||
|
||||
$iStartDate = time() - 200;
|
||||
$oStopwatch = $oUserRequest->Get('ttr');
|
||||
$oStopwatch->DefineThreshold(100, $iStartDate);
|
||||
$oUserRequest->Set('ttr', $oStopwatch);
|
||||
|
||||
$sValue = $oUserRequest->Get('ttr_escalation_deadline');
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($oUserRequest), 'ttr_escalation_deadline');
|
||||
|
||||
self::assertEquals('Missed by 3 min', $oAttDef->GetForTemplate($sValue, 'html', $oUserRequest));
|
||||
$oDateTime = new DateTime();
|
||||
$oDateTime->setTimestamp($iStartDate);
|
||||
$sDate = $oDateTime->format(AttributeDateTime::GetFormat());
|
||||
self::assertEquals($sDate, $oAttDef->GetForTemplate($sValue, 'label', $oUserRequest), 'label() should render the date in the format specified in the configuration file, in parameter "date_and_time_format"');
|
||||
self::assertEquals('Missed by 3 min', $oAttDef->GetForTemplate($sValue, 'text', $oUserRequest), 'text() should render the deadline as specified in the configuration file, in parameter "deadline_format", and depending on the user language');
|
||||
self::assertEquals($iStartDate, $oAttDef->GetForTemplate($sValue, '', $oUserRequest));
|
||||
}
|
||||
}
|
||||
@@ -1100,7 +1100,6 @@ class DBObjectTest extends ItopDataTestCase
|
||||
'fixed',
|
||||
[
|
||||
'ev_reopen',
|
||||
'ev_autoresolve',
|
||||
'ev_close',
|
||||
],
|
||||
],
|
||||
@@ -1122,7 +1121,6 @@ class DBObjectTest extends ItopDataTestCase
|
||||
'resolved',
|
||||
'relative',
|
||||
[
|
||||
'ev_autoresolve',
|
||||
'ev_close',
|
||||
'ev_reopen',
|
||||
],
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core;
|
||||
|
||||
use CMDBSource;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use DBSearch;
|
||||
use lnkFunctionalCIToTicket;
|
||||
use MetaModel;
|
||||
use ormLinkSet;
|
||||
use UserRequest;
|
||||
use UserRights;
|
||||
|
||||
class DBSearchFilterJoinTest extends ItopDataTestCase
|
||||
{
|
||||
private const RESTRICTED_PROFILE = 'Configuration Manager';
|
||||
private $aData = [];
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->RequireOnceItopFile('application/startup.inc.php');
|
||||
$this->aData = $this->CreateDBSearchFilterTestData();
|
||||
DBSearch::EnableQueryCache(false, false);
|
||||
$this->LoginRestrictedUser($this->aData['allowed_org_id'], self::RESTRICTED_PROFILE);
|
||||
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider JoinedAndNestedOqlProvider
|
||||
*/
|
||||
public function testDBSearchFilterAppliedToJoinsWhenEnabled(string $sOql, int $iExpectedCount): void
|
||||
{
|
||||
$this->EnableJoinFilterConfig(true);
|
||||
|
||||
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
|
||||
$this->assertEquals($iExpectedCount, $oSet->Count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider JoinedAndNestedOqlProvider
|
||||
*/
|
||||
public function testDBSearchFilterAppliedToJoinsWhenDisabled(string $sOql, int $iExpectedCount, int $iExpectedDisabledCount): void
|
||||
{
|
||||
$this->EnableJoinFilterConfig(false);
|
||||
|
||||
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
|
||||
$this->assertEquals($iExpectedDisabledCount, $oSet->Count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider JoinedAndNestedOqlProvider
|
||||
*/
|
||||
public function testAllowAllDataBypassesDBSearchFilterWhenEnabled(string $sOql, int $iExpectedCount, int $iExpectedDisabledCount): void
|
||||
{
|
||||
$this->EnableJoinFilterConfig(true);
|
||||
|
||||
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
|
||||
$oSearch->AllowAllData();
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
|
||||
$this->assertEquals($iExpectedDisabledCount, $oSet->Count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider JoinedAndNestedOqlProvider
|
||||
*/
|
||||
public function testAllowAllDataBypassesDBSearchFilterWhenDisabled(string $sOql, int $iExpectedCount, int $iExpectedDisabledCount): void
|
||||
{
|
||||
$this->EnableJoinFilterConfig(false);
|
||||
|
||||
$oSearch = DBObjectSearch::FromOQL($sOql, ['denied_org' => $this->aData['denied_org_name'], 'allowed_org' => $this->aData['allowed_org_name']]);
|
||||
$oSearch->AllowAllData();
|
||||
$oSet = new \DBObjectSet($oSearch);
|
||||
CMDBSource::TestQuery($oSearch->MakeSelectQuery());
|
||||
$this->assertEquals($iExpectedDisabledCount, $oSet->Count());
|
||||
}
|
||||
|
||||
public function JoinedAndNestedOqlProvider(): array
|
||||
{
|
||||
return [
|
||||
'join-filter-on-org' => [
|
||||
'oql' => "SELECT OSF FROM OSFamily AS OSF JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF.id JOIN Organization AS O ON VM.org_id = O.id WHERE O.name = :denied_org",
|
||||
'expected_filtered_count' => 0,
|
||||
'expected_unfiltered_count' => 1,
|
||||
],
|
||||
'nested-in-select' => [
|
||||
'oql' => "SELECT OSF FROM OSFamily AS OSF WHERE OSF.id IN (SELECT OSF1 FROM OSFamily AS OSF1 JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF1.id JOIN Organization AS O ON VM.org_id = O.id WHERE O.name = :denied_org)",
|
||||
'expected_filtered_count' => 0,
|
||||
'expected_unfiltered_count' => 1,
|
||||
|
||||
],
|
||||
'userrequest-join-person-org' => [
|
||||
'oql' => "SELECT OSF FROM OSFamily AS OSF JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF.id JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = VM.id JOIN UserRequest AS UR ON L.ticket_id = UR.id JOIN Person AS P ON UR.caller_id = P.id JOIN Organization AS O ON P.org_id = O.id WHERE O.name = :denied_org",
|
||||
'expected_filtered_count' => 0,
|
||||
'expected_unfiltered_count' => 1,
|
||||
],
|
||||
'union-join-filter-on-org' => [
|
||||
'oql' => "SELECT OSF FROM OSFamily AS OSF JOIN VirtualMachine AS VM ON VM.osfamily_id = OSF.id JOIN Organization AS O ON VM.org_id = O.id WHERE O.name = :denied_org UNION SELECT OSF2 FROM OSFamily AS OSF2 JOIN VirtualMachine AS VM2 ON VM2.osfamily_id = OSF2.id JOIN Organization AS O2 ON VM2.org_id = O2.id WHERE O2.name = :allowed_org",
|
||||
'expected_filtered_count' => 1,
|
||||
'expected_unfiltered_count' => 2,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function EnableJoinFilterConfig(bool $bEnabled): void
|
||||
{
|
||||
$oConfig = MetaModel::GetConfig();
|
||||
$oConfig->Set('security.disable_joined_classes_filter', !$bEnabled);
|
||||
}
|
||||
|
||||
private function CreateDBSearchFilterTestData(): array
|
||||
{
|
||||
$sSuffix = 'DBSearchFilterJoinTest';
|
||||
|
||||
$sAllowedOrgName = 'DBSearchFilterAllowedOrg-'.$sSuffix;
|
||||
$iAllowedOrgId = $this->GivenObjectInDB('Organization', [
|
||||
'name' => $sAllowedOrgName,
|
||||
]);
|
||||
|
||||
$this->debug("Org allowed id: $iAllowedOrgId");
|
||||
$sDeniedOrgName = 'DBSearchFilterDeniedOrg-'.$sSuffix;
|
||||
$iDeniedOrgId = $this->GivenObjectInDB('Organization', [
|
||||
'name' => $sDeniedOrgName,
|
||||
]);
|
||||
$this->debug("Org denied id: $iDeniedOrgId");
|
||||
|
||||
$iDeniedOsFamilyId = $this->GivenObjectInDB('OSFamily', [
|
||||
'name' => 'DBSearchFilterOsFamilyDenied-'.$sSuffix,
|
||||
]);
|
||||
|
||||
$iAllowedOsFamilyId = $this->GivenObjectInDB('OSFamily', [
|
||||
'name' => 'DBSearchFilterOsFamilyAllowed-'.$sSuffix,
|
||||
]);
|
||||
|
||||
$iDeniedVMId = $this->GivenObjectInDB('VirtualMachine', [
|
||||
'name' => 'DBSearchFilterVmDenied-'.$sSuffix,
|
||||
'org_id' => $iDeniedOrgId,
|
||||
'osfamily_id' => $iDeniedOsFamilyId,
|
||||
'virtualhost_id' => 1,
|
||||
]);
|
||||
|
||||
$iVirtualHostId = $this->GivenObjectInDB('Hypervisor', [
|
||||
'name' => 'DBSearchFilterVHost-'.$sSuffix,
|
||||
'org_id' => $iAllowedOrgId,
|
||||
]);
|
||||
|
||||
$this->GivenObjectInDB('VirtualMachine', [
|
||||
'name' => 'DBSearchFilterVmAllowed-'.$sSuffix,
|
||||
'org_id' => $iAllowedOrgId,
|
||||
'osfamily_id' => $iAllowedOsFamilyId,
|
||||
'virtualhost_id' => $iVirtualHostId,
|
||||
]);
|
||||
|
||||
$oDeniedPerson = $this->CreatePerson('Denied-'.$sSuffix, $iDeniedOrgId);
|
||||
|
||||
$oUserRequest = $this->CreateUserRequest('Denied'.$sSuffix, [
|
||||
'caller_id' => $oDeniedPerson->GetKey(),
|
||||
'org_id' => $iDeniedOrgId,
|
||||
]);
|
||||
|
||||
// Add Virtual Machine to UserRequest lnk
|
||||
$oLinkSet = new ormLinkSet(UserRequest::class, 'functionalcis_list', DBObjectSet::FromScratch(lnkFunctionalCIToTicket::class));
|
||||
|
||||
$oLink = MetaModel::NewObject(lnkFunctionalCIToTicket::class, ['functionalci_id' => $iDeniedVMId]);
|
||||
$oLinkSet->AddItem($oLink);
|
||||
|
||||
$oUserRequest->Set('functionalcis_list', $oLinkSet);
|
||||
$oUserRequest->DBUpdate();
|
||||
|
||||
return [
|
||||
'allowed_org_id' => $iAllowedOrgId,
|
||||
'allowed_org_name' => $sAllowedOrgName,
|
||||
'denied_org_name' => $sDeniedOrgName,
|
||||
];
|
||||
}
|
||||
|
||||
private function LoginRestrictedUser(int $iAllowedOrgId, string $sProfileName): void
|
||||
{
|
||||
$sLogin = $this->GivenUserRestrictedToAnOrganizationInDB($iAllowedOrgId, self::$aURP_Profiles[$sProfileName]);
|
||||
UserRights::Login($sLogin);
|
||||
}
|
||||
}
|
||||
@@ -230,14 +230,14 @@ class UserRightsTest extends ItopDataTestCase
|
||||
'User Portal UserRequest read' => [2, ['class' => 'UserRequest', 'action' => 1, 'res' => true]],
|
||||
'User Portal URP_UserProfile read' => [2, ['class' => 'URP_UserProfile', 'action' => 1, 'res' => false]],
|
||||
'User Portal UserLocal read' => [2, ['class' => 'UserLocal', 'action' => 1, 'res' => false]],
|
||||
'User Portal ModuleInstallation read' => [2, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => true]],
|
||||
'User Portal ModuleInstallation read' => [2, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => false]],
|
||||
|
||||
/* Configuration manager (1 = UR_ACTION_READ) */
|
||||
'Configuration manager FunctionalCI read' => [3, ['class' => 'FunctionalCI', 'action' => 1, 'res' => true]],
|
||||
'Configuration manager UserRequest read' => [3, ['class' => 'UserRequest', 'action' => 1, 'res' => true]],
|
||||
'Configuration manager URP_UserProfile read' => [3, ['class' => 'URP_UserProfile', 'action' => 1, 'res' => false]],
|
||||
'Configuration manager UserLocal read' => [3, ['class' => 'UserLocal', 'action' => 1, 'res' => false]],
|
||||
'Configuration manager ModuleInstallation read' => [3, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => true]],
|
||||
'Configuration manager ModuleInstallation read' => [3, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => false]],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -283,14 +283,14 @@ class UserRightsTest extends ItopDataTestCase
|
||||
'User Portal UserRequest' => [2, ['class' => 'UserRequest', 'action' => 2, 'res' => true]],
|
||||
'User Portal URP_UserProfile' => [2, ['class' => 'URP_UserProfile', 'action' => 2, 'res' => false]],
|
||||
'User Portal UserLocal' => [2, ['class' => 'UserLocal', 'action' => 2, 'res' => false]],
|
||||
'User Portal ModuleInstallation' => [2, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => true]],
|
||||
'User Portal ModuleInstallation' => [2, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => false]],
|
||||
|
||||
/* Configuration manager (2 = UR_ACTION_MODIFY) */
|
||||
'Configuration manager FunctionalCI' => [3, ['class' => 'FunctionalCI', 'action' => 2, 'res' => true]],
|
||||
'Configuration manager UserRequest' => [3, ['class' => 'UserRequest', 'action' => 2, 'res' => false]],
|
||||
'Configuration manager URP_UserProfile' => [3, ['class' => 'URP_UserProfile', 'action' => 2, 'res' => false]],
|
||||
'Configuration manager UserLocal' => [3, ['class' => 'UserLocal', 'action' => 2, 'res' => false]],
|
||||
'Configuration manager ModuleInstallation' => [3, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => true]],
|
||||
'Configuration manager ModuleInstallation' => [3, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => false]],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,36 @@
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core;
|
||||
|
||||
use Combodo\iTop\Application\WebPage\CaptureWebPage;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use ormDocument;
|
||||
use UserRights;
|
||||
|
||||
/**
|
||||
* Tests of the ormDocument class
|
||||
*/
|
||||
class ormDocumentTest extends ItopDataTestCase
|
||||
{
|
||||
private const RESTRICTED_PROFILE = 'Configuration Manager';
|
||||
private int $iUserOrg;
|
||||
private int $iOrgDifferentFromUser;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->iUserOrg = $this->GivenObjectInDB('Organization', [
|
||||
'name' => 'UserOrg',
|
||||
]);
|
||||
|
||||
$this->iOrgDifferentFromUser = $this->GivenObjectInDB('Organization', [
|
||||
'name' => 'OrgDifferentFromUser',
|
||||
]);
|
||||
|
||||
$this->LoginRestrictedUser($this->iUserOrg, self::RESTRICTED_PROFILE);
|
||||
$this->ResetMetaModelQueyCacheGetObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
@@ -248,4 +270,107 @@ class ormDocumentTest extends ItopDataTestCase
|
||||
$this->assertGreaterThanOrEqual($iMaxHeight, $aActualDimensions['height'], 'The new height should not be 0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that DownloadDocument enforces rights for documents
|
||||
*
|
||||
* @dataProvider DownloadDocumentRightsProvider
|
||||
*/
|
||||
public function testDownloadDocumentDifferentOrg(string $sTargetClass, string $sAttCode, string $sData, string $sFileName, ?string $sHostClass)
|
||||
{
|
||||
$iDeniedDocumentId = $this->CreateDownloadTargetInOrg($sTargetClass, $sAttCode, $this->iOrgDifferentFromUser, $sData, $sFileName, $sHostClass);
|
||||
|
||||
$oPageDenied = new CaptureWebPage();
|
||||
ormDocument::DownloadDocument($oPageDenied, $sTargetClass, $iDeniedDocumentId, $sAttCode);
|
||||
$sDeniedHtml = (string) $oPageDenied->GetHtml();
|
||||
$this->assertStringContainsString(
|
||||
'the object does not exist or you are not allowed to view it',
|
||||
$sDeniedHtml,
|
||||
'Expected error message when rights are missing.'
|
||||
);
|
||||
$this->assertStringNotContainsString($sData, $sDeniedHtml, 'Unexpected file data present when rights are missing.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that DownloadDocument allows to retrieve document with the same org (or host object org)
|
||||
*
|
||||
* @dataProvider DownloadDocumentRightsProvider
|
||||
*/
|
||||
public function testDownloadDocumentSameOrg(string $sTargetClass, string $sAttCode, string $sData, string $sFileName, ?string $sHostClass)
|
||||
{
|
||||
$iAllowedDocumentId = $this->CreateDownloadTargetInOrg($sTargetClass, $sAttCode, $this->iUserOrg, $sData, $sFileName, $sHostClass);
|
||||
|
||||
$oPageAllowed = new CaptureWebPage();
|
||||
ormDocument::DownloadDocument($oPageAllowed, $sTargetClass, $iAllowedDocumentId, $sAttCode);
|
||||
$sAllowedHtml = (string) $oPageAllowed->GetHtml();
|
||||
$this->assertStringContainsString($sData, $sAllowedHtml, 'Expected file data present when rights are sufficient.');
|
||||
$this->assertStringNotContainsString('the object does not exist or you are not allowed to view it', $sAllowedHtml, 'Unexpected error message when rights are sufficient.');
|
||||
}
|
||||
|
||||
public function DownloadDocumentRightsProvider(): array
|
||||
{
|
||||
return [
|
||||
'DocumentFile' => [
|
||||
'class' => 'DocumentFile',
|
||||
'data_attribute_id' => 'file',
|
||||
'data' => 'document_data',
|
||||
'file_name' => 'document.txt',
|
||||
'host_class' => null],
|
||||
'Attachment' => [
|
||||
'class' => 'Attachment',
|
||||
'data_attribute_id' => 'contents',
|
||||
'data' => 'attachment_data',
|
||||
'file_name' => 'attachment.txt',
|
||||
'host_class' => 'UserRequest'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to avoid duplicating object creation in tests
|
||||
* Created objects and host objects depending on the Document class
|
||||
* @param string $sTargetClass
|
||||
* @param string $sAttCode
|
||||
* @param int $iOrgId
|
||||
* @param string $sData
|
||||
* @param string $sFileName
|
||||
* @param string|null $sHostClass
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function CreateDownloadTargetInOrg(string $sTargetClass, string $sAttCode, int $iOrgId, string $sData, string $sFileName, ?string $sHostClass): int
|
||||
{
|
||||
|
||||
if ($sTargetClass === 'DocumentFile') {
|
||||
return $this->GivenObjectInDB($sTargetClass, [
|
||||
'name' => 'UnitTestDocFile_'.uniqid(),
|
||||
'org_id' => $iOrgId,
|
||||
$sAttCode => new ormDocument($sData, 'text/plain', $sFileName),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($sTargetClass === 'Attachment') {
|
||||
$iHostId = $this->GivenObjectInDB($sHostClass, [
|
||||
'title' => 'UnitTestUserRequest_'.uniqid(),
|
||||
'org_id' => $iOrgId,
|
||||
'description' => 'A user request for testing attachment download rights',
|
||||
]);
|
||||
|
||||
return $this->GivenObjectInDB('Attachment', [
|
||||
'item_class' => $sHostClass,
|
||||
'item_id' => $iHostId,
|
||||
'item_org_id' => $iOrgId,
|
||||
$sAttCode => new ormDocument($sData, 'text/plain', $sFileName),
|
||||
]);
|
||||
}
|
||||
|
||||
throw new \Exception("Unsupported target class: $sTargetClass");
|
||||
}
|
||||
|
||||
private function LoginRestrictedUser(int $iAllowedOrgId, string $sProfileName): void
|
||||
{
|
||||
if (UserRights::IsLoggedIn()) {
|
||||
UserRights::Logoff();
|
||||
}
|
||||
$sLogin = $this->GivenUserRestrictedToAnOrganizationInDB($iAllowedOrgId, self::$aURP_Profiles[$sProfileName]);
|
||||
UserRights::Login($sLogin);
|
||||
}
|
||||
}
|
||||
|
||||
180
tests/php-unit-tests/unitary-tests/pages/AjaxRenderTest.php
Normal file
180
tests/php-unit-tests/unitary-tests/pages/AjaxRenderTest.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Pages;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use Dict;
|
||||
use UserLocal;
|
||||
use UserRequest;
|
||||
|
||||
class AjaxRenderTest extends ItopDataTestCase
|
||||
{
|
||||
public const USE_TRANSACTION = false;
|
||||
public const AUTHENTICATION_PASSWORD = "tagada-Secret,007";
|
||||
|
||||
private static string $sLogin;
|
||||
private static int $iTicketId;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->BackupConfiguration();
|
||||
$this->oiTopConfig->Set('log_level_min', 'Error');
|
||||
$this->oiTopConfig->Set('login_debug', true);
|
||||
|
||||
$this->CreateTestOrganization();
|
||||
|
||||
// Add URL authentication mode
|
||||
$this->AddLoginModeAndSaveConfiguration('url');
|
||||
|
||||
// Create ticket
|
||||
$description = date('dmY H:i:s');
|
||||
$oTicket = $this->createObject('UserRequest', [
|
||||
'org_id' => $this->getTestOrgId(),
|
||||
"title" => "Houston, got a problem",
|
||||
"description" => $description,
|
||||
]);
|
||||
self::$iTicketId = $oTicket->GetKey();
|
||||
}
|
||||
|
||||
// Test that if a user with the right permissions tries to acquire the lock on a ticket, it succeeds and returns the correct success message
|
||||
public function testAcquireLockSuccess(): void
|
||||
{
|
||||
$sOutput = $this->CreateSupportAgentUserAndAcquireLock();
|
||||
$this->assertStringContainsString('"success":true', $sOutput);
|
||||
}
|
||||
|
||||
// Test that if a user tries to acquire the lock on an object that does not exist, it fails and logs the correct error message
|
||||
public function testAcquireLockFailsIfObjectDoesNotExist(): void
|
||||
{
|
||||
// Create a user with Support Agent Profile
|
||||
$this->CreateUserWithProfile(self::$aURP_Profiles['Support Agent']);
|
||||
|
||||
// Try to acquire the lock on a non-existent object
|
||||
$sOutput = $this->AcquireLockAsUser(self::$sLogin, 99999999);
|
||||
|
||||
// The output should indicate a fatal error because we hide the existence of the object when it does not exist or is not accessible by the user
|
||||
$this->assertEquals(Dict::S('UI:PageTitle:FatalError'), $sOutput);
|
||||
|
||||
// Check that the error log contains the expected error message about the object not existing
|
||||
$sLastErrorLogLines = $this->GetErrorLogLastLines(APPROOT.'log/error.log', 10);
|
||||
$this->assertStringContainsString(Dict::S('UI:ObjectDoesNotExist'), $sLastErrorLogLines);
|
||||
}
|
||||
|
||||
// Test that if a user tries to acquire the lock on an object for which they don't have modification rights, it fails and logs the correct error message
|
||||
public function testAcquireLockFailsIfUserHasNoModifyRights(): void
|
||||
{
|
||||
// Create a user with a profile without modification rights on UserRequest
|
||||
$this->CreateUserWithProfile(self::$aURP_Profiles['Configuration Manager']);
|
||||
|
||||
// Try to acquire the lock on the ticket
|
||||
$sOutput = $this->AcquireLockAsUser(self::$sLogin, self::$iTicketId);
|
||||
|
||||
// The output should indicate a fatal error because we hide the existence of the object when it does not exist or is not accessible by the user
|
||||
$this->assertEquals(Dict::S('UI:PageTitle:FatalError'), $sOutput);
|
||||
|
||||
// The user should not have the rights to acquire the lock, and an error should be logged
|
||||
$sLastErrorLogLines = $this->GetErrorLogLastLines(APPROOT.'log/error.log', 10);
|
||||
$this->assertStringContainsString(Dict::S('UI:ObjectDoesNotExist'), $sLastErrorLogLines);
|
||||
}
|
||||
|
||||
// Test that if a user tries to acquire the lock on an object that belongs to another organization, it fails and logs the correct error message
|
||||
public function testAcquireLockFailsIfObjectInOtherOrg(): void
|
||||
{
|
||||
// Create an organization and a ticket in this organization
|
||||
$iOtherOrgId = $this->createObject('Organization', ['name' => 'OtherOrg'])->GetKey();
|
||||
$oTicket = $this->createObject('UserRequest', [
|
||||
'org_id' => $iOtherOrgId,
|
||||
'title' => 'Ticket autre org',
|
||||
'description' => 'Test',
|
||||
]);
|
||||
|
||||
// Create a user who only has access to the main test organization
|
||||
$oUser = $this->CreateUserWithProfile(self::$aURP_Profiles['Support Agent']);
|
||||
$oAllowedOrgList = $oUser->Get('allowed_org_list');
|
||||
$oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $this->getTestOrgId()]);
|
||||
$oAllowedOrgList->AddItem($oUserOrg);
|
||||
$oUser->Set('allowed_org_list', $oAllowedOrgList);
|
||||
$oUser->DBWrite();
|
||||
|
||||
// Try to acquire the lock on the ticket of the other organization
|
||||
$sOutput = $this->AcquireLockAsUser(self::$sLogin, $oTicket->GetKey());
|
||||
|
||||
// The output should indicate a fatal error because we hide the existence of the object when it does not exist or is not accessible by the user
|
||||
$this->assertEquals(Dict::S('UI:PageTitle:FatalError'), $sOutput);
|
||||
|
||||
// The user should not have access to the ticket of the other organization, so an error should be logged
|
||||
$sLastErrorLogLines = $this->GetErrorLogLastLines(APPROOT.'log/error.log', 10);
|
||||
$this->assertStringContainsString(Dict::S('UI:ObjectDoesNotExist'), $sLastErrorLogLines);
|
||||
}
|
||||
|
||||
// Test that if a user has already acquired the lock on an object, another user cannot acquire it and gets the correct error message
|
||||
public function testAcquireLockFailsIfAlreadyLockedByAnotherUser(): void
|
||||
{
|
||||
// First, acquire the lock with a user (User A)
|
||||
$this->CreateSupportAgentUserAndAcquireLock();
|
||||
$sUserALogin = self::$sLogin;
|
||||
|
||||
// Create a second user (User B) who tries to acquire the lock
|
||||
$sOutput = $this->CreateSupportAgentUserAndAcquireLock();
|
||||
|
||||
// The second user should not be able to acquire the lock, and the output should contain the correct error message indicating that the object is already locked by User A
|
||||
$this->assertStringContainsString('"success":false', $sOutput);
|
||||
$this->assertStringContainsString('"message":"'.Dict::Format('UI:CurrentObjectIsSoftLockedBy_User', $sUserALogin).'"', $sOutput);
|
||||
}
|
||||
|
||||
// Helper method to create a user with Support Agent profile and acquire the lock on the ticket
|
||||
private function CreateSupportAgentUserAndAcquireLock(): string
|
||||
{
|
||||
// Create a user with Support Agent Profile
|
||||
$this->CreateUserWithProfile(self::$aURP_Profiles['Support Agent']);
|
||||
|
||||
return $this->AcquireLockAsUser(self::$sLogin, self::$iTicketId);
|
||||
}
|
||||
|
||||
// Helper method to create a user with a specific profile
|
||||
private function CreateUserWithProfile(int $iProfileId): UserLocal
|
||||
{
|
||||
self::$sLogin = uniqid('AjaxRenderTest');
|
||||
return $this->CreateContactlessUser(self::$sLogin, $iProfileId, self::AUTHENTICATION_PASSWORD);
|
||||
}
|
||||
|
||||
// Helper method to acquire the lock on a ticket as a specific user
|
||||
private function AcquireLockAsUser(string $sLogin, int $iTicketId): string
|
||||
{
|
||||
$aGetFields = [
|
||||
'operation' => 'acquire_lock',
|
||||
'auth_user' => $sLogin,
|
||||
'auth_pwd' => self::AUTHENTICATION_PASSWORD,
|
||||
'obj_class' => UserRequest::class,
|
||||
'obj_key' => $iTicketId,
|
||||
];
|
||||
|
||||
return $this->CallItopUri(
|
||||
"pages/ajax.render.php?".http_build_query($aGetFields),
|
||||
[],
|
||||
[
|
||||
CURLOPT_HTTPHEADER => ['X-Combodo-Ajax:1'],
|
||||
CURLOPT_POST => 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Returns the last lines of the error log containing only errors (Error level)
|
||||
private function GetErrorLogLastLines(string $sErrorLogPath, int $iLineNumbers = 1): string
|
||||
{
|
||||
if (!file_exists($sErrorLogPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$aLines = file($sErrorLogPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
|
||||
// Keep only lines containing '| Error |'
|
||||
$aErrorLines = array_filter($aLines, function ($line) {
|
||||
return preg_match('/\|\s*Error\s*\|/', $line);
|
||||
});
|
||||
|
||||
// Return the last requested lines
|
||||
return implode("\n", array_slice($aErrorLines, -$iLineNumbers));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user