Compare commits

...

2 Commits

Author SHA1 Message Date
Eric Espie
8a52604ed9 N°8761 - Assist in cleaning up data prior to uninstalling extensions 2026-03-16 17:44:55 +01:00
Eric Espie
37ecd8f63f N°8761 - Assist in cleaning up data prior to uninstalling extensions 2026-03-16 13:54:07 +01:00
11 changed files with 431 additions and 58 deletions

View File

@@ -691,6 +691,28 @@ abstract class LogAPI
static::$m_oMockMetaModelConfig = $oMetaModelConfig;
}
public static function Exception(string $sMessage, throwable $oException, string $sChannel = null, array $aContext = []): void
{
$aErrorLogs = [];
$aErrorLogs[] = static::PrepareErrorLog($sMessage, $oException, $aContext);
$oException = $oException->getPrevious();
while ($oException !== null) {
$aErrorLogs[] = static::PrepareErrorLog($oException->getMessage(), $oException, $aContext, true);
$oException = $oException->getPrevious();
}
$aErrorLogs = array_reverse($aErrorLogs);
foreach ($aErrorLogs as $aErrorLog) {
static::Error($aErrorLog['message'], $sChannel, $aErrorLog['context']);
}
}
private static function PrepareErrorLog(string $sMessage, throwable $oException, array $aContext, bool $isPrevious = false): array
{
$aContext['Error Message'] = $oException->getMessage();
$aContext['Stack Trace'] = $oException->getTraceAsString();
return ['message' => ($isPrevious ? "Previous " : '')."Exception: $sMessage", 'context' => $aContext];
}
public static function Error($sMessage, $sChannel = null, $aContext = [])
{
static::Log(self::LEVEL_ERROR, $sMessage, $sChannel, $aContext);

View File

@@ -8,4 +8,9 @@
<enable_admin_only>1</enable_admin_only>
</menu>
</menus>
<module_parameters>
<parameters id="combodo-data-feature-removal">
<max_count_estimation_for_safe_cleanup>100</max_count_estimation_for_safe_cleanup>
</parameters>
</module_parameters>
</itop_design>

View File

@@ -25,9 +25,10 @@ Dict::Add('EN US', 'English', 'English', [
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan',
'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Database tables to clean before continuing',
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s rows to clean before continuing',
'DataFeatureRemoval:DoDeletion:Title' => 'Do deletion',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Remove all the entries from the database',
'DataFeatureRemoval:DeletionPlan:ToManyOperations' => 'Too many entries to clean',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',

View File

@@ -10,48 +10,49 @@
*/
Dict::Add('FR FR', 'French', 'Français', [
'Menu:DataFeatureRemovalMenu' => 'Features Removal',
'combodo-data-feature-removal/Operation:Main/Title' => 'Features Removal',
'Menu:DataFeatureRemovalMenu' => 'Suppression de fonctionnalités',
'combodo-data-feature-removal/Operation:Main/Title' => 'Suppression de fonctionnalités',
'DataFeatureRemoval:Main:Title' => 'Features Removal',
'DataFeatureRemoval:Main:SubTitle' => 'Prepare features you want to enable/disable in a future setup',
'DataFeatureRemoval:Failure:Title' => 'Feature dry removal errors',
'DataFeatureRemoval:Helper:Title' => 'Enable or disable features that are installed in your iTop.',
'DataFeatureRemoval:Helper:Desc1' => 'It will prepare the setup step that proceeds to feature enabling or disabling.',
'DataFeatureRemoval:Helper:Desc2' => 'Analyze if there are any data or dependency preventing you from enabling/disabling a feature.',
'DataFeatureRemoval:Main:Title' => 'Suppression de fonctionnalités',
'DataFeatureRemoval:Main:SubTitle' => 'Préparez les fonctionnalités que vous souhaitez activer ou désactiver lors dune prochaine configuration',
'DataFeatureRemoval:Failure:Title' => 'Erreurs lors de la simulation de suppression de fonctionnalités',
'DataFeatureRemoval:Helper:Title' => 'Activez ou désactivez les fonctionnalités installées dans votre iTop.',
'DataFeatureRemoval:Helper:Desc1' => 'Cette étape prépare lassistant de configuration à activer ou désactiver des fonctionnalités.',
'DataFeatureRemoval:Helper:Desc2' => 'Analyse si des données ou des dépendances empêchent lactivation ou la désactivation dune fonctionnalité.',
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
'DataFeatureRemoval:Features:Title' => 'Fonctionnalités',
'DataFeatureRemoval:Analysis:Title' => 'Résultat de lanalyse',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s élément(s) à nettoyer avant de poursuivre',
'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan',
'DataFeatureRemoval:DeletionPlan:SubTitle' => 'Database tables to clean before continuing',
'DataFeatureRemoval:DoDeletion:Title' => 'Do deletion',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Remove all the entries from the database',
'DataFeatureRemoval:DeletionPlan:Title' => 'Plan de suppression',
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s ligne(s) à nettoyer avant de poursuivre',
'DataFeatureRemoval:DoDeletion:Title' => 'Exécuter la suppression',
'DataFeatureRemoval:DoDeletion:SubTitle' => 'Supprime toutes les entrées de la base de données',
'DataFeatureRemoval:DeletionPlan:ToManyOperations' => 'Trop dentrées à nettoyer',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Element to remove',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Feature name',
'DataFeatureRemoval:Table:Analysis:Module' => 'Module name',
'DataFeatureRemoval:Table:Analysis:ClassName' => 'Élément à supprimer',
'DataFeatureRemoval:Table:Analysis:FeatureName' => 'Fonctionnalité',
'DataFeatureRemoval:Table:Analysis:Module' => 'Module',
'DataFeatureRemoval:Table:Analysis:Occurrence' => 'Occurrence',
'UI:Button:Analyze' => 'Analyze',
'UI:Button:ModifyChoices' => 'Modify Choices',
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
'UI:Button:PlanDeletion' => 'Prepare deletion plan',
'UI:Button:DoDeletion' => 'Delete data',
'UI:Button:BackToMain' => 'Back to Feature Removal',
'UI:Button:Setup' => 'Back to setup',
'UI:Button:Analyze' => 'Analyser',
'UI:Button:ModifyChoices' => 'Modifier les choix',
'UI:Button:AnalyzeAndSetup' => 'Analyser et ouvrir lassistant de configuration',
'UI:Button:PlanDeletion' => 'Préparer le plan de suppression',
'UI:Button:DoDeletion' => 'Supprimer les données',
'UI:Button:BackToMain' => 'Retour à la suppression de fonctionnalités',
'UI:Button:Setup' => 'Retour à lassistant de configuration',
'UI:Action:ForceUninstall' => 'Force uninstall',
'UI:Action:MoreInfo' => 'More information',
'UI:Action:ForceUninstall' => 'Forcer la désinstallation',
'UI:Action:MoreInfo' => 'Plus dinformations',
'DataFeatureRemoval:Table:Empty' => 'No data to remove',
'DataFeatureRemoval:Table:Empty' => 'Aucune donnée à supprimer',
'DataFeatureRemoval:Column:Class' => 'Class',
'DataFeatureRemoval:Column:DeleteCount' => 'Entries to delete',
'DataFeatureRemoval:Column:UpdateCount' => 'Entries to update',
'DataFeatureRemoval:Column:Issue' => 'Issue',
'DataFeatureRemoval:Column:Class' => 'Classe',
'DataFeatureRemoval:Column:DeleteCount' => 'Entrées à supprimer',
'DataFeatureRemoval:Column:UpdateCount' => 'Entrées à mettre à jour',
'DataFeatureRemoval:Column:Issue' => 'Problème',
'DataFeatureRemoval:Column:DeletedCount' => 'Deleted entries',
'DataFeatureRemoval:Column:UpdatedCount' => 'Updated entries',
'DataFeatureRemoval:Column:DeletedCount' => 'Entrées supprimées',
'DataFeatureRemoval:Column:UpdatedCount' => 'Entrées mises à jour',
]);

View File

@@ -11,6 +11,7 @@ require_once APPROOT.'setup/feature_removal/SetupAudit.php';
require_once APPROOT.'setup/feature_removal/DryRemovalRuntimeEnvironment.php';
use Combodo\iTop\Application\TwigBase\Controller\Controller;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalConfig;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
@@ -107,6 +108,7 @@ class DataFeatureRemovalController extends Controller
$aDeletionPlanSummaryEntities = DeletionPlanService::GetInstance()->GetDeletionPlanSummary($aClasses);
$aColumns = ['Class', 'DeleteCount' , 'UpdateCount', 'Issue'];
$aRows = [];
$iQueryCount = 0;
foreach ($aDeletionPlanSummaryEntities as $oDeletionPlanSummaryEntity) {
$aRows[] = [
$oDeletionPlanSummaryEntity->sClass,
@@ -114,11 +116,15 @@ class DataFeatureRemovalController extends Controller
$oDeletionPlanSummaryEntity->iUpdateCount,
$oDeletionPlanSummaryEntity->sIssue ?? '',
];
$iQueryCount += $oDeletionPlanSummaryEntity->iDeleteCount;
$iQueryCount += $oDeletionPlanSummaryEntity->iUpdateCount;
}
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aDeletionPlanSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
$aParams['aClasses'] = $aClasses;
$aParams['iQueryCount'] = $iQueryCount;
$aParams['bDeletionPossible'] = ($iQueryCount <= DataFeatureRemovalConfig::GetInstance()->Get('max_count_estimation_for_safe_cleanup', 100));
$this->DisplayPage($aParams);
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\DataFeatureRemoval\Helper;
use Config;
use MetaModel;
use utils;
class DataFeatureRemovalConfig
{
private static DataFeatureRemovalConfig $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): DataFeatureRemovalConfig
{
if (!isset(static::$oInstance)) {
static::$oInstance = new DataFeatureRemovalConfig();
}
return static::$oInstance;
}
public function Get(string $sParamName, $default = null)
{
return MetaModel::GetModuleSetting(DataFeatureRemovalHelper::MODULE_NAME, $sParamName, $default);
}
public function GetBoolean(string $sParamName, $default = null): bool
{
$res = $this->Get($sParamName, $default);
return boolval($res);
}
public function IsEnabled(): bool
{
return $this->GetBoolean('enable', false);
}
public function Set(string $sParamName, $value)
{
$oConfig = utils::GetConfig();
$oConfig->SetModuleSetting(DataFeatureRemovalHelper::MODULE_NAME, $sParamName, $value);
}
/**
* @param \Config|null $oConfig
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
public function SaveItopConfiguration(Config $oConfig = null)
{
if (is_null($oConfig)) {
$oConfig = utils::GetConfig();
}
$sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php';
@chmod($sConfigFile, 0770); // Allow overwriting the file
$oConfig->WriteToFile($sConfigFile);
@chmod($sConfigFile, 0444); // Read-only
}
}

View File

@@ -126,20 +126,28 @@ class DeletionPlanService
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
foreach ($aDeletes as $sId => $aDelete) {
// Delete any existing change tracking about the current object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', $sClass, '=');
$oFilter->AddCondition('objkey', $sId, '=');
MetaModel::PurgeData($oFilter);
try {
CMDBSource::Query('START TRANSACTION');
// Delete any existing change tracking about the current object
$oFilter = new DBObjectSearch('CMDBChangeOp');
$oFilter->AddCondition('objclass', $sClass, '=');
$oFilter->AddCondition('objkey', $sId, '=');
MetaModel::PurgeData($oFilter);
// Delete the entry
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
foreach ($aClassesToRemove as $sParentClass) {
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
CMDBSource::DeleteFrom($sQuery);
// Delete the entry
$aClassesToRemove = array_merge(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL), MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_EXCLUDELEAF, false));
foreach ($aClassesToRemove as $sParentClass) {
$oFilter = DBObjectSearch::FromOQL_AllData("SELECT $sParentClass WHERE id=:id");
$sQuery = $oFilter->MakeDeleteQuery(['id' => $sId]);
CMDBSource::DeleteFrom($sQuery);
}
CMDBSource::Query('COMMIT');
} catch (\Exception $e) {
\IssueLog::Exception(__METHOD__.': Cleanup failed', $e);
CMDBSource::Query('ROLLBACK');
throw $e;
}
$oDeletionPlanSummaryEntity->iDeleteCount++;
}

View File

@@ -1,20 +1,24 @@
{# @copyright Copyright (C) 2010-2026 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:DeletionPlan:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:DeletionPlan:SubTitle'|dict_s } %}
{% UIPanel ForInformation { sTitle:'DataFeatureRemoval:DeletionPlan:Title'|dict_s, sSubTitle: 'DataFeatureRemoval:DeletionPlan:SubTitle'|dict_format(iQueryCount) } %}
{% UIDataTable ForForm { sRef:'aDeletionPlanSummary', aColumns:aDeletionPlanSummary.Columns, aData:aDeletionPlanSummary.Data} %}{% EndUIDataTable %}
{% EndUIPanel %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% if bDeletionPossible %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}
{% UIInput ForHidden { sName:'operation', sValue:'DoDeletion'} %}
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% else %}
{{ 'DataFeatureRemoval:DeletionPlan:ToManyOperations'|dict_s }}
{% endif %}
{% UIForm Standard {} %}
{% UIInput ForHidden { sName:'transaction_id', sValue:sTransactionId} %}

View File

@@ -8,6 +8,7 @@ $baseDir = dirname($vendorDir);
return array(
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => $baseDir . '/src/Controller/DataFeatureRemovalController.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanSummaryEntity' => $baseDir . '/src/Entity/DeletionPlanSummaryEntity.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => $baseDir . '/src/Helper/DataFeatureRemovalConfig.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => $baseDir . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => $baseDir . '/src/Helper/DataFeatureRemovalHelper.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => $baseDir . '/src/Helper/DataFeatureRemovalLog.php',

View File

@@ -27,6 +27,7 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
public static $classMap = array (
'Combodo\\iTop\\DataFeatureRemoval\\Controller\\DataFeatureRemovalController' => __DIR__ . '/../..' . '/src/Controller/DataFeatureRemovalController.php',
'Combodo\\iTop\\DataFeatureRemoval\\Entity\\DeletionPlanSummaryEntity' => __DIR__ . '/../..' . '/src/Entity/DeletionPlanSummaryEntity.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalConfig' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalConfig.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalHelper.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalLog.php',

View File

@@ -0,0 +1,253 @@
<?php
/*
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Module\DataFeatureRemoval\Service;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DeletionPlan;
/**
* Unit tests for the DeletionPlanService class in the Combodo Data Feature Removal module.
*
* These tests cover:
* - Singleton behavior: ensuring GetInstance always returns the same instance, and SetInstance can override it.
* - GetDeletionPlanSummary method: handling null and empty input, and verifying summary output for various delete/update scenarios.
* - ExecuteDeletionPlan method: confirming that an exception is thrown when issues are detected in the deletion plan.
*
* Key aspects tested:
* - Consistent singleton instance management.
* - Accurate summary generation for deletion and update operations, including mode and issue reporting per class.
* - Edge cases such as null, empty, and multiple classes.
* - Proper exception handling when the deletion plan contains issues.
*
* The tests use PHPUnit, mocks for DeletionPlan and DeletionPlanService, and data providers to cover multiple scenarios.
*
* @see DeletionPlanService
* @see DeletionPlanSummaryEntity
* @see ItopDataTestCase
*/
class DeletionPlanServiceTest extends ItopDataTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('env-production/combodo-data-feature-removal/vendor/autoload.php');
}
//--- GetDeletionPlanSummary tests ---
/**
* Tests that GetDeletionPlanSummary returns an empty array when passed null as input.
*/
public function testGetDeletionPlanSummaryReturnsEmptyArrayWhenNull(): void
{
$oService = DeletionPlanService::GetInstance();
$aResult = $oService->GetDeletionPlanSummary(null);
$this->assertIsArray($aResult);
$this->assertEmpty($aResult);
}
/**
* Tests that GetDeletionPlanSummary returns an empty array when the input class list is empty.
*/
public function testGetDeletionPlanSummaryReturnsEmptyArrayWhenEmptyClasses(): void
{
$oMockService = $this->getMockBuilder(DeletionPlanService::class)
->disableOriginalConstructor()
->onlyMethods(['GetDeletionPlan'])
->getMock();
$oDeletionPlan = new DeletionPlan();
$oMockService->method('GetDeletionPlan')->willReturn($oDeletionPlan);
DeletionPlanService::SetInstance($oMockService);
$aResult = $oMockService->GetDeletionPlanSummary([]);
$this->assertIsArray($aResult);
$this->assertEmpty($aResult);
}
/**
* Tests GetDeletionPlanSummary for various delete/update scenarios using a data provider.
* Verifies summary output for each class matches expected values.
*
* @dataProvider GetDeletionPlanSummaryWithDeletesProvider
*/
public function testGetDeletionPlanSummaryWithDeletes(array $aToDelete, array $aToUpdate, array $aExpected): void
{
$oDeletionPlan = $this->createMock(DeletionPlan::class);
$oDeletionPlan->method('ListDeletes')->willReturn($aToDelete);
$oDeletionPlan->method('ListUpdates')->willReturn($aToUpdate);
$oMockService = $this->getMockBuilder(DeletionPlanService::class)
->disableOriginalConstructor()
->onlyMethods(['GetDeletionPlan'])
->getMock();
$oMockService->method('GetDeletionPlan')->willReturn($oDeletionPlan);
$aResult = $oMockService->GetDeletionPlanSummary(['SomeClass']);
foreach ($aExpected as $sClass => $aExpectedValues) {
$this->assertArrayHasKey($sClass, $aResult);
$this->assertInstanceOf(DeletionPlanSummaryEntity::class, $aResult[$sClass]);
$this->assertEquals($aExpectedValues['iDeleteCount'], $aResult[$sClass]->iDeleteCount);
$this->assertEquals($aExpectedValues['iUpdateCount'], $aResult[$sClass]->iUpdateCount);
$this->assertEquals($aExpectedValues['iMode'], $aResult[$sClass]->iMode);
$this->assertEquals($aExpectedValues['sIssue'], $aResult[$sClass]->sIssue);
}
}
/**
* Provides multiple scenarios for testGetDeletionPlanSummaryWithDeletes, including deletes, updates, issues, and multiple classes.
*
* @return array
*/
public function GetDeletionPlanSummaryWithDeletesProvider(): array
{
return [
'single class with deletes only' => [
'aToDelete' => [
'Server' => [
1 => ['to_delete' => null, 'mode' => DEL_AUTO, 'requested_explicitely' => true],
2 => ['to_delete' => null, 'mode' => DEL_SILENT, 'requested_explicitely' => false],
],
],
'aToUpdate' => [],
'aExpected' => [
'Server' => [
'iDeleteCount' => 2,
'iUpdateCount' => 0,
'iMode' => DEL_AUTO,
'sIssue' => null,
],
],
],
'single class with deletes and issue' => [
'aToDelete' => [
'Server' => [
1 => ['to_delete' => null, 'mode' => DEL_MANUAL, 'requested_explicitely' => false, 'issue' => 'Cannot delete'],
],
],
'aToUpdate' => [],
'aExpected' => [
'Server' => [
'iDeleteCount' => 1,
'iUpdateCount' => 0,
'iMode' => DEL_MANUAL,
'sIssue' => 'Cannot delete',
],
],
],
'single class with updates only' => [
'aToDelete' => [],
'aToUpdate' => [
'Person' => [
10 => ['to_reset' => null, 'attributes' => ['org_id' => null]],
11 => ['to_reset' => null, 'attributes' => ['org_id' => null]],
12 => ['to_reset' => null, 'attributes' => ['org_id' => null]],
],
],
'aExpected' => [
'Person' => [
'iDeleteCount' => 0,
'iUpdateCount' => 3,
'iMode' => 0,
'sIssue' => null,
],
],
],
'class with both deletes and updates' => [
'aToDelete' => [
'Server' => [
1 => ['to_delete' => null, 'mode' => DEL_AUTO, 'requested_explicitely' => true],
],
],
'aToUpdate' => [
'Server' => [
5 => ['to_reset' => null, 'attributes' => ['org_id' => null]],
],
],
'aExpected' => [
'Server' => [
'iDeleteCount' => 1,
'iUpdateCount' => 1,
'iMode' => DEL_AUTO,
'sIssue' => null,
],
],
],
'multiple classes' => [
'aToDelete' => [
'Server' => [
1 => ['to_delete' => null, 'mode' => DEL_AUTO, 'requested_explicitely' => true],
],
'NetworkDevice' => [
3 => ['to_delete' => null, 'mode' => DEL_SILENT, 'requested_explicitely' => false],
4 => ['to_delete' => null, 'mode' => DEL_SILENT, 'requested_explicitely' => false],
],
],
'aToUpdate' => [
'Person' => [
10 => ['to_reset' => null, 'attributes' => ['org_id' => null]],
],
],
'aExpected' => [
'Server' => [
'iDeleteCount' => 1,
'iUpdateCount' => 0,
'iMode' => DEL_AUTO,
'sIssue' => null,
],
'NetworkDevice' => [
'iDeleteCount' => 2,
'iUpdateCount' => 0,
'iMode' => DEL_SILENT,
'sIssue' => null,
],
'Person' => [
'iDeleteCount' => 0,
'iUpdateCount' => 1,
'iMode' => 0,
'sIssue' => null,
],
],
],
];
}
//--- ExecuteDeletionPlan tests ---
/**
* Tests that ExecuteDeletionPlan throws a DataFeatureRemovalException when the deletion plan contains issues.
*/
public function testExecuteDeletionPlanThrowsExceptionWhenIssuesExist(): void
{
$this->RequireOnceItopFile('datamodels/2.x/combodo-data-feature-removal/src/Helper/DataFeatureRemovalException.php');
$oDeletionPlan = $this->createMock(DeletionPlan::class);
$oDeletionPlan->method('GetIssues')->willReturn(['Some issue']);
$oDeletionPlan->method('ListDeletes')->willReturn([]);
$oDeletionPlan->method('ListUpdates')->willReturn([]);
$oMockService = $this->getMockBuilder(DeletionPlanService::class)
->disableOriginalConstructor()
->onlyMethods(['GetDeletionPlan'])
->getMock();
$oMockService->method('GetDeletionPlan')->willReturn($oDeletionPlan);
$this->expectException(\Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException::class);
$this->expectExceptionMessage('Deletion Plan cannot be executed due to issues');
$oMockService->ExecuteDeletionPlan(['SomeClass']);
}
}