Skip to content

fixes - #29

Closed
ljonesfl wants to merge 3 commits into
developfrom
feature/fixes
Closed

ljonesfl wants to merge 3 commits into
developfrom
feature/fixes

Conversation

@ljonesfl

@ljonesfl ljonesfl commented Jan 6, 2026 •

Copy link
Copy Markdown
Member

Note

Introduces secure, flexible configuration and improves bootstrapping.

  • DB config: ConnectionFactory now supports database.url parsing/overrides (SQLite/MySQL/PostgreSQL) and merges with individual params
  • Installer: New --use-secrets mode encrypts sensitive settings (master.key, secrets.yml.enc), saves public vs secrets config, validates DB URLs, and provisions config/.gitignore; adds DB URL setup flow
  • Bootstrap: Switches to SettingManagerFactory to load base YAML, environment-specific files, encrypted secrets, and env vars; constructs Application directly and initializes ORM via ConnectionFactory
  • Config: Default security.enabled: true; expands neuron.yaml.example with DB URL examples and precedence notes
  • Auth: Updates forgot-password form action to /forgot-password
  • Tests: Comprehensive unit/integration tests for DB URL parsing/merging, secrets scenarios, and controller setup mocks

Written by Cursor Bugbot for commit 5bbbdd6. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Optional encrypted-secrets installation mode and installer option to emit secrets separately
    • Support for URL-based database configuration
    • New config .gitignore template and automatic gitignore handling
    • Default security.enabled flag set to true
  • Bug Fixes

    • Updated password-reset form endpoint routing
  • Documentation

    • Examples and guidance for database URL usage and env precedence
  • Tests

    • Extensive new and updated tests covering DB URL parsing, merging, and related flows

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 6, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Aggregates configuration from YAML, encrypted secrets, and environment variables via a multi-source SettingManager; adds encrypted-secrets support and gitignore template; introduces URL-based database configuration and parsing; updates installer to optionally persist secrets; adapts bootstrap and ConnectionFactory; adds tests and a small view form-action change.

Changes

Cohort / File(s) Summary
Config templates & schemas
resources/config/.gitignore.template, resources/config/neuron.yaml, resources/config/neuron.yaml.example
Adds a .gitignore template for keys/env overrides/temp files; adds security.enabled: true to neuron.yaml; expands neuron.yaml.example with URL-based DB configuration guidance and examples (MySQL, PostgreSQL, SQLite).
Bootstrap & config aggregation
src/Bootstrap.php
Replaces single-file YAML loading with SettingManagerFactory::createCustom to merge base YAML, env-specific YAML, encrypted secrets (global and env), and environment variables; builds Application with aggregated SettingManager, reads version from .version.json, and adds container fallback handling.
Installer & secrets handling
src/Cms/Cli/Commands/Install/InstallCommand.php
Adds --use-secrets/-s option and propagation, SecretManager usage for master key generation, saveConfigWithSecrets to persist secrets.yml.enc, copyGitignoreTemplate/updateGitignore helpers, and URL-based DB configuration via configureDatabaseUrl; updates method signatures to accept $useSecrets.
Database connection URL support
src/Cms/Database/ConnectionFactory.php
Adds URL parsing and merging (parseUrl) to accept adapter://user:pass@host:port/db?options, map schemes to adapters, handle SQLite variants (including :memory: and file paths), and let explicit config override URL-derived values; improves error messages.
View update
resources/views/auth/password_reset/forgot-password.php
Changed form action from /auth/password-reset/send to /forgot-password.
Test wiring — controller tests
tests/Unit/Cms/Controllers/Admin/PostsTest.php, tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php, tests/Unit/Cms/Controllers/Member/RegistrationTest.php
Add mocks for ISettingSource and configure SettingManager mock to return getSource() to provide controlled settings in tests.
Tests — ConnectionFactory & DB URL validation
tests/Unit/Cms/Database/ConnectionFactoryTest.php, tests/Unit/Cms/Database/ConnectionFactoryIntegrationTest.php, tests/Unit/Cms/Database/ConnectionFactoryUrlSecretsTest.php, tests/Unit/Cms/Database/NoConfiguredAdapterTest.php, tests/Unit/Cms/Cli/Commands/Install/DatabaseUrlValidationTest.php
Add comprehensive unit and integration tests for DB URL parsing/merging/validation across MySQL, PostgreSQL, and SQLite; include malformed/unsupported scenarios, override precedence, secrets-sourced URLs, and private-method coverage via reflection.

Sequence Diagram(s)

sequenceDiagram
    participant Bootstrap as Bootstrap
    participant SMF as SettingManagerFactory
    participant YAML as YAML Files
    participant Secrets as Encrypted Secrets
    participant Env as Environment
    participant SM as SettingManager
    participant App as Application

    Bootstrap->>SMF: createCustom(baseYamlPath, env)
    activate SMF
    SMF->>YAML: Load neuron.yaml / neuron.{env}.yaml
    YAML-->>SMF: base + env config
    SMF->>Secrets: Load secrets.yml.enc (and secrets.{env}.yml.enc)
    Secrets-->>SMF: decrypted secrets (scoped)
    SMF->>Env: Read environment variables
    Env-->>SMF: env values (highest priority)

    rect rgb(220,235,245)
    note over SMF: Merge sources (env > secrets > yaml)
    end

    SMF->>SM: Construct SettingManager with merged sources
    SM-->>SMF: SettingManager instance
    SMF-->>Bootstrap: return SettingManager
    Bootstrap->>App: new Application(version, SettingManager)
    App-->>Bootstrap: Application ready
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I nibbled through configs, neat and bright,

