diff --git a/bin/cfg b/bin/cfg index 3c7c0ef..2ce253f 100755 --- a/bin/cfg +++ b/bin/cfg @@ -22,9 +22,59 @@ foreach ($autoloadFiles as $autoloadFile) { } } -function verboseLog(bool $enabled, string $message): void -{ - if ($enabled) fwrite(STDERR, "[verbose] $message".PHP_EOL); +// Pre-parse site options so they can be passed after positional args. +// The input helper library does not handle this case reliably. +$preparsedSiteOptions = []; +$rawArgv = $_SERVER['argv'] ?? $GLOBALS['argv'] ?? null; +if (is_array($rawArgv) && !empty($rawArgv)) { + $filteredArgv = [$rawArgv[0]]; + for ($idx = 1; $idx < count($rawArgv); $idx++) { + $arg = $rawArgv[$idx]; + + if ($arg === '-s' || $arg === '--site') { + $preparsedSiteOptions[] = [ + 'name' => 'site', + 'value' => $rawArgv[$idx + 1] ?? '', + ]; + if (isset($rawArgv[$idx + 1])) $idx++; + continue; + } + if (str_starts_with($arg, '--site=')) { + $preparsedSiteOptions[] = [ + 'name' => 'site', + 'value' => substr($arg, strlen('--site=')), + ]; + continue; + } + + if ($arg === '--directory-name' || $arg === '--directoryName') { + $preparsedSiteOptions[] = [ + 'name' => 'directoryName', + 'value' => $rawArgv[$idx + 1] ?? '', + ]; + if (isset($rawArgv[$idx + 1])) $idx++; + continue; + } + if (str_starts_with($arg, '--directory-name=')) { + $preparsedSiteOptions[] = [ + 'name' => 'directoryName', + 'value' => substr($arg, strlen('--directory-name=')), + ]; + continue; + } + if (str_starts_with($arg, '--directoryName=')) { + $preparsedSiteOptions[] = [ + 'name' => 'directoryName', + 'value' => substr($arg, strlen('--directoryName=')), + ]; + continue; + } + + $filteredArgv[] = $arg; + } + + $_SERVER['argv'] = $filteredArgv; + $GLOBALS['argv'] = $filteredArgv; } $version = '0.4'; @@ -40,55 +90,15 @@ $collection = (new Input\InputCollection()) ->description('Display help text') ) // }}} - ->add( Input\InputTypeFactory::build('LongOption')->name('verbose')->short('v') // {{{ - ->flags(AbstractInputType::FLAG_OPTIONAL) - ->description('Print debug details to STDERR') + ->add( Input\InputTypeFactory::build('LongOption')->name('in')->short('i') // {{{ + ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) + ->description('Path to a json data file to to read') ) // }}} - ->add( Input\InputTypeFactory::build('LongOption')->name('in')->short('i') // {{{ - ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) - ->description('Path to a JSON data file to read (for write)') - ->validator(new Input\Validator( - function (AbstractInputType $input, AbstractInputHandler $context) - { - $path = $context->find('in'); - if ($path === null || $path === '') { - return null; - } - - if (!is_readable($path)) - { - throw new \Exception("Input file is not readable: $path"); - } - - $content = file_get_contents($path); - if ($content === false) - { - throw new \Exception("Can not read input file: $path"); - } - - try - { - $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); - } - catch (\JsonException $ex) - { - throw new \Exception(sprintf( - 'Input JSON is invalid (%s): %s', - (string)$ex->getCode(), - $ex->getMessage() - )); - } - - if (!is_array($data)) - { - throw new \Exception('Input JSON must decode to an object/array'); - } - - return $data; - } - )) - ) // }}} + ->add( Input\InputTypeFactory::build('LongOption')->name('out')->short('o') // {{{ + ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) + ->description('Path to a json file to to write to') + ) // }}} ->add( Input\InputTypeFactory::build('LongOption')->name('mode')->short('m') // {{{ ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) @@ -105,59 +115,15 @@ $collection = (new Input\InputCollection()) ->description('Path where the config/ directory of the package conf files is located, defaults to the working dir') ) // }}} - ->add( Input\InputTypeFactory::build('LongOption')->name('siteDir')->short('s') // {{{ - ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) - ->description('Site/instance directory below config/. Accepts owner_xyz or config/owner_xyz') - ->validator(new Input\Validator( - function (AbstractInputType $input, AbstractInputHandler $context) - { - $siteInput = $context->find('siteDir'); - if ($siteInput === null || $siteInput === false) { - return null; - } + ->add( Input\InputTypeFactory::build('LongOption')->name('site')->short('s') // {{{ + ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) + ->description('Site/instance directory below config/ (target: config//.conf.php), e.g. owner_xyz') + ) // }}} - $siteInput = trim((string)$siteInput); - $appPath = $context->find('appPath'); - if (!$appPath) { - $appPath = getcwd().'/'; - } - $appPath = rtrim($appPath, '/').'/'; - - // Accept both "owner_xyz" and "config/owner_xyz" and normalize to site key. - if (str_starts_with($siteInput, $appPath.'config/')) - { - $siteInput = substr($siteInput, strlen($appPath.'config/')); - } - elseif (str_starts_with($siteInput, 'config/')) - { - $siteInput = substr($siteInput, strlen('config/')); - } - - $siteInput = trim($siteInput, '/'); - if ($siteInput === '') - { - throw new \Exception('Option --siteDir is empty.'); - } - - // Block directory traversal and hidden-dot segments. - if (str_contains($siteInput, '..') || preg_match('~(^|/)\.(?:/|$)~', $siteInput)) - { - throw new \Exception("Invalid directory in --siteDir: '$siteInput'."); - } - - // Allow only predictable path segments for instance directories. - foreach (explode('/', $siteInput) as $part) - { - if ($part === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $part)) - { - throw new \Exception("Invalid directory name segment '$part' in --siteDir."); - } - } - - return $siteInput; - } - )) - ) // }}} + ->add( Input\InputTypeFactory::build('LongOption')->name('directoryName') // {{{ + ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) + ->description('Alias for --site. Accepts owner_xyz or config/owner_xyz (also supports --directory-name)') + ) // }}} ->add( Input\InputTypeFactory::build('Argument')->name('action') // {{{ ->flags(AbstractInputType::FLAG_REQUIRED) @@ -201,30 +167,18 @@ $collection = (new Input\InputCollection()) ->description( 'the settings you want to work on. With action "write" you can pass the value to set as JSON after an equal sign.' ) - ->validator(new Input\Validator( - function (AbstractInputType $input, AbstractInputHandler $context) - { - $setting = $context->find('setting'); - $action = $context->find('action'); - $inputPayload = $context->find('in'); - - if ($action === 'write') - { - if (($setting !== null && $setting !== '') && $inputPayload) { - throw new \Exception('Please use either SETTING or --in, not both.'); - } - } - - if ($setting === null || $setting === '') { - return ['key' => '', 'value' => null]; - } - + ->validator(new Input\Validator( + function (AbstractInputType $input, AbstractInputHandler $context) + { + $setting = $context->find('setting'); + $action = $context->find('action'); + $setting = explode('=', $setting); $settings['key'] = $setting[0]; if ($action === 'write') { - $value = $setting[1] ?? null; - if (! (isset($value) || $context->find('in'))) { + $value = $setting[1]; + if (! (isset($value) || $context->find('data'))) { throw new \Exception('You need a value to write'); } $specialValues = [ 'true', 'false', 'null' ]; @@ -250,50 +204,28 @@ $collection = (new Input\InputCollection()) $usage = Cli\manpage( basename(__FILE__), $version, 'read write settings', $collection, Colour::FG_GREEN, Colour::FG_WHITE, - [ - 'Examples' => - 'Basic usage:' - .PHP_EOL - .'cfg show VeruA db:host' - .PHP_EOL - .'cfg write VeruA \'db:host="newHost"\'' - .PHP_EOL - .PHP_EOL - .'Advance usage:' - .PHP_EOL - .'cfg write prefix \'module:enabled=true\'' - .PHP_EOL - .PHP_EOL - .'#Site specific write:' - .PHP_EOL - .'cfg write prefix \'module:enabled=true\' --siteDir=owner_xyz' - .PHP_EOL - .PHP_EOL - .'#Batch write from JSON file:' - .PHP_EOL - .'cfg write prefix --siteDir=owner_xyz -i /tmp/extension.json' - .PHP_EOL - .PHP_EOL - .'#Batch write from JSON string:' - .PHP_EOL - .'cfg write prefix \'module={"enabled":true,"timeout":30,"label":"example"}\' --siteDir=owner_xyz' - .PHP_EOL - .PHP_EOL - .'#Read merged value for a site:' - .PHP_EOL - .'cfg show prefix module:enabled --siteDir=owner_xyz' - .PHP_EOL - ] + [ + 'Examples' => + 'cfg write myCEESV \'auth:projectId="218523"\'' + .PHP_EOL + .'# writes local config: config/myCEESV.conf.php' + .PHP_EOL + .'cfg write myCEESV \'auth:projectId="218523"\' --directory-name=owner_xyz' + .PHP_EOL + .'# writes instance config: config/owner_xyz/myCEESV.conf.php' + .PHP_EOL + .'cfg show myCEESV auth:projectId --site=owner_xyz' + .PHP_EOL + .'# reads merged config including config/owner_xyz/myCEESV.conf.php' + .PHP_EOL + ] ).PHP_EOL; // Get the supplied input. Passing the collection will make the handler bind values // and validate the input according to our collection -try -{ +try { $argv = Input\InputHandlerFactory::build('Argv', $collection); -} -catch (\Exception $ex) -{ +} catch (\Exception $ex) { echo $usage; if (isset($argv[1])) { @@ -315,11 +247,27 @@ if ($argv->find( 'help' ) || $argv->find('action') == 'help') $prefix = $argv->find('prefix'); -$verbose = (bool)$argv->find('verbose'); +/* +echo $argv->find('action').PHP_EOL; +echo ($prefix).PHP_EOL; +echo $argv->find('setting')['key'].PHP_EOL; +echo $argv->find('setting')['value'].PHP_EOL; + */ +// var_dump($cfg); $appPath = $argv->find('appPath'); if (!$appPath) $appPath = getcwd().'/'; $appPath = rtrim($appPath, '/').'/'; +/* $it = new RecursiveDirectoryIterator($appPath); + +foreach(new RecursiveIteratorIterator($it) as $file) +{ + $configDir = $file->getPath(); + if ($file->isDir() && $file->getFilename() == '.' && basename($configDir) == 'config') { + echo "found config dir: $configDir\n"; + } +} + */ $mode = ($argv->find('mode') == '') ? null : $argv->find('mode'); $cfg = (new Settings([], $mode))->appPath($appPath)->prefix($prefix); // pkgPath points to package defaults (e.g. .default.conf.php) @@ -327,29 +275,70 @@ if ($pkgPath = $argv->find('pkgPath')) $cfg->pkgPath(rtrim($pkgPath, '/').'/'); $site = null; $siteFlag = 0x01; -$site = $argv->find('siteDir'); -if ($site !== null && $site !== false) -{ - // Reuse util-settings site resolution: config//.conf.php - $cfg->site($site); - verboseLog($verbose, "siteDir resolved to '$site'"); +// Only one site selector may be provided to avoid ambiguous targets. +$providedSiteOptions = []; +foreach ($preparsedSiteOptions as $option) { + $providedSiteOptions[$option['name']][] = $option['value'] ?? ''; +} +foreach (['site', 'directoryName'] as $optionName) { + $parsedValue = $argv->find($optionName); + if ($parsedValue !== null && $parsedValue !== '' && $parsedValue !== false) { + $providedSiteOptions[$optionName][] = $parsedValue; + } } -try -{ +if (count($providedSiteOptions) > 1) { + fwrite(STDERR, 'Please use only one of --site or --directoryName (alias: --directory-name).'.PHP_EOL); + exit(1); +} +if (!empty($providedSiteOptions)) { + $optionName = array_key_first($providedSiteOptions); + $siteInput = trim((string)end($providedSiteOptions[$optionName])); + + if ($optionName === 'directoryName') { + // Accept both "owner_xyz" and "config/owner_xyz" and normalize to site key. + if (str_starts_with($siteInput, $appPath.'config/')) { + $siteInput = substr($siteInput, strlen($appPath.'config/')); + } elseif (str_starts_with($siteInput, 'config/')) { + $siteInput = substr($siteInput, strlen('config/')); + } + } + + $siteInput = trim($siteInput, '/'); + if ($siteInput === '') { + fwrite(STDERR, "Option --$optionName is empty.".PHP_EOL); + exit(1); + } + // Block directory traversal and hidden-dot segments. + if (str_contains($siteInput, '..') || preg_match('~(^|/)\.(?:/|$)~', $siteInput)) { + fwrite(STDERR, "Invalid directory in --$optionName: '$siteInput'.".PHP_EOL); + exit(1); + } + // Allow only predictable path segments for instance directories. + foreach (explode('/', $siteInput) as $part) { + if ($part === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $part)) { + fwrite(STDERR, "Invalid directory name segment '$part' in --$optionName.".PHP_EOL); + exit(1); + } + } + + $site = $siteInput; + // Reuse util-settings site resolution: config//.conf.php + $cfg->site($site); +} + +try { if (is_readable($cfg->buildFileName('default'))) { $cfg->load(); } elseif (is_readable($cfgFile = $cfg->buildFileName())) { $cfg->load(require($cfgFile)); } -} -catch (\Throwable $e) -{ +} catch (\Throwable $e) { fwrite(STDERR, "Error: ".$e->getMessage().PHP_EOL); exit(1); } -verboseLog($verbose, 'config bootstrap loaded'); +//var_dump($cfg); $result = $cfg; $settings = $argv->find('setting') ?? $settings; @@ -374,57 +363,37 @@ if ($result instanceof Settings) $result = $result->toArray(); switch ($argv->find('action')) { case 'show': - verboseLog($verbose, "show action for prefix '$prefix'"); $out = (is_string($result)) ? $result : json_encode($result, JSON_PRETTY_PRINT); echo $out.PHP_EOL; break; case 'write': - $inputPayload = $argv->find('in'); - if (!$inputPayload && $settings['key'] === '') - { - fwrite(STDERR, 'Nothing to write: provide SETTING or --in.'.PHP_EOL); - exit(1); - } $path = ($settings['key'] !== '') ? explode(':', $settings['key']) : []; - verboseLog($verbose, 'write source: '.($inputPayload ? '--in' : 'SETTING')); - - if ($inputPayload) - { - $setting2write = $inputPayload; - } - else - { - $setting2write = $settings['value']; - } - verboseLog($verbose, 'write path: '.json_encode($path)); + + $setting2write = $settings['value']; while ( ! empty($path)) { $setting2write = [array_pop($path) => $setting2write]; } - $writeType = ($site !== null && $site !== false) ? $siteFlag : null; + $writeType = ($site !== null) ? $siteFlag : null; $file = $cfg->buildFileName($writeType); - verboseLog($verbose, "write target: $file"); if (is_readable($file)) { $setting2write = array_replace_recursive(require($file), $setting2write); copy($file, "$file.bak"); - verboseLog($verbose, "existing config merged from: $file"); } $targetDir = dirname($file); - if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) - { + if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) { fwrite(STDERR, "Can not create directory: $targetDir".PHP_EOL); exit(1); } $writeCfg = $cfg->create($setting2write); - verboseLog($verbose, 'payload prepared for writer'); - try - { +// var_dump($writeCfg->toArray()); + try { (new SettingsWriter($writeCfg, '', $writeType))->write(); echo "Written modified settings to: $file".PHP_EOL; } diff --git a/tests/CfgTest.php b/tests/CfgTest.php index 646bcb2..79167ea 100644 --- a/tests/CfgTest.php +++ b/tests/CfgTest.php @@ -8,7 +8,6 @@ class CfgTest extends TestCase { private string $tmpDir; - /* Lifecycle {{{ */ protected function setUp(): void { $this->tmpDir = rtrim(sys_get_temp_dir(), '/').'/util-settings-cfg-'.bin2hex(random_bytes(8)); @@ -16,8 +15,8 @@ class CfgTest extends TestCase mkdir($this->tmpDir.'/config', 0775, true); file_put_contents( - $this->tmpDir.'/config/Extension.default.conf.php', - " 'prod',\n\t'module' => [\n\t\t'code' => '',\n\t\t'label' => '',\n\t\t'flags' => [\n\t\t\t'enabled' => false,\n\t\t],\n\t],\n\t'feature' => [\n\t\t'endpoint' => '',\n\t],\n];\n" + $this->tmpDir.'/config/myCEESV.default.conf.php', + " 'prod',\n\t'auth' => [\n\t\t'projectId' => '',\n\t\t'clientId' => '',\n\t],\n];\n" ); } @@ -25,390 +24,261 @@ class CfgTest extends TestCase { $this->removeDir($this->tmpDir); } - /* }}} */ - /* Write Flow {{{ */ public function testWriteWithoutDirectoryNameUsesLocalConfigFile(): void { - $result = $this->runWrite([ - 'module:code="X100"', + $result = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="218523"', ]); - $this->assertCfgSuccess($result); + $this->assertSame(0, $result['code'], $result['output']); - $localFile = $this->tmpDir.'/config/Extension.conf.php'; + $localFile = $this->tmpDir.'/config/myCEESV.conf.php'; $this->assertFileExists($localFile); $cfg = require $localFile; - $this->assertSame('X100', $cfg['module']['code']); + $this->assertSame('218523', $cfg['auth']['projectId']); } public function testWriteWithoutModeFallsBackToDefaultMode(): void { file_put_contents( - $this->tmpDir.'/config/Extension.default.conf.php', - " [\n\t\t'code' => '',\n\t],\n];\n" + $this->tmpDir.'/config/myCEESV.default.conf.php', + " [\n\t\t'projectId' => '',\n\t],\n];\n" ); - $result = $this->runWrite([ - 'module:code="X100"', + $result = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="218523"', ]); - $this->assertCfgSuccess($result); - $this->assertFileExists($this->tmpDir.'/config/Extension.conf.php'); + $this->assertSame(0, $result['code'], $result['output']); + $this->assertFileExists($this->tmpDir.'/config/myCEESV.conf.php'); } - public function testWriteWithInputFileWritesMultipleSettings(): void + public function testWriteWithDirectoryNameCreatesAndWritesSiteConfig(): void { - $inFile = $this->createJsonInputFile([ - 'module' => [ - 'code' => 'X100', - 'label' => 'demo-module', - ], - 'feature' => [ - 'endpoint' => 'https://example.invalid/v1/resource', - ], + $result = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="218523"', + '--directory-name', + 'owner_xyz', ]); - $result = $this->runWrite([ - '--siteDir=owner_xyz', - '-i', - $inFile, - ]); + $this->assertSame(0, $result['code'], $result['output']); - $this->assertCfgSuccess($result); - $cfg = $this->loadWrittenConfig('owner_xyz'); - $this->assertSame('X100', $cfg['module']['code']); - $this->assertSame('demo-module', $cfg['module']['label']); - $this->assertSame('https://example.invalid/v1/resource', $cfg['feature']['endpoint']); - } - - public function testWriteAcceptsJsonStringValueInSetting(): void - { - $result = $this->runWrite([ - 'module={"code":"X100","label":"demo-module"}', - '--siteDir=owner_xyz', - ]); - - $this->assertCfgSuccess($result); - $cfg = $this->loadWrittenConfig('owner_xyz'); - $this->assertSame('X100', $cfg['module']['code']); - $this->assertSame('demo-module', $cfg['module']['label']); - } - - public function testWriteSupportsNestedPathWithColonNotation(): void - { - $result = $this->runWrite([ - 'module:flags:enabled=true', - '--siteDir=owner_xyz', - ]); - - $this->assertCfgSuccess($result); - $cfg = $this->loadWrittenConfig('owner_xyz'); - $this->assertTrue($cfg['module']['flags']['enabled']); - } - - public function testWriteCoercesSimpleStringValueToJsonString(): void - { - $result = $this->runWrite([ - 'module:code=X100', - '--siteDir=owner_xyz', - ]); - - $this->assertCfgSuccess($result); - $cfg = $this->loadWrittenConfig('owner_xyz'); - $this->assertSame('X100', $cfg['module']['code']); - } - - public function testWriteRejectsInvalidJsonStringValue(): void - { - $result = $this->runWrite([ - 'module={"code":', - '--siteDir=owner_xyz', - ]); - - $this->assertCfgFailure($result, 'The value does not appear to be a valid JSON string.'); - } - - public function testWriteWithSiteDirCreatesAndWritesSiteConfig(): void - { - $result = $this->runWrite([ - 'module:code="X100"', - '--siteDir=owner_xyz', - ]); - - $this->assertCfgSuccess($result); - - $siteFile = $this->tmpDir.'/config/owner_xyz/Extension.conf.php'; + $siteFile = $this->tmpDir.'/config/owner_xyz/myCEESV.conf.php'; $this->assertFileExists($siteFile); - $this->assertFileDoesNotExist($this->tmpDir.'/config/Extension.conf.php'); + $this->assertFileDoesNotExist($this->tmpDir.'/config/myCEESV.conf.php'); $cfg = require $siteFile; - $this->assertSame('X100', $cfg['module']['code']); + $this->assertSame('218523', $cfg['auth']['projectId']); } - public function testWriteWithSiteDirMergesIntoExistingSiteConfig(): void + public function testWriteWithDirectoryNameMergesIntoExistingSiteConfig(): void { - $firstWrite = $this->runWrite([ - 'module:code="X100"', - '--siteDir=owner_xyz', + $firstWrite = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="218523"', + '--directory-name', + 'owner_xyz', ]); - $this->assertCfgSuccess($firstWrite); + $this->assertSame(0, $firstWrite['code'], $firstWrite['output']); - $secondWrite = $this->runWrite([ - 'module:label="demo-module"', - '--siteDir=owner_xyz', + $secondWrite = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:clientId="service-9999-qual@myceesv.ch"', + '--directory-name', + 'owner_xyz', ]); - $this->assertCfgSuccess($secondWrite); + $this->assertSame(0, $secondWrite['code'], $secondWrite['output']); - $cfg = $this->loadWrittenConfig('owner_xyz'); + $siteFile = $this->tmpDir.'/config/owner_xyz/myCEESV.conf.php'; + $cfg = require $siteFile; - $this->assertSame('X100', $cfg['module']['code']); - $this->assertSame('demo-module', $cfg['module']['label']); + $this->assertSame('218523', $cfg['auth']['projectId']); + $this->assertSame('service-9999-qual@myceesv.ch', $cfg['auth']['clientId']); } - public function testWriteWithSiteDirUsingConfigPrefixIsNormalized(): void + public function testWriteWithDirectoryNameUsingConfigPrefixIsNormalized(): void { - $result = $this->runWrite([ - 'module:code="X100"', - '--siteDir=config/owner_xyz', + $result = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="218523"', + '--directory-name=config/owner_xyz', ]); - $this->assertCfgSuccess($result); - $this->assertFileExists($this->tmpDir.'/config/owner_xyz/Extension.conf.php'); - $this->assertFileDoesNotExist($this->tmpDir.'/config/config/owner_xyz/Extension.conf.php'); + $this->assertSame(0, $result['code'], $result['output']); + $this->assertFileExists($this->tmpDir.'/config/owner_xyz/myCEESV.conf.php'); + $this->assertFileDoesNotExist($this->tmpDir.'/config/config/owner_xyz/myCEESV.conf.php'); } - public function testWriteWithSiteDirUsingAbsoluteConfigPrefixIsNormalized(): void + public function testWriteWithSiteOptionWritesToSiteConfig(): void { - $result = $this->runWrite([ - 'module:code="X100"', - '--siteDir='.$this->tmpDir.'/config/owner_xyz', + $result = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="218523"', + '--site=owner_xyz', ]); - $this->assertCfgSuccess($result); - $this->assertFileExists($this->tmpDir.'/config/owner_xyz/Extension.conf.php'); + $this->assertSame(0, $result['code'], $result['output']); + $this->assertFileExists($this->tmpDir.'/config/owner_xyz/myCEESV.conf.php'); } - public function testWriteWithNestedSiteDirWritesToNestedSiteConfig(): void + public function testWriteWithDirectoryNameCamelCaseOptionWritesToSiteConfig(): void { - $result = $this->runWrite([ - 'module:code="X100"', - '--siteDir=owner_xyz/sub_a', + $result = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="218523"', + '--directoryName=owner_xyz', ]); - $this->assertCfgSuccess($result); - $this->assertFileExists($this->tmpDir.'/config/owner_xyz/sub_a/Extension.conf.php'); - } - - public function testWriteWithSiteDirWritesToSiteConfig(): void - { - $result = $this->runWrite([ - 'module:code="X100"', - '--siteDir=owner_xyz', - ]); - - $this->assertCfgSuccess($result); - $this->assertFileExists($this->tmpDir.'/config/owner_xyz/Extension.conf.php'); + $this->assertSame(0, $result['code'], $result['output']); + $this->assertFileExists($this->tmpDir.'/config/owner_xyz/myCEESV.conf.php'); } public function testWriteToExistingSiteConfigCreatesBackup(): void { - $firstWrite = $this->runWrite([ - 'module:code="first"', - '--siteDir=owner_xyz', + $firstWrite = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="first"', + '--directory-name=owner_xyz', ]); - $this->assertCfgSuccess($firstWrite); + $this->assertSame(0, $firstWrite['code'], $firstWrite['output']); - $secondWrite = $this->runWrite([ - 'module:code="second"', - '--siteDir=owner_xyz', + $secondWrite = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="second"', + '--directory-name=owner_xyz', ]); - $this->assertCfgSuccess($secondWrite); + $this->assertSame(0, $secondWrite['code'], $secondWrite['output']); - $siteFile = $this->tmpDir.'/config/owner_xyz/Extension.conf.php'; + $siteFile = $this->tmpDir.'/config/owner_xyz/myCEESV.conf.php'; $backupFile = $siteFile.'.bak'; $this->assertFileExists($backupFile); $current = require $siteFile; $backup = require $backupFile; - $this->assertSame('second', $current['module']['code']); - $this->assertSame('first', $backup['module']['code']); + $this->assertSame('second', $current['auth']['projectId']); + $this->assertSame('first', $backup['auth']['projectId']); } - /* }}} */ - /* Show Flow {{{ */ public function testShowWithSiteReturnsSiteSpecificValue(): void { - $this->runWrite([ - 'module:code="X100"', - '--siteDir=owner_xyz', + $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="218523"', + '--directory-name=owner_xyz', ]); - $show = $this->runShow([ - 'module:code', - '--siteDir=owner_xyz', + $show = $this->runCfg([ + '-a', + $this->tmpDir, + 'show', + 'myCEESV', + 'auth:projectId', + '--site=owner_xyz', ]); - $this->assertCfgSuccess($show); - $this->assertStringContainsString('X100', $show['output']); + $this->assertSame(0, $show['code'], $show['output']); + $this->assertStringContainsString('218523', $show['output']); } - /* }}} */ - /* Validation {{{ */ - public function testInvalidActionReturnsError(): void + public function testDirectoryNameRejectsTraversal(): void { $result = $this->runCfg([ '-a', $this->tmpDir, - 'delete', - 'Extension', + 'write', + 'myCEESV', + 'auth:projectId="218523"', + '--directory-name=../owner_xyz', ]); - $this->assertCfgFailure($result, "'delete' not found"); + $this->assertSame(1, $result['code']); + $this->assertStringContainsString('Invalid directory in --directoryName', $result['output']); } - public function testSiteDirRejectsEmptyValue(): void + public function testDirectoryNameRejectsInvalidSegment(): void { - $result = $this->runWrite([ - 'module:code="X100"', - '--siteDir=', - ]); - - $this->assertCfgFailure($result, 'a value is required for --siteDir'); - } - - /** - * @dataProvider siteDirValidationDataProvider - */ - public function testSiteDirValidation(string $siteDir, string $expectedMessage): void - { - $result = $this->runWrite([ - 'module:code="X100"', - '--siteDir='.$siteDir, - ]); - - $this->assertCfgFailure($result, $expectedMessage); - } - - /** - * @dataProvider inputFileValidationDataProvider - */ - public function testInputFileValidation(?string $fileContents, string $pathSuffix, string $expectedMessage): void - { - $inFile = $this->tmpDir.'/'.$pathSuffix; - if ($fileContents !== null) { - file_put_contents($inFile, $fileContents); - } - - $result = $this->runWrite([ - '-i', - $inFile, - ]); - - $this->assertCfgFailure($result, $expectedMessage); - } - - /** - * @dataProvider writePayloadSourceValidationDataProvider - */ - public function testWritePayloadSourceValidation(array $extraArgs, string $expectedMessage): void - { - $inFile = $this->createJsonInputFile(['module' => ['code' => 'X100']]); - - $args = array_merge( - [ - '-a', - $this->tmpDir, - 'write', - 'Extension', - ], - array_map( - fn (string $arg): string => $arg === '__INPUT_FILE__' ? $inFile : $arg, - $extraArgs - ) - ); - - $result = $this->runCfg($args); - - $this->assertCfgFailure($result, $expectedMessage); - } - - public function siteDirValidationDataProvider(): array - { - return [ - 'traversal' => ['../owner_xyz', 'Invalid directory in --siteDir'], - 'invalid segment' => ['owner xyz', "Invalid directory name segment 'owner xyz' in --siteDir."], - 'hidden dot segment' => ['owner_xyz/./secret', "Invalid directory in --siteDir: 'owner_xyz/./secret'."], - 'config root only' => ['config/', 'Option --siteDir is empty.'], - ]; - } - - public function inputFileValidationDataProvider(): array - { - return [ - 'missing file' => [null, 'missing.json', 'Input file is not readable'], - 'invalid json' => ['{"module":', 'invalid.json', 'Input JSON is invalid'], - 'scalar json' => ['true', 'scalar.json', 'Input JSON must decode to an object/array'], - ]; - } - - public function writePayloadSourceValidationDataProvider(): array - { - return [ - 'missing payload source' => [[], 'Nothing to write: provide SETTING or --in.'], - 'both payload sources' => [['module:code="x"', '-i', '__INPUT_FILE__'], 'Please use either SETTING or --in, not both.'], - ]; - } - /* }}} */ - - /* Helpers {{{ */ - private function runWrite(array $args): array - { - return $this->runCfg(array_merge([ + $result = $this->runCfg([ '-a', $this->tmpDir, 'write', - 'Extension', - ], $args)); + 'myCEESV', + 'auth:projectId="218523"', + '--directory-name=owner xyz', + ]); + + $this->assertSame(1, $result['code']); + $this->assertStringContainsString("Invalid directory name segment 'owner xyz' in --directoryName.", $result['output']); } - private function runShow(array $args): array + public function testDirectoryNameRejectsEmptyValue(): void { - return $this->runCfg(array_merge([ + $result = $this->runCfg([ '-a', $this->tmpDir, - 'show', - 'Extension', - ], $args)); + 'write', + 'myCEESV', + 'auth:projectId="218523"', + '--directory-name=', + ]); + + $this->assertSame(1, $result['code']); + $this->assertStringContainsString('Option --directoryName is empty.', $result['output']); } - private function createJsonInputFile(array $contents, string $name = 'extension-in.json'): string + public function testUsingSiteAndDirectoryNameTogetherReturnsError(): void { - $path = $this->tmpDir.'/'.$name; - file_put_contents($path, json_encode($contents, JSON_PRETTY_PRINT)); + $result = $this->runCfg([ + '-a', + $this->tmpDir, + 'write', + 'myCEESV', + 'auth:projectId="218523"', + '--site', + 'owner_xyz', + '--directory-name', + 'owner_xyz', + ]); - return $path; - } - - private function loadWrittenConfig(?string $siteDir = null): array - { - $path = $siteDir === null - ? $this->tmpDir.'/config/Extension.conf.php' - : $this->tmpDir.'/config/'.$siteDir.'/Extension.conf.php'; - - return require $path; - } - - private function assertCfgSuccess(array $result): void - { - $this->assertSame(0, $result['code'], $result['output']); - } - - private function assertCfgFailure(array $result, string $message): void - { - $this->assertSame(1, $result['code'], $result['output']); - $this->assertStringContainsString($message, $result['output']); + $this->assertSame(1, $result['code']); + $this->assertStringContainsString('Please use only one of --site or --directoryName (alias: --directory-name).', $result['output']); } private function runCfg(array $args): array @@ -453,5 +323,4 @@ class CfgTest extends TestCase rmdir($dir); } - /* }}} */ } diff --git a/tests/SettingsTest.php b/tests/SettingsTest.php index 09f311c..ae70ef7 100644 --- a/tests/SettingsTest.php +++ b/tests/SettingsTest.php @@ -224,17 +224,17 @@ class SettingsTest extends TestCase public function testBuildFileNameWithPrefixAndSite(): void { $cfg = new Settings(); - $cfg->appPath('./')->prefix('Extension')->site('owner_xyz'); + $cfg->appPath('./')->prefix('myCEESV')->site('owner_xyz'); - $this->assertEquals('./config/owner_xyz/Extension.conf.php', $cfg->buildFileName(0x01)); + $this->assertEquals('./config/owner_xyz/myCEESV.conf.php', $cfg->buildFileName(0x01)); } public function testBuildFileNameWithPrefixAndNestedSite(): void { $cfg = new Settings(); - $cfg->appPath('./')->prefix('Extension')->site('owner_xyz/sub_a'); + $cfg->appPath('./')->prefix('myCEESV')->site('owner_xyz/sub_a'); - $this->assertEquals('./config/owner_xyz/sub_a/Extension.conf.php', $cfg->buildFileName(0x01)); + $this->assertEquals('./config/owner_xyz/sub_a/myCEESV.conf.php', $cfg->buildFileName(0x01)); } public function fileNameData()