Task: #53687 Support instance-specific cfg write targets and add coverage V2 #4
Task: #53687 Support instance-specific cfg write targets and add coverage
157
bin/cfg
|
|
@ -22,7 +22,62 @@ foreach ($autoloadFiles as $autoloadFile) {
|
||||||||
}
|
}
|
||||||||
}
|
}
|
||||||||
|
|
||||||||
$version = '0.3';
|
// Pre-parse site options so they can be passed after positional args.
|
||||||||
|
alejandro.sosa marked this conversation as resolved
Outdated
alejandro.sosa marked this conversation as resolved
Outdated
norb
commented
can you do this as validators? e.g.
can you do this as validators? e.g. https://code.verua.online/rabe/helpers-cli-input/src/commit/c6fe64321ef06633c89bc055252f22d6ff676d52/example/example.php#L35
|
|||||||||
|
// 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';
|
||||||||
|
|
||||||||
$actions = [ 'show', 'write', 'help' ];
|
$actions = [ 'show', 'write', 'help' ];
|
||||||||
$settings = ['key' => '', 'value' => ''];
|
$settings = ['key' => '', 'value' => ''];
|
||||||||
|
|
@ -60,6 +115,16 @@ $collection = (new Input\InputCollection())
|
||||||||
->description('Path where the config/ directory of the package conf files is located, defaults to the working dir')
|
->description('Path where the config/ directory of the package conf files is located, defaults to the working dir')
|
||||||||
) // }}}
|
) // }}}
|
||||||||
|
|
||||||||
|
->add( Input\InputTypeFactory::build('LongOption')->name('site')->short('s') // {{{
|
||||||||
|
alejandro.sosa marked this conversation as resolved
Outdated
norb
commented
as agreed in chat, again to keep the code simple, use --siteDir without alias same argument, if we think we will need arguments, implement in used lib or use different lib as agreed in chat, again to keep the code simple, use --siteDir without alias
same argument, if we think we will need arguments, implement in used lib or use different lib
alejandro.sosa
commented
@norb wrote in #4 (comment):
👍🏼 Done, agreed. I removed aliases and kept only --siteDir to keep the CLI simple @norb wrote in https://code.verua.online/rabe/Util-Settings/pulls/4#issuecomment-3733:
> as agreed in chat, again to keep the code simple, use --siteDir without alias
>
> same argument, if we think we will need arguments, implement in used lib or use different lib
👍🏼 Done, agreed. I removed aliases and kept only --siteDir to keep the CLI simple
|
|||||||||
|
->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED)
|
||||||||
|
->description('Site/instance directory below config/ (target: config/<site>/<prefix>.conf.php), e.g. owner_xyz')
|
||||||||
|
) // }}}
|
||||||||
|
|
||||||||
|
->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') // {{{
|
->add( Input\InputTypeFactory::build('Argument')->name('action') // {{{
|
||||||||
->flags(AbstractInputType::FLAG_REQUIRED)
|
->flags(AbstractInputType::FLAG_REQUIRED)
|
||||||||
->description(
|
->description(
|
||||||||
|
|
@ -141,8 +206,18 @@ $usage = Cli\manpage( basename(__FILE__), $version,
|
||||||||
$collection, Colour::FG_GREEN, Colour::FG_WHITE,
|
$collection, Colour::FG_GREEN, Colour::FG_WHITE,
|
||||||||
[
|
[
|
||||||||
'Examples' =>
|
'Examples' =>
|
||||||||
'cfg show VeruA db:host'.PHP_EOL.
|
'cfg write myCEESV \'auth:projectId="218523"\''
|
||||||||
'cfg write VeruA \'db:host="newHost"\''.PHP_EOL
|
.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;
|
).PHP_EOL;
|
||||||||
|
|
||||||||
|
|
@ -181,6 +256,7 @@ echo $argv->find('setting')['value'].PHP_EOL;
|
||||||||
// var_dump($cfg);
|
// var_dump($cfg);
|
||||||||
$appPath = $argv->find('appPath');
|
$appPath = $argv->find('appPath');
|
||||||||
if (!$appPath) $appPath = getcwd().'/';
|
if (!$appPath) $appPath = getcwd().'/';
|
||||||||
|
$appPath = rtrim($appPath, '/').'/';
|
||||||||
|
|
||||||||
/* $it = new RecursiveDirectoryIterator($appPath);
|
/* $it = new RecursiveDirectoryIterator($appPath);
|
||||||||
|
|
||||||||
|
|
@ -194,7 +270,62 @@ foreach(new RecursiveIteratorIterator($it) as $file)
|
||||||||
*/
|
*/
|
||||||||
$mode = ($argv->find('mode') == '') ? null : $argv->find('mode');
|
$mode = ($argv->find('mode') == '') ? null : $argv->find('mode');
|
||||||||
$cfg = (new Settings([], $mode))->appPath($appPath)->prefix($prefix);
|
$cfg = (new Settings([], $mode))->appPath($appPath)->prefix($prefix);
|
||||||||
if ($pkgPath = $argv->find('pkgPath')) $cfg->pkgPath($pkgPath);
|
// pkgPath points to package defaults (e.g. <prefix>.default.conf.php)
|
||||||||
|
if ($pkgPath = $argv->find('pkgPath')) $cfg->pkgPath(rtrim($pkgPath, '/').'/');
|
||||||||
|
|
||||||||
|
$site = null;
|
||||||||
|
$siteFlag = 0x01;
|
||||||||
|
// Only one site selector may be provided to avoid ambiguous targets.
|
||||||||
|
$providedSiteOptions = [];
|
||||||||
|
foreach ($preparsedSiteOptions as $option) {
|
||||||||
|
alejandro.sosa marked this conversation as resolved
Outdated
norb
commented
our coding style would always put the opening curly bracket on a newline. I retain it specially important if the following codeblock is longer than a couple of lines our coding style would always put the opening curly bracket on a newline. I retain it specially important if the following codeblock is longer than a couple of lines
|
|||||||||
|
$providedSiteOptions[$option['name']][] = $option['value'] ?? '';
|
||||||||
|
}
|
||||||||
|
foreach (['site', 'directoryName'] as $optionName) {
|
||||||||
|
$parsedValue = $argv->find($optionName);
|
||||||||
|
if ($parsedValue !== null && $parsedValue !== '' && $parsedValue !== false) {
|
||||||||
|
$providedSiteOptions[$optionName][] = $parsedValue;
|
||||||||
|
}
|
||||||||
|
}
|
||||||||
|
|
||||||||
|
if (count($providedSiteOptions) > 1) {
|
||||||||
|
fwrite(STDERR, 'Please use only one of --site or --directoryName (alias: --directory-name).'.PHP_EOL);
|
||||||||
|
exit(1);
|
||||||||
|
}
|
||||||||
|
alejandro.sosa marked this conversation as resolved
Outdated
norb
commented
probably most of this should be done in the validator as well probably most of this should be done in the validator as well
|
|||||||||
|
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/<site>/<prefix>.conf.php
|
||||||||
|
$cfg->site($site);
|
||||||||
|
}
|
||||||||
|
|
||||||||
try {
|
try {
|
||||||||
if (is_readable($cfg->buildFileName('default'))) {
|
if (is_readable($cfg->buildFileName('default'))) {
|
||||||||
|
|
@ -238,7 +369,6 @@ case 'show':
|
||||||||
|
|
||||||||
case 'write':
|
case 'write':
|
||||||||
$path = ($settings['key'] !== '') ? explode(':', $settings['key']) : [];
|
$path = ($settings['key'] !== '') ? explode(':', $settings['key']) : [];
|
||||||||
var_dump($path);
|
|
||||||||
|
norb marked this conversation as resolved
Outdated
norb
commented
perhaps we could have a verbose mode? perhaps we could have a verbose mode?
alejandro.sosa
commented
@norb wrote in #4 (comment):
Hi Norber, for this round I kept the verbose implementation minimal on purpose. The current CLI helper library is not fully compatible with PHP 8 in the verbosity path (especially around incrementing flags), so going further right now would either require patching vendor code or introducing larger structural changes. I agree we should improve this longer term; a good replacement would be Symfony Console (https://symfony.com/doc/current/components/console.html), which I know well and that already provides robust argument parsing, verbosity levels, and many related features. For this version, I think it is safer to keep it simple and stable, and handle a full CLI migration in a dedicated follow-up. @norb wrote in https://code.verua.online/rabe/Util-Settings/pulls/4#issuecomment-3735:
> perhaps we could have a verbose mode?
Hi Norber, for this round I kept the verbose implementation minimal on purpose. The current CLI helper library is not fully compatible with PHP 8 in the verbosity path (especially around incrementing flags), so going further right now would either require patching vendor code or introducing larger structural changes. I agree we should improve this longer term; a good replacement would be Symfony Console (https://symfony.com/doc/current/components/console.html), which I know well and that already provides robust argument parsing, verbosity levels, and many related features. For this version, I think it is safer to keep it simple and stable, and handle a full CLI migration in a dedicated follow-up.
|
|||||||||
|
|
||||||||
$setting2write = $settings['value'];
|
$setting2write = $settings['value'];
|
||||||||
|
|
||||||||
|
|
@ -246,19 +376,30 @@ case 'write':
|
||||||||
{
|
{
|
||||||||
$setting2write = [array_pop($path) => $setting2write];
|
$setting2write = [array_pop($path) => $setting2write];
|
||||||||
}
|
}
|
||||||||
if (is_readable($file = $cfg->buildFileName()))
|
|
||||||||
|
$writeType = ($site !== null) ? $siteFlag : null;
|
||||||||
|
$file = $cfg->buildFileName($writeType);
|
||||||||
|
if (is_readable($file))
|
||||||||
|
alejandro.sosa marked this conversation as resolved
Outdated
norb
commented
probably this one too? probably this one too?
|
|||||||||
{
|
{
|
||||||||
$setting2write = array_replace_recursive(require($file), $setting2write);
|
$setting2write = array_replace_recursive(require($file), $setting2write);
|
||||||||
copy($file, "$file.bak");
|
copy($file, "$file.bak");
|
||||||||
}
|
}
|
||||||||
|
|
||||||||
|
$targetDir = dirname($file);
|
||||||||
|
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);
|
$writeCfg = $cfg->create($setting2write);
|
||||||||
// var_dump($writeCfg->toArray());
|
// var_dump($writeCfg->toArray());
|
||||||||
try {
|
try {
|
||||||||
(new SettingsWriter($writeCfg))->write();
|
(new SettingsWriter($writeCfg, '', $writeType))->write();
|
||||||||
echo "Written modified settings to: $file".PHP_EOL;
|
echo "Written modified settings to: $file".PHP_EOL;
|
||||||||
}
|
}
|
||||||||
catch (\Exception $e) {
|
catch (\Exception $e) {
|
||||||||
echo $e->getMessage().PHP_EOL;
|
fwrite(STDERR, $e->getMessage().PHP_EOL);
|
||||||||
|
exit(1);
|
||||||||
}
|
}
|
||||||||
|
|
||||||||
break;
|
break;
|
||||||||
|
|
|
||||||||
|
|
@ -22,6 +22,8 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace rabe\Util;
|
namespace rabe\Util;
|
||||||
|
|
||||||
|
use ErrorException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write Settings into a File
|
* Write Settings into a File
|
||||||
* @author Norbert.e.Wagner dev@norb.me
|
* @author Norbert.e.Wagner dev@norb.me
|
||||||
|
|
@ -45,12 +47,15 @@ class SettingsWriter
|
||||||
*
|
*
|
||||||
* @param Settings $settings an object of type settings
|
* @param Settings $settings an object of type settings
|
||||||
* @param String $name The name midfix for the settings File
|
* @param String $name The name midfix for the settings File
|
||||||
|
* @param String|int|null $type Optional type for Settings::buildFileName(), e.g. site flag
|
||||||
*/
|
*/
|
||||||
public function __construct( Settings $settings, $name='' )
|
public function __construct( Settings $settings, $name='', $type=null )
|
||||||
{
|
{
|
||||||
$this->settings = $settings;
|
$this->settings = $settings;
|
||||||
|
|
||||||
$file = $settings->buildFileName( $name );
|
$file = ( $type === null )
|
||||||
|
? $settings->buildFileName( $name )
|
||||||
|
: $settings->buildFileName( $type );
|
||||||
|
|
||||||
if ( ! $this->handle = fopen( $file, 'w' ) )
|
if ( ! $this->handle = fopen( $file, 'w' ) )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
307
tests/CfgTest.php
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace rabe\Util\tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class CfgTest extends TestCase
|
||||||
|
{
|
||||||
|
private string $tmpDir;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->tmpDir = rtrim(sys_get_temp_dir(), '/').'/util-settings-cfg-'.bin2hex(random_bytes(8));
|
||||||
|
mkdir($this->tmpDir, 0775, true);
|
||||||
|
mkdir($this->tmpDir.'/config', 0775, true);
|
||||||
|
|
||||||
|
file_put_contents(
|
||||||
|
$this->tmpDir.'/config/myCEESV.default.conf.php',
|
||||||
|
"<?php return [\n\t'mode' => 'prod',\n\t'auth' => [\n\t\t'projectId' => '',\n\t\t'clientId' => '',\n\t],\n];\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$this->removeDir($this->tmpDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteWithoutDirectoryNameUsesLocalConfigFile(): void
|
||||||
|
{
|
||||||
|
$result = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="218523"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(0, $result['code'], $result['output']);
|
||||||
|
|
||||||
|
$localFile = $this->tmpDir.'/config/myCEESV.conf.php';
|
||||||
|
$this->assertFileExists($localFile);
|
||||||
|
|
||||||
|
$cfg = require $localFile;
|
||||||
|
$this->assertSame('218523', $cfg['auth']['projectId']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteWithDirectoryNameCreatesAndWritesSiteConfig(): void
|
||||||
|
{
|
||||||
|
$result = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="218523"',
|
||||||
|
'--directory-name',
|
||||||
|
'owner_xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(0, $result['code'], $result['output']);
|
||||||
|
|
||||||
|
$siteFile = $this->tmpDir.'/config/owner_xyz/myCEESV.conf.php';
|
||||||
|
$this->assertFileExists($siteFile);
|
||||||
|
$this->assertFileDoesNotExist($this->tmpDir.'/config/myCEESV.conf.php');
|
||||||
|
|
||||||
|
$cfg = require $siteFile;
|
||||||
|
$this->assertSame('218523', $cfg['auth']['projectId']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteWithDirectoryNameMergesIntoExistingSiteConfig(): void
|
||||||
|
{
|
||||||
|
$firstWrite = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="218523"',
|
||||||
|
'--directory-name',
|
||||||
|
'owner_xyz',
|
||||||
|
]);
|
||||||
|
$this->assertSame(0, $firstWrite['code'], $firstWrite['output']);
|
||||||
|
|
||||||
|
$secondWrite = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:clientId="service-9999-qual@myceesv.ch"',
|
||||||
|
'--directory-name',
|
||||||
|
'owner_xyz',
|
||||||
|
]);
|
||||||
|
$this->assertSame(0, $secondWrite['code'], $secondWrite['output']);
|
||||||
|
|
||||||
|
$siteFile = $this->tmpDir.'/config/owner_xyz/myCEESV.conf.php';
|
||||||
|
$cfg = require $siteFile;
|
||||||
|
|
||||||
|
$this->assertSame('218523', $cfg['auth']['projectId']);
|
||||||
|
$this->assertSame('service-9999-qual@myceesv.ch', $cfg['auth']['clientId']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteWithDirectoryNameUsingConfigPrefixIsNormalized(): void
|
||||||
|
{
|
||||||
|
$result = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="218523"',
|
||||||
|
'--directory-name=config/owner_xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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 testWriteWithSiteOptionWritesToSiteConfig(): void
|
||||||
|
{
|
||||||
|
$result = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="218523"',
|
||||||
|
'--site=owner_xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(0, $result['code'], $result['output']);
|
||||||
|
$this->assertFileExists($this->tmpDir.'/config/owner_xyz/myCEESV.conf.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteWithDirectoryNameCamelCaseOptionWritesToSiteConfig(): void
|
||||||
|
{
|
||||||
|
$result = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="218523"',
|
||||||
|
'--directoryName=owner_xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(0, $result['code'], $result['output']);
|
||||||
|
$this->assertFileExists($this->tmpDir.'/config/owner_xyz/myCEESV.conf.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteToExistingSiteConfigCreatesBackup(): void
|
||||||
|
{
|
||||||
|
$firstWrite = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="first"',
|
||||||
|
'--directory-name=owner_xyz',
|
||||||
|
]);
|
||||||
|
$this->assertSame(0, $firstWrite['code'], $firstWrite['output']);
|
||||||
|
|
||||||
|
$secondWrite = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="second"',
|
||||||
|
'--directory-name=owner_xyz',
|
||||||
|
]);
|
||||||
|
$this->assertSame(0, $secondWrite['code'], $secondWrite['output']);
|
||||||
|
|
||||||
|
$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['auth']['projectId']);
|
||||||
|
$this->assertSame('first', $backup['auth']['projectId']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShowWithSiteReturnsSiteSpecificValue(): void
|
||||||
|
{
|
||||||
|
$this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="218523"',
|
||||||
|
'--directory-name=owner_xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$show = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'show',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId',
|
||||||
|
'--site=owner_xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(0, $show['code'], $show['output']);
|
||||||
|
$this->assertStringContainsString('218523', $show['output']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDirectoryNameRejectsTraversal(): void
|
||||||
|
{
|
||||||
|
$result = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="218523"',
|
||||||
|
'--directory-name=../owner_xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(1, $result['code']);
|
||||||
|
$this->assertStringContainsString('Invalid directory in --directoryName', $result['output']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDirectoryNameRejectsInvalidSegment(): void
|
||||||
|
{
|
||||||
|
$result = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDirectoryNameRejectsEmptyValue(): void
|
||||||
|
{
|
||||||
|
$result = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="218523"',
|
||||||
|
'--directory-name=',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(1, $result['code']);
|
||||||
|
$this->assertStringContainsString('Option --directoryName is empty.', $result['output']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsingSiteAndDirectoryNameTogetherReturnsError(): void
|
||||||
|
{
|
||||||
|
$result = $this->runCfg([
|
||||||
|
'-a',
|
||||||
|
$this->tmpDir,
|
||||||
|
'write',
|
||||||
|
'myCEESV',
|
||||||
|
'auth:projectId="218523"',
|
||||||
|
'--site',
|
||||||
|
'owner_xyz',
|
||||||
|
'--directory-name',
|
||||||
|
'owner_xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
$script = realpath(__DIR__.'/../bin/cfg');
|
||||||
|
$this->assertNotFalse($script);
|
||||||
|
|
||||||
|
$command = escapeshellarg(PHP_BINARY).' '.escapeshellarg($script);
|
||||||
|
foreach ($args as $arg) {
|
||||||
|
$command .= ' '.escapeshellarg($arg);
|
||||||
|
}
|
||||||
|
$command .= ' 2>&1';
|
||||||
|
|
||||||
|
$outputLines = [];
|
||||||
|
$exitCode = 0;
|
||||||
|
exec($command, $outputLines, $exitCode);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => $exitCode,
|
||||||
|
'output' => implode(PHP_EOL, $outputLines),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeDir(string $dir): void
|
||||||
|
{
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (scandir($dir) ?: [] as $entry) {
|
||||||
|
if ($entry === '.' || $entry === '..') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $dir.'/'.$entry;
|
||||||
|
if (is_dir($path)) {
|
||||||
|
$this->removeDir($path);
|
||||||
|
} else {
|
||||||
|
unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rmdir($dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -212,6 +212,22 @@ class SettingsTest extends TestCase
|
||||||
$this->assertEquals($path.$expected, $cfg->buildFileName($type));
|
$this->assertEquals($path.$expected, $cfg->buildFileName($type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testBuildFileNameWithPrefixAndSite(): void
|
||||||
|
{
|
||||||
|
$cfg = new Settings();
|
||||||
|
$cfg->appPath('./')->prefix('myCEESV')->site('owner_xyz');
|
||||||
|
|
||||||
|
$this->assertEquals('./config/owner_xyz/myCEESV.conf.php', $cfg->buildFileName(0x01));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBuildFileNameWithPrefixAndNestedSite(): void
|
||||||
|
{
|
||||||
|
$cfg = new Settings();
|
||||||
|
$cfg->appPath('./')->prefix('myCEESV')->site('owner_xyz/sub_a');
|
||||||
|
|
||||||
|
$this->assertEquals('./config/owner_xyz/sub_a/myCEESV.conf.php', $cfg->buildFileName(0x01));
|
||||||
|
}
|
||||||
|
|
||||||
public function fileNameData()
|
public function fileNameData()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
I don't really like this .. seems over complicating the code. It is just for beging able to have different order of arguments?
not that important to me .. and probably better put inside the input helper library or replace it altogether .. IIRC the one used is not maintained anymore
@norb wrote in #4 (comment):
You're right. After reviewing it again with the alias cleanup in place, I realized we can keep this much simpler. The pre-parse block was added as a defensive workaround while multiple option variants were still in play, but with --siteDir now unified and a couple of small follow-up fixes, the standard parser flow is sufficient. I removed that extra layer to keep the code cleaner and easier to maintain.