mirror of
https://github.com/Combodo/iTop.git
synced 2026-08-22 22:28:19 +02:00
N°3251 - Null in data synchro (fixes the regression introduced in a1f5d80)
- restore the initial behavior, thanks to hipska through PR#166 - refactor so as to make it clear that no SQL injection is possible (and will never be) - add PHPUnit tests on the data synchronization => up to 20s to execute - fix utils::ExeciTopScript to alow its usage within the automated test
This commit is contained in:
@@ -999,19 +999,19 @@ class utils
|
||||
throw new Exception("The path to php must not be empty. Please set a value for 'php_path' in your configuration file.");
|
||||
}
|
||||
|
||||
$sAuthUser = self::ReadParam('auth_user', '', 'raw_data');
|
||||
$sAuthPwd = self::ReadParam('auth_pwd', '', 'raw_data');
|
||||
$sParamFile = self::GetParamSourceFile('auth_user');
|
||||
if (is_null($sParamFile))
|
||||
{
|
||||
if (!isset($aArguments['auth_user'])) {
|
||||
$sAuthUser = self::ReadParam('auth_user', '', 'raw_data');
|
||||
$aArguments['auth_user'] = $sAuthUser;
|
||||
}
|
||||
if (!isset($aArguments['auth_pwd'])) {
|
||||
$sAuthPwd = self::ReadParam('auth_pwd', '', 'raw_data');
|
||||
$aArguments['auth_pwd'] = $sAuthPwd;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isset($aArguments['param_file'])) {
|
||||
$sParamFile = self::ReadParam('param_file', '', 'raw_data');
|
||||
$aArguments['param_file'] = $sParamFile;
|
||||
}
|
||||
|
||||
|
||||
$aArgs = array();
|
||||
foreach($aArguments as $sName => $value)
|
||||
{
|
||||
|
||||
@@ -534,11 +534,7 @@ try
|
||||
$aValues = array(); // Used to build the insert query
|
||||
foreach ($aRow as $iCol => $value)
|
||||
{
|
||||
if ($value === null) // Source CSV: "<NULL>"
|
||||
{
|
||||
$aValues[] = null;
|
||||
}
|
||||
elseif ($aIsDateToTransform[$iCol] !== false)
|
||||
if ($aIsDateToTransform[$iCol] !== false)
|
||||
{
|
||||
$bDateOnly = false;
|
||||
$sFormat = $sDateTimeFormat;
|
||||
@@ -550,7 +546,7 @@ try
|
||||
$sDate = ChangeDateFormat($value, $sFormat, $bDateOnly);
|
||||
if ($sDate === false)
|
||||
{
|
||||
$aValues[] = CMDBSource::Quote('');
|
||||
$aValues[] = '';
|
||||
if ($sOutput === 'details')
|
||||
{
|
||||
$oP->add("$iRow: Wrong format for {$aIsDateToTransform[$iCol]} column $iCol: '$value' does not match the expected format: '$sFormat' (column skipped)\n");
|
||||
@@ -558,15 +554,15 @@ try
|
||||
}
|
||||
else
|
||||
{
|
||||
$aValues[] = CMDBSource::Quote($sDate);
|
||||
$aValues[] = $sDate;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$aValues[] = CMDBSource::Quote($value);
|
||||
$aValues[] = $value;
|
||||
}
|
||||
}
|
||||
$sValues = implode(', ', $aValues);
|
||||
$sValues = implode(', ', CMDBSource::Quote($aValues));
|
||||
$sInsert = "INSERT INTO `$sTable` ($sInsertColumns) VALUES ($sValues)";
|
||||
try
|
||||
{
|
||||
|
||||
568
test/synchro/DataSynchroTest.php
Normal file
568
test/synchro/DataSynchroTest.php
Normal file
@@ -0,0 +1,568 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2013-2020 Combodo SARL
|
||||
* This file is part of iTop.
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* iTop is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Synchro;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use DBObjectSet;
|
||||
use DBSearch;
|
||||
use DBObjectSearch;
|
||||
use Exception;
|
||||
use MetaModel;
|
||||
use SynchroDataSource;
|
||||
use utils;
|
||||
use UserLocal;
|
||||
|
||||
|
||||
class DataSynchroTest extends ItopDataTestCase
|
||||
{
|
||||
protected const AUTH_USER = 'DataSynchroTest';
|
||||
protected const AUTH_PWD = 'sdf234(-fgh;,dfgDFG';
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Create the login account if it does not exist yet
|
||||
$oSearch = DBSearch::FromOQL('SELECT User WHERE login = "'.static::AUTH_USER.'"');
|
||||
$oSet = new DBObjectSet($oSearch);
|
||||
if ($oSet->Count() == 0)
|
||||
{
|
||||
$oProfileSearch = DBSearch::FromOQL('SELECT URP_Profiles WHERE name LIKE "administrator"');
|
||||
$oProfileSet = new DBObjectSet($oProfileSearch);
|
||||
$oAdminProfile = $oProfileSet->fetch();
|
||||
|
||||
$oUser = MetaModel::NewObject('UserLocal', array(
|
||||
'login' => static::AUTH_USER,
|
||||
'password' => static::AUTH_PWD,
|
||||
'expiration' => UserLocal::EXPIRE_NEVER,
|
||||
));
|
||||
$oProfiles = $oUser->Get('profile_list');
|
||||
$oProfiles->AddItem(MetaModel::NewObject('URP_UserProfile', array(
|
||||
'profileid' => $oAdminProfile->GetKey()
|
||||
)));
|
||||
$oUser->Set('profile_list', $oProfiles);
|
||||
$oUser->DBInsertNoReload();
|
||||
}
|
||||
}
|
||||
|
||||
protected function ExecSynchroImport($aParams)
|
||||
{
|
||||
$aParams['auth_user'] = static::AUTH_USER;
|
||||
$aParams['auth_pwd'] = static::AUTH_PWD;
|
||||
return utils::ExecITopScript('synchro/synchro_import.php', $aParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a series of data synchronization through the REST API
|
||||
*
|
||||
* @dataProvider SynchroScenariosProvider
|
||||
*
|
||||
* @param $sDescription
|
||||
* @param $sTargetClass
|
||||
* @param $aSourceProperties
|
||||
* @param $aSourceData
|
||||
* @param $aTargetData
|
||||
* @param $aAttributes
|
||||
*
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \CoreWarning
|
||||
* @throws \DictExceptionMissingString
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testSynchroImportPage($sDescription, $sTargetClass, $aSourceProperties, $aSourceData, $aTargetData, $aAttributes)
|
||||
{
|
||||
$sClass = $sTargetClass;
|
||||
|
||||
$aTargetAttributes = array_shift($aTargetData);
|
||||
$aSourceAttributes = array_shift($aSourceData);
|
||||
|
||||
if (count($aSourceData) + 1 != count($aTargetData))
|
||||
{
|
||||
throw new Exception("Target data must contain exactly ".(count($aSourceData) + 1)." items, found ".count($aTargetData));
|
||||
}
|
||||
|
||||
// Create the data source
|
||||
//
|
||||
$oDataSource = new SynchroDataSource();
|
||||
$oDataSource->Set('name', 'Test data sync '.time());
|
||||
$oDataSource->Set('description', 'unit test - created automatically');
|
||||
$oDataSource->Set('status', 'production');
|
||||
$oDataSource->Set('user_id', 0);
|
||||
$oDataSource->Set('scope_class', $sClass);
|
||||
foreach ($aSourceProperties as $sProperty => $value)
|
||||
{
|
||||
$oDataSource->Set($sProperty, $value);
|
||||
}
|
||||
$iDataSourceId = $oDataSource->DBInsert();
|
||||
|
||||
$oAttributeSet = $oDataSource->Get('attribute_list');
|
||||
while ($oAttribute = $oAttributeSet->Fetch())
|
||||
{
|
||||
if (array_key_exists($oAttribute->Get('attcode'), $aAttributes))
|
||||
{
|
||||
$aAttribInfo = $aAttributes[$oAttribute->Get('attcode')];
|
||||
if (array_key_exists('reconciliation_attcode', $aAttribInfo))
|
||||
{
|
||||
$oAttribute->Set('reconciliation_attcode', $aAttribInfo['reconciliation_attcode']);
|
||||
}
|
||||
$oAttribute->Set('update', $aAttribInfo['do_update']);
|
||||
$oAttribute->Set('reconcile', $aAttribInfo['do_reconcile']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$oAttribute->Set('update', false);
|
||||
$oAttribute->Set('reconcile', false);
|
||||
}
|
||||
$oAttribute->DBUpdate();
|
||||
}
|
||||
|
||||
// Prepare list of prefixes -> make sure objects are unique with regard to the reconciliation scheme
|
||||
$aPrefixes = array(); // attcode => prefix
|
||||
foreach($aSourceAttributes as $iDummy => $sAttCode)
|
||||
{
|
||||
$aPrefixes[$sAttCode] = ''; // init with something
|
||||
}
|
||||
foreach($aAttributes as $sAttCode => $aAttribInfo)
|
||||
{
|
||||
if (isset($aAttribInfo['automatic_prefix']) && $aAttribInfo['automatic_prefix'])
|
||||
{
|
||||
$aPrefixes[$sAttCode] = 'TEST_'.$iDataSourceId.'_';
|
||||
}
|
||||
}
|
||||
|
||||
// List existing objects (to be ignored in the analysis
|
||||
//
|
||||
$oAllObjects = new DBObjectSet(new DBObjectSearch($sClass));
|
||||
$aExisting = $oAllObjects->ToArray(true);
|
||||
$sExistingIds = implode(', ', array_keys($aExisting));
|
||||
|
||||
// Create the initial object list
|
||||
//
|
||||
$aInitialTarget = $aTargetData[0];
|
||||
foreach($aInitialTarget as $aObjFields)
|
||||
{
|
||||
$oNewTarget = MetaModel::NewObject($sClass);
|
||||
foreach($aTargetAttributes as $iAtt => $sAttCode)
|
||||
{
|
||||
$oNewTarget->Set($sAttCode, $aPrefixes[$sAttCode].$aObjFields[$iAtt]);
|
||||
}
|
||||
$oNewTarget->DBInsertNoReload();
|
||||
}
|
||||
|
||||
foreach($aTargetData as $iRow => $aExpectedObjects)
|
||||
{
|
||||
// Check the status (while ignoring existing objects)
|
||||
//
|
||||
if (empty($sExistingIds))
|
||||
{
|
||||
$oObjects = new DBObjectSet(DBObjectSearch::FromOQL("SELECT $sClass"));
|
||||
}
|
||||
else
|
||||
{
|
||||
$oObjects = new DBObjectSet(DBObjectSearch::FromOQL("SELECT $sClass WHERE id NOT IN($sExistingIds)"));
|
||||
}
|
||||
$aFound = $oObjects->ToArray();
|
||||
$aErrors_Unexpected = array();
|
||||
foreach($aFound as $iObj => $oObj)
|
||||
{
|
||||
// Is this object in the expected objects list
|
||||
$bFoundMatch = false;
|
||||
foreach($aExpectedObjects as $iExp => $aValues)
|
||||
{
|
||||
$bDoesMatch = true;
|
||||
foreach($aTargetAttributes as $iCol => $sAttCode)
|
||||
{
|
||||
if ($oObj->Get($sAttCode) != $aPrefixes[$sAttCode].$aValues[$iCol])
|
||||
{
|
||||
$bDoesMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($bDoesMatch)
|
||||
{
|
||||
$bFoundMatch = true;
|
||||
unset($aExpectedObjects[$iExp]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$bFoundMatch)
|
||||
{
|
||||
$aObjDesc = array();
|
||||
foreach($aTargetAttributes as $iCol => $sAttCode)
|
||||
{
|
||||
$aObjDesc[$sAttCode] = $oObj->Get($sAttCode);
|
||||
}
|
||||
$aErrors_Unexpected[get_class($oObj).'::'.$oObj->GetKey()] = $aObjDesc;
|
||||
}
|
||||
}
|
||||
|
||||
// Display the current status
|
||||
//
|
||||
$aErrors = array();
|
||||
if (count($aErrors_Unexpected) > 0) {
|
||||
$aErrors[] = "Unexpected objects found in iTop DB after step $iRow (starting at 0):\n".print_r($aErrors_Unexpected, true);
|
||||
}
|
||||
if (count($aExpectedObjects) > 0) {
|
||||
$aErrors[] = "Expected objects NOT found in iTop DB after step $iRow (starting at 0)\n".print_r($aExpectedObjects, true);
|
||||
}
|
||||
if (count($aErrors) > 0) {
|
||||
static::fail(implode("\n", $aErrors));
|
||||
}
|
||||
else {
|
||||
static::assertTrue(true);
|
||||
}
|
||||
|
||||
// If not on the final row, run a data exchange sequence
|
||||
//
|
||||
if (array_key_exists($iRow, $aSourceData))
|
||||
{
|
||||
$aToBeLoaded = $aSourceData[$iRow];
|
||||
|
||||
// First line
|
||||
$sCsvData = implode(';', $aSourceAttributes)."\n";
|
||||
|
||||
$sTextQualifier = '"';
|
||||
|
||||
foreach($aToBeLoaded as $aDataRow)
|
||||
{
|
||||
$aFinalData = array();
|
||||
foreach($aDataRow as $iCol => $value)
|
||||
{
|
||||
$sAttCode = $aSourceAttributes[$iCol];
|
||||
$sRawValue = $aPrefixes[$sAttCode].$value;
|
||||
|
||||
$sFrom = array("\r\n", $sTextQualifier);
|
||||
$sTo = array("\n", $sTextQualifier.$sTextQualifier);
|
||||
$sCSVValue = $sTextQualifier.str_replace($sFrom, $sTo, (string)$sRawValue).$sTextQualifier;
|
||||
|
||||
$aFinalData[] = $sCSVValue;
|
||||
}
|
||||
$sCsvData .= implode(';', $aFinalData)."\n";
|
||||
}
|
||||
$sCSVTmpFile = tempnam(sys_get_temp_dir(), "CSV");
|
||||
file_put_contents($sCSVTmpFile, $sCsvData);
|
||||
|
||||
$aParams = array(
|
||||
'csvfile' => $sCSVTmpFile,
|
||||
'data_source_id' => $iDataSourceId,
|
||||
'separator' => ';',
|
||||
'simulate' => 0,
|
||||
'output' => 'details',
|
||||
);
|
||||
list($iRetCode, $aOutputLines) = static::ExecSynchroImport($aParams);
|
||||
|
||||
unlink($sCSVTmpFile);
|
||||
|
||||
// Report the load results
|
||||
//
|
||||
if (strlen($sCsvData) > 5000)
|
||||
{
|
||||
$sCsvDataViewable = 'INPUT TOO LONG TO BE DISPLAYED ('.strlen($sCsvData).")\n".substr($sCsvData, 0, 500)."\n... TO BE CONTINUED";
|
||||
}
|
||||
else
|
||||
{
|
||||
$sCsvDataViewable = $sCsvData;
|
||||
}
|
||||
echo "Input Data:\n";
|
||||
echo $sCsvDataViewable;
|
||||
echo "\n";
|
||||
|
||||
$sResultsViewable = '| '.implode("\n| ", $aOutputLines);
|
||||
|
||||
echo "Results:\n";
|
||||
echo $sResultsViewable;
|
||||
echo "\n";
|
||||
|
||||
if ($iRetCode != 0)
|
||||
{
|
||||
static::fail("Execution of synchro_import failing with code '$iRetCode', see error.log for more details");
|
||||
}
|
||||
|
||||
if (stripos($sResultsViewable, 'exception') !== false)
|
||||
{
|
||||
self::fail('Encountered an Exception during the last import/synchro');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function SynchroScenariosProvider()
|
||||
{
|
||||
$aTestCases = array();
|
||||
$aTestCases['Load user logins'] = array(
|
||||
'desc' => 'Load user logins',
|
||||
'target_class' => 'UserLocal',
|
||||
'source_properties' => array(
|
||||
'full_load_periodicity' => 3600, // should be ignored in this case
|
||||
'reconciliation_policy' => 'use_attributes',
|
||||
'action_on_zero' => 'create',
|
||||
'action_on_one' => 'update',
|
||||
'action_on_multiple' => 'error',
|
||||
'delete_policy' => 'delete',
|
||||
'delete_policy_update' => '',
|
||||
'delete_policy_retention' => 0,
|
||||
),
|
||||
'source_data' => array(
|
||||
array('primary_key', 'login', 'password', 'profile_list'),
|
||||
array(
|
||||
array('user_A', 'login_A', 'password_A', 'profileid:10;reason:he/she is managing services'),
|
||||
),
|
||||
),
|
||||
'target_data' => array(
|
||||
array('login'),
|
||||
array(
|
||||
// Initial state
|
||||
),
|
||||
array(
|
||||
array('login_A'),
|
||||
),
|
||||
),
|
||||
'attributes' => array(
|
||||
'login' => array(
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'automatic_prefix' => true, // unique id (for unit testing)
|
||||
),
|
||||
'password' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
'profile_list' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
$aTestCases['Simple scenario with delete option (and extkey given as org/name)'] = array(
|
||||
'desc' => 'Simple scenario with delete option (and extkey given as org/name)',
|
||||
'target_class' => 'ApplicationSolution',
|
||||
'source_properties' => array(
|
||||
'full_load_periodicity' => 1,
|
||||
'reconciliation_policy' => 'use_attributes',
|
||||
'action_on_zero' => 'create',
|
||||
'action_on_one' => 'update',
|
||||
'action_on_multiple' => 'error',
|
||||
'delete_policy' => 'delete',
|
||||
'delete_policy_update' => '',
|
||||
'delete_policy_retention' => 0,
|
||||
),
|
||||
'source_data' => array(
|
||||
array('primary_key', 'org_id', 'name', 'status'),
|
||||
array(
|
||||
array('obj_A', '<NULL>', 'obj_A', 'active'), // org_id unchanged
|
||||
array('obj_B', '_DUMMY_', 'obj_B', 'active'), // error, '_DUMMY_' unknown
|
||||
array('obj_C', 'SOMECODE', 'obj_C', 'active'),
|
||||
array('obj_D', 'SOMECODE', 'obj_D', 'active'),
|
||||
array('obj_E', 'SOMECODE', 'obj_E', 'active'),
|
||||
),
|
||||
array(
|
||||
array('obj_D', 'SOMECODE', 'obj_D', 'inactive'),
|
||||
array('obj_E', 'SOMECODE', 'obj_E', '<NULL>'),
|
||||
),
|
||||
),
|
||||
'target_data' => array(
|
||||
array('org_id', 'name', 'status'),
|
||||
array(
|
||||
// Initial state
|
||||
array(2, 'obj_A', 'active'),
|
||||
array(2, 'obj_B', 'active'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'active'),
|
||||
array(2, 'obj_B', 'active'),
|
||||
array(1, 'obj_C', 'active'),
|
||||
array(1, 'obj_D', 'active'),
|
||||
array(1, 'obj_E', 'active'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'active'),
|
||||
array(2, 'obj_B', 'active'),
|
||||
array(1, 'obj_D', 'inactive'),
|
||||
array(1, 'obj_E', 'active'),
|
||||
),
|
||||
),
|
||||
'attributes' => array(
|
||||
'org_id' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
'reconciliation_attcode' => 'code',
|
||||
),
|
||||
'name' => array(
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'automatic_prefix' => true, // unique id
|
||||
),
|
||||
'status' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
$aTestCases['Update then delete with retention (to complete with manual testing) and reconciliation on org/name'] = array(
|
||||
'desc' => 'Update then delete with retention (to complete with manual testing) and reconciliation on org/name',
|
||||
'target_class' => 'ApplicationSolution',
|
||||
'source_properties' => array(
|
||||
'full_load_periodicity' => 0,
|
||||
'reconciliation_policy' => 'use_attributes',
|
||||
'action_on_zero' => 'create',
|
||||
'action_on_one' => 'update',
|
||||
'action_on_multiple' => 'error',
|
||||
'delete_policy' => 'update_then_delete',
|
||||
'delete_policy_update' => 'status:inactive',
|
||||
'delete_policy_retention' => 15,
|
||||
),
|
||||
'source_data' => array(
|
||||
array('primary_key', 'org_id', 'name', 'status'),
|
||||
array(
|
||||
array('obj_A', 'Demo', 'obj_A', 'active'),
|
||||
),
|
||||
array(
|
||||
),
|
||||
),
|
||||
'target_data' => array(
|
||||
array('org_id', 'name', 'status'),
|
||||
array(
|
||||
// Initial state
|
||||
),
|
||||
array(
|
||||
array(3, 'obj_A', 'active'),
|
||||
),
|
||||
array(
|
||||
array(3, 'obj_A', 'inactive'),
|
||||
// deleted !
|
||||
),
|
||||
),
|
||||
'attributes' => array(
|
||||
'org_id' => array(
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'reconciliation_attcode' => 'name',
|
||||
),
|
||||
'name' => array(
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'automatic_prefix' => true, // unique id
|
||||
),
|
||||
'status' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
$aTestCases['Simple scenario loading a few ApplicationSolution'] = array(
|
||||
'desc' => 'Simple scenario loading a few ApplicationSolution',
|
||||
'target_class' => 'ApplicationSolution',
|
||||
'source_properties' => array(
|
||||
'full_load_periodicity' => 0,
|
||||
'reconciliation_policy' => 'use_attributes',
|
||||
'action_on_zero' => 'create',
|
||||
'action_on_one' => 'update',
|
||||
'action_on_multiple' => 'error',
|
||||
'delete_policy' => 'update',
|
||||
'delete_policy_update' => 'status:inactive',
|
||||
'delete_policy_retention' => 0,
|
||||
|
||||
),
|
||||
'source_data' => array(
|
||||
array('primary_key', 'org_id', 'name', 'status'),
|
||||
array(
|
||||
array('obj_A', 2, 'obj_A', 'active'),
|
||||
array('obj_B', 2, 'obj_B', 'inactive'),
|
||||
array('obj_C', 2, 'obj_C', 'inactive'),
|
||||
),
|
||||
array(
|
||||
array('obj_A', 2, 'obj_A', 'active'),
|
||||
array('obj_B', 2, 'obj_B', 'inactive'),
|
||||
array('obj_C', 2, 'obj_C', 'inactive'),
|
||||
),
|
||||
array(
|
||||
array('obj_A', 2, 'obj_A', 'active'),
|
||||
array('obj_C', 2, 'obj_C', 'inactive'),
|
||||
array('obj_D', 2, 'obj_D', 'inactive'),
|
||||
),
|
||||
array(
|
||||
array('obj_C', 2, 'obj_C', 'active'),
|
||||
),
|
||||
array(
|
||||
array('obj_C', 2, 'obj_C', 'active'),
|
||||
),
|
||||
),
|
||||
'target_data' => array(
|
||||
array('org_id', 'name', 'status'),
|
||||
array(
|
||||
// Initial state
|
||||
array(2, 'obj_A', 'inactive'),
|
||||
array(2, 'obj_B', 'active'),
|
||||
array(2, 'obj_B', 'inactive'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'active'),
|
||||
array(2, 'obj_B', 'active'),
|
||||
array(2, 'obj_B', 'inactive'),
|
||||
array(2, 'obj_C', 'inactive'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'active'),
|
||||
array(2, 'obj_B', 'active'),
|
||||
array(2, 'obj_B', 'inactive'),
|
||||
array(2, 'obj_C', 'inactive'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'active'),
|
||||
array(2, 'obj_B', 'active'),
|
||||
array(2, 'obj_B', 'inactive'),
|
||||
array(2, 'obj_C', 'inactive'),
|
||||
array(2, 'obj_D', 'inactive'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'inactive'),
|
||||
array(2, 'obj_B', 'active'),
|
||||
array(2, 'obj_B', 'inactive'),
|
||||
array(2, 'obj_C', 'active'),
|
||||
array(2, 'obj_D', 'inactive'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'inactive'),
|
||||
array(2, 'obj_B', 'active'),
|
||||
array(2, 'obj_B', 'inactive'),
|
||||
array(2, 'obj_C', 'active'),
|
||||
array(2, 'obj_D', 'inactive'),
|
||||
),
|
||||
),
|
||||
'attributes' => array(
|
||||
'org_id' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
'name' => array(
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'automatic_prefix' => true, // unique id
|
||||
),
|
||||
'status' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
return $aTestCases;
|
||||
}
|
||||
}
|
||||
@@ -1414,546 +1414,6 @@ class TestImportRESTMassive extends TestImportREST
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Test data exchange
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class TestDataExchange extends TestBizModel
|
||||
{
|
||||
static public function GetName()
|
||||
{
|
||||
return 'Data exchange';
|
||||
}
|
||||
|
||||
static public function GetDescription()
|
||||
{
|
||||
return 'Test REST services: synchro_import and synchro_exec';
|
||||
}
|
||||
|
||||
protected function DoExecScenario($aSingleScenario)
|
||||
{
|
||||
echo "<div style=\"padding: 10;\">\n";
|
||||
echo "<h3 style=\"background-color: #ddddff; padding: 10;\">{$aSingleScenario['desc']}</h3>\n";
|
||||
|
||||
$sClass = $aSingleScenario['target_class'];
|
||||
|
||||
$aTargetData = $aSingleScenario['target_data'];
|
||||
$aSourceData = $aSingleScenario['source_data'];
|
||||
|
||||
$aTargetAttributes = array_shift($aTargetData);
|
||||
$aSourceAttributes = array_shift($aSourceData);
|
||||
|
||||
if (count($aSourceData) + 1 != count($aTargetData))
|
||||
{
|
||||
throw new Exception("Target data must contain exactly ".(count($aSourceData) + 1)." items, found ".count($aTargetData));
|
||||
}
|
||||
|
||||
// Create the data source
|
||||
//
|
||||
$oDataSource = new SynchroDataSource();
|
||||
$oDataSource->Set('name', 'Test data sync '.time());
|
||||
$oDataSource->Set('description', 'unit test - created automatically');
|
||||
$oDataSource->Set('status', 'production');
|
||||
$oDataSource->Set('user_id', 0);
|
||||
$oDataSource->Set('scope_class', $sClass);
|
||||
$oDataSource->Set('scope_restriction', '');
|
||||
$oDataSource->Set('full_load_periodicity', $aSingleScenario['full_load_periodicity']);
|
||||
$oDataSource->Set('reconciliation_policy', $aSingleScenario['reconciliation_policy']);
|
||||
$oDataSource->Set('action_on_zero', $aSingleScenario['action_on_zero']);
|
||||
$oDataSource->Set('action_on_one', $aSingleScenario['action_on_one']);
|
||||
$oDataSource->Set('action_on_multiple', $aSingleScenario['action_on_multiple']);
|
||||
$oDataSource->Set('delete_policy', $aSingleScenario['delete_policy']);
|
||||
$oDataSource->Set('delete_policy_update', $aSingleScenario['delete_policy_update']);
|
||||
$oDataSource->Set('delete_policy_retention', $aSingleScenario['delete_policy_retention']);
|
||||
$iDataSourceId = $this->ObjectToDB($oDataSource, true /* reload */);
|
||||
|
||||
$oAttributeSet = $oDataSource->Get('attribute_list');
|
||||
while ($oAttribute = $oAttributeSet->Fetch())
|
||||
{
|
||||
if (array_key_exists($oAttribute->Get('attcode'), $aSingleScenario['attributes']))
|
||||
{
|
||||
$aAttribInfo = $aSingleScenario['attributes'][$oAttribute->Get('attcode')];
|
||||
if (array_key_exists('reconciliation_attcode', $aAttribInfo))
|
||||
{
|
||||
$oAttribute->Set('reconciliation_attcode', $aAttribInfo['reconciliation_attcode']);
|
||||
}
|
||||
$oAttribute->Set('update', $aAttribInfo['do_update']);
|
||||
$oAttribute->Set('reconcile', $aAttribInfo['do_reconcile']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$oAttribute->Set('update', false);
|
||||
$oAttribute->Set('reconcile', false);
|
||||
}
|
||||
$this->UpdateObjectInDB($oAttribute);
|
||||
}
|
||||
|
||||
// Prepare list of prefixes -> make sure objects are unique with regard to the reconciliation scheme
|
||||
$aPrefixes = array(); // attcode => prefix
|
||||
foreach($aSourceAttributes as $iDummy => $sAttCode)
|
||||
{
|
||||
$aPrefixes[$sAttCode] = ''; // init with something
|
||||
}
|
||||
foreach($aSingleScenario['attributes'] as $sAttCode => $aAttribInfo)
|
||||
{
|
||||
if (isset($aAttribInfo['automatic_prefix']) && $aAttribInfo['automatic_prefix'])
|
||||
{
|
||||
$aPrefixes[$sAttCode] = 'TEST_'.$iDataSourceId.'_';
|
||||
}
|
||||
}
|
||||
|
||||
// List existing objects (to be ignored in the analysis
|
||||
//
|
||||
$oAllObjects = new DBObjectSet(new DBObjectSearch($sClass));
|
||||
$aExisting = $oAllObjects->ToArray(true);
|
||||
$sExistingIds = implode(', ', array_keys($aExisting));
|
||||
|
||||
// Create the initial object list
|
||||
//
|
||||
$aInitialTarget = $aTargetData[0];
|
||||
foreach($aInitialTarget as $aObjFields)
|
||||
{
|
||||
$oNewTarget = MetaModel::NewObject($sClass);
|
||||
foreach($aTargetAttributes as $iAtt => $sAttCode)
|
||||
{
|
||||
$oNewTarget->Set($sAttCode, $aPrefixes[$sAttCode].$aObjFields[$iAtt]);
|
||||
}
|
||||
$this->ObjectToDB($oNewTarget);
|
||||
}
|
||||
|
||||
foreach($aTargetData as $iRow => $aExpectedObjects)
|
||||
{
|
||||
sleep(2);
|
||||
// Check the status (while ignoring existing objects)
|
||||
//
|
||||
if (empty($sExistingIds))
|
||||
{
|
||||
$oObjects = new DBObjectSet(DBObjectSearch::FromOQL("SELECT $sClass"));
|
||||
}
|
||||
else
|
||||
{
|
||||
$oObjects = new DBObjectSet(DBObjectSearch::FromOQL("SELECT $sClass WHERE id NOT IN($sExistingIds)"));
|
||||
}
|
||||
$aFound = $oObjects->ToArray();
|
||||
$aErrors_Unexpected = array();
|
||||
foreach($aFound as $iObj => $oObj)
|
||||
{
|
||||
// Is this object in the expected objects list
|
||||
$bFoundMatch = false;
|
||||
foreach($aExpectedObjects as $iExp => $aValues)
|
||||
{
|
||||
$bDoesMatch = true;
|
||||
foreach($aTargetAttributes as $iCol => $sAttCode)
|
||||
{
|
||||
if ($oObj->Get($sAttCode) != $aPrefixes[$sAttCode].$aValues[$iCol])
|
||||
{
|
||||
$bDoesMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($bDoesMatch)
|
||||
{
|
||||
$bFoundMatch = true;
|
||||
unset($aExpectedObjects[$iExp]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$bFoundMatch)
|
||||
{
|
||||
$aErrors_Unexpected[] = $oObj->GetKey();
|
||||
}
|
||||
}
|
||||
|
||||
// Display the current status
|
||||
//
|
||||
echo "<p>Status at step $iRow</p>\n";
|
||||
$aCurrentDataSet = array();
|
||||
foreach($aFound as $iObj => $oObj)
|
||||
{
|
||||
$aObjDesc = array(
|
||||
'Status' => (in_array($iObj, $aErrors_Unexpected) ? 'unexpected' : 'ok'),
|
||||
'Object' => $oObj->GetHyperLink()
|
||||
);
|
||||
foreach($aTargetAttributes as $iCol => $sAttCode)
|
||||
{
|
||||
$aObjDesc[$sAttCode] = $oObj->Get($sAttCode);
|
||||
}
|
||||
$aCurrentDataSet[] = $aObjDesc;
|
||||
}
|
||||
if (count($aExpectedObjects) > 0)
|
||||
{
|
||||
foreach($aExpectedObjects as $iExp => $aValues)
|
||||
{
|
||||
$aObjDesc = array(
|
||||
'Status' => 'missing',
|
||||
'Object' => 'n/a'
|
||||
);
|
||||
foreach($aTargetAttributes as $iCol => $sAttCode)
|
||||
{
|
||||
$aObjDesc[$sAttCode] = $aPrefixes[$sAttCode].$aValues[$iCol];
|
||||
}
|
||||
$aCurrentDataSet[] = $aObjDesc;
|
||||
}
|
||||
}
|
||||
echo MyHelpers::make_table_from_assoc_array($aCurrentDataSet);
|
||||
|
||||
if ((count($aErrors_Unexpected) > 0) || (count($aExpectedObjects) > 0))
|
||||
{
|
||||
throw new UnitTestException("The current status in iTop does not match the expectations");
|
||||
}
|
||||
|
||||
// If not on the final row, run a data exchange sequence
|
||||
//
|
||||
if (array_key_exists($iRow, $aSourceData))
|
||||
{
|
||||
$aToBeLoaded = $aSourceData[$iRow];
|
||||
|
||||
$sCsvData = implode(';', $aSourceAttributes)."\n";
|
||||
foreach($aToBeLoaded as $aDataRow)
|
||||
{
|
||||
$aFinalData = array();
|
||||
foreach($aDataRow as $iCol => $value)
|
||||
{
|
||||
if (is_null($value))
|
||||
{
|
||||
$aFinalData[] = '<NULL>';
|
||||
}
|
||||
else
|
||||
{
|
||||
$sAttCode = $aSourceAttributes[$iCol];
|
||||
$aFinalData[] = $aPrefixes[$sAttCode].$value;
|
||||
}
|
||||
}
|
||||
$sCsvData .= implode(';', $aFinalData)."\n";
|
||||
}
|
||||
$aPostData = array('csvdata' => $sCsvData);
|
||||
|
||||
$aImportArgs = array(
|
||||
'data_source_id' => $iDataSourceId,
|
||||
'separator' => ';',
|
||||
'simulate' => 0,
|
||||
'output' => 'details',
|
||||
);
|
||||
|
||||
$aGetParams = array();
|
||||
$aGetParamReport = array();
|
||||
foreach($aImportArgs as $sArg => $sValue)
|
||||
{
|
||||
$aGetParams[] = $sArg.'='.urlencode($sValue);
|
||||
$aGetParamReport[] = $sArg.'='.$sValue;
|
||||
}
|
||||
$sGetParams = implode('&', $aGetParams);
|
||||
$sLogin = isset($aSingleScenario['login']) ? $aSingleScenario['login'] : 'admin';
|
||||
$sPassword = isset($aSingleScenario['password']) ? $aSingleScenario['password'] : 'admin';
|
||||
|
||||
$sRes = self::DoPostRequestAuth('../synchro/synchro_import.php?'.$sGetParams, $aPostData, $sLogin, $sPassword);
|
||||
|
||||
// Report the load results
|
||||
//
|
||||
if (strlen($sCsvData) > 5000)
|
||||
{
|
||||
$sCsvDataViewable = 'INPUT TOO LONG TO BE DISPLAYED ('.strlen($sCsvData).")\n".substr($sCsvData, 0, 500)."\n... TO BE CONTINUED";
|
||||
}
|
||||
else
|
||||
{
|
||||
$sCsvDataViewable = $sCsvData;
|
||||
}
|
||||
$sCsvDataViewable = htmlentities($sCsvDataViewable, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
echo "<div style=\"\">\n";
|
||||
echo " <pre class=\"vardump\">$sCsvDataViewable</pre>\n";
|
||||
echo "</div>\n";
|
||||
|
||||
echo "<pre class=\"vardump\" style=\"clear: both; padding: 15; background-color: black; color: green;\">$sRes</pre>\n";
|
||||
if (stripos($sRes, 'exception') !== false)
|
||||
{
|
||||
throw new UnitTestException('Encountered an Exception during the last import/synchro');
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
protected function DoExecute()
|
||||
{
|
||||
/*
|
||||
$aScenarios = array(
|
||||
array(
|
||||
'desc' => 'Load user logins',
|
||||
'login' => 'admin',
|
||||
'password' => 'admin',
|
||||
'target_class' => 'UserLocal',
|
||||
'full_load_periodicity' => 3600, // should be ignored in this case
|
||||
'reconciliation_policy' => 'use_attributes',
|
||||
'action_on_zero' => 'create',
|
||||
'action_on_one' => 'update',
|
||||
'action_on_multiple' => 'error',
|
||||
'delete_policy' => 'delete',
|
||||
'delete_policy_update' => '',
|
||||
'delete_policy_retention' => 0,
|
||||
'source_data' => array(
|
||||
array('primary_key', 'login', 'password', 'profile_list'),
|
||||
array(
|
||||
array('user_A', 'login_A', 'password_A', 'profileid:10;reason:he/she is managing services'),
|
||||
),
|
||||
),
|
||||
'target_data' => array(
|
||||
array('login'),
|
||||
array(
|
||||
// Initial state
|
||||
),
|
||||
array(
|
||||
array('login_A'),
|
||||
),
|
||||
),
|
||||
'attributes' => array(
|
||||
'login' => array(
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'automatic_prefix' => true, // unique id (for unit testing)
|
||||
),
|
||||
'password' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
'profile_list' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
*/
|
||||
$aScenarios = array(
|
||||
array(
|
||||
'desc' => 'Simple scenario with delete option (and extkey given as org/name)',
|
||||
'login' => 'admin',
|
||||
'password' => 'admin',
|
||||
'target_class' => 'ApplicationSolution',
|
||||
'full_load_periodicity' => 3600, // should be ignored in this case
|
||||
'reconciliation_policy' => 'use_attributes',
|
||||
'action_on_zero' => 'create',
|
||||
'action_on_one' => 'update',
|
||||
'action_on_multiple' => 'error',
|
||||
'delete_policy' => 'delete',
|
||||
'delete_policy_update' => '',
|
||||
'delete_policy_retention' => 0,
|
||||
'source_data' => array(
|
||||
array('primary_key', 'org_id', 'name', 'status'),
|
||||
array(
|
||||
array('obj_A', null, 'obj_A', 'production'), // org_id unchanged
|
||||
array('obj_B', '_DUMMY_', 'obj_B', 'production'), // error, '_DUMMY_' unknown
|
||||
array('obj_C', 'SOMECODE', 'obj_C', 'production'),
|
||||
array('obj_D', null, 'obj_D', 'production'),
|
||||
array('obj_E', '_DUMMY_', 'obj_E', 'production'),
|
||||
),
|
||||
array(
|
||||
),
|
||||
array(
|
||||
),
|
||||
),
|
||||
'target_data' => array(
|
||||
array('org_id', 'name', 'status'),
|
||||
array(
|
||||
// Initial state
|
||||
array(2, 'obj_A', 'production'),
|
||||
array(2, 'obj_B', 'production'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'production'),
|
||||
array(2, 'obj_B', 'production'),
|
||||
array(1, 'obj_C', 'production'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'production'),
|
||||
array(2, 'obj_B', 'production'),
|
||||
// deleted !
|
||||
),
|
||||
// The only diff here is into the log
|
||||
array(
|
||||
array(2, 'obj_A', 'production'),
|
||||
array(2, 'obj_B', 'production'),
|
||||
// deleted !
|
||||
),
|
||||
),
|
||||
'attributes' => array(
|
||||
'org_id' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
'reconciliation_attcode' => 'code',
|
||||
),
|
||||
'name' => array(
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'automatic_prefix' => true, // unique id
|
||||
),
|
||||
'status' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
// );
|
||||
// $aXXXXScenarios = array(
|
||||
array(
|
||||
'desc' => 'Update then delete with retention (to complete with manual testing) and reconciliation on org/name',
|
||||
'login' => 'admin',
|
||||
'password' => 'admin',
|
||||
'target_class' => 'ApplicationSolution',
|
||||
'full_load_periodicity' => 3600,
|
||||
'reconciliation_policy' => 'use_attributes',
|
||||
'action_on_zero' => 'create',
|
||||
'action_on_one' => 'update',
|
||||
'action_on_multiple' => 'error',
|
||||
'delete_policy' => 'update_then_delete',
|
||||
'delete_policy_update' => 'status:obsolete',
|
||||
'delete_policy_retention' => 15,
|
||||
'source_data' => array(
|
||||
array('primary_key', 'org_id', 'name', 'status'),
|
||||
array(
|
||||
array('obj_A', 'Demo', 'obj_A', 'production'),
|
||||
),
|
||||
array(
|
||||
),
|
||||
),
|
||||
'target_data' => array(
|
||||
array('org_id', 'name', 'status'),
|
||||
array(
|
||||
// Initial state
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'production'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'obsolete'),
|
||||
// deleted !
|
||||
),
|
||||
),
|
||||
'attributes' => array(
|
||||
'org_id' => array(
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'reconciliation_attcode' => 'name',
|
||||
),
|
||||
'name' => array(
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'automatic_prefix' => true, // unique id
|
||||
),
|
||||
'status' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
//);
|
||||
//$aXXScenarios = array(
|
||||
array(
|
||||
'desc' => 'Simple scenario loading a few ApplicationSolution',
|
||||
'login' => 'admin',
|
||||
'password' => 'admin',
|
||||
'target_class' => 'ApplicationSolution',
|
||||
'full_load_periodicity' => 3600,
|
||||
'reconciliation_policy' => 'use_attributes',
|
||||
'action_on_zero' => 'create',
|
||||
'action_on_one' => 'update',
|
||||
'action_on_multiple' => 'error',
|
||||
'delete_policy' => 'update',
|
||||
'delete_policy_update' => 'status:obsolete',
|
||||
'delete_policy_retention' => 0,
|
||||
'source_data' => array(
|
||||
array('primary_key', 'org_id', 'name', 'status'),
|
||||
array(
|
||||
array('obj_A', 2, 'obj_A', 'production'),
|
||||
array('obj_B', 2, 'obj_B', 'implementation'),
|
||||
array('obj_C', 2, 'obj_C', 'implementation'),
|
||||
),
|
||||
array(
|
||||
array('obj_A', 2, 'obj_A', 'production'),
|
||||
array('obj_B', 2, 'obj_B', 'implementation'),
|
||||
array('obj_C', 2, 'obj_C', 'implementation'),
|
||||
),
|
||||
array(
|
||||
array('obj_A', 2, 'obj_A', 'production'),
|
||||
array('obj_C', 2, 'obj_C', 'implementation'),
|
||||
array('obj_D', 2, 'obj_D', 'implementation'),
|
||||
),
|
||||
array(
|
||||
array('obj_C', 2, 'obj_C', 'production'),
|
||||
),
|
||||
array(
|
||||
array('obj_C', 2, 'obj_C', 'production'),
|
||||
),
|
||||
),
|
||||
'target_data' => array(
|
||||
array('org_id', 'name', 'status'),
|
||||
array(
|
||||
// Initial state
|
||||
array(2, 'obj_A', 'implementation'),
|
||||
array(2, 'obj_B', 'production'),
|
||||
array(2, 'obj_B', 'implementation'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'production'),
|
||||
array(2, 'obj_B', 'production'),
|
||||
array(2, 'obj_B', 'implementation'),
|
||||
array(2, 'obj_C', 'implementation'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'production'),
|
||||
array(2, 'obj_B', 'production'),
|
||||
array(2, 'obj_B', 'implementation'),
|
||||
array(2, 'obj_C', 'implementation'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'production'),
|
||||
array(2, 'obj_B', 'production'),
|
||||
array(2, 'obj_B', 'implementation'),
|
||||
array(2, 'obj_C', 'implementation'),
|
||||
array(2, 'obj_D', 'implementation'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'obsolete'),
|
||||
array(2, 'obj_B', 'production'),
|
||||
array(2, 'obj_B', 'implementation'),
|
||||
array(2, 'obj_C', 'production'),
|
||||
array(2, 'obj_D', 'obsolete'),
|
||||
),
|
||||
array(
|
||||
array(2, 'obj_A', 'obsolete'),
|
||||
array(2, 'obj_B', 'production'),
|
||||
array(2, 'obj_B', 'implementation'),
|
||||
array(2, 'obj_C', 'production'),
|
||||
array(2, 'obj_D', 'obsolete'),
|
||||
),
|
||||
),
|
||||
'attributes' => array(
|
||||
'org_id' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
'name' => array(
|
||||
'do_reconcile' => true,
|
||||
'do_update' => true,
|
||||
'automatic_prefix' => true, // unique id
|
||||
),
|
||||
'status' => array(
|
||||
'do_reconcile' => false,
|
||||
'do_update' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($aScenarios as $aSingleScenario)
|
||||
{
|
||||
$this->DoExecScenario($aSingleScenario);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Test SOAP services
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Reference in New Issue
Block a user