From 85e28931f5a915db4bd8f7bfef78a68432d25c7a Mon Sep 17 00:00:00 2001 From: odain Date: Wed, 17 Dec 2025 18:11:34 +0100 Subject: [PATCH] =?UTF-8?q?N=C2=B08981:=20prepare=20hub=20connector=20test?= =?UTF-8?q?=20cover?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sdk test enhancement : add call itop api fix ci ci: fix broken tests ci: cover hub setup on compile and launch steps code style ci: fix ModuleDiscoveryTest redundant class + add logs to investigate ci setup issues ci: fix log during setup tests --- datamodels/2.x/itop-hub-connector/ajax.php | 269 +--------------- .../src/Controller/HubController.php | 300 ++++++++++++++++++ .../src/Model/DBBackupWithErrorReporting.php | 37 +++ .../hubruntimeenvironment.class.inc.php | 13 + sources/Application/WebPage/JsonPage.php | 19 +- .../itop-hub-connector/HubControllerTest.php | 83 +++++ .../Service/UnitTestRunTimeEnvironment.php | 10 +- .../modulediscovery/ModuleDiscoveryTest.php | 50 --- .../setup/ModuleDiscoveryTest.php | 34 ++ 9 files changed, 504 insertions(+), 311 deletions(-) create mode 100644 datamodels/2.x/itop-hub-connector/src/Controller/HubController.php create mode 100644 datamodels/2.x/itop-hub-connector/src/Model/DBBackupWithErrorReporting.php rename datamodels/2.x/itop-hub-connector/{ => src/setup}/hubruntimeenvironment.class.inc.php (94%) create mode 100644 tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php delete mode 100644 tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php diff --git a/datamodels/2.x/itop-hub-connector/ajax.php b/datamodels/2.x/itop-hub-connector/ajax.php index b7fe109b7..23559f9e5 100644 --- a/datamodels/2.x/itop-hub-connector/ajax.php +++ b/datamodels/2.x/itop-hub-connector/ajax.php @@ -24,129 +24,13 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ -use Combodo\iTop\Application\WebPage\JsonPage; +use Combodo\iTop\HubConnector\Controller\HubController; require_once(APPROOT.'application/utils.inc.php'); require_once(APPROOT.'core/log.class.inc.php'); IssueLog::Enable(APPROOT.'log/error.log'); -require_once(APPROOT.'setup/runtimeenv.class.inc.php'); -require_once(APPROOT.'setup/backup.class.inc.php'); -require_once(APPROOT.'core/mutex.class.inc.php'); -require_once(APPROOT.'core/dict.class.inc.php'); -require_once(APPROOT.'setup/xmldataloader.class.inc.php'); -require_once(__DIR__.'/hubruntimeenvironment.class.inc.php'); - -/** - * Overload of DBBackup to handle logging - */ -class DBBackupWithErrorReporting extends DBBackup -{ - protected $aInfos = []; - - protected $aErrors = []; - - protected function LogInfo($sMsg) - { - $aInfos[] = $sMsg; - } - - protected function LogError($sMsg) - { - IssueLog::Error($sMsg); - $aErrors[] = $sMsg; - } - - public function GetInfos() - { - return $this->aInfos; - } - - public function GetErrors() - { - return $this->aErrors; - } -} - -/** - * - * @param string $sTargetFile - * @throws Exception - * @return DBBackupWithErrorReporting - */ -function DoBackup($sTargetFile) -{ - // Make sure the target directory exists - $sBackupDir = dirname($sTargetFile); - SetupUtils::builddir($sBackupDir); - - $oBackup = new DBBackupWithErrorReporting(); - $oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '')); - $sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE; - - $oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment()); - $oMutex->Lock(); - try { - $oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile); - } catch (Exception $e) { - $oMutex->Unlock(); - throw $e; - } - $oMutex->Unlock(); - return $oBackup; -} - -/** - * Outputs the status of the current ajax execution (as a JSON structure) - * - * @param string $sMessage - * @param bool $bSuccess - * @param number $iErrorCode - * @param array $aMoreFields - * Extra fields to pass to the caller, if needed - */ -function ReportStatus($sMessage, $bSuccess, $iErrorCode = 0, $aMoreFields = []) -{ - // Do not use AjaxPage during setup phases, because it uses InterfaceDiscovery in Twig compilation - $oPage = new JsonPage(); - $aResult = [ - 'code' => $iErrorCode, - 'message' => $sMessage, - 'fields' => $aMoreFields, - ]; - $oPage->SetData($aResult); - $oPage->SetOutputDataOnly(true); - $oPage->output(); -} - -/** - * Helper to output the status of a successful execution - * - * @param string $sMessage - * @param array $aMoreFields - * Extra fields to pass to the caller, if needed - */ -function ReportSuccess($sMessage, $aMoreFields = []) -{ - ReportStatus($sMessage, true, 0, $aMoreFields); -} - -/** - * Helper to output the status of a failed execution - * - * @param string $sMessage - * @param number $iErrorCode - * @param array $aMoreFields - * Extra fields to pass to the caller, if needed - */ -function ReportError($sMessage, $iErrorCode, $aMoreFields = []) -{ - if ($iErrorCode == 0) { - // 0 means no error, so change it if no meaningful error code is supplied - $iErrorCode = -1; - } - ReportStatus($sMessage, false, $iErrorCode, $aMoreFields); -} +require_once(__DIR__.'/src/Controller/HubController.php'); try { SetupUtils::ExitMaintenanceMode(false); // Reset maintenance mode in case of problem @@ -183,7 +67,7 @@ try { foreach ($aChecks as $oCheckResult) { if ($oCheckResult->iSeverity == CheckResult::ERROR) { $bFailed = true; - ReportError($oCheckResult->sLabel, -2); + HubController::GetInstance()->ReportError($oCheckResult->sLabel, -2); } } if (!$bFailed) { @@ -191,160 +75,27 @@ try { $fFreeSpace = SetupUtils::CheckDiskSpace($sDBBackupPath); if ($fFreeSpace !== false) { $sMessage = Dict::Format('iTopHub:BackupFreeDiskSpaceIn', SetupUtils::HumanReadableSize($fFreeSpace), dirname($sDBBackupPath)); - ReportSuccess($sMessage); + HubController::GetInstance()->ReportSuccess($sMessage); } else { - ReportError(Dict::S('iTopHub:FailedToCheckFreeDiskSpace'), -1); + HubController::GetInstance()->ReportError(Dict::S('iTopHub:FailedToCheckFreeDiskSpace'), -1); } } break; case 'do_backup': - require_once(APPROOT.'/application/startup.inc.php'); - require_once(APPROOT.'/application/loginwebpage.class.inc.php'); - LoginWebPage::DoLogin(true); // Check user rights and prompt if needed (must be admin) - - try { - if (MetaModel::GetConfig()->Get('demo_mode')) { - throw new Exception('Sorry the installation of extensions is not allowed in demo mode'); - } - SetupLog::Info('Backup starts...'); - set_time_limit(0); - $sBackupPath = APPROOT.'/data/backups/manual/backup-'; - $iSuffix = 1; - $sSuffix = ''; - // Generate a unique name... - do { - $sBackupFile = $sBackupPath.date('Y-m-d-His').$sSuffix; - $sSuffix = '-'.$iSuffix; - $iSuffix++ ; - } while (file_exists($sBackupFile)); - - $oBackup = DoBackup($sBackupFile); - $aErrors = $oBackup->GetErrors(); - if (count($aErrors) > 0) { - SetupLog::Error('Backup failed.'); - SetupLog::Error(implode("\n", $aErrors)); - ReportError(Dict::S('iTopHub:BackupFailed'), -1, $aErrors); - } else { - SetupLog::Info('Backup successfully completed.'); - ReportSuccess(Dict::S('iTopHub:BackupOk')); - } - } catch (Exception $e) { - SetupLog::Error($e->getMessage()); - ReportError($e->getMessage(), $e->getCode()); - } + HubController::GetInstance()->LaunchBackup(); break; case 'compile': - SetupLog::Info('Deployment starts...'); - $sAuthent = utils::ReadParam('authent', '', false, 'raw_data'); - if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) { - throw new SecurityException(Dict::S('iTopHub:FailAuthent')); - } - // First step: prepare the datamodel, if it fails, roll-back - $aSelectedExtensionCodes = utils::ReadParam('extension_codes', []); - $aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []); - - $oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build - $oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs); - - $oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE); - if ($oConfig->Get('demo_mode')) { - throw new Exception('Sorry the installation of extensions is not allowed in demo mode'); - } - - $aSelectModules = $oRuntimeEnv->CompileFrom('production', false); // WARNING symlinks does not seem to be compatible with manual Commit - - $oRuntimeEnv->UpdateIncludes($oConfig); - - $oRuntimeEnv->InitDataModel($oConfig, true /* model only */); - - // Safety check: check the inter dependencies, will throw an exception in case of inconsistency - $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true); - - $oRuntimeEnv->CheckMetaModel(); // Will throw an exception if a problem is detected - - // Everything seems Ok so far, commit in env-production! - $oRuntimeEnv->WriteConfigFileSafe($oConfig); - $oRuntimeEnv->Commit(); - - // Report the success in a way that will be detected by the ajax caller - SetupLog::Info('Compilation completed...'); - ReportSuccess('Ok'); // No access to Dict::S here + HubController::GetInstance()->LaunchCompile(); break; case 'move_to_production': - // Second step: update the schema and the data - // Everything happening below is based on env-production - $oRuntimeEnv = new RunTimeEnvironment('production', true); - - try { - SetupLog::Info('Move to production starts...'); - $sAuthent = utils::ReadParam('authent', '', false, 'raw_data'); - if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) { - throw new SecurityException(Dict::S('iTopHub:FailAuthent')); - } - unlink(utils::GetDataPath().'hub/compile_authent'); - // Load the "production" config file to clone & update it - $oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE); - SetupUtils::EnterReadOnlyMode($oConfig); - - $oRuntimeEnv->InitDataModel($oConfig, true /* model only */); - - $aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true); - - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation'); - - $oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade'); - - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation'); - - $oRuntimeEnv->UpdatePredefinedObjects(); - - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup'); - - $oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/); - - $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad'); - - // Record the installation so that the "about box" knows about the installed modules - $sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion(); - - $oExtensionsMap = new iTopExtensionsMap(); - - // Default choices = as before - $oExtensionsMap->LoadChoicesFromDatabase($oConfig); - foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) { - // Plus all "remote" extensions - if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) { - $oExtensionsMap->MarkAsChosen($oExtension->sCode); - } - } - $aSelectedExtensionCodes = []; - foreach ($oExtensionsMap->GetChoices() as $oExtension) { - $aSelectedExtensionCodes[] = $oExtension->sCode; - } - $aSelectedExtensions = $oExtensionsMap->GetChoices(); - $oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aAvailableModules, $aSelectedExtensionCodes, 'Done by the iTop Hub Connector'); - - // Report the success in a way that will be detected by the ajax caller - SetupLog::Info('Deployment successfully completed.'); - ReportSuccess(Dict::S('iTopHub:CompiledOK')); - } catch (Exception $e) { - if (file_exists(utils::GetDataPath().'hub/compile_authent')) { - unlink(utils::GetDataPath().'hub/compile_authent'); - } - // Note: at this point, the dictionnary is not necessarily loaded - SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage()); - SetupLog::Error('Debug trace: '.$e->getTraceAsString()); - ReportError($e->getMessage(), $e->getCode()); - } finally { - SetupUtils::ExitReadOnlyMode(); - } + HubController::GetInstance()->LaunchDeploy(); break; default: - ReportError("Invalid operation: '$sOperation'", -1); + HubController::GetInstance()->ReportError("Invalid operation: '$sOperation'", -1); } } catch (Exception $e) { SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage()); @@ -352,5 +103,5 @@ try { utils::PopArchiveMode(); - ReportError($e->getMessage(), $e->getCode()); + HubController::GetInstance()->ReportError($e->getMessage(), $e->getCode()); } diff --git a/datamodels/2.x/itop-hub-connector/src/Controller/HubController.php b/datamodels/2.x/itop-hub-connector/src/Controller/HubController.php new file mode 100644 index 000000000..fc0fccdae --- /dev/null +++ b/datamodels/2.x/itop-hub-connector/src/Controller/HubController.php @@ -0,0 +1,300 @@ +Get('demo_mode')) { + throw new Exception('Sorry the installation of extensions is not allowed in demo mode'); + } + SetupLog::Info('Backup starts...'); + set_time_limit(0); + $sBackupPath = APPROOT.'/data/backups/manual/backup-'; + $iSuffix = 1; + $sSuffix = ''; + // Generate a unique name... + do { + $sBackupFile = $sBackupPath.date('Y-m-d-His').$sSuffix; + $sSuffix = '-'.$iSuffix; + $iSuffix++ ; + } while (file_exists($sBackupFile)); + + $oBackup = $this->DoBackup($sBackupFile); + $aErrors = $oBackup->GetErrors(); + if (count($aErrors) > 0) { + SetupLog::Error('Backup failed.'); + SetupLog::Error(implode("\n", $aErrors)); + $this->ReportError(Dict::S('iTopHub:BackupFailed'), -1, $aErrors); + } else { + SetupLog::Info('Backup successfully completed.'); + $this->ReportSuccess(Dict::S('iTopHub:BackupOk')); + } + } catch (Exception $e) { + SetupLog::Error($e->getMessage()); + $this->ReportError($e->getMessage(), $e->getCode()); + } + } + + /** + * + * @param string $sTargetFile + * @throws Exception + * @return DBBackupWithErrorReporting + */ + public function DoBackup($sTargetFile): DBBackupWithErrorReporting + { + // Make sure the target directory exists + $sBackupDir = dirname($sTargetFile); + SetupUtils::builddir($sBackupDir); + + $oBackup = new DBBackupWithErrorReporting(); + $oBackup->SetMySQLBinDir(MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '')); + $sSourceConfigFile = APPCONF.utils::GetCurrentEnvironment().'/'.ITOP_CONFIG_FILE; + + $oMutex = new iTopMutex('backup.'.utils::GetCurrentEnvironment()); + $oMutex->Lock(); + try { + $oBackup->CreateCompressedBackup($sTargetFile, $sSourceConfigFile); + } catch (Exception $e) { + $oMutex->Unlock(); + throw $e; + } + $oMutex->Unlock(); + return $oBackup; + } + + public function LaunchCompile() + { + SetupLog::Info('Deployment starts...'); + $sAuthent = utils::ReadParam('authent', '', false, 'raw_data'); + if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) { + throw new SecurityException(Dict::S('iTopHub:FailAuthent')); + } + // First step: prepare the datamodel, if it fails, roll-back + $aSelectedExtensionCodes = utils::ReadParam('extension_codes', []); + $aSelectedExtensionDirs = utils::ReadParam('extension_dirs', []); + + $oRuntimeEnv = new HubRunTimeEnvironment('production', false); // use a temp environment: production-build + $oRuntimeEnv->MoveSelectedExtensions(APPROOT.'/data/downloaded-extensions/', $aSelectedExtensionDirs); + + $oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE); + if ($oConfig->Get('demo_mode')) { + throw new Exception('Sorry the installation of extensions is not allowed in demo mode'); + } + + $aSelectModules = $oRuntimeEnv->CompileFrom('production'); // WARNING symlinks does not seem to be compatible with manual Commit + + $oRuntimeEnv->UpdateIncludes($oConfig); + + $oRuntimeEnv->InitDataModel($oConfig, true /* model only */); + + // Safety check: check the inter dependencies, will throw an exception in case of inconsistency + $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true); + + $oRuntimeEnv->CheckMetaModel(); // Will throw an exception if a problem is detected + + // Everything seems Ok so far, commit in env-production! + $oRuntimeEnv->WriteConfigFileSafe($oConfig); + $oRuntimeEnv->Commit(); + + // Report the success in a way that will be detected by the ajax caller + SetupLog::Info('Compilation completed...'); + + $this->ReportSuccess('Ok'); // No access to Dict::S here + } + + public function LaunchDeploy() + { + // Second step: update the schema and the data + // Everything happening below is based on env-production + $oRuntimeEnv = new RunTimeEnvironment('production', true); + + try { + SetupLog::Info('Move to production starts...'); + $sAuthent = utils::ReadParam('authent', '', false, 'raw_data'); + if (!file_exists(utils::GetDataPath().'hub/compile_authent') || $sAuthent !== file_get_contents(utils::GetDataPath().'hub/compile_authent')) { + throw new SecurityException(Dict::S('iTopHub:FailAuthent')); + } + unlink(utils::GetDataPath().'hub/compile_authent'); + // Load the "production" config file to clone & update it + $oConfig = new Config(APPCONF.'production/'.ITOP_CONFIG_FILE); + SetupUtils::EnterReadOnlyMode($oConfig); + + $oRuntimeEnv->InitDataModel($oConfig, true /* model only */); + + $aAvailableModules = $oRuntimeEnv->AnalyzeInstallation($oConfig, $oRuntimeEnv->GetBuildDir(), true); + + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'BeforeDatabaseCreation'); + + $oRuntimeEnv->CreateDatabaseStructure($oConfig, 'upgrade'); + + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseCreation'); + + $oRuntimeEnv->UpdatePredefinedObjects(); + + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDatabaseSetup'); + + $oRuntimeEnv->LoadData($aAvailableModules, false /* no sample data*/); + + $oRuntimeEnv->CallInstallerHandlers($aAvailableModules, 'AfterDataLoad'); + + // Record the installation so that the "about box" knows about the installed modules + $sDataModelVersion = $oRuntimeEnv->GetCurrentDataModelVersion(); + + $oExtensionsMap = new iTopExtensionsMap(); + + // Default choices = as before + $oExtensionsMap->LoadChoicesFromDatabase($oConfig); + foreach ($oExtensionsMap->GetAllExtensions() as $oExtension) { + // Plus all "remote" extensions + if ($oExtension->sSource == iTopExtension::SOURCE_REMOTE) { + $oExtensionsMap->MarkAsChosen($oExtension->sCode); + } + } + $aSelectedExtensionCodes = []; + foreach ($oExtensionsMap->GetChoices() as $oExtension) { + $aSelectedExtensionCodes[] = $oExtension->sCode; + } + $aSelectedExtensions = $oExtensionsMap->GetChoices(); + $oRuntimeEnv->RecordInstallation($oConfig, $sDataModelVersion, $aAvailableModules, $aSelectedExtensionCodes, 'Done by the iTop Hub Connector'); + + // Report the success in a way that will be detected by the ajax caller + SetupLog::Info('Deployment successfully completed.'); + $this->ReportSuccess(Dict::S('iTopHub:CompiledOK')); + } catch (Exception $e) { + if (file_exists(utils::GetDataPath().'hub/compile_authent')) { + unlink(utils::GetDataPath().'hub/compile_authent'); + } + // Note: at this point, the dictionnary is not necessarily loaded + SetupLog::Error(get_class($e).': '.Dict::S('iTopHub:ConfigurationSafelyReverted')."\n".$e->getMessage()); + SetupLog::Error('Debug trace: '.$e->getTraceAsString()); + $this->ReportError($e->getMessage(), $e->getCode()); + } finally { + SetupUtils::ExitReadOnlyMode(); + } + } + + /** + * Outputs the status of the current ajax execution (as a JSON structure) + * + * @param string $sMessage + * @param bool $bSuccess + * @param number $iErrorCode + * @param array $aMoreFields + * Extra fields to pass to the caller, if needed + */ + public function ReportStatus($sMessage, $bSuccess, $iErrorCode = 0, $aMoreFields = []) + { + // Do not use AjaxPage during setup phases, because it uses InterfaceDiscovery in Twig compilation + $this->oLastJsonPage = new JsonPage(); + $this->oLastJsonPage->SetOutputHeaders($this->bOutputHeaders); + $aResult = [ + 'code' => $iErrorCode, + 'message' => $sMessage, + 'fields' => $aMoreFields, + ]; + $this->oLastJsonPage->SetData($aResult); + $this->oLastJsonPage->SetOutputDataOnly(true); + $this->oLastJsonPage->output(); + } + + private ?JsonPage $oLastJsonPage = null; + + public function GetLastJsonPage(): ?JsonPage + { + return $this->oLastJsonPage; + } + + /** + * Helper to output the status of a successful execution + * + * @param string $sMessage + * @param array $aMoreFields + * Extra fields to pass to the caller, if needed + */ + public function ReportSuccess($sMessage, $aMoreFields = []) + { + $this->ReportStatus($sMessage, true, 0, $aMoreFields); + } + + /** + * Helper to output the status of a failed execution + * + * @param string $sMessage + * @param number $iErrorCode + * @param array $aMoreFields + * Extra fields to pass to the caller, if needed + */ + public function ReportError($sMessage, $iErrorCode, $aMoreFields = []) + { + if ($iErrorCode == 0) { + // 0 means no error, so change it if no meaningful error code is supplied + $iErrorCode = -1; + } + $this->ReportStatus($sMessage, false, $iErrorCode, $aMoreFields); + } + + /** + * Dont print headers for testing purpose mainly + * @param bool bOutputHeaders + * + * @return void + */ + public function SetOutputHeaders(bool $bOutputHeaders): void + { + $this->bOutputHeaders = $bOutputHeaders; + } +} diff --git a/datamodels/2.x/itop-hub-connector/src/Model/DBBackupWithErrorReporting.php b/datamodels/2.x/itop-hub-connector/src/Model/DBBackupWithErrorReporting.php new file mode 100644 index 000000000..39bcabe85 --- /dev/null +++ b/datamodels/2.x/itop-hub-connector/src/Model/DBBackupWithErrorReporting.php @@ -0,0 +1,37 @@ +aInfos[] = $sMsg; + } + + protected function LogError($sMsg) + { + IssueLog::Error($sMsg); + $this->aErrors[] = $sMsg; + } + + public function GetInfos(): array + { + return $this->aInfos; + } + + public function GetErrors(): array + { + return $this->aErrors; + } +} diff --git a/datamodels/2.x/itop-hub-connector/hubruntimeenvironment.class.inc.php b/datamodels/2.x/itop-hub-connector/src/setup/hubruntimeenvironment.class.inc.php similarity index 94% rename from datamodels/2.x/itop-hub-connector/hubruntimeenvironment.class.inc.php rename to datamodels/2.x/itop-hub-connector/src/setup/hubruntimeenvironment.class.inc.php index c5c722cf7..8d848d967 100644 --- a/datamodels/2.x/itop-hub-connector/hubruntimeenvironment.class.inc.php +++ b/datamodels/2.x/itop-hub-connector/src/setup/hubruntimeenvironment.class.inc.php @@ -1,9 +1,17 @@ -modules + * * @param string $sDownloadedExtensionsDir The directory to scan * @param string[] $aSelectedExtensionDirs The list of folders to move + * * @throws Exception */ public function MoveSelectedExtensions($sDownloadedExtensionsDir, $aSelectedExtensionDirs) diff --git a/sources/Application/WebPage/JsonPage.php b/sources/Application/WebPage/JsonPage.php index 4f8895a81..15da52b65 100644 --- a/sources/Application/WebPage/JsonPage.php +++ b/sources/Application/WebPage/JsonPage.php @@ -25,6 +25,7 @@ class JsonPage extends WebPage * This can be useful when feeding response to a third party lib that doesn't understand the structured format. */ protected $bOutputDataOnly = false; + protected $bOutputHeaders = true; /** * JsonPage constructor. @@ -82,6 +83,19 @@ class JsonPage extends WebPage return $this; } + /** + * @see static::$bOutputHeaders + * @param bool $bFlag + * + * @return $this + */ + public function SetOutputHeaders(bool $bFlag) + { + $this->bOutputHeaders = $bFlag; + + return $this; + } + /** * Output the headers * @@ -119,7 +133,10 @@ class JsonPage extends WebPage public function output() { $oKpi = new ExecutionKPI(); - $this->OutputHeaders(); + if ($this->bOutputHeaders) { + $this->OutputHeaders(); + } + $sContent = $this->ComputeContent(); $oKpi->ComputeAndReport(get_class($this).' output'); diff --git a/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php b/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php new file mode 100644 index 000000000..fab403e50 --- /dev/null +++ b/tests/php-unit-tests/integration-tests/itop-hub-connector/HubControllerTest.php @@ -0,0 +1,83 @@ +SkipIfModuleNotPresent('itop-hub-connector'); + parent::setUp(); + $this->RequireOnceItopFile('env-production/itop-hub-connector/src/Controller/HubController.php'); + } + + public function testLaunchCompile(): void + { + $this->PrepareCompileAuthent(); + + $this->CopyProductionModulesIntoHubExtensionDir(); + + touch(MFCompiler::USE_SYMBOLIC_LINKS_FILE_PATH); + HubController::GetInstance()->SetOutputHeaders(false); + + try { + HubController::GetInstance()->LaunchCompile(); + } catch (DOMFormatException $e) { + SetupLog::Error(__METHOD__, null, [ 'production.delta.xml' => @file_get_contents(APPROOT.'data/production.delta.xml')]); + throw $e; + } + + $this->CheckReport('{"code":0,"message":"Ok","fields":[]}'); + } + + public function testLaunchDeploy(): void + { + $this->PrepareCompileAuthent(); + HubController::GetInstance()->LaunchDeploy(); + $this->CheckReport('{"code":0,"message":"Compilation successful.","fields":[]}'); + } + + private function CheckReport($sExpected) + { + $oJsonPage = HubController::GetInstance()->GetLastJsonPage(); + $this->assertEquals($sExpected, $this->InvokeNonPublicMethod(JsonPage::class, 'ComputeContent', $oJsonPage, [])); + + //keep line below to avoid: Test code or tested code did not (only) close its own output buffers + $this->InvokeNonPublicMethod(JsonPage::class, 'RenderContent', $oJsonPage, []); + } + + private function PrepareCompileAuthent() + { + $sUUID = 'hub_'.uniqid(); + $_REQUEST['authent'] = $sUUID; + $sPath = utils::GetDataPath().'hub/compile_authent'; + file_put_contents($sPath, $sUUID); + $this->aFileToClean[] = $sPath; + } + + private function CopyProductionModulesIntoHubExtensionDir() + { + $sProdModules = APPROOT.'/data/production-modules'; + $sExtensionDir = APPROOT.'/data/downloaded-extensions/'; + $this->aFileToClean[] = $sExtensionDir; + + SetupUtils::rrmdir($sExtensionDir); + SetupUtils::copydir($sExtensionDir, $sProdModules); + } +} diff --git a/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php b/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php index d486b582e..98fdd26fb 100644 --- a/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php +++ b/tests/php-unit-tests/src/Service/UnitTestRunTimeEnvironment.php @@ -8,11 +8,13 @@ namespace Combodo\iTop\Test\UnitTest\Service; use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase; +use DOMFormatException; use MFCoreModule; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionClass; use RunTimeEnvironment; +use SetupLog; use SetupUtils; use utils; @@ -64,7 +66,13 @@ class UnitTestRunTimeEnvironment extends RunTimeEnvironment } } - parent::CompileFrom($sSourceEnv, $bUseSymLinks); + try { + parent::CompileFrom($sSourceEnv, $bUseSymLinks); + } catch (DOMFormatException $e) { + $sFileName = $sSourceEnv.'.delta.xml'; + SetupLog::Error(__METHOD__, null, [$sFileName => @file_get_contents(APPROOT.'data/'.$sFileName)]); + throw $e; + } } public function IsUpToDate() diff --git a/tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php b/tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php deleted file mode 100644 index d863c6d64..000000000 --- a/tests/php-unit-tests/unitary-tests/modulediscovery/ModuleDiscoveryTest.php +++ /dev/null @@ -1,50 +0,0 @@ - [ - 'sModuleId' => 'a/1.2.3', - 'name' => 'a', - 'version' => '1.2.3', - ], - 'develop' => [ - 'sModuleId' => 'a/1.2.3-dev', - 'name' => 'a', - 'version' => '1.2.3-dev', - ], - 'missing version => 1.0.0' => [ - 'sModuleId' => 'a/', - 'name' => 'a', - 'version' => '1.0.0', - ], - 'missing everything except name' => [ - 'sModuleId' => 'a', - 'name' => 'a', - 'version' => '1.0.0', - ], - ]; - } - - protected function setUp(): void - { - parent::setUp(); - - $this->RequireOnceItopFile('setup/modulediscovery.class.inc.php'); - } - - /** - * @dataProvider GetModuleNameProvider - */ - public function testGetModuleName($sModuleId, $expectedName, $expectedVersion) - { - $this->assertEquals([$expectedName, $expectedVersion], \ModuleDiscovery::GetModuleName($sModuleId)); - } - -} diff --git a/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php b/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php index 4e31921c9..badaaafb6 100644 --- a/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/ModuleDiscoveryTest.php @@ -77,4 +77,38 @@ TXT; ModuleDiscovery::OrderModulesByDependencies($aModules, true, $aChoices); } + + public function GetModuleNameProvider() + { + return [ + 'nominal' => [ + 'sModuleId' => 'a/1.2.3', + 'name' => 'a', + 'version' => '1.2.3', + ], + 'develop' => [ + 'sModuleId' => 'a/1.2.3-dev', + 'name' => 'a', + 'version' => '1.2.3-dev', + ], + 'missing version => 1.0.0' => [ + 'sModuleId' => 'a/', + 'name' => 'a', + 'version' => '1.0.0', + ], + 'missing everything except name' => [ + 'sModuleId' => 'a', + 'name' => 'a', + 'version' => '1.0.0', + ], + ]; + } + + /** + * @dataProvider GetModuleNameProvider + */ + public function testGetModuleName($sModuleId, $expectedName, $expectedVersion) + { + $this->assertEquals([$expectedName, $expectedVersion], \ModuleDiscovery::GetModuleName($sModuleId)); + } }