diff options
author | mwithheld <796986+mwithheld@users.noreply.github.com> | 2021-09-15 16:27:37 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-15 16:27:37 +0300 |
commit | d0215334b3660faa04f7d601f634b82cb01dc003 (patch) | |
tree | 5ec0fac433619fd0401fdf0bc35a77ec80e294f0 /plugins/CoreAdminHome | |
parent | 9585236c88c591b377758bad315564852c27ef43 (diff) |
Add CLI config:get and config:delete (#17956)
* 16576 Add ConfigGet and tests
* 15676 config:get code and test updates
* 15676 Add config:delete code and tests
* 15676 Add config:delete code and tests
* Improve usage examples
Co-authored-by: Stefan Giehl <stefan@matomo.org>
* Fix Exception namespace
Co-authored-by: Stefan Giehl <stefan@matomo.org>
* Remove comment about Done output
Co-authored-by: Stefan Giehl <stefan@matomo.org>
* Clarify parsing format option
Co-authored-by: Stefan Giehl <stefan@matomo.org>
* Remove unneeded teardown()
Co-authored-by: Stefan Giehl <stefan@matomo.org>
* Remove isWritableByCurrentUser() check - CLI is always superuser
* Use Spyc yaml included in releases not Symfony
* PSR12 Fixes
# Conflicts:
# plugins/CoreAdminHome/Commands/ConfigDelete.php
# plugins/CoreAdminHome/Commands/ConfigGet.php
* PSR12 Fixes
* runCommandWithArguments: Fix missing dot; remove adding value param
* Fix paths in single-test usage example
* Add tests using args
* If there are multiple arguments just use the last one
* Add tests using args
* Remove debug code
* Restore accidentally deleted testUsingOptsNonExistentSectionAndSettingShouldYieldEmpty
* Remove debug comment and blank lines
* Fix testUsingArgs should use runCommandWithArguments()
* Apply remaining suggestions from code review
* Update and rename plugins/CoreAdminHome/tests/Integration/ConfigDeleteTest.php to plugins/CoreAdminHome/tests/Integration/Commands/ConfigDeleteTest.php
* Update and rename plugins/CoreAdminHome/tests/Integration/ConfigGetTest.php to plugins/CoreAdminHome/tests/Integration/Commands/ConfigGetTest.php
* Update and rename plugins/CoreAdminHome/tests/Integration/SetConfigTest.php to plugins/CoreAdminHome/tests/Integration/Commands/SetConfigTest.php
Co-authored-by: Stefan Giehl <stefan@matomo.org>
Diffstat (limited to 'plugins/CoreAdminHome')
-rw-r--r-- | plugins/CoreAdminHome/Commands/ConfigDelete.php | 282 | ||||
-rw-r--r-- | plugins/CoreAdminHome/Commands/ConfigGet.php | 311 | ||||
-rw-r--r-- | plugins/CoreAdminHome/tests/Integration/Commands/ConfigDeleteTest.php | 446 | ||||
-rw-r--r-- | plugins/CoreAdminHome/tests/Integration/Commands/ConfigGetTest.php | 662 | ||||
-rw-r--r-- | plugins/CoreAdminHome/tests/Integration/Commands/SetConfigTest.php (renamed from plugins/CoreAdminHome/tests/Integration/SetConfigTest.php) | 3 |
5 files changed, 1703 insertions, 1 deletions
diff --git a/plugins/CoreAdminHome/Commands/ConfigDelete.php b/plugins/CoreAdminHome/Commands/ConfigDelete.php new file mode 100644 index 0000000000..7d3c5b243e --- /dev/null +++ b/plugins/CoreAdminHome/Commands/ConfigDelete.php @@ -0,0 +1,282 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\CoreAdminHome\Commands; + +use Piwik\Config; +use Piwik\Plugin\ConsoleCommand; +use Piwik\Settings\FieldConfig; +use Piwik\Settings\Plugin\SystemConfigSetting; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ConfigDelete extends ConsoleCommand +{ + + // Message output if no matching setting is found. + private const MSG_NOTHING_FOUND = 'Nothing found'; + // Message output on success. + private const MSG_SUCCESS = 'Success: The setting has been deleted'; + + protected function configure() + { + $this->setName('config:delete'); + $this->setDescription('Delete a config setting'); + $this->addArgument( + 'argument', + InputArgument::OPTIONAL, + "A config setting in the format Section.key or Section.array_key[], e.g. 'Database.username' or 'PluginsInstalled.PluginsInstalled.CustomDimensions'" + ); + $this->addOption('section', 's', InputOption::VALUE_REQUIRED, 'The section the INI config setting belongs to.'); + $this->addOption('key', 'k', InputOption::VALUE_REQUIRED, 'The name of the INI config setting.'); + $this->addOption('value', 'i', InputOption::VALUE_REQUIRED, 'For arrays, specify the array value to be deleted.'); + + $this->setHelp("This command can be used to delete a INI config setting. + +You can delete config values per the two sections below, where: +- [Section] is the name of the section, e.g. database or General. +- [config_setting_name] the name of the setting, e.g. username. +- [array_value] For arrays, the specific array value to delete. + +(1) By settings options --section=[Section] and --key=[config_setting_name], and optionally --value=[array_value]. Examples: +#Delete this setting. +$ ./console %command.name% --section=database --key=username +#Delete one value in an array: +$ ./console %command.name% --section=PluginsInstalled --key=PluginsInstalled --value=DevicesDetection + +OR + +(2) By using a command argument in the format [Section].[config_setting_name].[array_value]. Examples: +#Delete this setting. +$ ./console %command.name% 'database.username' +#Delete one value in an array: +$ ./console %command.name% 'PluginsInstalled.PluginsInstalled.DevicesDetection' + +NOTES: +- Settings may still appear to exist if they are set in global.ini.php or elsewhere. +- Section names, key names, and array values are all case-sensitive; so e.g. --section=Database fails but --section=database works. Look in config.ini.php and global.ini.php for the proper case. +- If no matching section/setting is found, the string \"" . self::MSG_NOTHING_FOUND . "\" shows. +- For safety, this tool cannot be used to delete a whole section of settings or an array of values in a single command. +"); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + // Gather options, then discard ones that are empty so we do not need to check for empty later. + $options = array_filter([ + 'section' => $input->getOption('section'), + 'key' => $input->getOption('key'), + 'value' => $input->getOption('value'), + ]); + + $argument = trim($input->getArgument('argument')); + + // Sanity check inputs. + switch (true) { + case empty($argument) && empty($options): + throw new \InvalidArgumentException('You must set either an argument or set options --section, --key and optional --value'); + case (!empty($argument) && !empty($options)): + throw new \InvalidArgumentException('You cannot set both an argument (' . serialize($argument) . ') and options (' . serialize($argument) . ')'); + case empty($argument) && (!isset($options['section']) || empty($options['section']) || !isset($options['key']) || empty($options['key'])): + throw new \InvalidArgumentException('When using options, --section and --key must be set'); + case (!empty($argument)): + $settingStr = $argument; + break; + case (!empty($options)): + $settingStr = implode('.', $options); + break; + default: + // We should not get here, but just in case. + throw new \Exception('Some unexpected error occurred parsing input values'); + } + + // Convenience wrapper used to augment SystemConfigSetting without extending SystemConfigSetting or adding random properties to the instance. + $settingWrapped = (object) [ + 'setting' => null, + 'isArray' => false, + 'arrayVal' => '', + ]; + + // Parse the $settingStr into a $settingWrapped object. + $settingWrapped = self::parseSettingStr($settingStr, $settingWrapped); + + // Check the setting exists and user has permissions, then populates the $settingWrapped properties. + $settingWrapped = $this->checkAndPopulate($settingWrapped); + + if (!isset($settingWrapped->setting) || empty($settingWrapped->setting)) { + $output->writeln(self::wrapInTag('comment', self::MSG_NOTHING_FOUND)); + } else { + // Pass both static and array config items out to the delete logic. + $result = $this->deleteConfigSetting($settingWrapped); + + if ($result) { + $output->writeln($this->wrapInTag('info', self::MSG_SUCCESS)); + } + } + } + + /** + * Check the setting exists and user has permissions, then return a new, populated SystemConfigSetting wrapper. + * + * @param object $settingWrapped A wrapped SystemConfigSetting object e.g. what is returned from parseSettingStr(). + * @return object A new wrapped SystemConfigSetting object. + */ + private function checkAndPopulate(object $settingWrapped): object + { + + // Sanity check inputs. + if (!($settingWrapped->setting instanceof SystemConfigSetting)) { + throw new \InvalidArgumentException('This function expects $settingWrapped->setting to be a SystemConfigSetting instance'); + } + + $config = Config::getInstance(); + + // Check the setting exists and user has permissions. If so, put it in the wrapper. + switch (true) { + case empty($sectionName = $settingWrapped->setting->getConfigSectionName()): + throw new \InvalidArgumentException('A section name must be specified'); + case empty($settingName = $settingWrapped->setting->getName()): + throw new \InvalidArgumentException('A setting name must be specified'); + case empty($section = $config->__get($sectionName)): + return new \stdClass(); + case empty($section = (object) $section) || !isset($section->$settingName): + return new \stdClass(); + default: + // We have a valid scalar or array setting in a valid section, so just fall out of the switch statement. + break; + } + + $settingWrappedNew = clone($settingWrapped); + $settingWrappedNew->isArray = is_array($section->$settingName); + + if (!$settingWrappedNew->isArray && !empty($settingWrappedNew->arrayVal)) { + throw new \InvalidArgumentException('This config setting is not an array'); + } + if ($settingWrappedNew->isArray) { + if (empty($settingWrappedNew->arrayVal)) { + throw new \InvalidArgumentException('This config setting is an array, but no array value was specified for deletion'); + } + if (false === array_search($settingWrappedNew->arrayVal, $section->$settingName)) { + return new \stdClass(); + } + } + + return $settingWrappedNew; + } + + /** + * Delete a single config section.setting or section.setting[array_key]. + * + * @param object $settingWrapped Wrapper around a setting object describing what to get e.g. from self::make(). + * @return bool True on success. If the delete fails, throws an exception. + */ + private function deleteConfigSetting(object $settingWrapped): bool + { + + // Sanity check inputs. + if (!($settingWrapped->setting instanceof SystemConfigSetting)) { + throw new \InvalidArgumentException('This function expects $settingWrapped->setting to be a SystemConfigSetting instance'); + } + + // Make easy shortcuts to some info. + $sectionName = $settingWrapped->setting->getConfigSectionName(); + $settingName = $settingWrapped->setting->getName(); + + // Get the actual config section. + $config = Config::getInstance(); + $section = $config->$sectionName; + $setting = $section[$settingName]; + + // Do the delete. + // This does not do the job with value=null or value='': $config->setSetting($sectionName, $settingName, $value). + switch (true) { + case $settingWrapped->isArray === true && empty($settingWrapped->arrayVal): + throw new \InvalidArgumentException('This function refuses to delete config arrays. See usage for how to delete config array values.'); + case $settingWrapped->isArray === true: + // Array config values. + + $key = array_search($settingWrapped->arrayVal, $setting); + if ($key !== false) { + unset($setting[$key]); + } + + // Save the setting into the section. + $section[$settingName] = $setting; + break; + default: + // Scalar config values. + // Remove the setting from the section. + unset($section[$settingName]); + break; + } + + // Save the section into the config. + $config->$sectionName = $section; + + // Save the config. + $config->forceSave(); + + return true; + } + + /** + * Build a SystemConfigSetting object from a string. + * + * @param string $settingStr Config setting string to parse. + * @return object A new wrapped SystemConfigSetting object. + */ + public static function parseSettingStr(string $settingStr, object $settingWrapped): object + { + + $matches = []; + if (!preg_match('/^([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?(?:\[\])?(?:\.([a-zA-Z0-9_]+))?/', $settingStr, $matches) || empty($matches[1])) { + throw new \InvalidArgumentException("Invalid input string='{$settingStr}': expected section.name or section.name[]"); + } + + + $settingName = $matches[2] ?? null; + $arrayVal = $matches[3] ?? null; + + $systemConfigSetting = new SystemConfigSetting( + // Setting name. + $settingName, + // Default value. + '', + // Type. + FieldConfig::TYPE_STRING, + // Plugin name. + 'core', + // Section name. + $matches[1] + ); + + $settingWrappedNew = clone($settingWrapped); + $settingWrappedNew->setting = $systemConfigSetting; + if ($settingWrappedNew->isArray = !empty($arrayVal)) { + $settingWrappedNew->arrayVal = $arrayVal; + } + + return $settingWrappedNew; + } + + /** + * Wrap the input string in an open and closing HTML/XML tag. + * E.g. wrap_in_tag('info', 'my string') returns '<info>my string</info>' + * + * @param string $tagname Tag name to wrap the string in. + * @param string $str String to wrap with the tag. + * @return string The wrapped string. + */ + public static function wrapInTag(string $tagname, string $str): string + { + return "<$tagname>$str</$tagname>"; + } +} diff --git a/plugins/CoreAdminHome/Commands/ConfigGet.php b/plugins/CoreAdminHome/Commands/ConfigGet.php new file mode 100644 index 0000000000..f302040728 --- /dev/null +++ b/plugins/CoreAdminHome/Commands/ConfigGet.php @@ -0,0 +1,311 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\CoreAdminHome\Commands; + +use Piwik\Config; +use Piwik\Plugin\ConsoleCommand; +use Piwik\Settings\FieldConfig; +use Piwik\Settings\Plugin\SystemConfigSetting; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Spyc; + +class ConfigGet extends ConsoleCommand +{ + + //SystemConfigSetting throws an error if the setting name is empty, so use a fake one that is unlikely to actually exist, which we will check for later. + private const NO_SETTING_NAME_FOUND_PLACEHOLDER = 'ConfigGet_FAKE_SETTING_NAME'; + // Valid output formats. + public const OUTPUT_FORMATS = ['json', 'yaml', 'text']; + // Default output format. + public const OUTPUT_FORMAT_DEFAULT = 'json'; + // Message output if no matching setting is found. + private const MSG_NOTHING_FOUND = 'Nothing found'; + + protected function configure() + { + $this->setName('config:get'); + $this->setDescription('Get a config value or section'); + $this->addArgument( + 'argument', + InputArgument::OPTIONAL, + "A config setting in the format Section.key or Section.array_key[], e.g. 'Database.username' or 'PluginsInstalled'" + ); + $this->addOption('section', 's', InputOption::VALUE_REQUIRED, 'The section the INI config setting belongs to.'); + $this->addOption('key', 'k', InputOption::VALUE_REQUIRED, 'The name of the INI config setting.'); + $this->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'Format the output as ' . json_encode(self::OUTPUT_FORMATS) . '; Default is ' . self::OUTPUT_FORMAT_DEFAULT, self::OUTPUT_FORMAT_DEFAULT); + + $this->setHelp("This command can be used to get a INI config setting or a whole section of settings on a Piwik instance. + +You can get config values per the two sections below, where: +- [Section] is the name of the section, e.g. database or General. +- [config_setting_name] the name of the setting, e.g. username. + +(1) By settings options --section=[Section] and --key=[config_setting_name]. The option --section is required. Examples: +#Return all settings in this section. +$ ./console %command.name% --section=database +#Return only this setting. +$ ./console %command.name% --section=database --key=username + +OR + +(2) By using a command argument in the format [Section].[config_setting_name]. Examples: +#Return all settings in this section. +$ ./console %command.name% 'database' +#Return all settings in this array; square brackets are optional. +$ ./console %command.name% 'PluginsInstalled.PluginsInstalled' +$ ./console %command.name% 'PluginsInstalled.PluginsInstalled[]' +#Return only this setting. +$ ./console %command.name% 'database.username' + +NOTES: +- Section and key names are case-sensitive; so e.g. --section=Database fails but --section=database works. Look in global.ini.php for the proper case. +- If no matching section/setting is found, the string \"" . self::MSG_NOTHING_FOUND . "\" shows. +- Else the output will be shown JSON-encoded. You can use something like https://stedolan.github.io/jq to parse it. +- If you ask for 'PluginsInstalled.PluginsInstalled[\"some_array_item\"]', it will ignore the array key (\"some_array_item\") and you will get back the whole array of values (e.g. all PluginsInstalled[] values). +"); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + // Gather options, then discard ones with an empty value so we do not need to check for empty later. + $options = array_filter([ + 'section' => $input->getOption('section'), + 'key' => $input->getOption('key'), + ]); + + // If none specified, set default format. + $format = $input->getOption('format'); + if (empty($format) || !in_array($format, self::OUTPUT_FORMATS, true)) { + $format = self::OUTPUT_FORMAT_DEFAULT; + } + + $argument = trim($input->getArgument('argument')); + + // If there are multiple arguments, just use the last one. + $argument = array_slice(explode(' ', $argument), -1)[0]; + + // Sanity check inputs. + switch (true) { + case empty($argument) && empty($options): + throw new \InvalidArgumentException('You must set either an argument or set options --section and optional --key'); + case (!empty($argument) && !empty($options)): + throw new \InvalidArgumentException('You cannot set both an argument (' . serialize($argument) . ') and options (' . serialize($argument) . ')'); + case empty($argument) && (!isset($options['section']) || empty($options['section'])): + throw new \InvalidArgumentException('When using options, the --section value must be set'); + case (!empty($argument)): + $settingStr = $argument; + break; + case (!empty($options)): + $settingStr = implode('.', $options); + break; + default: + // We should not get here, but just in case. + throw new \Exception('Some unexpected error occurred in ' . __FUNCTION__ . ' at line ' . __LINE__); + } + + // Parse the $settingStr into a SystemConfigSetting object. + $setting = self::parseSettingStr($settingStr); + + $result = $this->getConfigValue(Config::getInstance(), $setting); + + if (empty($result)) { + $output->writeln(self::wrapInTag('comment', self::MSG_NOTHING_FOUND)); + } else { + $output->writeln($this->formatVariableForOutput($setting, $result, $format)); + } + + //Many matomo script output Done when they're done. IMO it's not needed: $output->writeln(self::wrapInTag('info', 'Done.')); + } + + /** + * Get a config section or section.value. + * + * @param Config $config A Matomo Config instance. + * @param SystemConfigSetting $setting A setting object describing what to get e.g. from self::make(). + * @return Mixed The config section or value. + */ + private function getConfigValue(Config $config, SystemConfigSetting $setting) + { + + // This should have been caught in the calling function, so assume a bad implementation and throw an error. + if (empty($sectionName = $setting->getConfigSectionName())) { + throw new \InvalidArgumentException('A section name must be specified'); + } + if (empty($section = $config->__get($sectionName))) { + return null; + } + + // Convert array to object since it is slightly cleaner/easier to work with. + $section = (object) $section; + + // Look for the specific setting. + $settingName = $setting->getName(); + + // Return the whole setting section if requested. + // The name=FAKE_SETTING_NAME is a placeholder for when no setting is specified. + if (empty($settingName) || $settingName === self::NO_SETTING_NAME_FOUND_PLACEHOLDER) { + $sectionContents = $section; + return (array) $sectionContents; + } + + + switch (true) { + case (!isset($section->$settingName)): + $settingValue = null; + break; + case is_array($settingValue = $section->$settingName): + break; + default: + $settingValue = $setting->getValue(); + } + + return $settingValue; + } + + /** + * Build a SystemConfigSetting object from a string. + * + * @param string $settingStr Config setting string to parse. + * @return SystemConfigSetting A SystemConfigSetting object. + */ + public static function parseSettingStr(string $settingStr): SystemConfigSetting + { + + $matches = []; + if (!preg_match('/^([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?(\[\])?/', $settingStr, $matches) || empty($matches[1])) { + throw new \InvalidArgumentException("Invalid input string='{$settingStr}' =expected section.name or section.name[]"); + } + + + return new SystemConfigSetting( + // Setting name. SystemConfigSetting throws an error if the setting name is empty, so use placeholder that flags that no setting was specified. + $matches[2] ?? self::NO_SETTING_NAME_FOUND_PLACEHOLDER, + // Default value. + '', + // Type. + FieldConfig::TYPE_STRING, + // Plugin name. + 'core', + // Section name. + $matches[1] + ); + } + + /** + * Wrap the input string in an open and closing HTML/XML tag. + * E.g. wrap_in_tag('info', 'my string') returns '<info>my string</info>' + * + * @param string $tagname Tag name to wrap the string in. + * @param string $str String to wrap with the tag. + * @return string The wrapped string. + */ + public static function wrapInTag(string $tagname, string $str): string + { + return "<$tagname>$str</$tagname>"; + } + + /** + * Format the config setting to the specified output format. + * + * @param SystemConfigSetting $setting The found SystemConfigSetting. + * @param mixed $var The config setting -- either scalar or an array of settings. + * @param string $format The output format: One of self::OUTPUT_FORMAT_DEFAULT. + * @return string The formatted output. + */ + private function formatVariableForOutput(SystemConfigSetting $setting, $var, string $format = self::OUTPUT_FORMAT_DEFAULT): string + { + + switch ($format) { + case 'json': + return $this->toJson($var); + case 'yaml': + return $this->toYaml($var); + case 'text': + return $this->toText($setting, $var); + default: + throw new \InvalidArgumentException('Unsupported output format'); + } + } + + /** + * Convert $var to a YAML string. + * Throws an error on invalid types (a PHP resource or object). + * + * @param mixed $var The variable to convert. + * @return string The Yaml-formatted string. + */ + private function toYaml($var): string + { + + // Remove leading dash and spaces Spyc adds so we just output the bare value. + return trim(ltrim(Spyc::YAMLDump($var, 2, 0, true), '-')); + } + + /** + * Convert $var to a JSON string. + * Throws an error on json_encode failure. + * + * @param mixed $var The variable to convert. + * @return string The JSON-formatted string. + */ + private function toJson($var): string + { + $result = json_encode($var, JSON_UNESCAPED_SLASHES); + if ($result === false) { + throw new \Exception('Failed to json_encode'); + } + + return $result; + } + + /** + * Convert $var to Symfony-colorized CLI output text. + * + * @param SystemConfigSetting $setting The found SystemConfigSetting. + * @param mixed $var The thing to format: Config scalar values lead to $var being scalar; Config array values lead to $var being an array of scalars; Config sections lead to $var being a mixed array of both scalar and array values. + * @return string The formatted result. + */ + private function toText(SystemConfigSetting $setting, $var): string + { + + // Strip off the NO_SETTING_NAME_FOUND_PLACEHOLDER. + $settingName = $setting->getName() === self::NO_SETTING_NAME_FOUND_PLACEHOLDER ? '' : $setting->getName(); + $sectionAndSettingName = implode('.', array_filter([$setting->getConfigSectionName(), $settingName])); + + $output = ''; + + switch (true) { + case is_array($var): + $output .= $this->wrapInTag('info', ($settingName ? $sectionAndSettingName : "[{$sectionAndSettingName}]") . PHP_EOL); + $output .= $this->wrapInTag('info', '--' . PHP_EOL); + foreach ($var as $thisSettingName => &$val) { + if (is_array($val)) { + foreach ($val as &$arrayVal) { + $output .= $this->wrapInTag('info', "{$thisSettingName}[] = " . $this->wrapInTag('comment', $arrayVal)) . PHP_EOL; + } + } else { + $output .= $this->wrapInTag('info', $thisSettingName . ' = ' . $this->wrapInTag('comment', $val)) . PHP_EOL; + } + } + $output .= $this->wrapInTag('info', '--' . PHP_EOL); + break; + case is_scalar($var): + $output .= $this->wrapInTag('info', $sectionAndSettingName . ' = ' . $this->wrapInTag('comment', $var)); + break; + default: + throw \InvalidArgumentException('Cannot output unknown type'); + } + + return $output; + } +} diff --git a/plugins/CoreAdminHome/tests/Integration/Commands/ConfigDeleteTest.php b/plugins/CoreAdminHome/tests/Integration/Commands/ConfigDeleteTest.php new file mode 100644 index 0000000000..017194de41 --- /dev/null +++ b/plugins/CoreAdminHome/tests/Integration/Commands/ConfigDeleteTest.php @@ -0,0 +1,446 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\CoreAdminHome\tests\Integration\Commands; + +use Psr\Container\ContainerInterface; +use Piwik\Application\Kernel\GlobalSettingsProvider; +use Piwik\Config; +use Piwik\Tests\Framework\TestCase\ConsoleCommandTestCase; + +/** + * @group Core + * @group CoreAdminHome + * @group Integration + */ +class ConfigDeleteTest extends ConsoleCommandTestCase +{ + /* + * The text config:delete outputs when no matching value is found. + */ + + private const COMMAND = 'config:delete'; + private const CLASS_NAME_SHORT = 'ConfigDeleteTest'; + private const MSG_NOTHING_FOUND = 'Nothing found'; + + /* + * Path to store the test config file. It should be deleted when done. + */ + private const TEST_CONFIG_PATH = '/tmp/test.config.ini.php'; + // Section 1. + private const TEST_SECTION_1_NAME = self::CLASS_NAME_SHORT . '_test_section_1'; + private const TEST_SETTING_1_1_NAME = self::CLASS_NAME_SHORT . '_test_setting_1'; + private const TEST_SETTING_1_1_VALUE = self::CLASS_NAME_SHORT . '_test_value_1'; + private const TEST_SETTING_1_2_NAME = self::CLASS_NAME_SHORT . '_test_setting_2'; + private const TEST_SETTING_1_2_VALUE = self::CLASS_NAME_SHORT . '_test_value_2'; + // Section 2. + private const TEST_SECTION_2_NAME = self::CLASS_NAME_SHORT . '_test_section_2'; + private const TEST_SETTING_2_1_NAME = self::CLASS_NAME_SHORT . '_array_setting'; + private const TEST_SETTING_2_1_VALUE_0 = self::CLASS_NAME_SHORT . '_arr_val_1'; + private const TEST_SETTING_2_1_VALUE_1 = self::CLASS_NAME_SHORT . '_arr_val_2'; + private const TEST_SETTING_2_1_VALUE_2 = self::CLASS_NAME_SHORT . '_arr_val_3'; + private const TEST_SETTING_2_1_VALUES = [self::TEST_SETTING_2_1_VALUE_0, self::TEST_SETTING_2_1_VALUE_1, self::TEST_SETTING_2_1_VALUE_2]; + + public static function setUpBeforeClass(): void + { + self::removeTestConfigFile(); + parent::setUpBeforeClass(); + } + + public function setUp(): void + { + self::removeTestConfigFile(); + parent::setUp(); + } + + private static function getTestConfigFilePath() + { + return PIWIK_INCLUDE_PATH . self::TEST_CONFIG_PATH; + } + + public static function provideContainerConfigBeforeClass() + { + return array( + // use a config instance that will save to a test INI file + 'Piwik\Config' => function (ContainerInterface $containerInterface) { + /** @var GlobalSettingsProvider $actualGlobalSettingsProvider */ + $actualGlobalSettingsProvider = $containerInterface->get('Piwik\Application\Kernel\GlobalSettingsProvider'); + + $config = self::makeTestConfig(); + + // copy over sections required for tests + $config->tests = $actualGlobalSettingsProvider->getSection('tests'); + $config->database = $actualGlobalSettingsProvider->getSection('database_tests'); + + return $config; + }, + ); + } + + private static function makeTestConfig() + { + $settingsProvider = new GlobalSettingsProvider(null, self::getTestConfigFilePath()); + $config = new Config($settingsProvider); + + // Add the first section. + $sectionName = self::TEST_SECTION_1_NAME; + $config->$sectionName[self::TEST_SETTING_1_1_NAME] = self::TEST_SETTING_1_1_VALUE; + $config->$sectionName[self::TEST_SETTING_1_2_NAME] = self::TEST_SETTING_1_2_VALUE; + + // Add a second section so we are testing that we do not accidentally return it. + $sectionName = self::TEST_SECTION_1_NAME . '_second_section'; + // Add a setting with the same name as in section #1 but with a random int value. + $config->$sectionName[self::TEST_SETTING_1_1_NAME] = random_int(PHP_INT_MIN, PHP_INT_MAX); + // Add another setting to the same section with some bogus content. + $config->$sectionName[self::TEST_SETTING_1_2_NAME . '_another'] = '127.0.0.1'; + + // Add an array value like section=PluginsInstalled; setting=PluginsInstalled[]. + $sectionName = self::TEST_SECTION_2_NAME; + // Add some values to the setting array. + $config->$sectionName[self::TEST_SETTING_2_1_NAME] = self::TEST_SETTING_2_1_VALUES; + + $config->forceSave(); + return $config; + } + + private static function removeTestConfigFile() + { + $configPath = self::getTestConfigFilePath(); + if (file_exists($configPath)) { + unlink($configPath); + } + } + + private static function makeNewConfig() + { + $settings = new GlobalSettingsProvider(null, self::getTestConfigFilePath()); + return new Config($settings); + } + + private function runCommandWithOptions(string $sectionName, string $settingName, string $value = ''): object + { + + $inputArr = [ + 'command' => self::COMMAND, + '--section' => $sectionName, + '--key' => $settingName, + '-vvv' => false, + ]; + if (!empty($value)) { + $inputArr['--value'] = $value; + } + $exitCode = $this->applicationTester->run($inputArr); + + // Pass true to getDisplay(true) to normalize line endings, then trim() bc CLI adds an \ automatically. + $output = trim($this->applicationTester->getDisplay(true)); + + // Put the results in an easy-to-handle object format. + return (object) ['exitCode' => $exitCode, 'output' => $output]; + } + + private function runCommandWithArguments(string $sectionName, string $settingName = '', string $value = ''): object + { + + $inputArr = [ + 'command' => self::COMMAND, + '-vvv' => false, + 'argument' => $sectionName . '.' . $settingName . (empty($value) ? '' : ".$value"), + ]; + + $exitCode = $this->applicationTester->run($inputArr); + + // Pass true to getDisplay(true) to normalize line endings, then trim() bc CLI adds an \ automatically. + $output = trim($this->applicationTester->getDisplay(true)); + + // Put the results in an easy-to-handle object format. + return (object) ['exitCode' => $exitCode, 'output' => $output]; + } + + // + //************************************************************************* + // Tests that should yield errors. + //************************************************************************* + // + public function testNoArgsShouldYieldError() + { + + $inputArr = [ + 'command' => 'config:get', + '-vvv' => false, + ]; + $exitCode = $this->applicationTester->run($inputArr); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $exitCode); + + // Pass true to getDisplay(true) to normalize line endings, then trim() bc CLI adds an \ automatically. + $output = trim($this->applicationTester->getDisplay(true)); + + $this->assertStringContainsString('InvalidArgumentException', $output); + } + + public function testEmptyArgsShouldYieldError() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithArguments(''); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $resultObj->exitCode); + + $this->assertStringContainsString('InvalidArgumentException', $resultObj->output); + } + + public function testEmptyOptionsShouldYieldError() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions('', ''); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $resultObj->exitCode); + + $this->assertStringContainsString('InvalidArgumentException', $resultObj->output); + } + + public function testSetArgsAndOptionsShouldYieldError() + { + $inputArr = [ + 'command' => 'config:get', + 'argument' => self::TEST_SECTION_1_NAME . '.' . self::TEST_SETTING_1_1_NAME, + '--section' => self::TEST_SECTION_1_NAME, + '--key' => self::TEST_SETTING_1_1_NAME, + '-vvv' => false, + ]; + $exitCode = $this->applicationTester->run($inputArr); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $exitCode); + + // Pass true to getDisplay(true) to normalize line endings, then trim() bc CLI adds an \ automatically. + $output = trim($this->applicationTester->getDisplay(true)); + + $this->assertStringContainsString('InvalidArgumentException', $output); + } + + public function testEmptySectionShouldYieldError() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions('', self::TEST_SETTING_1_1_NAME); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $resultObj->exitCode); + + $this->assertStringContainsString('InvalidArgumentException', $resultObj->output); + } + + public function testScalarSettingWithArrayValShouldYieldError() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, self::TEST_SETTING_1_1_NAME, self::CLASS_NAME_SHORT . '_Array_key_does_not_exist'); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $resultObj->exitCode); + + $this->assertStringContainsString('InvalidArgumentException', $resultObj->output); + } + + public function testArrayWithNoValShouldYieldError() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_2_NAME, self::TEST_SETTING_2_1_NAME); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $resultObj->exitCode); + + $this->assertStringContainsString('InvalidArgumentException', $resultObj->output); + } + + // + //************************************************************************* + // Tests for nonexistent data. + //************************************************************************* + // + public function testUsingOptsNonExistentSectionShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions(self::CLASS_NAME_SHORT . '_Section_does_not_exist', self::TEST_SETTING_1_1_NAME); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsNonExistentSectionShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithArguments(self::CLASS_NAME_SHORT . '_Section_does_not_exist', self::TEST_SETTING_1_1_NAME); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsNonExistentSectionAndSettingShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions(self::CLASS_NAME_SHORT . '_Section_does_not_exist', self::CLASS_NAME_SHORT . '_Setting_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsNonExistentSectionAndSettingShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithArguments(self::CLASS_NAME_SHORT . '_Section_does_not_exist', self::CLASS_NAME_SHORT . '_Setting_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsNonExistentSettingShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, self::CLASS_NAME_SHORT . '_Setting_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsNonExistentSettingShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_1_NAME, self::CLASS_NAME_SHORT . '_Setting_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsArrayWithInvalidValShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_2_NAME, self::TEST_SETTING_2_1_NAME, self::CLASS_NAME_SHORT . '_Array_key_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsArrayWithInvalidValShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_2_NAME, self::TEST_SETTING_2_1_NAME, self::CLASS_NAME_SHORT . '_Array_key_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + // + //************************************************************************* + // Tests for existing data. + //************************************************************************* + // + + public function testUsingOptsDeleteSingleSetting() + { + + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, self::TEST_SETTING_1_1_NAME); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $this->assertStringContainsString('Success:', $resultObj->output); + + $config = $this->makeNewConfig(); + $configDump = $config->dumpConfig(); + $needle = self::TEST_SETTING_1_1_NAME . ' = ' . self::TEST_SETTING_1_1_VALUE; + $this->assertStringNotContainsString($needle, $configDump); + } + + public function testUsingArgsDeleteSingleSetting() + { + + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_1_NAME, self::TEST_SETTING_1_2_NAME); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $this->assertStringContainsString('Success:', $resultObj->output); + + $config = $this->makeNewConfig(); + $configDump = $config->dumpConfig(); + $needle = self::TEST_SETTING_1_1_NAME . ' = ' . self::TEST_SETTING_1_1_VALUE; + $this->assertStringNotContainsString($needle, $configDump); + } + + public function testUsingOptsDeleteArraySetting() + { + + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_2_NAME, self::TEST_SETTING_2_1_NAME, self::TEST_SETTING_2_1_VALUE_0); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $this->assertStringContainsString('Success:', $resultObj->output); + + $config = $this->makeNewConfig(); + $configDump = $config->dumpConfig(); + $needle = self::TEST_SETTING_1_1_NAME . ' = ' . self::TEST_SETTING_1_1_VALUE; + $this->assertStringNotContainsString($needle, $configDump); + } + + public function testUsingArgsDeleteArraySetting() + { + + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_2_NAME, self::TEST_SETTING_2_1_NAME, self::TEST_SETTING_2_1_VALUE_2); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $this->assertStringContainsString('Success:', $resultObj->output); + + $config = $this->makeNewConfig(); + $configDump = $config->dumpConfig(); + $needle = self::TEST_SETTING_1_1_NAME . ' = ' . self::TEST_SETTING_1_1_VALUE; + $this->assertStringNotContainsString($needle, $configDump); + } +} diff --git a/plugins/CoreAdminHome/tests/Integration/Commands/ConfigGetTest.php b/plugins/CoreAdminHome/tests/Integration/Commands/ConfigGetTest.php new file mode 100644 index 0000000000..4263d106a7 --- /dev/null +++ b/plugins/CoreAdminHome/tests/Integration/Commands/ConfigGetTest.php @@ -0,0 +1,662 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\CoreAdminHome\tests\Integration\Commands; + +use Psr\Container\ContainerInterface; +use Piwik\Application\Kernel\GlobalSettingsProvider; +use Piwik\Config; +use Piwik\Tests\Framework\TestCase\ConsoleCommandTestCase; +use Symfony\Component\Yaml\Yaml; + +/** + * @group Core + * @group CoreAdminHome + * @group Integration + */ +class ConfigGetTest extends ConsoleCommandTestCase +{ + /* + * The text this command outputs when no matching value is found. + */ + + private const COMMAND = 'config:get'; + private const CLASS_NAME_SHORT = 'ConfigDeleteTest'; + private const MSG_NOTHING_FOUND = 'Nothing found'; + + /* + * Path to store the test config file. It should be deleted when done. + */ + private const TEST_CONFIG_PATH = '/tmp/test.config.ini.php'; + // Section 1. + private const TEST_SECTION_1_NAME = self::CLASS_NAME_SHORT . '_test_section_1'; + private const TEST_SETTING_1_1_NAME = self::CLASS_NAME_SHORT . '_test_setting_1'; + private const TEST_SETTING_1_1_VALUE = self::CLASS_NAME_SHORT . '_test_value_1'; + private const TEST_SETTING_1_2_NAME = self::CLASS_NAME_SHORT . '_test_setting_2'; + private const TEST_SETTING_1_2_VALUE = self::CLASS_NAME_SHORT . '_test_value_2'; + private const TEST_SETTING_1_SUMMARIZED = [ + self::TEST_SETTING_1_1_NAME => self::TEST_SETTING_1_1_VALUE, + self::TEST_SETTING_1_2_NAME => self::TEST_SETTING_1_2_VALUE, + ]; + // Section 2. + private const TEST_SECTION_2_NAME = self::CLASS_NAME_SHORT . '_test_section_2'; + private const TEST_SETTING_2_1_NAME = self::CLASS_NAME_SHORT . '_array_setting'; + private const TEST_SETTING_2_1_VALUE_0 = self::CLASS_NAME_SHORT . '_arr_val_1'; + private const TEST_SETTING_2_1_VALUE_1 = self::CLASS_NAME_SHORT . '_arr_val_2'; + private const TEST_SETTING_2_1_VALUE_2 = self::CLASS_NAME_SHORT . '_arr_val_3'; + private const TEST_SETTING_2_1_VALUES = [self::TEST_SETTING_2_1_VALUE_0, self::TEST_SETTING_2_1_VALUE_1, self::TEST_SETTING_2_1_VALUE_2]; + + public static function setUpBeforeClass(): void + { + self::removeTestConfigFile(); + parent::setUpBeforeClass(); + } + + public function setUp(): void + { + self::removeTestConfigFile(); + parent::setUp(); + } + + public function tearDown(): void + { + parent::tearDown(); + } + + private static function getTestConfigFilePath() + { + return PIWIK_INCLUDE_PATH . self::TEST_CONFIG_PATH; + } + + public static function provideContainerConfigBeforeClass() + { + return array( + // use a config instance that will save to a test INI file + 'Piwik\Config' => function (ContainerInterface $containerInterface) { + /** @var GlobalSettingsProvider $actualGlobalSettingsProvider */ + $actualGlobalSettingsProvider = $containerInterface->get('Piwik\Application\Kernel\GlobalSettingsProvider'); + + $config = self::makeTestConfig(); + + // copy over sections required for tests + $config->tests = $actualGlobalSettingsProvider->getSection('tests'); + $config->database = $actualGlobalSettingsProvider->getSection('database_tests'); + + return $config; + }, + ); + } + + private static function makeTestConfig() + { + $settingsProvider = new GlobalSettingsProvider(null, self::getTestConfigFilePath()); + $config = new Config($settingsProvider); + + // Add the first section. + $sectionName = self::TEST_SECTION_1_NAME; + $config->$sectionName[self::TEST_SETTING_1_1_NAME] = self::TEST_SETTING_1_1_VALUE; + $config->$sectionName[self::TEST_SETTING_1_2_NAME] = self::TEST_SETTING_1_2_VALUE; + + // Add a second section so we are testing that we do not accidentally return it. + $sectionName = self::TEST_SECTION_1_NAME . '_second_section'; + // Add a setting with the same name as in section #1 but with a random int value. + $config->$sectionName[self::TEST_SETTING_1_1_NAME] = random_int(PHP_INT_MIN, PHP_INT_MAX); + // Add another setting to the same section with some bogus content. + $config->$sectionName[self::TEST_SETTING_1_2_NAME . '_another'] = '127.0.0.1'; + + // Add an array value like section=PluginsInstalled; setting=PluginsInstalled[]. + $sectionName = self::TEST_SECTION_2_NAME; + // Add some values to the setting array. + $config->$sectionName[self::TEST_SETTING_2_1_NAME] = self::TEST_SETTING_2_1_VALUES; + + $config->forceSave(); + return $config; + } + + private static function removeTestConfigFile() + { + $configPath = self::getTestConfigFilePath(); + if (file_exists($configPath)) { + unlink($configPath); + } + } + + private function runCommandWithOptions(string $sectionName, string $settingName = '', $format = 'json'): object + { + + $inputArr = [ + 'command' => self::COMMAND, + '--section' => $sectionName, + '--key' => $settingName, + '-vvv' => false, + ]; + // To allow using default format, only add the format option if specified. + if (!empty($format)) { + $inputArr['--format'] = $format; + } + $exitCode = $this->applicationTester->run($inputArr); + + // Pass true to getDisplay(true) to normalize line endings, then trim() bc CLI adds an \ automatically. + $output = trim($this->applicationTester->getDisplay(true)); + + // Put the results in an easy-to-handle object format. + return (object) ['exitCode' => $exitCode, 'output' => $output]; + } + + private function runCommandWithArguments(string $sectionName, string $settingName = '', $format = 'json'): object + { + + $inputArr = [ + 'command' => self::COMMAND, + '-vvv' => false, + 'argument' => $sectionName . (empty($settingName) ? '' : ".$settingName"), + ]; + // To allow using default format, only add the format option if specified. + if (!empty($format)) { + $inputArr['--format'] = $format; + } + $exitCode = $this->applicationTester->run($inputArr); + + // Pass true to getDisplay(true) to normalize line endings, then trim() bc CLI adds an \ automatically. + $output = trim($this->applicationTester->getDisplay(true)); + + // Put the results in an easy-to-handle object format. + return (object) ['exitCode' => $exitCode, 'output' => $output]; + } + + // + //************************************************************************* + // Tests that should yield errors. + //************************************************************************* + // + + public function testNoArgsShouldYieldError() + { + + $inputArr = [ + 'command' => self::COMMAND, + '-vvv' => false, + ]; + $exitCode = $this->applicationTester->run($inputArr); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $exitCode); + + // Pass true to getDisplay(true) to normalize line endings, then trim() bc CLI adds an \ automatically. + $output = trim($this->applicationTester->getDisplay(true)); + + $this->assertStringContainsString('InvalidArgumentException', $output); + } + + public function testEmptyArgsShouldYieldError() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithArguments(''); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $resultObj->exitCode); + + $this->assertStringContainsString('InvalidArgumentException', $resultObj->output); + } + + public function testEmptyOptionsShouldYieldError() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions(''); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $resultObj->exitCode); + + $this->assertStringContainsString('InvalidArgumentException', $resultObj->output); + } + + public function testSetArgsAndOptionsShouldYieldError() + { + $inputArr = [ + 'command' => self::COMMAND, + 'argument' => self::TEST_SECTION_1_NAME . '.' . self::TEST_SETTING_1_1_NAME, + '--section' => self::TEST_SECTION_1_NAME, + '--key' => self::TEST_SETTING_1_1_NAME, + '-vvv' => false, + ]; + $exitCode = $this->applicationTester->run($inputArr); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $exitCode); + + // Pass true to getDisplay(true) to normalize line endings, then trim() bc CLI adds an \ automatically. + $output = trim($this->applicationTester->getDisplay(true)); + + $this->assertStringContainsString('InvalidArgumentException', $output); + } + + public function testEmptySectionShouldYieldError() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions('', self::TEST_SETTING_1_1_NAME); + + // The CLI error code should be >0 indicating failure. + $this->assertGreaterThan(0, $resultObj->exitCode); + + $this->assertStringContainsString('InvalidArgumentException', $resultObj->output); + } + + // + //************************************************************************* + // Tests for nonexistent data. + //************************************************************************* + // + public function testUsingOptsNonExistentSectionShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions(self::CLASS_NAME_SHORT . '_Section_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsNonExistentSectionShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithArguments(self::CLASS_NAME_SHORT . '_Section_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsNonExistentSectionAndSettingShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions(self::CLASS_NAME_SHORT . '_Section_does_not_exist', self::CLASS_NAME_SHORT . '_Setting_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsNonExistentSectionAndSettingShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithArguments(self::CLASS_NAME_SHORT . '_Section_does_not_exist', self::CLASS_NAME_SHORT . '_Setting_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsNonExistentSettingShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, self::CLASS_NAME_SHORT . '_Setting_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsNonExistentSettingShouldYieldEmpty() + { + + // Pass empty section name. + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_1_NAME, self::CLASS_NAME_SHORT . '_Setting_does_not_exist'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = self::MSG_NOTHING_FOUND; + $this->assertEquals($expectedValue, $resultObj->output); + } + // + //************************************************************************* + // Tests for existing data. + //************************************************************************* + // + + /** + * Assumes default --format=json. + */ + public function testUsingOptsGetSingleSettingFormatDefault() + { + + // Specifically set format='' (empty string) so we use the CLI default --format=json. + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, self::TEST_SETTING_1_1_NAME, ''); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + // With the default --format=json, the result should be JSON-encoded, meaning value=MyString gets wrapped in quotes like this: "MyString". + $expectedValue = json_encode(self::TEST_SETTING_1_1_VALUE); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsGetSingleSettingFormatDefault() + { + + // Specifically set format='' (empty string) so we use the CLI default --format=json. + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_1_NAME, self::TEST_SETTING_1_1_NAME, ''); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + // With the default --format=json, the result should be JSON-encoded, meaning value=MyString gets wrapped in quotes like this: "MyString". + $expectedValue = json_encode(self::TEST_SETTING_1_1_VALUE); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsGetSingleSettingFormatYaml() + { + + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, self::TEST_SETTING_1_1_NAME, 'yaml'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + // With --format=yaml, a single value=MyString comes back with no quoting or brackets, e.g.: MyString. + $expectedValue = self::TEST_SETTING_1_1_VALUE; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsGetSingleSettingFormatYaml() + { + + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_1_NAME, self::TEST_SETTING_1_1_NAME, 'yaml'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + // With --format=yaml, a single value=MyString comes back with no quoting or brackets, e.g.: MyString. + $expectedValue = self::TEST_SETTING_1_1_VALUE; + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsGetSingleSettingFormatText() + { + + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, self::TEST_SETTING_1_1_NAME, 'text'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $resultArr = explode(PHP_EOL, $resultObj->output); + $resultArrLineCounter = 0; + + $expectedValue = self::TEST_SECTION_1_NAME . '.' . self::TEST_SETTING_1_1_NAME . ' = ' . self::TEST_SETTING_1_1_VALUE; + $this->assertEquals($expectedValue, $resultArr[$resultArrLineCounter++]); + } + + public function testUsingArgsGetSingleSettingFormatText() + { + + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_1_NAME, self::TEST_SETTING_1_1_NAME, 'text'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $resultArr = explode(PHP_EOL, $resultObj->output); + $resultArrLineCounter = 0; + + $expectedValue = self::TEST_SECTION_1_NAME . '.' . self::TEST_SETTING_1_1_NAME . ' = ' . self::TEST_SETTING_1_1_VALUE; + $this->assertEquals($expectedValue, $resultArr[$resultArrLineCounter++]); + } + + /** + * Assumes default --format=json. + */ + public function testUsingOptsGetSectionFormatDefault() + { + + // Specifically set format='' (empty string) so we use the CLI default --format=json. + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, false, ''); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = json_encode((object) self::TEST_SETTING_1_SUMMARIZED); + $this->assertEquals($expectedValue, $resultObj->output); + } + + /** + * Assumes default --format=json. + */ + public function testUsingArgsGetSectionFormatDefault() + { + + // Specifically set format='' (empty string) so we use the CLI default --format=json. + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_1_NAME, false, ''); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = json_encode((object) self::TEST_SETTING_1_SUMMARIZED); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsGetSectionFormatYaml() + { + + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, false, 'yaml'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = trim(Yaml::dump(self::TEST_SETTING_1_SUMMARIZED, 2, 2, true)); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsGetSectionFormatYaml() + { + + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_1_NAME, false, 'yaml'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = trim(Yaml::dump(self::TEST_SETTING_1_SUMMARIZED, 2, 2, true)); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsGetSectionNoArrayFormatText() + { + + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, false, 'text'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $resultArr = explode(PHP_EOL, $resultObj->output); + $resultArrLineCounter = 0; + $this->assertStringContainsString('[' . self::TEST_SECTION_1_NAME . ']', $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString('--', $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString(self::TEST_SETTING_1_1_NAME . ' = ' . self::TEST_SETTING_1_1_VALUE, $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString(self::TEST_SETTING_1_2_NAME . ' = ' . self::TEST_SETTING_1_2_VALUE, $resultArr[$resultArrLineCounter++]); + } + + public function testUsingArgsGetSectionNoArrayFormatText() + { + + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_1_NAME, false, 'text'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $resultArr = explode(PHP_EOL, $resultObj->output); + $resultArrLineCounter = 0; + $this->assertStringContainsString('[' . self::TEST_SECTION_1_NAME . ']', $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString('--', $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString(self::TEST_SETTING_1_1_NAME . ' = ' . self::TEST_SETTING_1_1_VALUE, $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString(self::TEST_SETTING_1_2_NAME . ' = ' . self::TEST_SETTING_1_2_VALUE, $resultArr[$resultArrLineCounter++]); + } + + public function testUsingOptsGetSectionWithArrayFormatText() + { + + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_1_NAME, false, 'text'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $resultArr = explode(PHP_EOL, $resultObj->output); + $resultArrLineCounter = 0; + $this->assertStringContainsString('[' . self::TEST_SECTION_1_NAME . ']', $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString('--', $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString(self::TEST_SETTING_1_1_NAME . ' = ' . self::TEST_SETTING_1_1_VALUE, $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString(self::TEST_SETTING_1_2_NAME . ' = ' . self::TEST_SETTING_1_2_VALUE, $resultArr[$resultArrLineCounter++]); + } + + public function testUsingArgsGetSectionWithArrayFormatText() + { + + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_1_NAME, false, 'text'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $resultArr = explode(PHP_EOL, $resultObj->output); + $resultArrLineCounter = 0; + $this->assertStringContainsString('[' . self::TEST_SECTION_1_NAME . ']', $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString('--', $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString(self::TEST_SETTING_1_1_NAME . ' = ' . self::TEST_SETTING_1_1_VALUE, $resultArr[$resultArrLineCounter++]); + $this->assertStringContainsString(self::TEST_SETTING_1_2_NAME . ' = ' . self::TEST_SETTING_1_2_VALUE, $resultArr[$resultArrLineCounter++]); + } + + public function testUsingOptsGetSectionWithArray() + { + + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_2_NAME); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $settingName = self::TEST_SETTING_2_1_NAME; + $expectedValue = json_encode((object) [$settingName => self::TEST_SETTING_2_1_VALUES]); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsGetSectionWithArray() + { + + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_2_NAME); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $settingName = self::TEST_SETTING_2_1_NAME; + $expectedValue = json_encode((object) [$settingName => self::TEST_SETTING_2_1_VALUES]); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsGetArraySettingFromSection() + { + + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_2_NAME, self::TEST_SETTING_2_1_NAME); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = json_encode(self::TEST_SETTING_2_1_VALUES); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsGetArraySettingFromSection() + { + + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_2_NAME, self::TEST_SETTING_2_1_NAME); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = json_encode(self::TEST_SETTING_2_1_VALUES); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsGetArraySettingWithBrackets() + { + + $resultObj = $this->runCommandWithOptions(self::TEST_SECTION_2_NAME, self::TEST_SETTING_2_1_NAME . '[]'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = json_encode(self::TEST_SETTING_2_1_VALUES); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingArgsGetArraySettingWithBrackets() + { + + $resultObj = $this->runCommandWithArguments(self::TEST_SECTION_2_NAME, self::TEST_SETTING_2_1_NAME . '[]'); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $resultObj->exitCode, $this->getCommandDisplayOutputErrorMessage()); + + $expectedValue = json_encode(self::TEST_SETTING_2_1_VALUES); + $this->assertEquals($expectedValue, $resultObj->output); + } + + public function testUsingOptsCallWithMultipleSectionsReturnsLastSectionOnly() + { + + $inputArr = [ + 'command' => self::COMMAND, + '--section' => self::TEST_SECTION_2_NAME, + '--section' => self::TEST_SECTION_1_NAME, + '-vvv' => false, + ]; + $exitCode = $this->applicationTester->run($inputArr); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $exitCode); + + // Pass true to getDisplay(true) to normalize line endings, then trim() bc CLI adds an \ automatically. + $output = trim($this->applicationTester->getDisplay(true)); + + $expectedValue = json_encode((object) self::TEST_SETTING_1_SUMMARIZED); + $this->assertEquals($expectedValue, $output); + } + + public function testUsingArgsCallWithMultipleSectionsReturnsLastSectionOnly() + { + + $inputArr = [ + 'command' => self::COMMAND, + '-vvv' => false, + 'argument' => self::TEST_SECTION_2_NAME . ' ' . self::TEST_SECTION_1_NAME, + ]; + $exitCode = $this->applicationTester->run($inputArr); + + // The CLI error code should be 0 indicating success. + $this->assertEquals(0, $exitCode); + + // Pass true to getDisplay(true) to normalize line endings, then trim() bc CLI adds an \ automatically. + $output = trim($this->applicationTester->getDisplay(true)); + + $expectedValue = json_encode((object) self::TEST_SETTING_1_SUMMARIZED); + $this->assertEquals($expectedValue, $output); + } +} diff --git a/plugins/CoreAdminHome/tests/Integration/SetConfigTest.php b/plugins/CoreAdminHome/tests/Integration/Commands/SetConfigTest.php index 09da98f6ee..9049f1ac09 100644 --- a/plugins/CoreAdminHome/tests/Integration/SetConfigTest.php +++ b/plugins/CoreAdminHome/tests/Integration/Commands/SetConfigTest.php @@ -15,8 +15,9 @@ use Piwik\Tests\Framework\TestCase\ConsoleCommandTestCase; use Piwik\Url; /** + * @group Core * @group CoreAdminHome - * @group CoreAdminHome_Integration + * @group Integration */ class SetConfigTest extends ConsoleCommandTestCase { |