Hid master keys where no one spies by night,
I sniffed URLs and parsed each host and port,
Bootstrap now gathers sources from every sort,
Gitignore snug, the installer hops to light. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'fixes' is vague and does not meaningfully describe the substantial changes made in this pull request. Use a more descriptive title that captures the main focus, such as 'Add secure configuration and URL-based database setup' or 'Implement encrypted secrets and database URL support'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 93.18% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/Bootstrap.php
Comment thread src/Cms/Cli/Commands/Install/InstallCommand.php
Comment thread src/Cms/Database/ConnectionFactory.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Bootstrap.php (1)

49-103: Undefined $settings if configuration loading fails early.

If SettingManagerFactory::createCustom() throws an exception (e.g., missing neuron.yaml), the catch block only logs the error but doesn't return or re-throw. Execution continues to line 92 (Container::build($settings)) and line 101 ($settings->get(...)) where $settings is undefined, causing a fatal error.

Either re-throw the exception or ensure a graceful failure:

🔎 Proposed fix
 catch( \Exception $e )
 {
 	\Neuron\Log\Log::error( 'Container initialization failed: ' . $e->getMessage() );
+	throw $e; // Configuration is critical - cannot continue without it
 }

Or alternatively, move the $settings creation outside the try block and only wrap optional operations.

🤖 Fix all issues with AI Agents
In @src/Bootstrap.php:
- Around line 73-84: The encrypted config sources are added without defensive
existence checks; update the arrays passed to createCustom() so each encrypted
source mirrors the YAML source pattern by wrapping the environment-specific
encrypted entry in a file_exists() conditional and using array_filter() to drop
nulls (i.e., replace the plain second encrypted array with
file_exists("$configPath/secrets/$environment.yml.enc") ? [
'type'=>'encrypted','path'=>"$configPath/secrets/$environment.yml.enc",'key'=>"$configPath/secrets/$environment.key",'name'=>"secrets:$environment"
] : null ), ensuring the created list only includes existing files before
calling createCustom().

In @src/Cms/Cli/Commands/Install/InstallCommand.php:
- Around line 1050-1054: Remove the unused temporary variable $testConfig in
InstallCommand.php: locate the try block where $testConfig is assigned (the
variable name $testConfig) and delete that assignment since it is never used;
ensure no other logic depends on $testConfig and run tests/static analysis to
confirm the warning is resolved.
- Around line 978-984: The email public config is reading $email['transport']
while configureEmail() sets the key as 'driver', causing transport to always
default; update the public config assembly in InstallCommand (the block setting
$publicConfig['email']) to read $email['driver'] (or otherwise normalize $email
keys from configureEmail()) and ensure the keys emitted remain non-sensitive
(keep from_address and from_name) so the 'driver' value is preserved instead of
always falling back to 'mail'.

In @src/Cms/Database/ConnectionFactory.php:
- Around line 184-192: The code assigns raw parsed['user'] and parsed['pass']
into $config without URL-decoding, so percent-encoded credentials can fail;
update the assignment in ConnectionFactory (the block handling $parsed['user']
and $parsed['pass']) to pass values through urldecode() (e.g., $config['user'] =
urldecode($parsed['user']); and $config['pass'] = urldecode($parsed['pass']);)
so credentials like p%40ssword decode correctly.
- Around line 138-152: The SQLite path handling currently checks for '//' before
'///', causing paths like '///storage/db.sqlite3' to match the wrong branch and
strip only two slashes; update the logic in ConnectionFactory (the block that
examines $path) to check for the '///' prefix first and strip three characters
for that case (substr($path, 3)), and only then handle the '//' case by
stripping two characters (substr($path, 2)), so absolute triple-slash paths are
normalized correctly.

In @tests/Unit/Cms/Database/ConnectionFactoryTest.php:
- Around line 211-222: Remove the unused private helper method
assertDoesNotThrow from the test class: locate the private function
assertDoesNotThrow(callable $callable): void in the test file and delete the
entire method (including its try/catch body) to eliminate dead code and satisfy
static analysis. Confirm no other tests reference assertDoesNotThrow before
removal.
- Around line 92-107: The test currently only checks SettingManager values;
modify it to call ConnectionFactory::createFromConfig with the $config produced
from SettingManager (after creating $source = new Memory() and setting the URL
plus overrides) and then assert the resulting connection's configuration (or
DSN) reflects the overrides: that the connection created by
ConnectionFactory::createFromConfig has host == 'overridehost' and port == 9999;
use the ConnectionFactory::createFromConfig return value or its accessible
methods/properties to inspect the resolved host/port and assert equality.
🧹 Nitpick comments (5)
tests/Unit/Cms/Controllers/Member/RegistrationTest.php (1)

51-79: Consider consolidating duplicate mock callbacks.

The mockSettingSource callback (lines 53-63) duplicates the logic in mockSettings.get() callback (lines 66-76). While this works correctly, you could simplify by reusing the same callback function or referencing one from the other to reduce duplication.

🔎 Proposed refactor to reduce duplication
+		// Create a shared callback for settings values
+		$settingsCallback = function( $section, $key = null ) {
+			if( $section === 'site' && $key === 'name' ) return 'Test Site';
+			if( $section === 'site' && $key === 'title' ) return 'Test Site';
+			if( $section === 'site' && $key === 'description' ) return 'Test Description';
+			if( $section === 'site' && $key === 'url' ) return 'https://test.com';
+			if( $section === 'site' && $key === 'theme' ) return 'flatly';
+			if( $section === 'views' && $key === 'path' ) return __DIR__ . '/../../../../../resources/views';
+			if( $section === 'cache' && $key === 'enabled' ) return false;
+			if( $section === 'member' && $key === 'require_email_verification' ) return true;
+			return null;
+		};
+
 		// Create a mock setting source for the SettingManager
 		$mockSettingSource = $this->createMock( \Neuron\Data\Settings\Source\ISettingSource::class );
