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/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" ); } protected function tearDown(): void { $this->removeDir($this->tmpDir); } public function testWriteWithoutDirectoryNameUsesLocalConfigFile(): void { $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="X100"', ]); $this->assertSame(0, $result['code'], $result['output']); $localFile = $this->tmpDir.'/config/Extension.conf.php'; $this->assertFileExists($localFile); $cfg = require $localFile; $this->assertSame('X100', $cfg['module']['code']); } public function testWriteWithoutModeFallsBackToDefaultMode(): void { file_put_contents( $this->tmpDir.'/config/Extension.default.conf.php', " [\n\t\t'code' => '',\n\t],\n];\n" ); $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="X100"', ]); $this->assertSame(0, $result['code'], $result['output']); $this->assertFileExists($this->tmpDir.'/config/Extension.conf.php'); } public function testWriteWithInputFileWritesMultipleSettings(): void { $inFile = $this->tmpDir.'/extension-in.json'; file_put_contents( $inFile, json_encode([ 'module' => [ 'code' => 'X100', 'label' => 'demo-module', ], 'feature' => [ 'endpoint' => 'https://example.invalid/v1/resource', ], ], JSON_PRETTY_PRINT) ); $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', '--siteDir=owner_xyz', '-i', $inFile, ]); $this->assertSame(0, $result['code'], $result['output']); $cfg = require $this->tmpDir.'/config/owner_xyz/Extension.conf.php'; $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->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module={"code":"X100","label":"demo-module"}', '--siteDir=owner_xyz', ]); $this->assertSame(0, $result['code'], $result['output']); $cfg = require $this->tmpDir.'/config/owner_xyz/Extension.conf.php'; $this->assertSame('X100', $cfg['module']['code']); $this->assertSame('demo-module', $cfg['module']['label']); } public function testWriteSupportsNestedPathWithColonNotation(): void { $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:flags:enabled=true', '--siteDir=owner_xyz', ]); $this->assertSame(0, $result['code'], $result['output']); $cfg = require $this->tmpDir.'/config/owner_xyz/Extension.conf.php'; $this->assertTrue($cfg['module']['flags']['enabled']); } public function testWriteWithInputFileAndSettingReturnsError(): void { $inFile = $this->tmpDir.'/extension-in.json'; file_put_contents($inFile, json_encode(['module' => ['code' => 'X100']])); $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="x"', '-i', $inFile, ]); $this->assertSame(1, $result['code']); $this->assertStringContainsString('Please use either SETTING or --in, not both.', $result['output']); } public function testWriteWithSiteDirCreatesAndWritesSiteConfig(): void { $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="X100"', '--siteDir=owner_xyz', ]); $this->assertSame(0, $result['code'], $result['output']); $siteFile = $this->tmpDir.'/config/owner_xyz/Extension.conf.php'; $this->assertFileExists($siteFile); $this->assertFileDoesNotExist($this->tmpDir.'/config/Extension.conf.php'); $cfg = require $siteFile; $this->assertSame('X100', $cfg['module']['code']); } public function testWriteWithSiteDirMergesIntoExistingSiteConfig(): void { $firstWrite = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="X100"', '--siteDir=owner_xyz', ]); $this->assertSame(0, $firstWrite['code'], $firstWrite['output']); $secondWrite = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:label="demo-module"', '--siteDir=owner_xyz', ]); $this->assertSame(0, $secondWrite['code'], $secondWrite['output']); $siteFile = $this->tmpDir.'/config/owner_xyz/Extension.conf.php'; $cfg = require $siteFile; $this->assertSame('X100', $cfg['module']['code']); $this->assertSame('demo-module', $cfg['module']['label']); } public function testWriteWithSiteDirUsingConfigPrefixIsNormalized(): void { $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="X100"', '--siteDir=config/owner_xyz', ]); $this->assertSame(0, $result['code'], $result['output']); $this->assertFileExists($this->tmpDir.'/config/owner_xyz/Extension.conf.php'); $this->assertFileDoesNotExist($this->tmpDir.'/config/config/owner_xyz/Extension.conf.php'); } public function testWriteWithSiteDirWritesToSiteConfig(): void { $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="X100"', '--siteDir=owner_xyz', ]); $this->assertSame(0, $result['code'], $result['output']); $this->assertFileExists($this->tmpDir.'/config/owner_xyz/Extension.conf.php'); } public function testWriteToExistingSiteConfigCreatesBackup(): void { $firstWrite = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="first"', '--siteDir=owner_xyz', ]); $this->assertSame(0, $firstWrite['code'], $firstWrite['output']); $secondWrite = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="second"', '--siteDir=owner_xyz', ]); $this->assertSame(0, $secondWrite['code'], $secondWrite['output']); $siteFile = $this->tmpDir.'/config/owner_xyz/Extension.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']); } public function testShowWithSiteReturnsSiteSpecificValue(): void { $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="X100"', '--siteDir=owner_xyz', ]); $show = $this->runCfg([ '-a', $this->tmpDir, 'show', 'Extension', 'module:code', '--siteDir=owner_xyz', ]); $this->assertSame(0, $show['code'], $show['output']); $this->assertStringContainsString('X100', $show['output']); } public function testSiteDirRejectsTraversal(): void { $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="X100"', '--siteDir=../owner_xyz', ]); $this->assertSame(1, $result['code']); $this->assertStringContainsString('Invalid directory in --siteDir', $result['output']); } public function testSiteDirRejectsInvalidSegment(): void { $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="X100"', '--siteDir=owner xyz', ]); $this->assertSame(1, $result['code']); $this->assertStringContainsString("Invalid directory name segment 'owner xyz' in --siteDir.", $result['output']); } public function testSiteDirRejectsEmptyValue(): void { $result = $this->runCfg([ '-a', $this->tmpDir, 'write', 'Extension', 'module:code="X100"', '--siteDir=', ]); $this->assertSame(1, $result['code']); $this->assertStringContainsString('a value is required for --siteDir', $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); } }