From 3cdadf3c6e54dad1d4678fe855f7ac76f1e6a0ef Mon Sep 17 00:00:00 2001 From: Lenaick Date: Tue, 31 Mar 2026 17:01:22 +0200 Subject: [PATCH] :white_check_mark: Add tests for lock acquisition functionality (#865) --- .../src/BaseTestCase/ItopDataTestCase.php | 15 ++ .../src/BaseTestCase/ItopTestCase.php | 4 +- .../unitary-tests/pages/AjaxRenderTest.php | 180 ++++++++++++++++++ 3 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 tests/php-unit-tests/unitary-tests/pages/AjaxRenderTest.php diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index 966eaae10..fa1833bdb 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -1470,4 +1470,19 @@ abstract class ItopDataTestCase extends ItopTestCase @chmod($sConfigPath, 0440); @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); + } } diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php index 9507f4dfa..753b1a337 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php @@ -668,7 +668,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); @@ -676,7 +676,7 @@ abstract class ItopTestCase extends KernelTestCase curl_setopt_array($ch, $aCurlOptions); if ($this->IsArrayOfArray($aPostFields)) { curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($aPostFields)); - } else { + } elseif (!empty($aPostFields)) { curl_setopt($ch, CURLOPT_POSTFIELDS, $aPostFields); } diff --git a/tests/php-unit-tests/unitary-tests/pages/AjaxRenderTest.php b/tests/php-unit-tests/unitary-tests/pages/AjaxRenderTest.php new file mode 100644 index 000000000..3177261d3 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/pages/AjaxRenderTest.php @@ -0,0 +1,180 @@ +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)); + } +}