From 58299218d96dab4c0d4c9dd3626e314c6e75a06b Mon Sep 17 00:00:00 2001 From: Lee Jones Date: Tue, 6 Jan 2026 11:09:44 -0600 Subject: [PATCH 1/5] secrets commands. --- composer.json | 4 +- src/Cli/Commands/Secrets/EditCommand.php | 127 +++++++++++ .../Commands/Secrets/Key/GenerateCommand.php | 155 +++++++++++++ src/Cli/Commands/Secrets/ShowCommand.php | 167 ++++++++++++++ .../Cli/Commands/Secrets/EditCommandTest.php | 212 ++++++++++++++++++ .../Secrets/Key/GenerateCommandTest.php | 178 +++++++++++++++ .../Cli/Commands/Secrets/ShowCommandTest.php | 186 +++++++++++++++ 7 files changed, 1028 insertions(+), 1 deletion(-) create mode 100644 src/Cli/Commands/Secrets/EditCommand.php create mode 100644 src/Cli/Commands/Secrets/Key/GenerateCommand.php create mode 100644 src/Cli/Commands/Secrets/ShowCommand.php create mode 100644 tests/Cli/Commands/Secrets/EditCommandTest.php create mode 100644 tests/Cli/Commands/Secrets/Key/GenerateCommandTest.php create mode 100644 tests/Cli/Commands/Secrets/ShowCommandTest.php diff --git a/composer.json b/composer.json index 2834438..28c8ece 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,9 @@ "minimum-stability": "stable", "require": { "php": "^8.4", - "neuron-php/application": "0.8.*" + "neuron-php/application": "0.8.*", + "neuron-php/data": "0.7.*", + "symfony/yaml": "^6.4" }, "require-dev": { "phpunit/phpunit": "^9.0", diff --git a/src/Cli/Commands/Secrets/EditCommand.php b/src/Cli/Commands/Secrets/EditCommand.php new file mode 100644 index 0000000..223f4b6 --- /dev/null +++ b/src/Cli/Commands/Secrets/EditCommand.php @@ -0,0 +1,127 @@ +addOption( 'env', 'e', true, 'Environment to edit (default: base secrets)' ); + $this->addOption( 'editor', null, true, 'Editor to use (default: vi)' ); + $this->addOption( 'config', 'c', true, 'Config directory path (default: config)' ); + } + + /** + * @inheritDoc + */ + public function execute(): int + { + $configPath = $this->input->getOption( 'config', 'config' ); + $env = $this->input->getOption( 'env' ); + $editor = $this->input->getOption( 'editor' ) ?? $_ENV['EDITOR'] ?? 'vi'; + + // Determine paths based on environment + if( $env ) + { + $credentialsPath = $configPath . '/secrets/' . $env . '.yml.enc'; + $keyPath = $configPath . '/secrets/' . $env . '.key'; + $this->output->info( "Editing {$env} environment secrets..." ); + } + else + { + $credentialsPath = $configPath . '/secrets.yml.enc'; + $keyPath = $configPath . '/master.key'; + $this->output->info( "Editing base secrets..." ); + } + + // Create SecretManager + $this->secretManager = new SecretManager(); + + try + { + // Ensure key exists + if( !file_exists( $keyPath ) ) + { + $this->output->warning( "Key file not found at: {$keyPath}" ); + $this->output->info( "Generating new encryption key..." ); + + $key = $this->secretManager->generateKey( $keyPath ); + $this->output->success( "Generated new key at: {$keyPath}" ); + $this->output->warning( "IMPORTANT: Add {$keyPath} to .gitignore!" ); + } + + // Edit the secrets + $result = $this->secretManager->edit( $credentialsPath, $keyPath, $editor ); + + if( $result ) + { + $this->output->success( "Secrets saved to: {$credentialsPath}" ); + + // First time setup reminder + if( !$env && !file_exists( $configPath . '/.gitignore' ) ) + { + $this->output->newLine(); + $this->output->warning( "Remember to:" ); + $this->output->write( "1. Add {$keyPath} to .gitignore" ); + $this->output->write( "2. Commit {$credentialsPath} to version control" ); + $this->output->write( "3. Share {$keyPath} securely with your team" ); + } + } + else + { + $this->output->error( "Failed to save secrets" ); + return 1; + } + } + catch( \Exception $e ) + { + $this->output->error( "Error editing secrets: " . $e->getMessage() ); + + if( $this->input->hasOption( 'verbose' ) ) + { + $this->output->write( $e->getTraceAsString() ); + } + + return 1; + } + + return 0; + } +} \ No newline at end of file diff --git a/src/Cli/Commands/Secrets/Key/GenerateCommand.php b/src/Cli/Commands/Secrets/Key/GenerateCommand.php new file mode 100644 index 0000000..c8b8c90 --- /dev/null +++ b/src/Cli/Commands/Secrets/Key/GenerateCommand.php @@ -0,0 +1,155 @@ +addOption( 'env', 'e', true, 'Environment for the key (default: master key)' ); + $this->addOption( 'config', 'c', true, 'Config directory path (default: config)' ); + $this->addOption( 'force', 'f', false, 'Overwrite existing key file' ); + $this->addOption( 'show', 's', false, 'Display the generated key' ); + } + + /** + * @inheritDoc + */ + public function execute(): int + { + $configPath = $this->input->getOption( 'config', 'config' ); + $env = $this->input->getOption( 'env' ); + $force = $this->input->hasOption( 'force' ); + $show = $this->input->hasOption( 'show' ); + + // Determine key path based on environment + if( $env ) + { + $keyPath = $configPath . '/secrets/' . $env . '.key'; + $keyName = $env . ' environment key'; + + // Ensure directory exists + $dir = dirname( $keyPath ); + if( !is_dir( $dir ) ) + { + if( !mkdir( $dir, 0755, true ) ) + { + $this->output->error( "Failed to create directory: {$dir}" ); + return 1; + } + } + } + else + { + $keyPath = $configPath . '/master.key'; + $keyName = 'master key'; + } + + // Check if key already exists + if( file_exists( $keyPath ) && !$force ) + { + $this->output->error( "Key file already exists: {$keyPath}" ); + $this->output->info( "Use --force to overwrite the existing key." ); + $this->output->warning( "WARNING: Overwriting will make existing encrypted files unreadable!" ); + return 1; + } + + // Warn about overwriting + if( file_exists( $keyPath ) && $force ) + { + $this->output->warning( "You are about to overwrite an existing key!" ); + $this->output->warning( "This will make any files encrypted with the old key unreadable." ); + + if( !$this->confirm( "Are you absolutely sure you want to continue?" ) ) + { + $this->output->info( "Operation cancelled." ); + return 0; + } + } + + // Create SecretManager and generate key + $this->secretManager = new SecretManager(); + + try + { + $key = $this->secretManager->generateKey( $keyPath, $force ); + + $this->output->success( "Generated {$keyName} at: {$keyPath}" ); + + // Show the key if requested + if( $show ) + { + $this->output->newLine(); + $this->output->section( "Generated Key" ); + $this->output->write( $key ); + $this->output->newLine(); + $this->output->warning( "This key is shown only once. Store it securely!" ); + } + + // Display instructions + $this->output->newLine(); + $this->output->info( "Next steps:" ); + $this->output->write( "1. Add {$keyPath} to .gitignore (NEVER commit this file)" ); + $this->output->write( "2. Share this key securely with your team" ); + $this->output->write( "3. Use 'neuron secrets:edit" . ($env ? " --env={$env}" : "") . "' to add secrets" ); + + // Environment variable alternative + $envVar = 'NEURON_' . strtoupper( + str_replace( ['/', '.', '-'], '_', basename( $keyPath, '.key' ) ) + ) . '_KEY'; + $this->output->newLine(); + $this->output->info( "Alternative: Set the key as an environment variable:" ); + $this->output->write( "export {$envVar}={$key}" ); + } + catch( \Exception $e ) + { + $this->output->error( "Error generating key: " . $e->getMessage() ); + + if( $this->input->hasOption( 'verbose' ) ) + { + $this->output->write( $e->getTraceAsString() ); + } + + return 1; + } + + return 0; + } +} \ No newline at end of file diff --git a/src/Cli/Commands/Secrets/ShowCommand.php b/src/Cli/Commands/Secrets/ShowCommand.php new file mode 100644 index 0000000..4c2ce35 --- /dev/null +++ b/src/Cli/Commands/Secrets/ShowCommand.php @@ -0,0 +1,167 @@ +addOption( 'env', 'e', true, 'Environment to show (default: base secrets)' ); + $this->addOption( 'key', 'k', true, 'Show only specific key/section' ); + $this->addOption( 'config', 'c', true, 'Config directory path (default: config)' ); + $this->addOption( 'force', 'f', false, 'Skip confirmation prompt' ); + } + + /** + * @inheritDoc + */ + public function execute(): int + { + $configPath = $this->input->getOption( 'config', 'config' ); + $env = $this->input->getOption( 'env' ); + $specificKey = $this->input->getOption( 'key' ); + $force = $this->input->hasOption( 'force' ); + + // Security confirmation for production + if( !$force && $env === 'production' ) + { + $this->output->warning( "You are about to display production secrets!" ); + + if( !$this->output->confirm( "Are you sure you want to continue?" ) ) + { + $this->output->info( "Operation cancelled." ); + return 0; + } + } + + // Determine paths based on environment + if( $env ) + { + $credentialsPath = $configPath . '/secrets/' . $env . '.yml.enc'; + $keyPath = $configPath . '/secrets/' . $env . '.key'; + $title = ucfirst( $env ) . " Environment Secrets"; + } + else + { + $credentialsPath = $configPath . '/secrets.yml.enc'; + $keyPath = $configPath . '/master.key'; + $title = "Base Secrets"; + } + + // Check if files exist + if( !file_exists( $credentialsPath ) ) + { + $this->output->error( "Secrets file not found: {$credentialsPath}" ); + $this->output->info( "Use 'neuron secrets:edit" . ($env ? " --env={$env}" : "") . "' to create it." ); + return 1; + } + + if( !file_exists( $keyPath ) && !$this->checkEnvironmentKey( $keyPath ) ) + { + $this->output->error( "Key file not found: {$keyPath}" ); + $this->output->info( "The key might be in an environment variable or you need to obtain it from your team." ); + return 1; + } + + // Create SecretManager and decrypt + $this->secretManager = new SecretManager(); + + try + { + $decrypted = $this->secretManager->show( $credentialsPath, $keyPath ); + $data = Yaml::parse( $decrypted ); + + // Filter to specific key if requested + if( $specificKey ) + { + if( isset( $data[$specificKey] ) ) + { + $data = [$specificKey => $data[$specificKey]]; + } + else + { + $this->output->error( "Key '{$specificKey}' not found in secrets" ); + return 1; + } + } + + // Display the secrets + $this->output->section( $title ); + $this->output->newLine(); + + // Format and display YAML + $formatted = Yaml::dump( $data, 4, 2 ); + $this->output->write( $formatted ); + + // Security reminder + $this->output->newLine(); + $this->output->warning( "Remember: Never share or commit decrypted secrets!" ); + } + catch( \Exception $e ) + { + $this->output->error( "Error decrypting secrets: " . $e->getMessage() ); + + if( $this->input->hasOption( 'verbose' ) ) + { + $this->output->write( $e->getTraceAsString() ); + } + + return 1; + } + + return 0; + } + + /** + * Check if key exists in environment variable + * + * @param string $keyPath + * @return bool + */ + private function checkEnvironmentKey( string $keyPath ): bool + { + $envKey = 'NEURON_' . strtoupper( + str_replace( ['/', '.', '-'], '_', basename( $keyPath, '.key' ) ) + ) . '_KEY'; + + return isset( $_ENV[$envKey] ); + } +} \ No newline at end of file diff --git a/tests/Cli/Commands/Secrets/EditCommandTest.php b/tests/Cli/Commands/Secrets/EditCommandTest.php new file mode 100644 index 0000000..92981c8 --- /dev/null +++ b/tests/Cli/Commands/Secrets/EditCommandTest.php @@ -0,0 +1,212 @@ +testConfigPath = sys_get_temp_dir() . '/test_secrets_' . uniqid(); + mkdir( $this->testConfigPath, 0755, true ); + + $this->command = new EditCommand(); + $this->command->configure(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + // Clean up test files + $this->removeDirectory( $this->testConfigPath ); + } + + /** + * Test that the command has the correct name + */ + public function testGetName(): void + { + $this->assertEquals( 'secrets:edit', $this->command->getName() ); + } + + /** + * Test that the command has a description + */ + public function testGetDescription(): void + { + $this->assertEquals( 'Edit encrypted secrets file', $this->command->getDescription() ); + } + + /** + * Test that the command configures options correctly + */ + public function testConfigure(): void + { + // Test that options are configured by checking if we can create input with them + $input = new Input( [ + '--config=' . $this->testConfigPath, + '--env=test', + '--editor=vim' + ] ); + $input->parse( $this->command ); + + // Options should be available + $this->assertEquals( $this->testConfigPath, $input->getOption( 'config', 'config' ) ); + $this->assertEquals( 'test', $input->getOption( 'env' ) ); + $this->assertEquals( 'vim', $input->getOption( 'editor' ) ); + } + + /** + * Test editing base secrets when key exists + */ + public function testExecuteWithExistingKey(): void + { + // Create a test key file + $keyPath = $this->testConfigPath . '/master.key'; + $secretManager = new SecretManager(); + $key = $secretManager->generateKey( $keyPath ); + + // Create a test credentials file + $credentialsPath = $this->testConfigPath . '/secrets.yml.enc'; + $tempPlaintextPath = $this->testConfigPath . '/temp_plaintext.yml'; + $testData = "test:\n secret: value"; + file_put_contents( $tempPlaintextPath, $testData ); + $secretManager->encrypt( $tempPlaintextPath, $credentialsPath, $keyPath ); + unlink( $tempPlaintextPath ); + + // Create input with options + $input = new Input( [ + '--config=' . $this->testConfigPath, + '--editor=echo' // Use echo as a no-op editor for testing + ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Execute should succeed + $this->assertEquals( 0, $result ); + + // Key and credentials should still exist + $this->assertFileExists( $keyPath ); + $this->assertFileExists( $credentialsPath ); + + // Check output messages + $this->assertStringContainsString( 'Editing base secrets...', $outputContent ); + $this->assertStringContainsString( "Secrets saved to: {$credentialsPath}", $outputContent ); + } + + /** + * Test editing environment-specific secrets + */ + public function testExecuteWithEnvironment(): void + { + // Create secrets directory + mkdir( $this->testConfigPath . '/secrets', 0755, true ); + + // Create input with options + $input = new Input( [ + '--config=' . $this->testConfigPath, + '--env=production', + '--editor=echo' // Use echo as a no-op editor + ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + $keyPath = $this->testConfigPath . '/secrets/production.key'; + $credentialsPath = $this->testConfigPath . '/secrets/production.yml.enc'; + + // Execute should succeed + $this->assertEquals( 0, $result ); + + // Key should be generated + $this->assertFileExists( $keyPath ); + $this->assertFileExists( $credentialsPath ); + + // Check output messages + $this->assertStringContainsString( 'Editing production environment secrets...', $outputContent ); + $this->assertStringContainsString( "Key file not found at: {$keyPath}", $outputContent ); + $this->assertStringContainsString( "Secrets saved to: {$credentialsPath}", $outputContent ); + } + + /** + * Test that error is handled gracefully + */ + public function testExecuteWithError(): void + { + // Use a path that will cause an error (non-writable) + $badPath = '/root/cannot_write_here'; + + // Create input with options + $input = new Input( [ + '--config=' . $badPath + ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Execute should fail + $this->assertEquals( 1, $result ); + + // Check error message + $this->assertStringContainsString( 'Error editing secrets:', $outputContent ); + } + + /** + * Helper to remove directory recursively + */ + private function removeDirectory( string $dir ): void + { + if( !is_dir( $dir ) ) + { + return; + } + + $files = array_diff( scandir( $dir ), ['.', '..'] ); + foreach( $files as $file ) + { + $path = $dir . '/' . $file; + is_dir( $path ) ? $this->removeDirectory( $path ) : unlink( $path ); + } + rmdir( $dir ); + } +} \ No newline at end of file diff --git a/tests/Cli/Commands/Secrets/Key/GenerateCommandTest.php b/tests/Cli/Commands/Secrets/Key/GenerateCommandTest.php new file mode 100644 index 0000000..f25daa4 --- /dev/null +++ b/tests/Cli/Commands/Secrets/Key/GenerateCommandTest.php @@ -0,0 +1,178 @@ +testConfigPath = sys_get_temp_dir() . '/test_secrets_generate_' . uniqid(); + mkdir( $this->testConfigPath, 0755, true ); + + $this->command = new GenerateCommand(); + $this->command->configure(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + // Clean up test files + $this->removeDirectory( $this->testConfigPath ); + } + + /** + * Test that the command has the correct name + */ + public function testGetName(): void + { + $this->assertEquals( 'secrets:key:generate', $this->command->getName() ); + } + + /** + * Test that the command has a description + */ + public function testGetDescription(): void + { + $this->assertEquals( 'Generate a new encryption key for secrets', $this->command->getDescription() ); + } + + /** + * Test generating master key + */ + public function testExecuteGeneratesMasterKey(): void + { + // Create input with options + $input = new Input( [ '--config=' . $this->testConfigPath ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Execute should succeed + $this->assertEquals( 0, $result ); + + // Key file should exist + $keyPath = $this->testConfigPath . '/master.key'; + $this->assertFileExists( $keyPath ); + + // Key should be 64 hex characters + $key = file_get_contents( $keyPath ); + $this->assertEquals( 64, strlen( $key ) ); + $this->assertMatchesRegularExpression( '/^[a-f0-9]{64}$/i', $key ); + + // Check output contains success message + $this->assertStringContainsString( "Generated master key at: {$keyPath}", $outputContent ); + $this->assertStringContainsString( "Next steps:", $outputContent ); + } + + /** + * Test generating environment-specific key + */ + public function testExecuteGeneratesEnvironmentKey(): void + { + // Create input with options + $input = new Input( [ + '--config=' . $this->testConfigPath, + '--env=production' + ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Execute should succeed + $this->assertEquals( 0, $result ); + + // Directory and key file should exist + $this->assertDirectoryExists( $this->testConfigPath . '/secrets' ); + $keyPath = $this->testConfigPath . '/secrets/production.key'; + $this->assertFileExists( $keyPath ); + + // Check output contains success message + $this->assertStringContainsString( "Generated production environment key at: {$keyPath}", $outputContent ); + } + + /** + * Test error when key already exists without force + */ + public function testExecuteErrorWhenKeyExistsWithoutForce(): void + { + // Create an existing key file + $keyPath = $this->testConfigPath . '/master.key'; + file_put_contents( $keyPath, 'existing_key' ); + + // Create input with options + $input = new Input( [ '--config=' . $this->testConfigPath ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Execute should fail + $this->assertEquals( 1, $result ); + + // Original key should still exist + $this->assertEquals( 'existing_key', file_get_contents( $keyPath ) ); + + // Check error messages + $this->assertStringContainsString( "Key file already exists: {$keyPath}", $outputContent ); + $this->assertStringContainsString( 'Use --force to overwrite the existing key.', $outputContent ); + $this->assertStringContainsString( 'WARNING: Overwriting will make existing encrypted files unreadable!', $outputContent ); + } + + /** + * Helper to remove directory recursively + */ + private function removeDirectory( string $dir ): void + { + if( !is_dir( $dir ) ) + { + return; + } + + $files = array_diff( scandir( $dir ), ['.', '..'] ); + foreach( $files as $file ) + { + $path = $dir . '/' . $file; + is_dir( $path ) ? $this->removeDirectory( $path ) : unlink( $path ); + } + rmdir( $dir ); + } +} \ No newline at end of file diff --git a/tests/Cli/Commands/Secrets/ShowCommandTest.php b/tests/Cli/Commands/Secrets/ShowCommandTest.php new file mode 100644 index 0000000..76c077b --- /dev/null +++ b/tests/Cli/Commands/Secrets/ShowCommandTest.php @@ -0,0 +1,186 @@ +testConfigPath = sys_get_temp_dir() . '/test_secrets_show_' . uniqid(); + mkdir( $this->testConfigPath, 0755, true ); + + $this->command = new ShowCommand(); + $this->command->configure(); + $this->secretManager = new SecretManager(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + // Clean up test files + $this->removeDirectory( $this->testConfigPath ); + } + + /** + * Test that the command has the correct name + */ + public function testGetName(): void + { + $this->assertEquals( 'secrets:show', $this->command->getName() ); + } + + /** + * Test that the command has a description + */ + public function testGetDescription(): void + { + $this->assertEquals( 'Show decrypted secrets', $this->command->getDescription() ); + } + + /** + * Test showing base secrets + */ + public function testExecuteShowsBaseSecrets(): void + { + // Create test secrets + $keyPath = $this->testConfigPath . '/master.key'; + $credentialsPath = $this->testConfigPath . '/secrets.yml.enc'; + + $key = $this->secretManager->generateKey( $keyPath ); + + $tempPlaintextPath = $this->testConfigPath . '/temp_plaintext.yml'; + $testData = "database:\n password: secret123\napi:\n key: abc123"; + file_put_contents( $tempPlaintextPath, $testData ); + $this->secretManager->encrypt( $tempPlaintextPath, $credentialsPath, $keyPath ); + unlink( $tempPlaintextPath ); + + // Create input with options + $input = new Input( [ '--config=' . $this->testConfigPath ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Execute should succeed + $this->assertEquals( 0, $result ); + + // Check output contains secrets + $this->assertStringContainsString( 'Base Secrets', $outputContent ); + $this->assertStringContainsString( 'database:', $outputContent ); + $this->assertStringContainsString( 'Remember: Never share or commit decrypted secrets!', $outputContent ); + } + + /** + * Test showing specific key + */ + public function testExecuteShowsSpecificKey(): void + { + // Create test secrets + $keyPath = $this->testConfigPath . '/master.key'; + $credentialsPath = $this->testConfigPath . '/secrets.yml.enc'; + + $key = $this->secretManager->generateKey( $keyPath ); + + $tempPlaintextPath = $this->testConfigPath . '/temp_plaintext.yml'; + $testData = "database:\n password: secret123\napi:\n key: abc123"; + file_put_contents( $tempPlaintextPath, $testData ); + $this->secretManager->encrypt( $tempPlaintextPath, $credentialsPath, $keyPath ); + unlink( $tempPlaintextPath ); + + // Create input with options + $input = new Input( [ + '--config=' . $this->testConfigPath, + '--key=database' + ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Execute should succeed + $this->assertEquals( 0, $result ); + + // Check output contains only the database key + $this->assertStringContainsString( 'database:', $outputContent ); + $this->assertStringNotContainsString( 'api:', $outputContent ); + } + + /** + * Test error when secrets file not found + */ + public function testExecuteErrorWhenSecretsFileNotFound(): void + { + // Create input with options (no secrets file exists) + $input = new Input( [ '--config=' . $this->testConfigPath ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Execute should fail + $this->assertEquals( 1, $result ); + + $credentialsPath = $this->testConfigPath . '/secrets.yml.enc'; + + // Check error messages + $this->assertStringContainsString( "Secrets file not found: {$credentialsPath}", $outputContent ); + $this->assertStringContainsString( "Use 'neuron secrets:edit' to create it.", $outputContent ); + } + + /** + * Helper to remove directory recursively + */ + private function removeDirectory( string $dir ): void + { + if( !is_dir( $dir ) ) + { + return; + } + + $files = array_diff( scandir( $dir ), ['.', '..'] ); + foreach( $files as $file ) + { + $path = $dir . '/' . $file; + is_dir( $path ) ? $this->removeDirectory( $path ) : unlink( $path ); + } + rmdir( $dir ); + } +} \ No newline at end of file From cc909a0e7a405d83347b423cfcee55aa46f41508 Mon Sep 17 00:00:00 2001 From: Lee Jones Date: Tue, 6 Jan 2026 11:34:43 -0600 Subject: [PATCH 2/5] bug fixes. --- src/Cli/Commands/Secrets/EditCommand.php | 12 +++ .../Commands/Secrets/Key/GenerateCommand.php | 1 + src/Cli/Commands/Secrets/ShowCommand.php | 3 +- .../Cli/Commands/Secrets/EditCommandTest.php | 61 +++++++++++- .../Cli/Commands/Secrets/ShowCommandTest.php | 98 +++++++++++++++++++ 5 files changed, 170 insertions(+), 5 deletions(-) diff --git a/src/Cli/Commands/Secrets/EditCommand.php b/src/Cli/Commands/Secrets/EditCommand.php index 223f4b6..b8b8205 100644 --- a/src/Cli/Commands/Secrets/EditCommand.php +++ b/src/Cli/Commands/Secrets/EditCommand.php @@ -46,6 +46,7 @@ public function configure(): void $this->addOption( 'env', 'e', true, 'Environment to edit (default: base secrets)' ); $this->addOption( 'editor', null, true, 'Editor to use (default: vi)' ); $this->addOption( 'config', 'c', true, 'Config directory path (default: config)' ); + $this->addOption( 'verbose', 'v', false, 'Verbose output' ); } /** @@ -82,6 +83,17 @@ public function execute(): int $this->output->warning( "Key file not found at: {$keyPath}" ); $this->output->info( "Generating new encryption key..." ); + // Ensure directory exists + $dir = dirname( $keyPath ); + if( !is_dir( $dir ) ) + { + if( !mkdir( $dir, 0755, true ) ) + { + $this->output->error( "Failed to create directory: {$dir}" ); + return 1; + } + } + $key = $this->secretManager->generateKey( $keyPath ); $this->output->success( "Generated new key at: {$keyPath}" ); $this->output->warning( "IMPORTANT: Add {$keyPath} to .gitignore!" ); diff --git a/src/Cli/Commands/Secrets/Key/GenerateCommand.php b/src/Cli/Commands/Secrets/Key/GenerateCommand.php index c8b8c90..450496d 100644 --- a/src/Cli/Commands/Secrets/Key/GenerateCommand.php +++ b/src/Cli/Commands/Secrets/Key/GenerateCommand.php @@ -47,6 +47,7 @@ public function configure(): void $this->addOption( 'config', 'c', true, 'Config directory path (default: config)' ); $this->addOption( 'force', 'f', false, 'Overwrite existing key file' ); $this->addOption( 'show', 's', false, 'Display the generated key' ); + $this->addOption( 'verbose', 'v', false, 'Verbose output' ); } /** diff --git a/src/Cli/Commands/Secrets/ShowCommand.php b/src/Cli/Commands/Secrets/ShowCommand.php index 4c2ce35..ecb0b28 100644 --- a/src/Cli/Commands/Secrets/ShowCommand.php +++ b/src/Cli/Commands/Secrets/ShowCommand.php @@ -48,6 +48,7 @@ public function configure(): void $this->addOption( 'key', 'k', true, 'Show only specific key/section' ); $this->addOption( 'config', 'c', true, 'Config directory path (default: config)' ); $this->addOption( 'force', 'f', false, 'Skip confirmation prompt' ); + $this->addOption( 'verbose', 'v', false, 'Verbose output' ); } /** @@ -65,7 +66,7 @@ public function execute(): int { $this->output->warning( "You are about to display production secrets!" ); - if( !$this->output->confirm( "Are you sure you want to continue?" ) ) + if( !$this->confirm( "Are you sure you want to continue?" ) ) { $this->output->info( "Operation cancelled." ); return 0; diff --git a/tests/Cli/Commands/Secrets/EditCommandTest.php b/tests/Cli/Commands/Secrets/EditCommandTest.php index 92981c8..910c239 100644 --- a/tests/Cli/Commands/Secrets/EditCommandTest.php +++ b/tests/Cli/Commands/Secrets/EditCommandTest.php @@ -159,17 +159,66 @@ public function testExecuteWithEnvironment(): void $this->assertStringContainsString( "Secrets saved to: {$credentialsPath}", $outputContent ); } + /** + * Test that environment directory is created when missing + */ + public function testExecuteCreatesEnvironmentDirectory(): void + { + // Do NOT create the secrets directory - let the command create it + + // Create input with options + $input = new Input( [ + '--config=' . $this->testConfigPath, + '--env=staging', + '--editor=echo' // Use echo as a no-op editor + ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + $secretsDir = $this->testConfigPath . '/secrets'; + $keyPath = $secretsDir . '/staging.key'; + $credentialsPath = $secretsDir . '/staging.yml.enc'; + + // Execute should succeed + $this->assertEquals( 0, $result ); + + // Directory should be created + $this->assertDirectoryExists( $secretsDir ); + + // Key and credentials should be generated + $this->assertFileExists( $keyPath ); + $this->assertFileExists( $credentialsPath ); + + // Check output messages + $this->assertStringContainsString( 'Editing staging environment secrets...', $outputContent ); + $this->assertStringContainsString( "Key file not found at: {$keyPath}", $outputContent ); + $this->assertStringContainsString( "Generating new encryption key...", $outputContent ); + $this->assertStringContainsString( "Generated new key at: {$keyPath}", $outputContent ); + $this->assertStringContainsString( "Secrets saved to: {$credentialsPath}", $outputContent ); + } + /** * Test that error is handled gracefully */ public function testExecuteWithError(): void { - // Use a path that will cause an error (non-writable) - $badPath = '/root/cannot_write_here'; + // Create a non-writable directory + $nonWritablePath = sys_get_temp_dir() . '/test_non_writable_' . uniqid(); + mkdir( $nonWritablePath, 0000, true ); // Create with no permissions - // Create input with options + // Create input with options pointing to the non-writable path $input = new Input( [ - '--config=' . $badPath + '--config=' . $nonWritablePath ] ); $input->parse( $this->command ); @@ -189,6 +238,10 @@ public function testExecuteWithError(): void // Check error message $this->assertStringContainsString( 'Error editing secrets:', $outputContent ); + + // Clean up: restore permissions and remove directory + chmod( $nonWritablePath, 0755 ); + rmdir( $nonWritablePath ); } /** diff --git a/tests/Cli/Commands/Secrets/ShowCommandTest.php b/tests/Cli/Commands/Secrets/ShowCommandTest.php index 76c077b..1d02672 100644 --- a/tests/Cli/Commands/Secrets/ShowCommandTest.php +++ b/tests/Cli/Commands/Secrets/ShowCommandTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use Neuron\Cli\Console\Input; use Neuron\Cli\Console\Output; +use Neuron\Cli\IO\TestInputReader; class ShowCommandTest extends TestCase { @@ -135,6 +136,103 @@ public function testExecuteShowsSpecificKey(): void $this->assertStringNotContainsString( 'api:', $outputContent ); } + /** + * Test production environment confirmation prompt + */ + public function testExecuteProductionConfirmation(): void + { + // Create test secrets for production environment + mkdir( $this->testConfigPath . '/secrets', 0755, true ); + $keyPath = $this->testConfigPath . '/secrets/production.key'; + $credentialsPath = $this->testConfigPath . '/secrets/production.yml.enc'; + + $key = $this->secretManager->generateKey( $keyPath ); + + $tempPlaintextPath = $this->testConfigPath . '/temp_plaintext.yml'; + $testData = "database:\n password: production_secret"; + file_put_contents( $tempPlaintextPath, $testData ); + $this->secretManager->encrypt( $tempPlaintextPath, $credentialsPath, $keyPath ); + unlink( $tempPlaintextPath ); + + // Test 1: User confirms - secrets should be shown + $input = new Input( [ + '--config=' . $this->testConfigPath, + '--env=production' + ] ); + $input->parse( $this->command ); + + $output = new Output( false ); + + // Set up test input reader to confirm + $inputReader = new TestInputReader(); + $inputReader->addResponse( 'yes' ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + $this->command->setInputReader( $inputReader ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Should succeed and show secrets + $this->assertEquals( 0, $result ); + $this->assertStringContainsString( 'You are about to display production secrets!', $outputContent ); + $this->assertStringContainsString( 'production_secret', $outputContent ); + + // Test 2: User cancels - secrets should NOT be shown + $input2 = new Input( [ + '--config=' . $this->testConfigPath, + '--env=production' + ] ); + $input2->parse( $this->command ); + + $output2 = new Output( false ); + + // Set up test input reader to cancel + $inputReader2 = new TestInputReader(); + $inputReader2->addResponse( 'no' ); + + $this->command->setInput( $input2 ); + $this->command->setOutput( $output2 ); + $this->command->setInputReader( $inputReader2 ); + + // Capture output + ob_start(); + $result2 = $this->command->execute(); + $outputContent2 = ob_get_clean(); + + // Should exit gracefully without showing secrets + $this->assertEquals( 0, $result2 ); + $this->assertStringContainsString( 'Operation cancelled.', $outputContent2 ); + $this->assertStringNotContainsString( 'production_secret', $outputContent2 ); + + // Test 3: Force flag should skip confirmation + $input3 = new Input( [ + '--config=' . $this->testConfigPath, + '--env=production', + '--force' + ] ); + $input3->parse( $this->command ); + + $output3 = new Output( false ); + + $this->command->setInput( $input3 ); + $this->command->setOutput( $output3 ); + // No input reader needed - force skips confirmation + + // Capture output + ob_start(); + $result3 = $this->command->execute(); + $outputContent3 = ob_get_clean(); + + // Should succeed without confirmation prompt + $this->assertEquals( 0, $result3 ); + $this->assertStringNotContainsString( 'You are about to display production secrets!', $outputContent3 ); + $this->assertStringContainsString( 'production_secret', $outputContent3 ); + } + /** * Test error when secrets file not found */ From d6ea23e057c4d4ad5d47ae8cb75e616c2238c4a3 Mon Sep 17 00:00:00 2001 From: Lee Jones Date: Tue, 6 Jan 2026 11:43:53 -0600 Subject: [PATCH 3/5] bug fixes. --- composer.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 28c8ece..42c840b 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "^8.4", "neuron-php/application": "0.8.*", - "neuron-php/data": "0.7.*", + "neuron-php/data": "0.9.*", "symfony/yaml": "^6.4" }, "require-dev": { @@ -27,8 +27,7 @@ }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/", - "Neuron\\Core\\": "../core/src/Core/" + "Tests\\": "tests/" } }, "bin": [ From 6086bf1655839fd2878557eb894321349131abfc Mon Sep 17 00:00:00 2001 From: Lee Jones Date: Tue, 6 Jan 2026 12:06:10 -0600 Subject: [PATCH 4/5] bug fixes. --- src/Cli/Commands/Secrets/EditCommand.php | 4 +- .../Commands/Secrets/Key/GenerateCommand.php | 20 +++- src/Cli/Commands/Secrets/ShowCommand.php | 2 +- .../Secrets/Key/GenerateCommandTest.php | 105 ++++++++++++++++++ .../Cli/Commands/Secrets/ShowCommandTest.php | 49 ++++++++ 5 files changed, 176 insertions(+), 4 deletions(-) diff --git a/src/Cli/Commands/Secrets/EditCommand.php b/src/Cli/Commands/Secrets/EditCommand.php index b8b8205..7fbbcd7 100644 --- a/src/Cli/Commands/Secrets/EditCommand.php +++ b/src/Cli/Commands/Secrets/EditCommand.php @@ -56,7 +56,7 @@ public function execute(): int { $configPath = $this->input->getOption( 'config', 'config' ); $env = $this->input->getOption( 'env' ); - $editor = $this->input->getOption( 'editor' ) ?? $_ENV['EDITOR'] ?? 'vi'; + $editor = $this->input->getOption( 'editor' ) ?? getenv( 'EDITOR' ) ?: 'vi'; // Determine paths based on environment if( $env ) @@ -94,7 +94,7 @@ public function execute(): int } } - $key = $this->secretManager->generateKey( $keyPath ); + $this->secretManager->generateKey( $keyPath ); $this->output->success( "Generated new key at: {$keyPath}" ); $this->output->warning( "IMPORTANT: Add {$keyPath} to .gitignore!" ); } diff --git a/src/Cli/Commands/Secrets/Key/GenerateCommand.php b/src/Cli/Commands/Secrets/Key/GenerateCommand.php index 450496d..b135bc0 100644 --- a/src/Cli/Commands/Secrets/Key/GenerateCommand.php +++ b/src/Cli/Commands/Secrets/Key/GenerateCommand.php @@ -81,6 +81,17 @@ public function execute(): int { $keyPath = $configPath . '/master.key'; $keyName = 'master key'; + + // Ensure directory exists + $dir = dirname( $keyPath ); + if( !is_dir( $dir ) ) + { + if( !mkdir( $dir, 0755, true ) ) + { + $this->output->error( "Failed to create directory: {$dir}" ); + return 1; + } + } } // Check if key already exists @@ -137,7 +148,14 @@ public function execute(): int ) . '_KEY'; $this->output->newLine(); $this->output->info( "Alternative: Set the key as an environment variable:" ); - $this->output->write( "export {$envVar}={$key}" ); + if( $show ) + { + $this->output->write( "export {$envVar}={$key}" ); + } + else + { + $this->output->write( "export {$envVar}=" ); + } } catch( \Exception $e ) { diff --git a/src/Cli/Commands/Secrets/ShowCommand.php b/src/Cli/Commands/Secrets/ShowCommand.php index ecb0b28..10a09a0 100644 --- a/src/Cli/Commands/Secrets/ShowCommand.php +++ b/src/Cli/Commands/Secrets/ShowCommand.php @@ -163,6 +163,6 @@ private function checkEnvironmentKey( string $keyPath ): bool str_replace( ['/', '.', '-'], '_', basename( $keyPath, '.key' ) ) ) . '_KEY'; - return isset( $_ENV[$envKey] ); + return getenv( $envKey ) !== false; } } \ No newline at end of file diff --git a/tests/Cli/Commands/Secrets/Key/GenerateCommandTest.php b/tests/Cli/Commands/Secrets/Key/GenerateCommandTest.php index f25daa4..07d90e6 100644 --- a/tests/Cli/Commands/Secrets/Key/GenerateCommandTest.php +++ b/tests/Cli/Commands/Secrets/Key/GenerateCommandTest.php @@ -121,6 +121,111 @@ public function testExecuteGeneratesEnvironmentKey(): void $this->assertStringContainsString( "Generated production environment key at: {$keyPath}", $outputContent ); } + /** + * Test that config directory is created for master key when missing + */ + public function testExecuteCreatesMasterKeyDirectory(): void + { + // Use a non-existent config path + $nonExistentPath = sys_get_temp_dir() . '/test_config_' . uniqid() . '/config'; + + // Create input with options pointing to non-existent path + $input = new Input( [ '--config=' . $nonExistentPath ] ); + $input->parse( $this->command ); + + // Create output + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Execute should succeed + $this->assertEquals( 0, $result ); + + // Directory should be created + $this->assertDirectoryExists( $nonExistentPath ); + + // Key file should exist + $keyPath = $nonExistentPath . '/master.key'; + $this->assertFileExists( $keyPath ); + + // Check output contains success message + $this->assertStringContainsString( "Generated master key at: {$keyPath}", $outputContent ); + + // Clean up + unlink( $keyPath ); + rmdir( $nonExistentPath ); + rmdir( dirname( $nonExistentPath ) ); + } + + /** + * Test that key is only shown when --show flag is used + */ + public function testKeyOnlyShownWithShowFlag(): void + { + // Test WITHOUT --show flag (default) + $input = new Input( [ '--config=' . $this->testConfigPath ] ); + $input->parse( $this->command ); + + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + ob_start(); + $result = $this->command->execute(); + $outputContentNoShow = ob_get_clean(); + + $this->assertEquals( 0, $result ); + + $keyPath = $this->testConfigPath . '/master.key'; + $this->assertFileExists( $keyPath ); + + // Read the actual key + $actualKey = file_get_contents( $keyPath ); + + // Key should NOT be in the output (except in the placeholder) + $this->assertStringNotContainsString( "export NEURON_MASTER_KEY={$actualKey}", $outputContentNoShow ); + $this->assertStringContainsString( "export NEURON_MASTER_KEY=", $outputContentNoShow ); + $this->assertStringNotContainsString( "Generated Key", $outputContentNoShow ); + + // Clean up before second test + unlink( $keyPath ); + + // Test WITH --show flag + $input2 = new Input( [ + '--config=' . $this->testConfigPath, + '--show' + ] ); + $input2->parse( $this->command ); + + $output2 = new Output( false ); + + $this->command->setInput( $input2 ); + $this->command->setOutput( $output2 ); + + ob_start(); + $result2 = $this->command->execute(); + $outputContentWithShow = ob_get_clean(); + + $this->assertEquals( 0, $result2 ); + + // Read the new key + $actualKey2 = file_get_contents( $keyPath ); + + // Key SHOULD be in the output + $this->assertStringContainsString( "export NEURON_MASTER_KEY={$actualKey2}", $outputContentWithShow ); + $this->assertStringNotContainsString( "assertStringContainsString( "Generated Key", $outputContentWithShow ); + $this->assertStringContainsString( $actualKey2, $outputContentWithShow ); + $this->assertStringContainsString( "This key is shown only once", $outputContentWithShow ); + } + /** * Test error when key already exists without force */ diff --git a/tests/Cli/Commands/Secrets/ShowCommandTest.php b/tests/Cli/Commands/Secrets/ShowCommandTest.php index 1d02672..8e37fce 100644 --- a/tests/Cli/Commands/Secrets/ShowCommandTest.php +++ b/tests/Cli/Commands/Secrets/ShowCommandTest.php @@ -233,6 +233,55 @@ public function testExecuteProductionConfirmation(): void $this->assertStringContainsString( 'production_secret', $outputContent3 ); } + /** + * Test showing secrets using environment variable for key + */ + public function testExecuteWithEnvironmentVariable(): void + { + // Create test secrets + $keyPath = $this->testConfigPath . '/master.key'; + $credentialsPath = $this->testConfigPath . '/secrets.yml.enc'; + + // Generate a key and store it + $key = $this->secretManager->generateKey( $keyPath ); + + // Create encrypted secrets + $tempPlaintextPath = $this->testConfigPath . '/temp_plaintext.yml'; + $testData = "database:\n host: localhost\n password: env_secret"; + file_put_contents( $tempPlaintextPath, $testData ); + $this->secretManager->encrypt( $tempPlaintextPath, $credentialsPath, $keyPath ); + unlink( $tempPlaintextPath ); + + // Read the key value and delete the key file + $keyValue = trim( file_get_contents( $keyPath ) ); + unlink( $keyPath ); + + // Set the key as an environment variable + putenv( "NEURON_MASTER_KEY={$keyValue}" ); + + // Create input and output + $input = new Input( [ '--config=' . $this->testConfigPath ] ); + $input->parse( $this->command ); + + $output = new Output( false ); + + $this->command->setInput( $input ); + $this->command->setOutput( $output ); + + // Capture output + ob_start(); + $result = $this->command->execute(); + $outputContent = ob_get_clean(); + + // Should succeed using the environment variable + $this->assertEquals( 0, $result ); + $this->assertStringContainsString( 'Base Secrets', $outputContent ); + $this->assertStringContainsString( 'env_secret', $outputContent ); + + // Clean up environment variable + putenv( "NEURON_MASTER_KEY" ); + } + /** * Test error when secrets file not found */ From aa1560d2c3d49a1419004c1d6f5e7a337a17cfc8 Mon Sep 17 00:00:00 2001 From: Lee Jones Date: Tue, 6 Jan 2026 12:22:15 -0600 Subject: [PATCH 5/5] bug fixes. --- src/Cli/Commands/Secrets/EditCommand.php | 18 +++++- src/Cli/Commands/Secrets/ShowCommand.php | 2 + .../Cli/Commands/Secrets/EditCommandTest.php | 64 +++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/Cli/Commands/Secrets/EditCommand.php b/src/Cli/Commands/Secrets/EditCommand.php index 7fbbcd7..6e73633 100644 --- a/src/Cli/Commands/Secrets/EditCommand.php +++ b/src/Cli/Commands/Secrets/EditCommand.php @@ -56,7 +56,17 @@ public function execute(): int { $configPath = $this->input->getOption( 'config', 'config' ); $env = $this->input->getOption( 'env' ); - $editor = $this->input->getOption( 'editor' ) ?? getenv( 'EDITOR' ) ?: 'vi'; + + // Handle editor option - could be null, true (flag without value), or a string + $editorOption = $this->input->getOption( 'editor' ); + if( is_string( $editorOption ) && $editorOption !== '' ) + { + $editor = $editorOption; + } + else + { + $editor = getenv( 'EDITOR' ) ?: 'vi'; + } // Determine paths based on environment if( $env ) @@ -106,8 +116,10 @@ public function execute(): int { $this->output->success( "Secrets saved to: {$credentialsPath}" ); - // First time setup reminder - if( !$env && !file_exists( $configPath . '/.gitignore' ) ) + // First time setup reminder - check for .gitignore in project root + $projectRoot = dirname( $configPath ); + $gitignorePath = $projectRoot . '/.gitignore'; + if( !$env && !file_exists( $gitignorePath ) ) { $this->output->newLine(); $this->output->warning( "Remember to:" ); diff --git a/src/Cli/Commands/Secrets/ShowCommand.php b/src/Cli/Commands/Secrets/ShowCommand.php index 10a09a0..0c5a6d8 100644 --- a/src/Cli/Commands/Secrets/ShowCommand.php +++ b/src/Cli/Commands/Secrets/ShowCommand.php @@ -103,10 +103,12 @@ public function execute(): int } // Create SecretManager and decrypt + // Note: SecretManager::show() internally calls readKey() which handles both file and environment variable sources $this->secretManager = new SecretManager(); try { + // The show() method will read the key from either the file or environment variable $decrypted = $this->secretManager->show( $credentialsPath, $keyPath ); $data = Yaml::parse( $decrypted ); diff --git a/tests/Cli/Commands/Secrets/EditCommandTest.php b/tests/Cli/Commands/Secrets/EditCommandTest.php index 910c239..bf8f80e 100644 --- a/tests/Cli/Commands/Secrets/EditCommandTest.php +++ b/tests/Cli/Commands/Secrets/EditCommandTest.php @@ -207,6 +207,70 @@ public function testExecuteCreatesEnvironmentDirectory(): void $this->assertStringContainsString( "Secrets saved to: {$credentialsPath}", $outputContent ); } + /** + * Test editor option handling with various input types + */ + public function testEditorOptionHandling(): void + { + // Create a key file first + $keyPath = $this->testConfigPath . '/master.key'; + $secretManager = new SecretManager(); + $secretManager->generateKey( $keyPath ); + + // Create initial credentials file + $credentialsPath = $this->testConfigPath . '/secrets.yml.enc'; + $tempPlaintextPath = $this->testConfigPath . '/temp.yml'; + file_put_contents( $tempPlaintextPath, "test: value" ); + $secretManager->encrypt( $tempPlaintextPath, $credentialsPath, $keyPath ); + unlink( $tempPlaintextPath ); + + // Test 1: Editor option with value + $input1 = new Input( [ + '--config=' . $this->testConfigPath, + '--editor=echo' // Use echo as a no-op editor + ] ); + $input1->parse( $this->command ); + + $output1 = new Output( false ); + $this->command->setInput( $input1 ); + $this->command->setOutput( $output1 ); + + // This should use 'echo' as editor + ob_start(); + $result1 = $this->command->execute(); + $outputContent1 = ob_get_clean(); + + // Should succeed with echo editor + $this->assertEquals( 0, $result1 ); + $this->assertStringContainsString( "Secrets saved to", $outputContent1 ); + + // Test 2: Editor option without value (becomes boolean true) + $input2 = new Input( [ + '--config=' . $this->testConfigPath, + '--editor' // No value - becomes boolean true + ] ); + $input2->parse( $this->command ); + + $output2 = new Output( false ); + $this->command->setInput( $input2 ); + $this->command->setOutput( $output2 ); + + // This should fall back to EDITOR env var or 'vi' + // Set a test editor to avoid vi + putenv( 'EDITOR=echo' ); + + ob_start(); + $result2 = $this->command->execute(); + $outputContent2 = ob_get_clean(); + + // Should succeed with fallback to env var + $this->assertEquals( 0, $result2 ); + $this->assertStringContainsString( "Secrets saved to", $outputContent2 ); + + // Clean up env var + putenv( 'EDITOR' ); + } + /** * Test that error is handled gracefully */