// namespace Combodo\iTop\Test\UnitTest\Core; use Attachment; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use DBObject; use InvalidExternalKeyValueException; use lnkPersonToTeam; use MetaModel; use Organization; use Person; use Team; use User; use UserRights; use utils; /** * @group specificOrgInSampleData */ class DBObjectTest extends ItopDataTestCase { const CREATE_TEST_ORG = true; const INVALID_OBJECT_KEY = 123456789; protected function setUp(): void { parent::setUp(); $this->RequireOnceItopFile('core/dbobject.class.php'); } /** * Test default page name * @covers DBObject::GetUIPage */ public function testGetUIPage() { static::assertEquals('UI.php', DBObject::GetUIPage()); } /** * Test PKey validation * @dataProvider keyProviderOK * @param $key * @param $res */ public function testIsValidPKeyOK($key, $res) { static::assertEquals(DBObject::IsValidPKey($key), $res); } public function keyProviderOK() { return array( array(1, true), array('255', true), array(-24576, true), array(0123, true), array(0xCAFE, true), array(PHP_INT_MIN, true), array(PHP_INT_MAX, true), array('test', false), array('', false), array('a255', false), array('PHP_INT_MIN', false)); } /** * @group itopRequestMgmt * @covers DBObject::GetOriginal */ public function testGetOriginal() { $oObject = $this->CreateUserRequest(190664); static::assertNull($oObject->GetOriginal('sla_tto_passed')); } /** * @return void * @throws \Exception */ public function testListPreviousValuesForUpdatedAttributes() { $oOrg = $this->CreateOrganization('testListPreviousValuesForUpdatedAttributes'); $this->assertCount(0, $oOrg->ListChanges()); $oOrg->Set('code', strtoupper('testListPreviousValuesForUpdatedAttributes')); $this->assertCount(1, $oOrg->ListChanges()); $oOrg->DBUpdate(); $this->assertCount(0, $oOrg->ListChanges()); $this->assertCount(1, $oOrg->ListPreviousValuesForUpdatedAttributes()); $oOrg->DBUpdate(); $this->assertCount(0, $oOrg->ListChanges()); $this->assertCount(1, $oOrg->ListPreviousValuesForUpdatedAttributes()); $oOrg->DBDelete(); $oOrg = MetaModel::NewObject('Organization'); $oOrg->Set('name', 'testListPreviousValuesForUpdatedAttributes'); $oOrg->DBInsert(); $oOrg->Set('code', strtoupper('testListPreviousValuesForUpdatedAttributes')); $oOrg->DBUpdate(); $oOrg->DBUpdate(); $this->markTestIncomplete('This test has not been implemented yet. wait for N°4967 fix'); $this->debug("ERROR: N°4967 - 'Previous Values For Updated Attributes' not updated if DBUpdate is called without modifying the object"); //$this->assertCount(0, $oOrg->ListPreviousValuesForUpdatedAttributes()); } /** * @covers DBObject::NewObject * @covers DBObject::Get * @covers DBObject::Set */ public function testAttributeRefresh_FriendlyName() { $oObject = \MetaModel::NewObject('Person', array('name' => 'Foo', 'first_name' => 'John', 'org_id' => 3, 'location_id' => 2)); static::assertEquals('John Foo', $oObject->Get('friendlyname')); $oObject->Set('name', 'Who'); static::assertEquals('John Who', $oObject->Get('friendlyname')); } /** * @covers MetaModel::GetObject * @covers DBObject::Get * @covers DBObject::Set */ public function testAttributeRefresh_FriendlyNameFromDB() { $oObject = \MetaModel::NewObject('Person', array('name' => 'Gary', 'first_name' => 'Romain', 'org_id' => 3, 'location_id' => 2)); $oObject->DBInsert(); $iObjKey = $oObject->GetKey(); $oObject = \MetaModel::GetObject('Person', $iObjKey); static::assertEquals('Romain Gary', $oObject->Get('friendlyname')); $oObject->Set('name', 'Duris'); static::assertEquals('Romain Duris', $oObject->Get('friendlyname')); } /** * @covers DBObject::NewObject * @covers DBObject::Get */ public function testPartialAttributeEvaluation() { $oObject = \MetaModel::NewObject('Person', array('name' => 'Foo', 'org_id' => 3, 'location_id' => 2)); static::assertEquals('', $oObject->Get('friendlyname')); } /** * @covers DBObject::NewObject * @covers DBObject::Get */ public function testEmptyAttributeEvaluation() { $oObject = \MetaModel::NewObject('Person', array('org_id' => 3, 'location_id' => 2)); static::assertEquals('', $oObject->Get('friendlyname')); } /** * @covers DBObject::Get * @covers DBObject::Set */ public function testFriendlyNameLnk() { $oUserProfile = new \URP_UserProfile(); $oUserProfile->Set('profileid', 2); static::assertEquals('', $oUserProfile->Get('friendlyname')); } /** * @covers DBObject::NewObject * @covers DBObject::Get * @covers DBObject::Set */ public function testAttributeRefresh_ObsolescenceFlag() { $oObject = \MetaModel::NewObject('Person', array('name' => 'Foo', 'first_name' => 'John', 'org_id' => 3, 'location_id' => 2)); static::assertEquals(false, (bool)$oObject->Get('obsolescence_flag')); $oObject->Set('status', 'inactive'); static::assertEquals(true, (bool)$oObject->Get('obsolescence_flag')); } /** * @covers DBObject::NewObject * @covers DBObject::Get * @covers DBObject::Set */ public function testAttributeRefresh_ExternalKeysAndFields() { static::assertDBQueryCount(0, function() use (&$oObject){ $oObject = \MetaModel::NewObject('Person', array('name' => 'Foo', 'first_name' => 'John', 'org_id' => 3, 'location_id' => 2)); }); static::assertDBQueryCount(2, function() use (&$oObject){ static::assertEquals('Demo', $oObject->Get('org_id_friendlyname')); static::assertEquals('Grenoble', $oObject->Get('location_id_friendlyname')); }); // External key given as an id static::assertDBQueryCount(1, function() use (&$oObject){ $oObject->Set('org_id', 2); static::assertEquals('IT Department', $oObject->Get('org_id_friendlyname')); }); // External key given as an object static::assertDBQueryCount(1, function() use (&$oBordeaux){ $oBordeaux = \MetaModel::GetObject('Location', 1); }); static::assertDBQueryCount(0, function() use (&$oBordeaux, &$oObject){ $oObject->Set('location_id', $oBordeaux); static::assertEquals('IT Department', $oObject->Get('org_id_friendlyname')); static::assertEquals('IT Department', $oObject->Get('org_name')); static::assertEquals('Bordeaux', $oObject->Get('location_id_friendlyname')); }); } public function testSetExtKeyUnsetDependentAttribute() { $oObject = \MetaModel::NewObject('Person', array('name' => 'Foo', 'first_name' => 'John', 'org_id' => 3, 'location_id' => 2)); $oOrg = \MetaModel::GetObject('Organization', 2); $oObject->Set('org_id', $oOrg); // though it's a dependent field, it keeps its value (not OQL based nor External field) static::assertEquals(2, $oObject->Get('location_id')); // Dependent external field is updated because the Set('org_id') is done with an object static::assertDBQueryCount(0, function() use (&$oObject){ static::assertNotEmpty($oObject->Get('org_name')); }); // Dependent external field is reset and reloaded from DB $oObject->Set('org_id', 3); static::assertDBQueryCount(1, function() use (&$oObject){ static::assertNotEmpty($oObject->Get('org_name')); }); } /** * @group Integration */ public function testModelExpressions() { foreach (\MetaModel::GetClasses() as $sClass) { if (\MetaModel::IsAbstract($sClass)) continue; $oObject = \MetaModel::NewObject($sClass); foreach (\MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) { if ($oAttDef->IsBasedOnOQLExpression()) { $this->debug("$sClass::$sAttCode"); static::assertDBQueryCount(0, function() use (&$oObject, &$oAttDef){ $oObject->EvaluateExpression($oAttDef->GetOQLExpression()); }); } } } } private function GetAlwaysTrueCallback(): callable { return static function () { return true; }; } private function GetAlwaysFalseCallback(): callable { return static function () { return false; }; } /** * @covers DBObject::CheckChangedExtKeysValues() */ public function testCheckExtKeysSiloOnAttributeExternalKey() { //--- Preparing data... $this->bIsUsingSilo = true; $oAlwaysTrueCallback = $this->GetAlwaysTrueCallback(); $oAlwaysFalseCallback = $this->GetAlwaysFalseCallback(); /** @var Organization $oDemoOrg */ $oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo'); /** @var Organization $oMyCompanyOrg */ $oMyCompanyOrg = MetaModel::GetObjectByName(Organization::class, 'My Company/Department'); /** @var Person $oPersonOfDemoOrg */ $oPersonOfDemoOrg = MetaModel::GetObjectByName(Person::class, 'Agatha Christie'); /** @var Person $oPersonOfMyCompanyOrg */ $oPersonOfMyCompanyOrg = MetaModel::GetObjectByName(Person::class, 'My first name My last name'); $sConfigurationManagerProfileId = 3; // Access to Person objects $oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigurationManagerProfileId); $oAdminUser = MetaModel::GetObjectByName(User::class, 'admin', false); if (is_null($oAdminUser)) { $oAdminUser = $this->CreateUser('admin', 1); } /** @var Person $oPersonObject */ $oPersonObject = $this->CreatePerson(0, $oMyCompanyOrg->GetKey()); //--- Now we can do some tests ! UserRights::Login($oUserWithAllowedOrgs->Get('login')); $this->ResetMetaModelQueyCacheGetObject(); try { $oPersonObject->CheckChangedExtKeysValues(); } catch (InvalidExternalKeyValueException $eCannotSave) { $this->fail('Should skip external keys already written in Database'); } $oPersonObject->Set('manager_id', $oPersonOfDemoOrg->GetKey()); try { $oPersonObject->CheckChangedExtKeysValues(); } catch (InvalidExternalKeyValueException $eCannotSave) { $this->fail('Should allow objects in the same org as the current user'); } try { $oPersonObject->CheckChangedExtKeysValues($oAlwaysFalseCallback); $this->fail('Should consider the callback returning "false"'); } catch (InvalidExternalKeyValueException $eCannotSave) { // Ok, the exception was expected } $oPersonObject->Set('manager_id', $oPersonOfMyCompanyOrg->GetKey()); try { $oPersonObject->CheckChangedExtKeysValues(); $this->fail('Should not allow objects not being in the allowed orgs of the current user'); } catch (InvalidExternalKeyValueException $eCannotSave) { $this->assertEquals('manager_id', $eCannotSave->GetAttCode(), 'Should report the wrong external key attcode'); $this->assertEquals($oMyCompanyOrg->GetKey(), $eCannotSave->GetAttValue(), 'Should report the unauthorized external key value'); } try { $oPersonObject->CheckChangedExtKeysValues($oAlwaysTrueCallback); } catch (InvalidExternalKeyValueException $eCannotSave) { $this->fail('Should consider the callback returning "true"'); } UserRights::Logoff(); $this->ResetMetaModelQueyCacheGetObject(); UserRights::Login($oAdminUser->Get('login')); $oPersonObject->CheckChangedExtKeysValues(); $this->assertTrue(true, 'Admin user can create objects in any org'); } /** * @covers DBObject::CheckChangedExtKeysValues() */ public function testCheckExtKeysOnAttributeLinkedSetIndirect() { //--- Preparing data... $this->bIsUsingSilo = true; /** @var Organization $oDemoOrg */ $oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo'); /** @var Person $oPersonOnItDepartmentOrg */ $oPersonOnItDepartmentOrg = MetaModel::GetObjectByName(Person::class, 'Anna Gavalda'); /** @var Person $oPersonOnDemoOrg */ $oPersonOnDemoOrg = MetaModel::GetObjectByName(Person::class, 'Claude Monet'); $sConfigManagerProfileId = 3; // access to Team and Contact objects $oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigManagerProfileId); //--- Now we can do some tests ! UserRights::Login($oUserWithAllowedOrgs->Get('login')); $this->ResetMetaModelQueyCacheGetObject(); $oTeam = MetaModel::NewObject(Team::class, [ 'name' => 'The A Team', 'org_id' => $oDemoOrg->GetKey() ]); // Part 1 - Test with an invalid id (non-existing object) // $oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class); $oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [ 'person_id' => self::INVALID_OBJECT_KEY, 'team_id' => $oTeam->GetKey(), ])); $oTeam->Set('persons_list', $oPersonLinks); try { $oTeam->CheckChangedExtKeysValues(); $this->fail('An unknown object should be detected as invalid'); } catch (InvalidExternalKeyValueException $e) { // we are getting the exception on the lnk class // In consequence attcode is `lnkPersonToTeam.person_id` instead of `Team.persons_list` $this->assertEquals('person_id', $e->GetAttCode(), 'The reported attcode should be the external key on the link'); $this->assertEquals(self::INVALID_OBJECT_KEY, $e->GetAttValue(), 'The reported value should be the external key on the link'); } try { $oTeam->CheckChangedExtKeysValues($this->GetAlwaysTrueCallback()); } catch (InvalidExternalKeyValueException $e) { $this->fail('Should have no error when callback returns true'); } // Part 2 - Test with an allowed object // $oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class); $oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [ 'person_id' => $oPersonOnDemoOrg->GetKey(), 'team_id' => $oTeam->GetKey(), ])); $oTeam->Set('persons_list', $oPersonLinks); try { $oTeam->CheckChangedExtKeysValues(); } catch (InvalidExternalKeyValueException $e) { $this->fail('An authorized object should be detected as valid'); } try { $oTeam->CheckChangedExtKeysValues($this->GetAlwaysFalseCallback()); $this->fail('Should cascade the callback result when it is "false"'); } catch (InvalidExternalKeyValueException $e) { // Ok, the exception was expected } // Part 3 - Test with a not allowed object // $oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class); $oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [ 'person_id' => $oPersonOnItDepartmentOrg->GetKey(), 'team_id' => $oTeam->GetKey(), ])); $oTeam->Set('persons_list', $oPersonLinks); try { $oTeam->CheckChangedExtKeysValues(); $this->fail('An unauthorized object should be detected as invalid'); } catch (InvalidExternalKeyValueException $e) { // Ok, the exception was expected } try { $oTeam->CheckChangedExtKeysValues($this->GetAlwaysTrueCallback()); } catch (InvalidExternalKeyValueException $e) { $this->fail('Should cascade the callback result when it is "true"'); } $oTeam->DBInsert(); // persisting invalid value and resets the object changed values try { $oTeam->CheckChangedExtKeysValues(); } catch (InvalidExternalKeyValueException $e) { $this->fail('An unauthorized value should be ignored when it is not being modified'); } } /** * @covers DBObject::CheckChangedExtKeysValues() */ public function testCheckExtKeysSiloOnAttributeObjectKey() { //--- Preparing data... $this->bIsUsingSilo = true; /** @var Organization $oDemoOrg */ $oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo'); /** @var Person $oPersonOnItDepartmentOrg */ $oPersonOnItDepartmentOrg = MetaModel::GetObjectByName(Person::class, 'Anna Gavalda'); /** @var Person $oPersonOnDemoOrg */ $oPersonOnDemoOrg = MetaModel::GetObjectByName(Person::class, 'Claude Monet'); $sConfigManagerProfileId = 3; // access to Team and Contact objects $oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigManagerProfileId); //--- Now we can do some tests ! UserRights::Login($oUserWithAllowedOrgs->Get('login')); $this->ResetMetaModelQueyCacheGetObject(); $oAttachment = MetaModel::NewObject(Attachment::class, [ 'item_class' => Person::class, 'item_id' => $oPersonOnDemoOrg->GetKey(), ]); try { $oAttachment->CheckChangedExtKeysValues(); } catch (InvalidExternalKeyValueException $e) { $this->fail('Should be allowed to create an attachment pointing to a ticket in the allowed org list'); } $oAttachment = MetaModel::NewObject(Attachment::class, [ 'item_class' => Person::class, 'item_id' => $oPersonOnItDepartmentOrg->GetKey(), ]); $this->ResetMetaModelQueyCacheGetObject(); try { $oAttachment->CheckChangedExtKeysValues(); $this->fail('There should be an error on attachment pointing to a non allowed org object'); } catch (InvalidExternalKeyValueException $e) { $this->assertEquals('item_id', $e->GetAttCode(), 'Should report the object key attribute'); $this->assertEquals($oPersonOnItDepartmentOrg->GetKey(), $e->GetAttValue(), 'Should report the object key value'); } } private function CreateDemoOrgUser(Organization $oDemoOrg, string $sProfileId): User { utils::GetConfig()->SetModuleSetting('authent-local', 'password_validation.pattern', ''); $oUserWithAllowedOrgs = $this->CreateContactlessUser('demo_test_' . uniqid(__CLASS__, true), $sProfileId); /** @var \URP_UserOrg $oUserOrg */ $oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $oDemoOrg->GetKey(),]); $oAllowedOrgList = $oUserWithAllowedOrgs->Get('allowed_org_list'); $oAllowedOrgList->AddItem($oUserOrg); $oUserWithAllowedOrgs->Set('allowed_org_list', $oAllowedOrgList); $oUserWithAllowedOrgs->DBWrite(); return $oUserWithAllowedOrgs; } /** * @covers DBObject::SetComputedDate * @return void */ public function testSetComputedDateOnAttributeDate() { $oObject = MetaModel::NewObject(\CustomerContract::class, ['name' => 'Test contract', 'org_id' => '3', 'provider_id' => '2']); $oObject->Set('start_date', time()); $oObject->SetComputedDate('end_date', "+2 weeks", 'start_date'); $this->assertTrue(true, 'No fatal error on computing date'); } /** * @covers DBObject::SetComputedDate * @return void */ public function testSetComputedDateOnAttributeDateTime() { $oObject = MetaModel::NewObject(\WorkOrder::class, ['name' => 'Test workorder', 'description' => 'Toto']); $oObject->Set('start_date', '2024-01-01 09:45:00'); $oObject->SetComputedDate('end_date', "+2 weeks", 'start_date'); $this->assertTrue(true, 'No fatal error on computing date'); $this->assertEquals("2024-01-15 09:45:00", $oObject->Get('end_date'), 'SetComputedDate +2 weeks on a WorkOrder DateTimeAttribute'); } }