diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 0213896a1..2b3169672 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -3439,8 +3439,18 @@ EOF } $sInputType = ''; $sInputId = 'att_'.$iFieldIndex; + $value = $this->Get($sAttCode); + $sDisplayValue = $this->GetEditValue($sAttCode); + if ($oAttDef instanceof AttributeDateTime && !$oAttDef->IsNullAllowed() && $value === $oAttDef->GetNullValue()) { + $value = $oAttDef->GetDefaultValue($this); + if ($value !== $oAttDef->GetNullValue()) { + // Set default date + $this->Set($sAttCode, $value); + $sDisplayValue = $this->GetEditValue($sAttCode); + } + } $sHTMLValue = cmdbAbstractObject::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, - $this->Get($sAttCode), $this->GetEditValue($sAttCode), $sInputId, '', $iExpectCode, + $value, $sDisplayValue, $sInputId, '', $iExpectCode, $aArgs, true, $sInputType); $aAttrib = array( 'label' => ''.$oAttDef->GetLabel().'', diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 048363d5c..c5d86d2a5 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -6372,7 +6372,11 @@ class AttributeDateTime extends AttributeDBField $oFormField = parent::MakeFormField($oObject, $oFormField); // After call to the parent as it sets the current value - $oFormField->SetCurrentValue($this->GetFormat()->Format($oObject->Get($this->GetCode()))); + $oValue = $oObject->Get($this->GetCode()); + if ($oValue === $this->GetNullValue()) { + $oValue = $this->GetDefaultValue($oObject); + } + $oFormField->SetCurrentValue($this->GetFormat()->Format($oValue)); return $oFormField; } @@ -6458,8 +6462,20 @@ class AttributeDateTime extends AttributeDBField public function GetDefaultValue(DBObject $oHostObject = null) { - if (!$this->IsNullAllowed()) { - return date($this->GetInternalFormat()); + $sDefaultValue = $this->Get('default_value'); + if (!$this->IsNullAllowed() && utils::IsNotNullOrEmptyString($sDefaultValue)) { + try { + $oDate = new DateTimeImmutable($sDefaultValue); + } + catch (Exception $e) { + IssueLog::Error($e->getMessage(), null, [ + 'class' => get_class($this), + 'name' => $this->GetCode(), + 'stack' => $e->getTraceAsString()]); + return $this->GetNullValue(); + } + + return $oDate->format($this->GetInternalFormat()); } return $this->GetNullValue(); } diff --git a/core/dbobject.class.php b/core/dbobject.class.php index bf51353eb..48b3364ed 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -2867,6 +2867,14 @@ abstract class DBObject implements iDisplay protected function ListChangedValues(array $aProposal) { $aDelta = array(); + $sClass = get_class($this); + if (MetaModel::HasLifecycle($sClass) && utils::IsNotNullOrEmptyString($this->sStimulusBeingApplied)) { + $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); + if (!in_array($sStateAttCode, $aProposal)) { + // Same state but the transition was asked, act as if the state was changed + $aDelta[$sStateAttCode] = $this->m_aCurrValues[$sStateAttCode]; + } + } foreach ($aProposal as $sAtt => $proposedValue) { if (!array_key_exists($sAtt, $this->m_aOrigValues)) diff --git a/core/event.class.inc.php b/core/event.class.inc.php index 41c626a7e..7cf5c4d44 100644 --- a/core/event.class.inc.php +++ b/core/event.class.inc.php @@ -39,7 +39,7 @@ class Event extends DBObject implements iDisplay MetaModel::Init_Params($aParams); //MetaModel::Init_InheritAttributes(); MetaModel::Init_AddAttribute(new AttributeText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>"now", "is_null_allowed"=>false, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); // MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); diff --git a/core/oql/expression.class.inc.php b/core/oql/expression.class.inc.php index 88cc64b2e..ea797a2b5 100644 --- a/core/oql/expression.class.inc.php +++ b/core/oql/expression.class.inc.php @@ -575,6 +575,9 @@ class BinaryExpression extends Expression case 'LIKE': $sType = 'like'; break; + case 'IN': + $sType = 'in'; + break; default: throw new Exception("Operator '$sOperator' not yet supported"); } @@ -641,6 +644,9 @@ class BinaryExpression extends Expression $sEscaped = str_replace(array('%', '_', '\\\\.*', '\\\\.'), array('.*', '.', '%', '_'), $sEscaped); $result = (int) preg_match("/$sEscaped/i", $mLeft); break; + case 'in': + $result = in_array($mLeft, $mRight); + break; } return $result; } @@ -2250,7 +2256,12 @@ class ListExpression extends Expression */ public function Evaluate(array $aArgs) { - throw new Exception('list expression not yet supported'); + //throw new Exception('list expression not yet supported'); + $aResult = []; + foreach ($this->m_aExpressions as $oExpressions) { + $aResult[] = $oExpressions->Evaluate($aArgs); + } + return $aResult; } /** diff --git a/datamodels/2.x/itop-portal-base/portal/templates/bricks/manage/layout-table.html.twig b/datamodels/2.x/itop-portal-base/portal/templates/bricks/manage/layout-table.html.twig index 9d018912f..2f01faf12 100644 --- a/datamodels/2.x/itop-portal-base/portal/templates/bricks/manage/layout-table.html.twig +++ b/datamodels/2.x/itop-portal-base/portal/templates/bricks/manage/layout-table.html.twig @@ -23,25 +23,23 @@ {% set iTableCount = 0 %} {% if aGroupingAreasData|length > 0 %} {% for aAreaData in aGroupingAreasData %} - {% if aAreaData.iItemsCount > 0 %} - {% set iTableCount = iTableCount + 1 %} -
-
-

{{ aAreaData.sTitle }}

- {% if bCanExport %} - - - - {% endif %} -
-
-
-
+ {% set iTableCount = iTableCount + 1 %} +
+
+

{{ aAreaData.sTitle }}

+ {% if bCanExport %} + + + + {% endif %}
- {% endif %} +
+
+
+
{% endfor %} {% endif %} diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index c48f5d695..8da300791 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -440,6 +440,14 @@ abstract class ItopDataTestCase extends ItopTestCase $this->RemoveObjects($sTagClass, "SELECT $sTagClass WHERE code = '$sTagCode'"); } + public function RemoveAllObjects($sClassName) + { + $oSet = new \DBObjectSet(new \DBObjectSearch($sClassName)); + while ($oObject = $oSet->Fetch()) { + $oObject->DBDelete(); + } + } + private function RemoveObjects($sClass, $sOQL) { $oFilter = DBSearch::FromOQL($sOQL); @@ -930,11 +938,12 @@ abstract class ItopDataTestCase extends ItopTestCase * * @param $iExpectedCount Number of MySQL queries that should be executed * @param callable $oFunction Operations to perform + * @param string $sMessage Message to display in case of failure * * @throws \MySQLException * @throws \MySQLQueryHasNoResultException */ - protected function assertDBQueryCount($iExpectedCount, callable $oFunction) + protected function assertDBQueryCount($iExpectedCount, callable $oFunction, $sMessage = '') { $iInitialCount = (int) CMDBSource::QueryToScalar("SHOW SESSION STATUS LIKE 'Queries'", 1); $oFunction(); @@ -942,7 +951,13 @@ abstract class ItopDataTestCase extends ItopTestCase $iCount = $iFinalCount - 1 - $iInitialCount; if ($iCount != $iExpectedCount) { - $this->fail("Expected $iExpectedCount queries. $iCount have been executed."); + if ($sMessage === '') { + $sMessage = "Expected $iExpectedCount queries. $iCount have been executed."; + } + else { + $sMessage .= " - Expected $iExpectedCount queries. $iCount have been executed."; + } + $this->fail($sMessage); } else { @@ -965,6 +980,18 @@ abstract class ItopDataTestCase extends ItopTestCase $this->assertEquals($iExpectedCount, $iCount, "Found $iCount changes for object $sClass::$iId"); } + /** + * @since 3.2.1 + */ + protected static function assertIsDBObject(string $sExpectedClass, ?int $iExpectedKey, $oObject, ?string $sMessage = '') + { + self::assertNotNull($oObject, $sMessage); + self::assertInstanceOf($sExpectedClass, $oObject, $sMessage); + if ($iExpectedKey !== null) { + self::assertEquals($iExpectedKey, $oObject->GetKey(), $sMessage); + } + } + /** * Import a set of XML files describing a consistent set of iTop objects * @param string[] $aFiles @@ -1464,4 +1491,28 @@ abstract class ItopDataTestCase extends ItopTestCase $oSet = new DBObjectSet($oSearch); $this->assertEquals(1, $oSet->Count(), $sMessage); } + + static protected function StartStopwatchInThePast(DBObject $oObject, string $sStopwatchAttCode, int $iDelayInSecond) + { + $iStartDate = time() - $iDelayInSecond; + /** @var \ormStopWatch $oStopwatch */ + $oStopwatch = $oObject->Get($sStopwatchAttCode); + $oAttDef = MetaModel::GetAttributeDef(get_class($oObject), $sStopwatchAttCode); + $oStopwatch->Start($oObject, $oAttDef, $iStartDate); + $oStopwatch->ComputeDeadlines($oObject, $oAttDef); + $oObject->Set($sStopwatchAttCode, $oStopwatch); + } + + + static protected function StopStopwatchInTheFuture(DBObject $oObject, string $sStopwatchAttCode, int $iDelayInSecond) + { + $iEndDate = time() + $iDelayInSecond; + /** @var \ormStopWatch $oStopwatch */ + $oStopwatch = $oObject->Get($sStopwatchAttCode); + $oAttDef = MetaModel::GetAttributeDef(get_class($oObject), $sStopwatchAttCode); + $oStopwatch->Stop($oObject, $oAttDef, $iEndDate); + $oStopwatch->ComputeDeadlines($oObject, $oAttDef); + $oObject->Set($sStopwatchAttCode, $oStopwatch); + } + } diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php index 463d1c44b..52cbe3436 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php @@ -9,9 +9,9 @@ namespace Combodo\iTop\Test\UnitTest; use CMDBSource; use DeprecatedCallsLog; use MySQLTransactionNotClosedException; +use PHPUnit\Framework\TestCase; use ReflectionMethod; use SetupUtils; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpKernel\KernelInterface; use const DEBUG_BACKTRACE_IGNORE_ARGS; @@ -23,7 +23,7 @@ use const DEBUG_BACKTRACE_IGNORE_ARGS; * * @since 3.0.4 3.1.1 3.2.0 N°6658 move some setUp/tearDown code to the corresponding methods *BeforeClass to speed up tests process time. */ -abstract class ItopTestCase extends KernelTestCase +abstract class ItopTestCase extends TestCase { public const TEST_LOG_DIR = 'test'; diff --git a/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php b/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php index 4c9961ece..053fef232 100644 --- a/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php +++ b/tests/php-unit-tests/unitary-tests/application/MenuNodeTest.php @@ -20,16 +20,6 @@ class MenuNodeTest extends ItopDataTestCase { $this->oUR = $this->CreateUserRequest(666, $aUserRequestCustomParams); } - private function StartStopwatchInThePast(\UserRequest $oTicket, string $sAttCode, int $iSecDelay) - { - $iStartDate = time() - $iSecDelay; - /** @var \ormStopWatch $oStopwatch */ - $oStopwatch = $oTicket->Get($sAttCode); - $oAttDef = \MetaModel::GetAttributeDef(get_class($oTicket), $sAttCode); - $oStopwatch->Start($oTicket, $oAttDef, $iStartDate); - $oStopwatch->ComputeDeadlines($oTicket, $oAttDef); - $oTicket->Set($sAttCode, $oStopwatch); - } public function RenderOQLSearchProvider() { @@ -70,7 +60,7 @@ OQL; */ public function testRenderOQLSearchOqlWithDateFormatOnDeadline() { - $this->StartStopwatchInThePast($this->oUR, 'ttr', 10); + static::StartStopwatchInThePast($this->oUR, 'ttr', 10); $sOql = <<assertEquals($bComputationExpected, $oAttDef->HasPHPComputation(), "Standard DataModel should be configured with property 'has_php_computation'=$sComputationExpected for $sClass:$sAttCode"); } - public function WithConstraintParameterProvider() + public static function WithConstraintParameterProvider() { return [ ['User', 'profile_list', true, true], @@ -238,4 +240,109 @@ PHP ['Ticket', 'functionalcis_list', false, true], ]; } + + public function testDateTimeEmptyDefaultReturnsNullAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDateTime('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => '', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + self::assertNull($defaultValue, 'Empty default value for DateTime attribute should give null default value'); + self::assertEmpty($oField->GetCurrentValue(), 'Empty default value for DateTime attribute should give empty form field'); + } + + public function testDateEmptyDefaultReturnsNullAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDate('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => '', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + self::assertNull($defaultValue, 'Empty default value for Date attribute should give null default value'); + self::assertEmpty($oField->GetCurrentValue(), 'Empty default value for DateTime attribute should give empty form field'); + } + + + public function testDateTimeNowAsDefaultGivesCurrentDateAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDateTime('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => 'now', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + $sNow = date($oDateAttribute->GetInternalFormat()); + self::assertEquals($sNow, $defaultValue, 'Now as default value for DateTime attribute should give current date as default value'); + self::assertEquals($sNow, $oField->GetCurrentValue(), 'Now as default value for DateTime attribute should give current date as form field'); + } + + + public function testDateNowAsDefaultGivesCurrentDateAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDate('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => 'now', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + $sNow = date($oDateAttribute->GetInternalFormat()); + self::assertEquals($sNow, $defaultValue, 'Now as default value for Date attribute should give current date as default value'); + self::assertEquals($sNow, $oField->GetCurrentValue(), 'Now as default value for Date attribute should give current date as form field'); + } + + public function testDateTimeIntervalAsDefaultGivesCorrectDateAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDateTime('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => '+1day', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + $oDate = new \DateTimeImmutable('+1day'); + $sExpected = $oDate->format($oDateAttribute->GetInternalFormat()); + self::assertEquals($sExpected, $defaultValue, 'Interval as default value for DateTime attribute should give correct date as default value'); + self::assertEquals($sExpected, $oField->GetCurrentValue(), 'Interval as default value for DateTime attribute should give correct date as form field'); + } + + public function testDateIntervalAsDefaultGivesCorrectDateAsDefaultValue() + { + // Given + $oDateAttribute = new AttributeDate('start_date', ['sql' => 'start_date', 'is_null_allowed' => false, 'default_value' => '+1day', 'allowed_values' => null, 'depends_on' => [], 'always_load_in_tables' => false]); + $oDateAttribute->SetHostClass('WorkOrder'); + $oWorkOrder = MetaModel::NewObject('WorkOrder'); + + //When + $defaultValue = $oDateAttribute->GetDefaultValue(); + $oField = $oDateAttribute->MakeFormField($oWorkOrder); + + // Then + $oDate = new \DateTimeImmutable('+1day'); + $sExpected = $oDate->format($oDateAttribute->GetInternalFormat()); + self::assertEquals($sExpected, $defaultValue, 'Interval as default value for Date attribute should give correct date as default value'); + self::assertEquals($sExpected, $oField->GetCurrentValue(), 'Interval as default value for Date attribute should give correct date as form field'); + } + } \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php b/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php index 62faa35a4..400a37180 100644 --- a/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php +++ b/tests/php-unit-tests/unitary-tests/core/EventIssueTest.php @@ -62,7 +62,7 @@ class EventIssueTest extends ItopDataTestCase $oEventIssue->DBInsert(); } catch (CoreException $e) { - $this->fail('we should be able to persist the object though it contains long values in its attributes'); + $this->fail('we should be able to persist the object though it contains long values in its attributes: '.$e->getMessage()); } $this->assertTrue(true); } diff --git a/tests/php-unit-tests/unitary-tests/core/ExpressionEvaluateTest.php b/tests/php-unit-tests/unitary-tests/core/ExpressionEvaluateTest.php index 1d6cb5307..6b9847fe0 100644 --- a/tests/php-unit-tests/unitary-tests/core/ExpressionEvaluateTest.php +++ b/tests/php-unit-tests/unitary-tests/core/ExpressionEvaluateTest.php @@ -90,7 +90,7 @@ class ExpressionEvaluateTest extends ItopDataTestCase { $aExpressions = array( // Test case to isolate for troubleshooting purposes - array('1+1', 2), + array("'a' IN ('a', 'b')", true), ); } else @@ -141,6 +141,9 @@ class ExpressionEvaluateTest extends ItopDataTestCase array('"2020-06-12 17:35:13" < "2020-06-12"', 0), array('"2020-06-12 00:00:00" = "2020-06-12"', 0), + // IN operator + array("'a' IN ('a', 'b')", true), + // Logical operators array('0 AND 0', 0), array('1 AND 0', 0), diff --git a/tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php b/tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php new file mode 100644 index 000000000..f7e878354 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/core/TriggerOnStateEnterTest.php @@ -0,0 +1,89 @@ +RemoveAllObjects(\Trigger::class); + $this->RemoveAllObjects(\EventNotificationEmail::class); + } + + public function testIsTriggeredOnTransition() + { + $iTrigger = $this->GivenTriggerWithAction('TriggerOnStateEnter', 'assigned'); + $oUserRequest = $this->GivenUserRequest('new'); + + $oUserRequest->ApplyStimulus('ev_assign'); + $this->AssertTriggerExecuted($iTrigger, 1, 'The trigger should have been executed'); + + $oUserRequest->ApplyStimulus('ev_assign'); + $this->AssertTriggerExecuted($iTrigger, 1, 'The trigger should not be executed when stimulus not expected in current state'); + } + + public function testIsTriggeredOnTransitionStayingInSameState() + { + $iTrigger = $this->GivenTriggerWithAction('TriggerOnStateEnter', 'assigned'); + $oUserRequest = $this->GivenUserRequest('new'); + $oUserRequest->ApplyStimulus('ev_assign'); + + $bTransitioned = $oUserRequest->ApplyStimulus('ev_reassign'); + $this->assertTrue($bTransitioned, 'The stimulus should have been accepted'); + + $this->AssertTriggerExecuted($iTrigger, 2, 'The trigger should have been executed twice'); + } + public function testIsTriggeredOnNewObject() + { + $iTrigger = $this->GivenTriggerWithAction('TriggerOnStateEnter', 'new'); + $oUserRequest = $this->GivenUserRequest('new'); + $this->AssertTriggerExecuted($iTrigger, 0, 'The trigger TriggerOnStateEnter should not be executed on created object'); + } + + private function GivenTriggerWithAction(string $sTriggerClass, string $sState) + { + $iTrigger = $this->GivenObjectInDB($sTriggerClass, [ + 'description' => 'Description', + 'target_class' => 'UserRequest', + 'state' => $sState, + ]); + $this->GivenObjectInDB('ActionEmail', [ + 'from' => 'test@combodo.com', + 'subject' => 'Subject', + 'body' => 'Body', + 'description' => 'Description', + 'test_recipient' => 'test@combodo.com', + 'name' => 'UserRequest', + 'asynchronous' => 'yes', + 'trigger_list' => [ + "trigger_id:$iTrigger", + ], + ]); + return $iTrigger; + } + + private function AssertTriggerExecuted(int $iTrigger, $iCount, $sMessage = '') + { + $oSearch = new \DBObjectSearch('EventNotificationEmail'); + $oSearch->AddCondition('trigger_id', $iTrigger); + $oSet = new \DBObjectSet($oSearch); + $this->assertEquals($iCount, $oSet->Count(), $sMessage); + } + + public function GivenUserRequest(string $sStatus): ?\DBObject + { + $iUserRequest = $this->GivenObjectInDB('UserRequest', [ + 'title' => 'Title', + 'description' => 'Description', + 'status' => $sStatus, + ]); + return MetaModel::GetObject('UserRequest', $iUserRequest); + } +} \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php index 6a3d3d7a2..5a743a0e8 100644 --- a/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php +++ b/tests/php-unit-tests/unitary-tests/core/UserRightsTest.php @@ -491,52 +491,65 @@ class UserRightsTest extends ItopDataTestCase public function testFindUser_ExistingInternalUser() { - $sLogin = 'UserRightsFindUser'.uniqid(); - $iKey = $this->CreateUser($sLogin, self::$aURP_Profiles['Administrator'])->GetKey(); - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); + $sLogin = 'AnInternalUser'.uniqid(); + $iKey = $this->GivenObjectInDB(\UserLocal::class, ['login' => $sLogin]); - $this->assertNotNull($oUser); - $this->assertEquals($iKey, $oUser->GetKey()); - $this->assertEquals(\UserLocal::class, get_class($oUser)); + $this->assertDBQueryCount( + 1, + fn() => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), + 'A query should be performed the first time FindUser is called' + ); - $this->assertDBQueryCount(0, function() use ($sLogin, $iKey){ - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - static::assertEquals($iKey, $oUser->GetKey()); - static::assertEquals(\UserLocal::class, get_class($oUser)); - }); + $this->assertDBQueryCount( + 0, + fn() => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), + 'The cache should prevent additional queries on subsequent calls' + ); } public function testFindUser_ExistingExternalUser() { - $sLogin = 'UserRightsFindUser'.uniqid(); + $sLogin = 'AnExternalUser'.uniqid(); + $iKey = $this->GivenObjectInDB(\UserExternal::class, ['login' => $sLogin]); - $iKey = $this->GivenObjectInDB(\UserExternal::class, [ - 'login' => $sLogin, - 'language' => 'EN US', - ]); + $this->assertDBQueryCount( + 2, + fn() => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), + 'Some queries should be performed the first time FindUser is called' + ); - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - - $this->assertNotNull($oUser); - $this->assertEquals($iKey, $oUser->GetKey()); - $this->assertEquals(\UserExternal::class, get_class($oUser)); - - $this->assertDBQueryCount(0, function() use ($sLogin, $iKey){ - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - static::assertEquals($iKey, $oUser->GetKey()); - static::assertEquals(\UserExternal::class, get_class($oUser)); - }); + $this->assertDBQueryCount( + 0, + fn() => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), + 'The cache should prevent additional queries on subsequent calls' + ); } - public function testFindUser_UnknownLogin_AvoidSameSqlQueryTwice() + public function testFindUser_UnknownLogin() { - $sLogin = 'UserRightsFindUser'.uniqid(); - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - $this->assertNull($oUser); + $sLogin = 'NobodyLogin'; - $this->assertDBQueryCount(0, function() use ($sLogin){ - $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); - $this->assertNull($oUser); - }); + $this->assertDBQueryCount( + 2, + fn() => $this->FindUserAndAssertItWasNotFound($sLogin), + 'Some queries should be performed the first time FindUser is called' + ); + + $this->assertDBQueryCount( + 0, + fn() => $this->FindUserAndAssertItWasNotFound($sLogin), + 'The cache should prevent additional queries on subsequent calls' + ); + } + + public function FindUserAndAssertItHasBeenFound($sLogin, $iExpectedKey) + { + $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); + static::assertIsDBObject(\User::class, $iExpectedKey, $oUser, 'FindUser should return the User object corresponding to the login'); + } + public function FindUserAndAssertItWasNotFound($sLogin) + { + $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); + static::assertNull($oUser, 'FindUser should return null when the login is unknown'); } }