Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormwithheld <796986+mwithheld@users.noreply.github.com>2021-09-15 16:27:37 +0300
committerGitHub <noreply@github.com>2021-09-15 16:27:37 +0300
commitd0215334b3660faa04f7d601f634b82cb01dc003 (patch)
tree5ec0fac433619fd0401fdf0bc35a77ec80e294f0 /plugins/CoreAdminHome
parent9585236c88c591b377758bad315564852c27ef43 (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.php282
-rw-r--r--plugins/CoreAdminHome/Commands/ConfigGet.php311
-rw-r--r--plugins/CoreAdminHome/tests/Integration/Commands/ConfigDeleteTest.php446
-rw-r--r--plugins/CoreAdminHome/tests/Integration/Commands/ConfigGetTest.php662
-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
{