// namespace Combodo\iTop\Test\UnitTest\Module\iTopTickets; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use Exception; use MetaModel; use Organization; use UserRights; /** * @group itopVirtualizationMgmt * @group itopConfigMgmt * @group itopTickets */ class UpdateImpactedItemsTest extends ItopDataTestCase { const CREATE_TEST_ORG = true; /** * @var Object Names to Ids */ private array $aCIs = []; protected function setUp(): void { parent::setUp(); $this->aCIs = []; } protected function tearDown(): void { parent::tearDown(); UserRights::Logoff(); $this->ResetMetaModelQueyCacheGetObject(); } public function testImpactShouldBePropagatedToDirectDescendants() { /** * Server1 +----> Hypervisor1 * +====> Person1 */ $this->GivenCITreeInDB(<< Server_1 Server_1 <-> Person_1 EOF); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Server_1' => 'manual' ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Server_1' => 'manual', 'Hypervisor_1' => 'computed', 'Test Person_1' => 'computed' ]); } public function testImpactShouldBePropagatedInOneWayOnly() { /** * Server1 +----> Hypervisor1 * Server2 +----> Hypervisor1 */ $this->GivenCITreeInDB(<< Server_1 Hypervisor_1 -> Server_2 EOF); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Server_1' => 'manual' ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Server_1' => 'manual', 'Hypervisor_1' => 'computed' ]); } public function testImpactShouldBePropagatedRecursively() { /** * Server1 +----> Hypervisor1 +----> Farm +====> Person1 */ $this->GivenCITreeInDB(<< Server_1, Farm_1 Farm_1 <-> Person_1 EOF); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Server_1' => 'manual' ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Server_1' => 'manual', 'Hypervisor_1' => 'computed', 'Farm_1' => 'computed', 'Test Person_1' => 'computed' ]); } public function testImpactShouldNotBeFurtherPropagatedWhenCINotAllowed() { /** * Server1 +----> Hypervisor1 +----> Farm */ $this->GivenCITreeInDB(<< Server_1, Farm_1 EOF); $this->GivenCINotAllowedToCurrentUser('Hypervisor_1'); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Server_1' => 'manual' ]); MetaModel::GetConfig()->Set('relations.complete_analysis', false); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Server_1' => 'manual', ]); } public function testImpactShouldBeFurtherPropagatedAboveCINotAllowedWhenCompleteAnalysisRequested() { /** * Server1 +----> Hypervisor1 +----> Farm */ $this->GivenCITreeInDB(<< Server_1, Farm_1 EOF); $this->GivenCINotAllowedToCurrentUser('Hypervisor_1'); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Server_1' => 'manual' ]); MetaModel::GetConfig()->Set('relations.complete_analysis', true); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Server_1' => 'manual', 'Farm_1' => 'computed', ]); } public function testImpactShouldBePropagatedToAllDescendantsOfSameClass() { /** * Server1 +----> Hypervisor1 * +----> Hypervisor2 */ $this->GivenCITreeInDB(<< Server_1 Hypervisor_2 -> Server_1 DOT); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Server_1' => 'manual', ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Server_1' => 'manual', 'Hypervisor_1' => 'computed', 'Hypervisor_2' => 'computed', ]); } public function testNodesImpactedByTwoWaysShouldBeFoundOnce() { /** * +------------------+ * Hypervisor1 | * +====> Person1 +----> Farm1 * Hypervisor2 | * +------------------+ */ $this->GivenCITreeInDB(<< Farm_1 Hypervisor_2 -> Farm_1 Hypervisor_1 <-> Person_1 Hypervisor_2 <-> Person_1 DOT); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Hypervisor_1' => 'manual', 'Hypervisor_2' => 'manual', ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Hypervisor_1' => 'manual', 'Hypervisor_2' => 'manual', 'Farm_1' => 'computed', 'Test Person_1' => 'computed' ]); } public function testPreviouslyComputedNodesShouldBeIgnored() { /** * Server1 +----> Hypervisor1 +----> Person1 * * Server2 +----> Person2 */ $this->GivenCITreeInDB(<< Server_1 Hypervisor_1 <-> Person_1 Server_2 <-> Person_2 EOF); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Server_1' => 'manual', 'Server_2' => 'computed', 'Person_2' => 'computed' ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Server_1' => 'manual', 'Hypervisor_1' => 'computed', 'Test Person_1' => 'computed' ]); } public function testPreviouslyComputedNodesShouldBeIgnoredCausingTheListToCollapse() { /** * Server1 +----> Hypervisor1 +----> Person1 */ $this->GivenCITreeInDB(<< Server_1 Hypervisor_1 <-> Person_1 EOF); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Hypervisor_1' => 'computed', 'Person_1' => 'computed' ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, []); } public function testNoImpactWhenNoCI() { $oTicket = $this->GivenTicketWithCIsOrPersons([]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, []); } public function testRedundancyShouldPreventPropagationOfImpact() { /** * Hypervisor1 * +----> Farm1 * Hypervisor2 */ $this->GivenCITreeInDB(<< Farm_1 Hypervisor_2 -> Farm_1 DOT); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Hypervisor_1' => 'manual' ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Hypervisor_1' => 'manual' ]); } public function testRedundancyShouldNotPreventPropagationWhenEverySourceIsImpacted() { /** * Hypervisor1 * +----> Farm1 * Hypervisor2 */ $this->GivenCITreeInDB(<< Farm_1 Hypervisor_2 -> Farm_1 DOT); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Hypervisor_1' => 'manual', 'Hypervisor_2' => 'manual' ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Hypervisor_1' => 'manual', 'Hypervisor_2' => 'manual', 'Farm_1' => 'computed' ]); } public function testCIsMarkedAsNotImpactedShouldRemainMarkedAndShouldNotPropagateTheImpact() { /** * Server1 +----> Hypervisor1 +----> Farm1 * +====> Person1 */ $this->GivenCITreeInDB(<< Server_1, Farm_1 Hypervisor_1 <-> Person_1 DOT); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Server_1' => 'manual', 'Hypervisor_1' => 'not_impacted', 'Person_1' => 'do_not_notify' ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Server_1' => 'manual', 'Hypervisor_1' => 'not_impacted', 'Test Person_1' => 'do_not_notify' ]); } public function testCIsMarkedAsNotImpactedShouldRemainMarkedWhenNotInImpactGraph() { /** * Hypervisor1 +====> Person1 */ $this->GivenCITreeInDB(<< Person_1 DOT); $oTicket = $this->GivenTicketWithCIsOrPersons([ 'Hypervisor_1' => 'not_impacted', 'Person_1' => 'do_not_notify' ]); $oTicket->UpdateImpactedItems(); // impact analysis $this->assertCIsOrPersonsListEquals($oTicket, [ 'Hypervisor_1' => 'not_impacted', 'Test Person_1' => 'do_not_notify' ]); } public function testRedundancyShouldBeEvaluatedOnOtherTicketsToo() { /** * Hypervisor1 * +----> Farm1 * Hypervisor2 */ $this->GivenCITreeInDB(<< Farm_1 Hypervisor_2 -> Farm_1 DOT); $oTicket1 = $this->GivenTicketWithCIsOrPersons([ 'Hypervisor_1' => 'manual', ]); $oTicket1->DBWrite(); // impact analysis $this->GivenRelationContextQueryWillFindTicket($oTicket1->GetKey()); $oTicket2 = $this->GivenTicketWithCIsOrPersons([ 'Hypervisor_2' => 'manual', ]); /// TEST Begins Here $oTicket2->UpdateImpactedItems(); // The second ticket should have the impact propagated $this->assertCIsOrPersonsListEquals($oTicket2, [ 'Hypervisor_2' => 'manual', 'Farm_1' => 'computed', ]); $this->ReloadObject($oTicket1); // reload the links // The first ticket should remain in its initial state $this->assertCIsOrPersonsListEquals($oTicket1, [ 'Hypervisor_1' => 'manual', ]); } private function assertCIsOrPersonsListEquals(\UserRequest $oTicket, array $aExpected) { $aActual = []; foreach ($oTicket->Get('functionalcis_list') as $oLnk) { $sKey = $oLnk->Get('functionalci_id_friendlyname'); $aActual[$sKey] = $oLnk->Get('impact_code'); } foreach ($oTicket->Get('contacts_list') as $oLnk) { $sKey = $oLnk->Get('contact_id_friendlyname'); $aActual[$sKey] = $oLnk->Get('role_code'); } $this->assertEquals($aExpected, $aActual, 'Unexpected value for functionalcis_list'); } /** * @param int|string|null $sTicketId * * @return void */ public function GivenRelationContextQueryWillFindTicket(int|string|null $sTicketId): void { $aRelationContext = \MetaModel::GetConfig()->GetModuleSetting('itop-tickets', 'relation_context'); $aRelationContext['UserRequest']['impacts']['down']['items'][0]['oql'] = "SELECT FCI, R FROM FunctionalCI AS FCI JOIN lnkFunctionalCIToTicket AS L ON L.functionalci_id = FCI.id JOIN UserRequest AS R ON L.ticket_id = R.id WHERE (R.id = $sTicketId)"; \MetaModel::GetConfig()->SetModuleSetting('itop-tickets', 'relation_context', $aRelationContext); } private function GivenCITreeInDB(string $sTree) { $aTree = explode("\n", $sTree); foreach ($aTree as $sLine) { $this->GivenCITreeLineInDB($sLine); } } private function GivenCITreeLineInDB(string $sLine) { if (strpos($sLine, '<->') !== false) { list($sCI, $sPerson) = explode('<->', $sLine); $sPersonId = $this->GivenCIOrPersonInDB(trim($sPerson)); $sCIId = $this->GivenCIOrPersonInDB(trim($sCI)); $this->GivenLnkContactToFunctionalCIInDB($sPersonId, $sCIId); return; } list($sCIParent, $sChildren) = explode('->', $sLine); $aChildren = explode(',', $sChildren); $aChildren = array_map('trim', $aChildren); $aChildrenIdsByClass = []; foreach ($aChildren as $sChildCI) { list($sChildClass, ) = explode('_', $sChildCI, 2); $aChildrenIdsByClass[$sChildClass] = $this->GivenCIOrPersonInDB($sChildCI); } $this->GivenCIOrPersonInDB($sCIParent, $aChildrenIdsByClass); } private function GivenCIOrPersonInDB(string $sDescriptor, array $aChildrenIdsByClass = []): string { $sDescriptor = trim($sDescriptor); if (isset($this->aCIs[$sDescriptor])) { return $this->aCIs[$sDescriptor]; } list($sClass, $sRef) = explode('_', $sDescriptor, 2); switch ($sClass) { case 'Server': $sCIId = $this->GivenServerInDB($sRef); break; case 'Hypervisor': $sCIId = $this->GivenHypervisorInDB($sRef, $aChildrenIdsByClass['Server'] ?? 0, $aChildrenIdsByClass['Farm'] ?? 0); break; case 'Person': $sCIId = $this->GivenPersonInDB($sRef); break; case 'Farm': $sCIId = $this->GivenFarmInDB($sRef); break; case 'VirtualMachine': $sCIId = $this->GivenVirtualMachineInDB($sRef, $aChildrenIdsByClass['Farm']); break; default: throw new Exception("Unhandled class $sClass"); } $this->aCIs[$sDescriptor] = $sCIId; return $this->aCIs[$sDescriptor]; } private function GivenTicketWithCIsOrPersons(array $aLinkedObjects) : \UserRequest { $oTicket = $this->GivenTicketObject(1); foreach ($aLinkedObjects as $sObjectDescriptor => $sRole) { $sClass = trim(explode('_', $sObjectDescriptor)[0]); if ($sClass === 'Person') { $this->AddLnkContactToTicketObject($this->GivenCIOrPersonInDB($sObjectDescriptor), $oTicket, $sRole); continue; } $this->AddLnkFunctionalCIToTicketObject($this->GivenCIOrPersonInDB($sObjectDescriptor), $oTicket, $sRole); } return $oTicket; } private function GivenCINotAllowedToCurrentUser(string $sCIDescriptor) { $iAnotherOrg = $this->GivenObjectInDB(Organization::class, ['name' => 'Another Org']); // Change the organization of the CI to 'Another Org' $sCIDescriptor = trim($sCIDescriptor); if (!isset($this->aCIs[$sCIDescriptor])) { throw new Exception("CI $sCIDescriptor not found"); } $sCIId = $this->aCIs[$sCIDescriptor]; $oCI = \MetaModel::GetObject('FunctionalCI', $sCIId); $oCI->Set('org_id', $iAnotherOrg); $oCI->DBUpdate(); $sConfigManagerProfileId = 3; // access to CIs $sLogin = $this->GivenUserRestrictedToAnOrganizationInDB($this->getTestOrgId(), $sConfigManagerProfileId); UserRights::Login($sLogin); $this->ResetMetaModelQueyCacheGetObject(); } }