-		$mockSettingSource->method( 'get' )->willReturnCallback( function( $section, $key = null ) {
-			if( $section === 'site' && $key === 'name' ) return 'Test Site';
-			if( $section === 'site' && $key === 'title' ) return 'Test Site';
-			if( $section === 'site' && $key === 'description' ) return 'Test Description';
-			if( $section === 'site' && $key === 'url' ) return 'https://test.com';
-			if( $section === 'site' && $key === 'theme' ) return 'flatly';
-			if( $section === 'views' && $key === 'path' ) return __DIR__ . '/../../../../../resources/views';
-			if( $section === 'cache' && $key === 'enabled' ) return false;
-			if( $section === 'member' && $key === 'require_email_verification' ) return true;
-			return null;
-		});
+		$mockSettingSource->method( 'get' )->willReturnCallback( $settingsCallback );
 
 		// Setup default mock settings
-		$this->mockSettings->method( 'get' )->willReturnCallback( function( $section, $key = null ) {
-			if( $section === 'site' && $key === 'name' ) return 'Test Site';
-			if( $section === 'site' && $key === 'title' ) return 'Test Site';
-			if( $section === 'site' && $key === 'description' ) return 'Test Description';
-			if( $section === 'site' && $key === 'url' ) return 'https://test.com';
-			if( $section === 'site' && $key === 'theme' ) return 'flatly';
-			if( $section === 'views' && $key === 'path' ) return __DIR__ . '/../../../../../resources/views';
-			if( $section === 'cache' && $key === 'enabled' ) return false;
-			if( $section === 'member' && $key === 'require_email_verification' ) return true;
-			return null;
-		});
+		$this->mockSettings->method( 'get' )->willReturnCallback( $settingsCallback );
 
 		// IMPORTANT: Mock the getSource() method to return the mock setting source
 		$this->mockSettings->method( 'getSource' )->willReturn( $mockSettingSource );
resources/config/.gitignore.template (1)

1-4: Consider the scope of the *.key pattern.

The pattern *.key on line 4 will ignore all .key files in the config directory and subdirectories. If there are any legitimate non-secret .key files (e.g., public keys for verification) that should be tracked, this pattern might be too broad.

Consider being more specific if needed, such as:

