Compare commits

..

11 Commits

Author SHA1 Message Date
lenaick.moreira
c6e50a0ed7 N°9830 - Fix setup error when trying to remove class with a TagSet attribute 2026-07-23 11:39:42 +02:00
Molkobain
76a410e00d Revert "N°9541 itop-virtualization-mgmt cannot currently be uninstalled" now that N°9831 has been fixed
This reverts commit 52fca750b0.
2026-07-23 10:45:44 +02:00
Molkobain
c68887c2bc N°9831 - Fix ext. management deletion plan analysis by avoiding overwriting entity for a previously processed class 2026-07-23 10:21:31 +02:00
Molkobain
837b433622 N°9831 - Fix DataCleanupService so it handles external keys the same way as \Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan::DeletionPlanForReferencingClasses and \DBObject::MakeDeletionPlan
- Check if ext. key is nullable first
- Then check if its a DEL_MANUAL option
2026-07-23 10:21:31 +02:00
Timmy38
52fca750b0 N°9541 itop-virtualization-mgmt cannot currently be uninstalled 2026-07-22 17:37:19 +02:00
Timmy38
d1aedee245 N°9692 - Fix flag name 2026-07-21 17:04:37 +02:00
lenaick.moreira
85d50f31bd N°9692 - Fix null coalescing for extension installation status and clean up test cases 2026-07-17 17:05:06 +02:00
lenaick.moreira
3c301affb3 N°9679 - Add remote extensions as uninstallable extension in "Extension management" 2026-07-17 14:32:46 +02:00
Lenaick
147fae7f8c N°9679 - Forced uninstallation is not logged when done by extensions management (#956) 2026-07-17 11:25:36 +02:00
odain-cbd
a5aa8c279f fix broken cache warmup after setup (#974)
* fix broken cache warmup after setup

* clear all caches after setup
2026-07-17 10:40:55 +02:00
lenaick.moreira
04f91ab4a2 Fix error CI 2026-07-16 09:40:33 +02:00
28 changed files with 225 additions and 162 deletions

View File

@@ -5368,7 +5368,6 @@ JS
/**
* @param array $aChanges
* @param bool $bIsNew
* @param string|null $sStimulusBeingApplied
*
* @return void
* @throws \ArchivedObjectException
@@ -5382,20 +5381,6 @@ JS
$this->FireEvent(EVENT_DB_AFTER_WRITE, ['is_new' => $bIsNew, 'changes' => $aChanges, 'stimulus_applied' => $sStimulusBeingApplied, 'cmdb_change' => self::GetCurrentChange()]);
}
//////////////
/// READ
///
/**
* @return void
* @throws \CoreException
* @since 3.3.0
*/
final public function FireEventReadDetails(string $sExportType): void
{
$this->FireEvent(EVENT_DATA_EXPORT, ['export_type' => $sExportType]);
}
//////////////
/// DELETE
///

View File

@@ -519,31 +519,6 @@ Call $this->AddInitialAttributeFlags($sAttCode, $iFlags) for all the initial att
</event_datum>
</event_data>
</event>
<event id="EVENT_DATA_EXPORT" _delta="define">
<name>Object details read from outside iTop</name>
<description><![CDATA[An object details has been read during an export]]></description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The object unarchived</description>
<type>DBObject</type>
</event_datum>
<event_datum id="attributes">
<description>Attribute codes exposed (empty means potentially all attributes)</description>
<type>array</type>
</event_datum>
<event_datum id="export_type">
<description>Type of export</description>
<type>string</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_DOWNLOAD_DOCUMENT" _delta="define">
<name>Document downloaded</name>
<description><![CDATA[A document has been downloaded from the GUI]]></description>

View File

@@ -25,11 +25,11 @@
*/
use Combodo\iTop\Application\Branding;
use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Application\WebPage\ErrorPage;
use Combodo\iTop\Application\WebPage\NiceWebPage;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Service\Session\Session;
/**
* Web page used for displaying the login form
@@ -369,7 +369,16 @@ class LoginWebPage extends NiceWebPage
public static function ResetSession()
{
// Unset all of the session variables.
Session::UnsetAll();
Session::Unset('auth_user');
Session::Unset('login_state');
Session::Unset('can_logoff');
Session::Unset('archive_mode');
Session::Unset('impersonate_user');
Session::Unset('PluginProperties');
Session::Unset('UrlMakerClass');
Session::Unset('itop_env');
Session::Unset('obj_messages');
Session::Unset('profile_list');
UserRights::_ResetSessionCache();
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!

View File

@@ -334,14 +334,12 @@ EOF
while ($aRow = $oSet->FetchAssoc()) {
set_time_limit(intval($iLoopTimeLimit));
$aData = [];
$aExportedObjects = [];
foreach ($this->aStatusInfo['fields'] as $aFieldSpec) {
foreach ($this->aStatusInfo['fields'] as $iCol => $aFieldSpec) {
$sAlias = $aFieldSpec['sAlias'];
$sAttCode = $aFieldSpec['sAttCode'];
$sField = '';
$oObj = $aRow[$sAlias];
$aExportedObjects[] = $oObj;
if ($oObj != null) {
switch ($sAttCode) {
case 'id':
@@ -368,13 +366,7 @@ EOF
}
$sData .= implode($this->aStatusInfo['separator'], $aData)."\n";
$iCount++;
$aExportedObjects = array_unique($aExportedObjects);
foreach ($aExportedObjects as $oExportedObject) {
$oExportedObject->FireEventReadDetails(get_class($this));
}
}
// Restore original date & time formats
AttributeDateTime::SetFormat($oPrevDateTimeFormat);
AttributeDate::SetFormat($oPrevDateFormat);

View File

@@ -6247,9 +6247,7 @@ abstract class DBObject implements iDisplay
}
/**
* @param array $aChanges
* @param bool $bIsNew
* @param string|null $sStimulusBeingApplied
*
* @return void
* @since 3.1.0
@@ -6258,18 +6256,6 @@ abstract class DBObject implements iDisplay
{
}
//////////////
/// READ
///
/**
* @return void
* @since 3.3.0
*/
public function FireEventReadDetails(string $sExportType): void
{
}
//////////////
/// DELETE
///

View File

@@ -288,13 +288,11 @@ EOF
while ($aRow = $oSet->FetchAssoc()) {
set_time_limit(intval($iLoopTimeLimit));
$aData = [];
$aExportedObjects = [];
foreach ($this->aStatusInfo['fields'] as $iCol => $aFieldSpec) {
$sAlias = $aFieldSpec['sAlias'];
$sAttCode = $aFieldSpec['sAttCode'];
$oObj = $aRow[$sAlias];
$aExportedObjects[] = $oObj;
$sField = '';
if ($oObj) {
$sField = $this->GetValue($oObj, $sAttCode);
@@ -303,11 +301,6 @@ EOF
}
fwrite($hFile, json_encode($aData)."\n");
$iCount++;
$aExportedObjects = array_unique($aExportedObjects);
foreach ($aExportedObjects as $oExportedObject) {
$oExportedObject->FireEventReadDetails(get_class($this));
}
}
set_time_limit(intval($iPreviousTimeLimit));
$this->aStatusInfo['position'] += $this->iChunkSize;

View File

@@ -139,7 +139,6 @@ class HTMLBulkExport extends TabularBulkExport
} else {
$sData .= "<tr>";
}
$aExportedObjects = [];
foreach ($this->aStatusInfo['fields'] as $iCol => $aFieldSpec) {
$sAlias = $aFieldSpec['sAlias'];
$sAttCode = $aFieldSpec['sAttCode'];
@@ -148,18 +147,12 @@ class HTMLBulkExport extends TabularBulkExport
$sField = '';
if ($oObj) {
$sField = $this->GetValue($oObj, $sAttCode);
$aExportedObjects[] = $oObj;
}
$sValue = ($sField === '') ? '&nbsp;' : $sField;
$sData .= "<td>$sValue</td>";
}
$sData .= "</tr>";
$iCount++;
$aExportedObjects = array_unique($aExportedObjects);
foreach ($aExportedObjects as $oExportedObject) {
$oExportedObject->FireEventReadDetails(get_class($this));
}
}
set_time_limit(intval($iPreviousTimeLimit));
$this->aStatusInfo['position'] += $this->iChunkSize;

View File

@@ -625,7 +625,6 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
}
while ($oObject = $oObjectSet->Fetch()) {
$oObject->FireEventReadDetails(get_class($this));
$oResult->AddObject(0, '', $oObject, $aShowFields, RestUtils::HasRequestedExtendedOutput($sShowFields));
}
$oResult->message = "Found: ".$oObjectSet->Count();
@@ -700,7 +699,6 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
if ($oElement instanceof RelationObjectNode) {
$oObject = $oElement->GetProperty('object');
if ($oObject) {
$oObject->FireEventReadDetails(get_class($this));
if ($bEnableRedundancy && $sDirection == 'down') {
// Add only the "reached" objects
if ($oElement->GetProperty('is_reached')) {

View File

@@ -233,6 +233,7 @@ EOF
public function GetNextChunk(&$aStatus)
{
$sRetCode = 'run';
$iPercentage = 0;
$oSet = new DBObjectSet($this->oSearch);
$oSet->SetLimit($this->iChunkSize, $this->aStatusInfo['position']);
@@ -253,8 +254,7 @@ EOF
set_time_limit(intval($iLoopTimeLimit));
$sData .= "<tr>";
$aExportedObjects = [];
foreach ($this->aStatusInfo['fields'] as $aFieldSpec) {
foreach ($this->aStatusInfo['fields'] as $iCol => $aFieldSpec) {
$sAlias = $aFieldSpec['sAlias'];
$sAttCode = $aFieldSpec['sAttCode'];
@@ -265,7 +265,7 @@ EOF
$sData .= "<td x:str></td>";
continue;
}
$aExportedObjects[] = $oObj;
switch ($sAttCode) {
case 'id':
$sField = $oObj->GetKey();
@@ -322,11 +322,6 @@ EOF
}
$sData .= "</tr>";
$iCount++;
$aExportedObjects = array_unique($aExportedObjects);
foreach ($aExportedObjects as $oExportedObject) {
$oExportedObject->FireEventReadDetails(get_class($this));
}
}
set_time_limit(intval($iPreviousTimeLimit));
$this->aStatusInfo['position'] += $this->iChunkSize;

View File

@@ -135,11 +135,6 @@ abstract class Trigger extends cmdbAbstractObject
if ($oAction->IsActive()) {
$oKPI = new ExecutionKPI();
$aContextArgs['action->object()'] = $oAction;
if (array_key_exists('this->object()', $aContextArgs)) {
/** @var \DBObject $oObject */
$oObject = $aContextArgs['this->object()'];
$oObject->FireEventReadDetails(get_class($oAction));
}
$oAction->DoExecute($this, $aContextArgs);
$oKPI->ComputeStatsForExtension($oAction, 'DoExecute');
}

View File

@@ -144,7 +144,6 @@ class XMLBulkExport extends BulkExport
if (count($aAuthorizedClasses) > 1) {
$sData .= "<Row>\n";
}
$aExportedObjects = [];
foreach ($aAuthorizedClasses as $sAlias => $sClassName) {
$oObj = $aObjects[$sAlias];
if (is_null($oObj)) {
@@ -152,7 +151,6 @@ class XMLBulkExport extends BulkExport
} else {
$sClassName = get_class($oObj);
$sData .= "<$sClassName alias=\"$sAlias\" id=\"".$oObj->GetKey()."\">\n";
$aExportedObjects[] = $oObj;
}
foreach ($aClass2Attributes[$sAlias] as $sAttCode => $oAttDef) {
if (is_null($oObj)) {
@@ -168,11 +166,6 @@ class XMLBulkExport extends BulkExport
$sData .= "</Row>\n";
}
$iCount++;
$aExportedObjects = array_unique($aExportedObjects);
foreach ($aExportedObjects as $oExportedObject) {
$oExportedObject->FireEventReadDetails(get_class($this));
}
}
set_time_limit(intval($iPreviousTimeLimit));

View File

@@ -347,7 +347,7 @@ class DataFeatureRemovalController extends Controller
'uninstallable' => $oExtension->CanBeUninstalled(),
'remote' => $oExtension->IsRemote(),
'missing' => $oExtension->bRemovedFromDisk,
'cannot-be-installed' => $oExtension->HasDependencyIssue(),
'dependency_issue' => $oExtension->HasDependencyIssue(),
],
];
@@ -442,7 +442,7 @@ class DataFeatureRemovalController extends Controller
if (! $this->bForcedUninstallation && $aExtensionData['extra_flags']['uninstallable']) {
$this->bForcedUninstallation = true;
}
if (false === $aExtensionData['extra_flags']['uninstallable']) {
if (false === $aExtensionData['extra_flags']['uninstallable'] || true === $aExtensionData['extra_flags']['remote']) {
$this->aExtensionsToCheck['extensions_not_uninstallable'][] = $sCode;
}
} elseif (!$aExtensionData['installed'] && $aSelectedExtensionsFromUI[$sCode] === 'on') {

View File

@@ -134,10 +134,6 @@ class DataCleanupService
/** @var DBObject $oDependentObj */
while ($oDependentObj = $oSet->Fetch()) {
$iDeletePropagationOption = $oExtKeyAttDef->GetDeletionPropagationOption();
if ($iDeletePropagationOption == DEL_MANUAL) {
$this->oObjectService->SetIssue(get_class($oDependentObj));
continue;
}
if ($oExtKeyAttDef->IsNullAllowed()) {
// Optional external key, list to reset
@@ -152,6 +148,12 @@ class DataCleanupService
return false;
}
} else {
// Mandatory external key
if ($iDeletePropagationOption == DEL_MANUAL) {
// Cannot be deleted automatically, must be handled manually
$this->oObjectService->SetIssue(get_class($oDependentObj));
continue;
}
// Propagate deletion only if not visited
if ($this->IsVisited($oDependentObj)) {
continue;

View File

@@ -59,9 +59,11 @@ class StaticDeletionPlan
{
foreach ($aClasses as $sClass) {
$oDeletionPlanItem = $this->GetInitialClassDeletionPlan($sClass);
$oDeletionPlanEntity = new DeletionPlanEntity();
$oDeletionPlanEntity->oDelete->Merge($oDeletionPlanItem);
$this->aDeletionPlan[$sClass] = $oDeletionPlanEntity;
// N°9831 Do not overwrite existing entity as it may already exist for this class if a previously processed class references it (issues/updates already accumulated must be kept)
if (false === array_key_exists($sClass, $this->aDeletionPlan)) {
$this->aDeletionPlan[$sClass] = new DeletionPlanEntity();
}
$this->aDeletionPlan[$sClass]->oDelete->Merge($oDeletionPlanItem);
$this->DeletionPlanForReferencingClasses($sClass);
}

View File

@@ -49,6 +49,8 @@ if (Session::IsSet('auth_user')) {
UserRights::Login($sAuthUser); // Set the user's language
}
LoginWebPage::ResetSession();
$bLoginDebug = MetaModel::GetConfig()->Get('login_debug');
if ($bLoginDebug) {
IssueLog::Info("---------------------------------");
@@ -86,7 +88,7 @@ if ($bLoginDebug) {
IssueLog::Info("--> Display logout page");
}
LoginWebPage::ResetSession();
LoginWebPage::ResetSession(true);
if ($bLoginDebug) {
$sSessionLog = session_id().' '.utils::GetSessionLog();
IssueLog::Info("SESSION: $sSessionLog");

View File

@@ -8,6 +8,7 @@ use DBObjectSet;
use IssueLog;
use MetaModel;
use SetupLog;
use TagSetFieldData;
require_once APPROOT.'setup/feature_removal/ModelReflectionSerializer.php';
@@ -68,6 +69,10 @@ abstract class AbstractSetupAudit
continue;
}
if (MetaModel::IsSameFamily($sClass, TagSetFieldData::class)) {
continue;
}
if (!MetaModel::IsStandaloneClass($sClass)) {
$iCount = $this->Count($sClass);
$this->aFinalClassesToCleanup[$sClass] = $iCount;

View File

@@ -176,7 +176,7 @@ class iTopExtension
};
}
public function IsRemote(): string
public function IsRemote(): bool
{
return $this->sSource === self::SOURCE_REMOTE;
}

View File

@@ -933,6 +933,7 @@ class RunTimeEnvironment
@chmod($sFinalConfig, 0440); // Read-only for owner and group, nothing for others
SetupUtils::rrmdir(dirname($sBuildConfig)); // Cleanup the temporary build dir if empty
MetaModel::ResetAllCaches($this->sBuildEnv);
MetaModel::ResetAllCaches($this->sFinalEnv);
if (! isset($_SESSION)) {

View File

@@ -750,8 +750,8 @@ EOF
$bSelected = isset($aSelectedComponents[$sChoiceId]) && ($aSelectedComponents[$sChoiceId] === $sChoiceId);
$bMissingFromDisk = isset($aChoice['missing']) && $aChoice['missing'] === true;
$bMandatory = (isset($aChoice['mandatory']) && $aChoice['mandatory']);
$bInstalled = $bMissingFromDisk || $oITopExtension->bInstalled;
$bDependencyIssue = $oITopExtension->HasDependencyIssue();
$bInstalled = $bMissingFromDisk || $oITopExtension?->bInstalled ?? false;
$bDependencyIssue = $oITopExtension?->HasDependencyIssue() ?? false;
$bChecked = $bSelected;
$bDisabled = false;

View File

@@ -131,23 +131,6 @@ class Session
}
}
/**
* Unset all session variables, no matter if they were set by iTop or not
*
* @return void
* @since 3.3.0 N°9625
*/
public static function UnsetAll(): void
{
if (session_status() !== PHP_SESSION_ACTIVE) {
self::Start();
$_SESSION = [];
self::WriteClose();
} else {
$_SESSION = [];
}
}
/**
* @param string|array $key key to access to the session variable. To access to $_SESSION['a']['b'] $key must be ['a', 'b']
* @param $default
@@ -200,10 +183,6 @@ class Session
public static function ListVariables(): array
{
if (!isset($_SESSION)) {
return [];
}
return array_keys($_SESSION);
}

View File

@@ -123,23 +123,6 @@ class SessionTest extends ItopTestCase
$this->assertFalse(Session::IsSet(['test1', 'test2', 'test3']));
}
public function testUnsetAll()
{
Session::Start();
Session::Set('test', 'OK');
Session::Set(['test1', 'test2', 'test3'], 'OK');
Session::Set('another_test', ['foo' => 'bar']);
$this->assertTrue(Session::IsSet('test'));
$this->assertTrue(Session::IsSet(['test1', 'test2', 'test3']));
$this->assertTrue(Session::IsSet('another_test'));
Session::UnsetAll();
$this->assertEmpty($_SESSION);
$this->assertFalse(Session::IsSet('test'));
$this->assertFalse(Session::IsSet(['test1', 'test2', 'test3']));
$this->assertFalse(Session::IsSet('another_test'));
}
public function testRegenerateId()
{
Session::Start();

View File

@@ -163,6 +163,58 @@ class DataCleanupServiceTest extends \AbstractCleanup
$this->AssertSummaryEquals($aExpected, $aRes);
}
/**
* N°9831
*
* A nullable external key configured as DEL_MANUAL must NOT be treated as an issue: it is simply
* reset (set to null), exactly like core DBObject::MakeDeletionPlan does. DEL_MANUAL only blocks
* for mandatory keys. This mirrors the real "Hypervisor.farm_id -> Farm" case (nullable + DEL_MANUAL).
*
* Regression guard for the ordering of the IsNullAllowed()/DEL_MANUAL checks in RecursiveDeletion:
* reverting that change would make ExecuteCleanup throw here instead of resetting the object.
*/
public function testExecuteCleanup_NullableManualShouldResetAndNotThrow()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRManualNullable_1
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$oService = new DataCleanupService();
// Must NOT throw: the nullable external key is reset, not reported as an issue
$aRes = $oService->ExecuteCleanup($aClasses);
$aExpected = [
['DFRManualNullable', 1, 0 ],
['DFRToRemoveLeaf', 0, 1 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
/**
* N°9831
*
* Summary counterpart of the test above: a nullable DEL_MANUAL external key is reported as an
* update (reset), with an issue count of 0. Reverting the RecursiveDeletion change would report
* it as an issue instead.
*/
public function testGetCleanupSummary_NullableManualShouldBeUpdateNotIssue()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRManualNullable_1
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$oService = new DataCleanupService();
$aRes = $oService->GetCleanupSummary($aClasses);
$aExpected = [
['DFRManualNullable', 1, 0, 0 ],
['DFRToRemoveLeaf', 0, 1, 0 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
private function AssertSummaryEquals(array $expected, $actual, $sMessage = '')
{
$aExpected = [];

View File

@@ -9,7 +9,6 @@ namespace Combodo\iTop\Test\UnitTest\Module\DataFeatureRemoval;
use Combodo\iTop\DataFeatureRemoval\Entity\DataCleanupSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Service\StaticDeletionPlan;
use MetaModel;
require_once __DIR__."/AbstractCleanup.php";
class StaticDeletionPlanTest extends \AbstractCleanup
@@ -150,6 +149,66 @@ EOF);
$this->AssertSummaryEquals($aExpected, $aRes);
}
/**
* N°9831
*
* A DEL_MANUAL issue must still be reported when the referencing class (DFRManual) is itself part
* of the classes to remove AND is processed AFTER the class it points to (DFRToRemoveLeaf).
*
* This mirrors the real "VirtualMachine -> VirtualHost" case: the setup audit sorts the removed
* classes alphabetically and drops abstract classes, so a referencing class such as VirtualMachine
* ends up processed after its concrete targets (Farm/Hypervisor/Cloud). Here DFRToRemove is abstract
* and DFRToRemoveLeaf is its concrete child, while DFRManual points to DFRToRemove with a mandatory
* DEL_MANUAL external key.
*
* Regression: the plan entity accumulating the issue used to be overwritten when the referencing
* class was processed as a top-level class, silently dropping the issue (no blocking message on the
* summary page, then a crash at execution time).
*/
public function testGetCleanupSummary_IssueKeptWhenReferencingClassListedAfterTarget(): void
{
$this->GivenDFRObjectsInDB(<<<EOF
create DFRToRemoveLeaf (name = DFRToRemoveLeaf_1)
create DFRManual (name = DFRManual_1, extkey_id = DFRToRemoveLeaf_1)
EOF);
// Target listed BEFORE the referencing class, as produced by the alphabetically sorted audit
$aClasses = ['DFRToRemoveLeaf', 'DFRManual'];
$oService = new StaticDeletionPlan();
$aRes = $oService->GetCleanupSummary($aClasses);
$aExpected = [
'DFRToRemoveLeaf' => ['iDeleteCount' => 1],
'DFRManual' => ['iDeleteCount' => 1, 'iIssueCount' => 1],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
/**
* N°9831
*
* Control case: the same issue must also be reported when the referencing class is listed BEFORE
* its target (this order already worked, e.g. "Enclosure -> Rack"). Together with the test above,
* this locks in that issue detection is independent of the processing order.
*/
public function testGetCleanupSummary_IssueKeptWhenReferencingClassListedBeforeTarget(): void
{
$this->GivenDFRObjectsInDB(<<<EOF
create DFRToRemoveLeaf (name = DFRToRemoveLeaf_1)
create DFRManual (name = DFRManual_1, extkey_id = DFRToRemoveLeaf_1)
EOF);
$aClasses = ['DFRManual', 'DFRToRemoveLeaf'];
$oService = new StaticDeletionPlan();
$aRes = $oService->GetCleanupSummary($aClasses);
$aExpected = [
'DFRToRemoveLeaf' => ['iDeleteCount' => 1],
'DFRManual' => ['iDeleteCount' => 1, 'iIssueCount' => 1],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testCircularRefsShouldNotRunInfinitely()
{
$this->GivenDFRObjectsInDB(<<<EOF

View File

@@ -319,6 +319,58 @@
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRManualNullable" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrmanualnullable</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="extkey_id" xsi:type="AttributeExternalKey">
<sql>extkey_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>true</is_null_allowed>
<target_class>DFRToRemove</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRLeafNotToRemove" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
@@ -718,6 +770,14 @@
<entry id="Class:DFRManual/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
<entry id="Class:DFRManual/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManualNullable/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManualNullable/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManualNullable" _delta="define"><![CDATA[DFRManualNullable]]></entry>
<entry id="Class:DFRManualNullable+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManualNullable/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRManualNullable/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManualNullable/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
<entry id="Class:DFRManualNullable/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
</entries>
</dictionary>
</dictionaries>

View File

@@ -78,9 +78,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
],
'A missing extension should be disabled and unchecked' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
@@ -102,9 +99,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
],
'A missing extension should always be disabled and unchecked, even when mandatory' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',
@@ -126,9 +120,6 @@ class WizStepModulesChoiceTest extends ItopTestCase
],
'A missing extension should always be disabled and unchecked, even when non-uninstallable' => [
'aExtensionsOnDiskOrDb' => [
'itop-ext1' => [
'installed' => false,
],
],
'aWizardStepDefinition' => [
'extension_code' => 'itop-ext1',

View File

@@ -79,6 +79,7 @@ class SetupAuditTest extends ItopCustomDatamodelTestCase
"Feature1Module1MyClass",
"FinalClassFeature2Module1MyClass",
"FinalClassFeature2Module1MyFinalClassFromLocation",
"TagSetFieldDataFor_FinalClassFeature2Module1MyFinalClassFromLocation__domains",
];
$this->assertEqualsCanonicalizing($expected, $oSetupAudit->GetRemovedClasses());

View File

@@ -64,6 +64,12 @@
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
</field>
<field id="domains" xsi:type="AttributeTagSet">
<sql>domains</sql>
<is_null_allowed>true</is_null_allowed>
<tracking_level>all</tracking_level>
<max_items>12</max_items>
</field>
</fields>
<methods/>
<presentation/>

View File

@@ -31,6 +31,12 @@
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
<field id="domains" xsi:type="AttributeTagSet">
<sql>domains</sql>
<is_null_allowed>true</is_null_allowed>
<tracking_level>all</tracking_level>
<max_items>12</max_items>
</field>
</fields>
<methods/>
<presentation/>