/master.key
/secrets/*.key
/encryption*.key

However, if all .key files in this directory are indeed sensitive, the current pattern is appropriate.

resources/config/neuron.yaml (1)

48-50: Approve the defensive default, but consider a clearer comment.

The enabled: true placeholder is a reasonable approach to prevent null errors in PHP 8.4's stricter handling. However, the comment "Dummy value" might confuse future maintainers. Consider rephrasing to clarify its purpose:

 # Security Configuration
 security:
-  enabled: true  # Dummy value to prevent null errors in PHP 8.4
+  enabled: true  # Placeholder to ensure security section is not null (required for PHP 8.4+ compatibility)
tests/Unit/Cms/Database/ConnectionFactoryTest.php (1)

77-86: Consider using try/finally for temp file cleanup.

If ConnectionFactory::createFromSettings throws an exception, the temp file won't be cleaned up. Using try/finally ensures cleanup even on failure:

🔎 Proposed fix
 // Test with absolute path
 $tempFile = tempnam( sys_get_temp_dir(), 'test_db_' ) . '.sqlite3';
-$settings = $this->createSettingsWithUrl( 'sqlite:///' . $tempFile );
-$pdo = ConnectionFactory::createFromSettings( $settings );
-$this->assertInstanceOf( PDO::class, $pdo );
-
-// Clean up
-if( file_exists( $tempFile ) )
-{
-	unlink( $tempFile );
-}
+try
+{
+	$settings = $this->createSettingsWithUrl( 'sqlite:///' . $tempFile );
+	$pdo = ConnectionFactory::createFromSettings( $settings );
+	$this->assertInstanceOf( PDO::class, $pdo );
+}
+finally
+{
+	if( file_exists( $tempFile ) )
+	{
+		unlink( $tempFile );
+	}
+}
src/Cms/Cli/Commands/Install/InstallCommand.php (1)

943-948: Consider splitting multiple statements for readability.

Multiple statements on one line reduce readability and make debugging harder:

🔎 Proposed fix
 // Put username and password in secrets
 if( isset( $db['user'] ) || isset( $db['pass'] ) )
 {
 	$secretsConfig['database'] = [];
-	if( isset( $db['user'] ) ) $secretsConfig['database']['user'] = $db['user'];
-	if( isset( $db['pass'] ) ) $secretsConfig['database']['pass'] = $db['pass'];
+	if( isset( $db['user'] ) )
+	{
+		$secretsConfig['database']['user'] = $db['user'];
+	}
+	if( isset( $db['pass'] ) )
+	{
+		$secretsConfig['database']['pass'] = $db['pass'];
+	}
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c5ad03 and a6aaeee.

📒 Files selected for processing (11)
  • resources/config/.gitignore.template
  • resources/config/neuron.yaml
  • resources/config/neuron.yaml.example
  • resources/views/auth/password_reset/forgot-password.php
  • src/Bootstrap.php
  • src/Cms/Cli/Commands/Install/InstallCommand.php
  • src/Cms/Database/ConnectionFactory.php
  • tests/Unit/Cms/Controllers/Admin/PostsTest.php
  • tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php
  • tests/Unit/Cms/Controllers/Member/RegistrationTest.php
  • tests/Unit/Cms/Database/ConnectionFactoryTest.php
🧰 Additional context used
🧬 Code graph analysis (1)
tests/Unit/Cms/Database/ConnectionFactoryTest.php (1)
src/Cms/Database/ConnectionFactory.php (2)
  • ConnectionFactory (17-280)
  • createFromSettings (26-36)
🪛 PHPMD (2.15.0)
tests/Unit/Cms/Database/ConnectionFactoryTest.php

211-222: Avoid unused private methods such as 'assertDoesNotThrow'. (undefined)

(UnusedPrivateMethod)

src/Cms/Cli/Commands/Install/InstallCommand.php

1053-1053: Avoid unused local variables such as '$testConfig'. (undefined)

(UnusedLocalVariable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: build-test (sqlite)
  • GitHub Check: build-test (postgres)
  • GitHub Check: build-test (mysql)
🔇 Additional comments (9)
tests/Unit/Cms/Controllers/Admin/PostsTest.php (1)

39-44: LGTM! Test setup properly updated for multi-source configuration.

The addition of ISettingSource mocking and wiring it through SettingManager.getSource() aligns with the PR's refactoring of the configuration pipeline to support multiple sources. This ensures tests have a controlled configuration surface and properly mock the new dependency chain.

tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php (1)

52-57: LGTM! Consistent test setup for multi-source configuration.

The mock setup for ISettingSource and its integration with SettingManager is consistent with the pattern used in other test files (e.g., PostsTest.php). This properly supports the new configuration pipeline introduced in the PR.

resources/config/.gitignore.template (1)

1-23: Well-structured gitignore template for security and local overrides.

The template comprehensively covers:

  • Encryption keys (critical for security)
  • Local environment overrides
  • Environment-specific configurations
  • Temporary files
  • Optional IDE settings

The organization with clear section comments makes it easy to understand and maintain.

resources/views/auth/password_reset/forgot-password.php (1)

9-9: No action needed — the route is properly registered and configured.

The new /forgot-password route is correctly registered:

  • GET route at #[Get('/forgot-password', name: 'forgot_password')] displays the form
  • POST route at #[Post('/forgot-password', name: 'forgot_password_post', filters: ['csrf'])] processes the form with CSRF protection
  • The handler validates form data through a DTO and properly redirects on success or error
  • No remaining references to the old route /auth/password-reset/send exist in the codebase
resources/config/neuron.yaml.example (2)

101-121: LGTM! Well-documented URL-based configuration.

The documentation clearly explains the URL format, provides examples for all supported database adapters, documents environment variable precedence, and shows how to mix URL with individual overrides. This aligns well with the ConnectionFactory::parseUrl implementation.


127-132: LGTM!

The SQLite URL format example is correctly placed and follows the standard sqlite:///path convention.

src/Cms/Database/ConnectionFactory.php (1)

51-60: LGTM! URL merge logic is correct.

The merge correctly gives precedence to explicitly provided config values over URL-parsed values by placing $urlConfig first in array_merge and filtering out nulls and the URL key itself from the override.

src/Cms/Cli/Commands/Install/InstallCommand.php (2)

1252-1331: LGTM! Solid encrypted secrets implementation.

The saveConfigWithSecrets method properly:

  • Generates a master key if it doesn't exist
  • Uses SecretManager for encryption
  • Provides clear security instructions to the user
  • Updates .gitignore to prevent accidental key commits

1336-1392: LGTM! Proper .gitignore handling.

The method correctly:

  • Avoids duplicate entries by checking existing patterns
  • Creates both root and config-level .gitignore files
  • Includes appropriate patterns for keys and local env files

Comment thread src/Bootstrap.php Outdated
Comment thread src/Cms/Cli/Commands/Install/InstallCommand.php
Comment thread src/Cms/Cli/Commands/Install/InstallCommand.php Outdated
Comment thread src/Cms/Database/ConnectionFactory.php
Comment thread src/Cms/Database/ConnectionFactory.php
Comment thread tests/Unit/Cms/Database/ConnectionFactoryTest.php
Comment thread tests/Unit/Cms/Database/ConnectionFactoryTest.php Outdated
@codecov

codecov Bot commented Jan 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 24.73498% with 213 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Cms/Cli/Commands/Install/InstallCommand.php 0.00% 207 Missing ⚠️
src/Bootstrap.php 87.09% 4 Missing ⚠️
src/Cms/Database/ConnectionFactory.php 95.55% 2 Missing ⚠️
Flag Coverage Δ Complexity Δ
cms 73.58% <24.73%> (-1.69%) 2099.00 <64.00> (+63.00) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ Complexity Δ
src/Cms/Database/ConnectionFactory.php 92.92% <95.55%> (+2.18%) 25.00 <15.00> (+17.00)
src/Bootstrap.php 82.22% <87.09%> (-10.09%) 0.00 <0.00> (ø)
src/Cms/Cli/Commands/Install/InstallCommand.php 26.24% <0.00%> (-6.80%) 189.00 <49.00> (+46.00) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6aaeee and a9ea9c6.

📒 Files selected for processing (4)
  • src/Bootstrap.php
  • src/Cms/Cli/Commands/Install/InstallCommand.php
  • src/Cms/Database/ConnectionFactory.php
  • tests/Unit/Cms/Database/ConnectionFactoryTest.php
🧰 Additional context used
🧬 Code graph analysis (2)
src/Bootstrap.php (1)
src/Cms/Container/Container.php (2)
  • Container (18-87)
  • build (33-66)
src/Cms/Cli/Commands/Install/InstallCommand.php (2)
src/Cms/Email/helpers.php (1)
  • email (98-101)
src/Cms/Maintenance/MaintenanceManager.php (1)
  • getMessage (171-176)
🪛 PHPMD (2.15.0)
tests/Unit/Cms/Database/ConnectionFactoryTest.php

244-255: Avoid unused private methods such as 'assertDoesNotThrow'. (undefined)

(UnusedPrivateMethod)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: build-test (mysql)
  • GitHub Check: build-test (sqlite)
  • GitHub Check: build-test (postgres)
🔇 Additional comments (8)
src/Bootstrap.php (2)

45-50: Bootstrap settings/container initialization is now robust

Initializing $settings/$container up-front and constructing a minimal SettingManager in the catch block cleanly avoids the previous undefined-variable fatal and still allows the app to boot with basic YAML config when the advanced multi-source setup fails. The subsequent application construction and base-path resolution from settings/env look consistent with the new configuration model.

Also applies to: 95-107, 108-113


59-76: Multi-source configuration wiring looks consistent with new secrets flow

The SettingManagerFactory::createCustom() call correctly layers base YAML, optional environment-specific YAML, encrypted secrets (global and environment-specific), and environment variables, matching the installer and docs story. The testing-only override for neuron.testing.yaml is a reasonable special case and the default to neuron.yaml for other environments keeps behavior predictable.

Also applies to: 77-93

src/Cms/Cli/Commands/Install/InstallCommand.php (3)

61-72: use-secrets option and plumbing into setupDatabase/saveCompleteConfig look good

The new --use-secrets / -s flag is correctly parsed once in execute(), passed only into setupDatabase(), and then threaded into saveCompleteConfig(). The conditional branch in the installation loop that special-cases setupDatabase while leaving other steps untouched keeps the execution model simple and backward compatible.

Also applies to: 78-82, 124-143, 542-603


337-365: Gitignore template and secrets/key handling are safe and idempotent

copyGitignoreTemplate() defensively creates config/.gitignore from the component template only when missing, and updateGitignore() extends the root .gitignore plus creates a minimal config/.gitignore if needed. Combined with saveConfigWithSecrets() generating master.key and secrets.yml.enc, this gives a reasonable out-of-the-box security posture without overwriting existing ignore rules.

Also applies to: 372-374, 1264-1343, 1348-1404


1041-1090: configureDatabaseUrl keeps installer UX simple and defers strict validation to ConnectionFactory

The URL-based database configuration prompt clearly communicates expected formats, stores only the URL under database.url, and leaves detailed validation/parsing to ConnectionFactory::parseUrl(). That separation of concerns keeps the installer logic straightforward while leveraging the central factory for scheme-specific handling.

src/Cms/Database/ConnectionFactory.php (2)

49-61: URL merge behavior and DSN construction align with the new config model

createFromConfig()’s pattern of parsing url first, then overlaying non-null explicit config values (excluding url itself) gives a clear precedence model while reusing existing DSN construction for sqlite/mysql/pgsql. The requirement that name be non-empty across adapters, and the reuse of the same initialization path after URL merging, keeps behavior predictable for both legacy and URL-based configurations.

Also applies to: 62-73, 74-91, 93-107


121-155: parseUrl correctly handles sqlite, scheme mapping, query params, and encoded credentials

The private parseUrl() helper:

  • Handles sqlite URLs (including :memory: and triple-slash absolute paths) without relying on parse_url.
  • Maps mysql and postgresql/postgres/pgsql schemes to the existing adapter names.
  • Extracts host/port/user/pass/name and decodes user/pass via rawurldecode, which is important for special characters in credentials.
  • Recognizes common query parameters like charset for mysql.

This matches the installer’s documented URL formats and the new unit tests’ expectations.

Also applies to: 157-220

tests/Unit/Cms/Database/ConnectionFactoryTest.php (1)

20-88: Good coverage of URL parsing, error paths, and sqlite specifics

The tests exercise mysql/pgsql/sqlite URL parsing via reflection, malformed/unsupported schemes, query parameters (charset), encoded credentials, and sqlite-specific formats (including in-memory and triple-slash absolute paths). The backward-compatibility check for non-URL sqlite configs ensures existing configurations remain supported.

Also applies to: 112-163, 168-213, 218-229

Comment thread src/Cms/Cli/Commands/Install/InstallCommand.php
Comment thread src/Cms/Cli/Commands/Install/InstallCommand.php
Comment thread src/Cms/Cli/Commands/Install/InstallCommand.php Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (7)
tests/Unit/Cms/Database/ConnectionFactoryUrlSecretsTest.php (2)

67-86: Clarify the test intent or add exception expectations.

This test demonstrates a broken configuration scenario but doesn't validate that it actually fails. The comment on line 84-85 mentions it "would cause" an error, but the test doesn't assert this. Consider either:

  • Adding $this->expectException() and attempting to call ConnectionFactory::createFromConfig($config) to verify the error is thrown, or
  • Renaming the test to indicate it's documenting the broken state without testing runtime behavior (e.g., testBrokenConfigurationStructure)
♻️ Option to add exception validation
 	public function testBrokenConfigurationWithPlaceholderAdapter(): void
 	{
 		$source = new Memory();
 		// This simulates the BROKEN configuration that would cause the error
 		$source->set( 'database', 'url', 'mysql://user:pass@localhost:3306/testdb' );
 		$source->set( 'database', 'adapter', 'configured' ); // This would break it!
 
 		$settings = new SettingManager( $source );
 		$config = $settings->getSection( 'database' );
 
 		// Both keys would be present
 		$this->assertArrayHasKey( 'url', $config );
 		$this->assertArrayHasKey( 'adapter', $config );
 
 		// The adapter would be the broken placeholder
 		$this->assertEquals( 'configured', $config['adapter'] );
 
-		// This configuration would cause "Unsupported database adapter: configured" error
-		// because the placeholder would override the URL-parsed adapter
+		// Verify this configuration causes the expected error
+		$this->expectException( \Exception::class );
+		$this->expectExceptionMessage( 'Unsupported database adapter: configured' );
+		ConnectionFactory::createFromConfig( $config );
 	}

91-116: Consider testing the public API instead of replicating internal logic.

This test duplicates the internal merging logic from ConnectionFactory::createFromConfig (lines 104-109), creating tight coupling between test and implementation. If the merging algorithm changes but produces the same results, this test would need updates.

Consider refactoring to test the public createFromConfig method directly with various input combinations and verify the resulting PDO connection properties, rather than reimplementing the merge logic in the test.

♻️ Alternative approach testing the public API
 	public function testCreateFromConfigMergingLogic(): void
 	{
-		// Test the exact merging that happens in ConnectionFactory::createFromConfig
-		$config = [ 'url' => 'postgresql://user:pass@host:5432/dbname' ];
-
-		$reflection = new \ReflectionClass( ConnectionFactory::class );
-		$parseMethod = $reflection->getMethod( 'parseUrl' );
-		$parseMethod->setAccessible( true );
-
-		// Parse URL
-		$urlConfig = $parseMethod->invokeArgs( null, [ $config['url'] ] );
-
-		// Simulate the merge that happens in createFromConfig
-		$mergedConfig = array_merge(
-			$urlConfig,
-			array_filter( $config, function( $value, $key ) {
-				return $key !== 'url' && $value !== null;
-			}, ARRAY_FILTER_USE_BOTH )
-		);
-
-		// The adapter from URL should be preserved (no override)
-		$this->assertEquals( 'pgsql', $mergedConfig['adapter'] );
-		$this->assertEquals( 'host', $mergedConfig['host'] );
-		$this->assertEquals( 5432, $mergedConfig['port'] );
-		$this->assertEquals( 'dbname', $mergedConfig['name'] );
+		// Test that URL-based config creates a working PDO with correct driver
+		$config = [ 'url' => 'sqlite::memory:' ];
+		
+		$pdo = ConnectionFactory::createFromConfig( $config );
+		
+		$this->assertInstanceOf( \PDO::class, $pdo );
+		$this->assertEquals( 'sqlite', $pdo->getAttribute( \PDO::ATTR_DRIVER_NAME ) );
 	}

Note: This is just one approach. The existing URL parsing validation is already covered by ConnectionFactoryUrlSecretsTest::testUrlFromSecretsWithoutAdapterConflict.

tests/Unit/Cms/Database/NoConfiguredAdapterTest.php (1)

18-44: Simplify the test by removing the try-catch block.

The try-catch pattern (lines 32-43) is unnecessary and makes the test harder to understand. Since the goal is to verify that the configuration succeeds without throwing the "configured" adapter error, simply call the method and assert success. If an unexpected exception is thrown, PHPUnit will automatically fail the test.

♻️ Simplified test structure
 	public function testNoConfiguredAdapterError(): void
 	{
 		// Simulate the EXACT scenario: URL in secrets, nothing in public
 		$publicSource = new Memory();
 		// Public has NO database section at all
 
 		$secretsSource = new Memory();
 		$secretsSource->set( 'database', 'url', 'sqlite::memory:' );
 
 		// Merge like the app does
 		$settings = new SettingManager( $publicSource );
 		$settings->addSource( $secretsSource );
 
-		// This should NOT throw "Unsupported database adapter: configured"
-		try {
-			$pdo = ConnectionFactory::createFromSettings( $settings );
-			$this->assertNotNull( $pdo );
-			$this->assertEquals( 'sqlite', $pdo->getAttribute( \PDO::ATTR_DRIVER_NAME ) );
-		} catch ( \Exception $e ) {
-			// If we get here, check it's NOT the "configured" error
-			$this->assertStringNotContainsString(
-				'Unsupported database adapter: configured',
-				$e->getMessage(),
-				'Should not get "configured" adapter error'
-			);
-		}
+		// This should succeed without throwing any exception
+		$pdo = ConnectionFactory::createFromSettings( $settings );
+		$this->assertNotNull( $pdo );
+		$this->assertEquals( 'sqlite', $pdo->getAttribute( \PDO::ATTR_DRIVER_NAME ) );
 	}
tests/Unit/Cms/Cli/Commands/Install/DatabaseUrlValidationTest.php (1)

15-31: Consider whether testing parse_url behavior adds value.

This test method primarily validates PHP's standard parse_url function behavior rather than application-specific logic. Since parse_url is a well-tested standard library function, these assertions may not provide significant value. Consider focusing tests on how your application uses and validates URLs through ConnectionFactory::parseUrl instead.

tests/Unit/Cms/Database/ConnectionFactoryTest.php (1)

111-165: Remove unused variable and unreachable catch block.

The static analysis correctly identifies that $createMethod (line 114) is assigned but never used. Additionally, the catch (\PDOException $e) block (lines 162-165) is unreachable since no PDO connection is attempted within the try block.

♻️ Proposed cleanup
 		// Now test that createFromConfig properly applies the overrides
 		// We'll use a mock PDO to test the DSN construction without actual connection
-		try {
-			// Call createFromConfig through reflection to catch the DSN
-			$reflection = new \ReflectionClass( ConnectionFactory::class );
-			$createMethod = $reflection->getMethod( 'createFromConfig' );
-
-			// Instead of creating actual PDO, let's verify the merge logic
-			// by inspecting what createFromConfig would do
-			$parseMethod = $reflection->getMethod( 'parseUrl' );
-			$parseMethod->setAccessible( true );
+		// Instead of creating actual PDO, let's verify the merge logic
+		// by inspecting what createFromConfig would do
+		$reflection = new \ReflectionClass( ConnectionFactory::class );
+		$parseMethod = $reflection->getMethod( 'parseUrl' );
+		$parseMethod->setAccessible( true );
 
-			// Parse the URL to get base config
-			$urlConfig = $parseMethod->invokeArgs( null, [ $config['url'] ] );
+		// Parse the URL to get base config
+		$urlConfig = $parseMethod->invokeArgs( null, [ $config['url'] ] );
 
-			// Apply the same merge logic that createFromConfig uses
-			$finalConfig = array_merge(
-				$urlConfig,
-				array_filter( $config, function( $value, $key ) {
-					return $key !== 'url' && $value !== null;
-				}, ARRAY_FILTER_USE_BOTH )
-			);
+		// Apply the same merge logic that createFromConfig uses
+		$finalConfig = array_merge(
+			$urlConfig,
+			array_filter( $config, function( $value, $key ) {
+				return $key !== 'url' && $value !== null;
+			}, ARRAY_FILTER_USE_BOTH )
+		);
 
-			// Verify that overrides were applied
-			$this->assertEquals( 'overridehost', $finalConfig['host'], 'Host should be overridden' );
-			$this->assertEquals( 9999, $finalConfig['port'], 'Port should be overridden' );
+		// Verify that overrides were applied
+		$this->assertEquals( 'overridehost', $finalConfig['host'], 'Host should be overridden' );
+		$this->assertEquals( 9999, $finalConfig['port'], 'Port should be overridden' );
 
-			// Verify other values from URL are preserved
-			$this->assertEquals( 'user', $finalConfig['user'], 'User from URL should be preserved' );
-			$this->assertEquals( 'pass', $finalConfig['pass'], 'Password from URL should be preserved' );
-			$this->assertEquals( 'urldb', $finalConfig['name'], 'Database name from URL should be preserved' );
-			$this->assertEquals( 'mysql', $finalConfig['adapter'], 'Adapter from URL should be preserved' );
+		// Verify other values from URL are preserved
+		$this->assertEquals( 'user', $finalConfig['user'], 'User from URL should be preserved' );
+		$this->assertEquals( 'pass', $finalConfig['pass'], 'Password from URL should be preserved' );
+		$this->assertEquals( 'urldb', $finalConfig['name'], 'Database name from URL should be preserved' );
+		$this->assertEquals( 'mysql', $finalConfig['adapter'], 'Adapter from URL should be preserved' );
 
-			// Verify the DSN that would be constructed uses the overrides
-			$expectedDsn = sprintf(
-				"mysql:host=%s;port=%s;dbname=%s;charset=%s",
-				'overridehost',  // Override host
-				9999,           // Override port
-				'urldb',        // From URL
-				'utf8mb4'       // Default charset
-			);
+		// Verify the DSN that would be constructed uses the overrides
+		$expectedDsn = sprintf(
+			"mysql:host=%s;port=%s;dbname=%s;charset=%s",
+			'overridehost',  // Override host
+			9999,           // Override port
+			'urldb',        // From URL
+			'utf8mb4'       // Default charset
+		);
 
-			// Build actual DSN using same logic as ConnectionFactory
-			$actualDsn = sprintf(
-				"mysql:host=%s;port=%s;dbname=%s;charset=%s",
-				$finalConfig['host'] ?? 'localhost',
-				$finalConfig['port'] ?? 3306,
-				$finalConfig['name'],
-				$finalConfig['charset'] ?? 'utf8mb4'
-			);
+		// Build actual DSN using same logic as ConnectionFactory
+		$actualDsn = sprintf(
+			"mysql:host=%s;port=%s;dbname=%s;charset=%s",
+			$finalConfig['host'] ?? 'localhost',
+			$finalConfig['port'] ?? 3306,
+			$finalConfig['name'],
+			$finalConfig['charset'] ?? 'utf8mb4'
+		);
 
-			$this->assertEquals( $expectedDsn, $actualDsn, 'DSN should use overridden host and port' );
-
-		} catch ( \PDOException $e ) {
-			// If PDO fails (no actual MySQL), that's expected - we're testing config merge logic
-			$this->markTestSkipped( 'Cannot test actual PDO connection without MySQL server' );
-		}
+		$this->assertEquals( $expectedDsn, $actualDsn, 'DSN should use overridden host and port' );
 	}
src/Cms/Cli/Commands/Install/InstallCommand.php (2)

1304-1307: Consider security implications of displaying master key.

The encryption key is output to the console (line 1306). While helpful for initial setup, this could pose risks if terminal output is logged or screen is shared. Since the key is already saved to config/master.key, consider making the console output optional or requiring an explicit flag.

♻️ Suggested approach
 			$this->output->success( "Generated master key: {$keyPath}" );
 			$this->output->warning( "IMPORTANT: Add this file to .gitignore - NEVER commit it!" );
-			$this->output->info( "Key value (save this securely): {$key}" );
+			$this->output->info( "Key saved to: {$keyPath}" );
+			$this->output->info( "To view the key: cat {$keyPath}" );
 			$this->output->writeln( "" );

1296-1322: Minor: SecretManager instance reuse opportunity.

Two separate SecretManager instances are created (lines 1301 and 1317). If the class is stateless, the same instance could be reused. This is a minor optimization.

♻️ Suggested optimization
+		$secretManager = new SecretManager();
+
 		// Generate master key if it doesn't exist
 		if( !file_exists( $keyPath ) )
 		{
 			$this->output->writeln( "\nGenerating encryption key..." );
 
-			$secretManager = new SecretManager();
 			$key = $secretManager->generateKey( $keyPath );
 			// ... rest of key generation
 		}
 
 		// Save encrypted secrets
 		if( !empty( $secretsConfig ) )
 		{
 			$this->output->writeln( "Encrypting sensitive configuration..." );
 
-			$secretManager = new SecretManager();
 			$yamlContent = YamlParser::dump( $secretsConfig, 4, 2 );
 			// ... rest of encryption
 		}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9ea9c6 and 5bbbdd6.

📒 Files selected for processing (7)
  • src/Bootstrap.php
  • src/Cms/Cli/Commands/Install/InstallCommand.php
  • tests/Unit/Cms/Cli/Commands/Install/DatabaseUrlValidationTest.php
  • tests/Unit/Cms/Database/ConnectionFactoryIntegrationTest.php
  • tests/Unit/Cms/Database/ConnectionFactoryTest.php
  • tests/Unit/Cms/Database/ConnectionFactoryUrlSecretsTest.php
  • tests/Unit/Cms/Database/NoConfiguredAdapterTest.php
🧰 Additional context used
🧬 Code graph analysis (4)
tests/Unit/Cms/Database/NoConfiguredAdapterTest.php (1)
src/Cms/Database/ConnectionFactory.php (2)
  • ConnectionFactory (17-284)
  • createFromSettings (26-36)
tests/Unit/Cms/Database/ConnectionFactoryIntegrationTest.php (1)
src/Cms/Database/ConnectionFactory.php (2)
  • ConnectionFactory (17-284)
  • createFromSettings (26-36)
tests/Unit/Cms/Database/ConnectionFactoryUrlSecretsTest.php (1)
src/Cms/Database/ConnectionFactory.php (1)
  • ConnectionFactory (17-284)
tests/Unit/Cms/Database/ConnectionFactoryTest.php (1)
src/Cms/Database/ConnectionFactory.php (2)
  • ConnectionFactory (17-284)
  • createFromSettings (26-36)
🪛 PHPMD (2.15.0)
tests/Unit/Cms/Database/ConnectionFactoryTest.php

114-114: Avoid unused local variables such as '$createMethod'. (undefined)

(UnusedLocalVariable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: build-test (sqlite)
  • GitHub Check: build-test (mysql)
  • GitHub Check: build-test (postgres)
🔇 Additional comments (12)
tests/Unit/Cms/Database/ConnectionFactoryUrlSecretsTest.php (1)

21-61: LGTM! Well-structured test for URL-from-secrets scenario.

The test correctly simulates the runtime behavior with separate public and secrets sources, and appropriately uses reflection to verify the internal URL parsing logic. The assertions comprehensively validate all expected URL components.

tests/Unit/Cms/Database/ConnectionFactoryIntegrationTest.php (1)

19-88: LGTM! Comprehensive integration test coverage.

The test suite effectively covers the main configuration scenarios:

  • URL from secrets (with driver verification)
  • Mixed public and secrets configuration
  • Backward compatibility with traditional non-URL config
  • URL-only configuration

All tests use SQLite in-memory databases, which is appropriate for integration testing without external dependencies.

tests/Unit/Cms/Database/NoConfiguredAdapterTest.php (1)

49-79: LGTM! Clear test cases for broken and fixed scenarios.

Lines 49-62 properly use expectException to validate the broken scenario, and lines 67-79 provide a clean positive test case for the URL-only configuration.

tests/Unit/Cms/Cli/Commands/Install/DatabaseUrlValidationTest.php (2)

107-160: LGTM! Good coverage of URL requirement validation.

This test method appropriately validates application-specific requirements for different database URL types, ensuring that MySQL/PostgreSQL URLs include necessary components (host and database) while handling SQLite's special URL format.


36-102: The assertion at line 58 is correct. parse_url('sqlite:///path/to/database.db') returns false in PHP 8.2.29. There is no evidence of version-dependent behavior across supported PHP versions, and the test accurately reflects the actual behavior of PHP's parse_url function. No changes needed.

Likely an incorrect or invalid review comment.

src/Bootstrap.php (3)

47-111: LGTM! Undefined variable issue resolved with proper fallback.

The initialization of $settings to null (line 48) combined with the fallback creation in the catch block (lines 109-110) ensures $settings is never undefined when accessed at line 115. This addresses the critical issue raised in past review comments while maintaining application resilience.


78-98: LGTM! Encrypted sources now include defensive file existence checks.

The encrypted source configurations (lines 78-90) now properly check for file existence before inclusion, matching the defensive pattern recommended in past review comments. The use of ternary operators with null followed by array_filter (line 98) cleanly handles optional encrypted sources.


69-74: Environment configuration uses base YAML for development and production.

The environment-specific YAML selection only provides an override for testing (lines 70-72), while development and production environments use the base neuron.yaml. This appears intentional given the PR's approach of using environment-specific encrypted secrets for environment differentiation rather than separate config files.

tests/Unit/Cms/Database/ConnectionFactoryTest.php (1)

1-36: Good test structure for URL parsing.

The test file provides comprehensive coverage for the new URL-based database configuration feature. The use of reflection to test the private parseUrl method is appropriate for unit testing internal logic.

src/Cms/Cli/Commands/Install/InstallCommand.php (3)

923-933: URL+secrets handling now correctly avoids adapter conflict.

The previous implementation set adapter => 'configured' in public config which conflicted with URL-parsed adapters. This has been fixed - when using URL configuration with secrets, only the URL is stored in secrets with no placeholder in public config.


1062-1103: URL validation improvements address previous concerns.

The URL validation now properly handles:

  • SQLite URLs with special sqlite: prefix handling
  • Malformed URLs where parse_url returns null/false for scheme
  • Unsupported database schemes with clear error messages
  • Missing required components (host, database name) for MySQL/PostgreSQL

991-1018: Email secrets separation correctly implemented.

The driver key mismatch from previous review has been addressed. Non-sensitive SMTP settings (host, port, encryption) are kept in public config while credentials (username, password) are properly moved to secrets.

@ljonesfl ljonesfl closed this Jan 7, 2026
@ljonesfl
ljonesfl deleted the feature/fixes branch January 7, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant