diff --git a/.gitignore b/.gitignore
index f823126..9ee74e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,3 +36,7 @@ coverage*
.env.*.local
var/
+
+# Runtime directories (logs, cache, temporary files)
+storage/
+resources/storage/
diff --git a/config/neuron.yaml b/config/neuron.yaml
index e8a072c..976d62d 100644
--- a/config/neuron.yaml
+++ b/config/neuron.yaml
@@ -3,7 +3,7 @@
system:
timezone: UTC
- base_path: resources
+ base_path: .
routes_path: resources/config
logging:
diff --git a/resources/views/http_codes/404.php b/resources/views/http_codes/404.php
index 3ba231e..0d98690 100644
--- a/resources/views/http_codes/404.php
+++ b/resources/views/http_codes/404.php
@@ -1,6 +1,11 @@
-
The page you are looking for does not exist.
+ 404
+
+
+
+
+
The page you are looking for does not exist.
diff --git a/resources/views/layouts/default.php b/resources/views/layouts/default.php
index 72c37fa..7763371 100644
--- a/resources/views/layouts/default.php
+++ b/resources/views/layouts/default.php
@@ -51,7 +51,7 @@
- Powered by
NeuronPHP.
+ Powered by
NeuronCMS.
diff --git a/resources/views/layouts/member.php b/resources/views/layouts/member.php
index d7ee1c8..7c4d6e1 100644
--- a/resources/views/layouts/member.php
+++ b/resources/views/layouts/member.php
@@ -63,7 +63,7 @@
- Powered by
NeuronPHP.
+ Powered by
NeuronCMS.
diff --git a/src/Cms/Cli/Commands/User/ResetPasswordCommand.php b/src/Cms/Cli/Commands/User/ResetPasswordCommand.php
new file mode 100644
index 0000000..9d06a95
--- /dev/null
+++ b/src/Cms/Cli/Commands/User/ResetPasswordCommand.php
@@ -0,0 +1,252 @@
+addOption( 'username', 'u', true, 'Username of the user' );
+ $this->addOption( 'email', 'e', true, 'Email of the user' );
+ }
+
+ /**
+ * Execute the command
+ */
+ public function execute( array $parameters = [] ): int
+ {
+ $this->output->writeln( "\n╔═══════════════════════════════════════╗" );
+ $this->output->writeln( "║ Neuron CMS - Reset User Password ║" );
+ $this->output->writeln( "╚═══════════════════════════════════════╝\n" );
+
+ // Load database configuration
+ $repository = $this->getUserRepository();
+ if( !$repository )
+ {
+ return 1;
+ }
+
+ $hasher = new PasswordHasher();
+
+ // Load password policy from configuration
+ try
+ {
+ $settings = Registry::getInstance()->get( RegistryKeys::SETTINGS );
+
+ if( $settings )
+ {
+ // Read password policy from auth.passwords section
+ $minLength = $settings->get( 'auth', 'passwords', 'min_length' );
+ $requireUppercase = $settings->get( 'auth', 'passwords', 'require_uppercase' );
+ $requireLowercase = $settings->get( 'auth', 'passwords', 'require_lowercase' );
+ $requireNumbers = $settings->get( 'auth', 'passwords', 'require_numbers' );
+ $requireSpecialChars = $settings->get( 'auth', 'passwords', 'require_special_chars' );
+
+ // Configure hasher with policy
+ if( $minLength !== null )
+ {
+ $hasher->setMinLength( (int)$minLength );
+ }
+
+ if( $requireUppercase !== null )
+ {
+ $hasher->setRequireUppercase( (bool)$requireUppercase );
+ }
+
+ if( $requireLowercase !== null )
+ {
+ $hasher->setRequireLowercase( (bool)$requireLowercase );
+ }
+
+ if( $requireNumbers !== null )
+ {
+ $hasher->setRequireNumbers( (bool)$requireNumbers );
+ }
+
+ if( $requireSpecialChars !== null )
+ {
+ $hasher->setRequireSpecialChars( (bool)$requireSpecialChars );
+ }
+ }
+ }
+ catch( \Exception $e )
+ {
+ // Fall back to defaults on any exception
+ }
+
+ // Get username or email from options or prompt
+ $username = $this->input->getOption( 'username' );
+ $email = $this->input->getOption( 'email' );
+
+ // If neither provided, prompt for identifier
+ if( !$username && !$email )
+ {
+ $identifier = $this->prompt( "Enter username or email: " );
+ $identifier = trim( $identifier );
+
+ if( empty( $identifier ) )
+ {
+ $this->output->error( "Username or email is required!" );
+ return 1;
+ }
+
+ // Determine if it's an email or username
+ if( filter_var( $identifier, FILTER_VALIDATE_EMAIL ) )
+ {
+ $email = $identifier;
+ }
+ else
+ {
+ $username = $identifier;
+ }
+ }
+
+ // Find the user
+ $user = null;
+ if( $username )
+ {
+ $user = $repository->findByUsername( $username );
+ if( !$user )
+ {
+ $this->output->error( "User '$username' not found!" );
+ return 1;
+ }
+ }
+ elseif( $email )
+ {
+ $user = $repository->findByEmail( $email );
+ if( !$user )
+ {
+ $this->output->error( "User with email '$email' not found!" );
+ return 1;
+ }
+ }
+
+ // Display user info
+ $this->output->writeln( "User found:" );
+ $this->output->writeln( " ID: " . $user->getId() );
+ $this->output->writeln( " Username: " . $user->getUsername() );
+ $this->output->writeln( " Email: " . $user->getEmail() );
+ $this->output->writeln( " Role: " . $user->getRole() );
+ $this->output->writeln( "" );
+
+ // Confirm action
+ $confirm = $this->prompt( "Reset password for this user? (yes/no) [no]: " );
+ if( strtolower( trim( $confirm ) ) !== 'yes' )
+ {
+ $this->output->warning( "Password reset cancelled." );
+ return 0;
+ }
+
+ // Get new password
+ $password = $this->secret( "\nEnter new password: " );
+
+ // Validate password against configured policy
+ if( !$hasher->meetsRequirements( $password ) )
+ {
+ $this->output->error( "Password does not meet requirements:" );
+
+ foreach( $hasher->getValidationErrors( $password ) as $error )
+ {
+ $this->output->writeln( " - $error" );
+ }
+
+ $this->output->writeln( "" );
+ return 1;
+ }
+
+ // Confirm password
+ $confirmPassword = $this->secret( "Confirm new password: " );
+
+ if( $password !== $confirmPassword )
+ {
+ $this->output->error( "Passwords do not match!" );
+ return 1;
+ }
+
+ // Update password
+ try
+ {
+ $user->setPasswordHash( $hasher->hash( $password ) );
+ $user->setUpdatedAt( new \DateTimeImmutable() );
+
+ // Clear any lockout
+ $user->setFailedLoginAttempts( 0 );
+ $user->setLockedUntil( null );
+
+ $success = $repository->update( $user );
+
+ if( !$success )
+ {
+ $this->output->error( "Failed to update password in database" );
+ return 1;
+ }
+
+ $this->output->success( "Password reset successfully for user: " . $user->getUsername() );
+ $this->output->writeln( "" );
+
+ return 0;
+ }
+ catch( \Exception $e )
+ {
+ $this->output->error( "Error resetting password: " . $e->getMessage() );
+ return 1;
+ }
+ }
+
+ /**
+ * Get user repository
+ *
+ * Protected to allow mocking in tests
+ */
+ protected function getUserRepository(): ?DatabaseUserRepository
+ {
+ try
+ {
+ $settings = Registry::getInstance()->get( RegistryKeys::SETTINGS );
+
+ if( !$settings )
+ {
+ $this->output->error( "Application not initialized: Settings not found in Registry" );
+ $this->output->writeln( "This is a configuration error - the application should load settings into the Registry" );
+ return null;
+ }
+
+ return new DatabaseUserRepository( $settings );
+ }
+ catch( \Exception $e )
+ {
+ $this->output->error( "Database connection failed: " . $e->getMessage() );
+ return null;
+ }
+ }
+}
diff --git a/src/Cms/Cli/Provider.php b/src/Cms/Cli/Provider.php
index 1ab31c5..eb42d02 100644
--- a/src/Cms/Cli/Provider.php
+++ b/src/Cms/Cli/Provider.php
@@ -45,6 +45,11 @@ public static function register( Registry $registry ): void
'Neuron\\Cms\\Cli\\Commands\\User\\DeleteCommand'
);
+ $registry->register(
+ 'cms:user:reset-password',
+ 'Neuron\\Cms\\Cli\\Commands\\User\\ResetPasswordCommand'
+ );
+
// Maintenance mode commands
$registry->register(
'cms:maintenance:enable',
diff --git a/tests/Unit/Cms/Cli/Commands/User/ResetPasswordCommandTest.php b/tests/Unit/Cms/Cli/Commands/User/ResetPasswordCommandTest.php
new file mode 100644
index 0000000..159da08
--- /dev/null
+++ b/tests/Unit/Cms/Cli/Commands/User/ResetPasswordCommandTest.php
@@ -0,0 +1,567 @@
+root = vfsStream::setup('test');
+
+ // Create config directory
+ vfsStream::newDirectory('config')->at($this->root);
+
+ // Create test config
+ $configContent = <<at($this->root)
+ ->setContent($configContent);
+
+ // Set up test dependencies
+ $this->inputReader = new TestInputReader();
+ $this->output = new Output(false); // No colors in tests
+ $this->input = new Input([]);
+ }
+
+ protected function tearDown(): void
+ {
+ // Clean up Registry
+ Registry::getInstance()->reset();
+ }
+
+ public function testConfigureSetupCommandMetadata(): void
+ {
+ $command = new ResetPasswordCommand();
+
+ // Use reflection to access protected method
+ $reflection = new \ReflectionClass($command);
+ $configureMethod = $reflection->getMethod('configure');
+ $configureMethod->setAccessible(true);
+ $configureMethod->invoke($command);
+
+ // Verify command metadata is set
+ $this->assertEquals('cms:user:reset-password', $command->getName());
+ $this->assertNotEmpty($command->getDescription());
+ }
+
+ public function testGetUserRepositoryWithMissingConfig(): void
+ {
+ // Clear Registry to simulate missing Settings
+ \Neuron\Patterns\Registry::getInstance()->reset();
+
+ // Create command instance
+ $command = new ResetPasswordCommand();
+
+ // Mock the output to capture error messages
+ $output = $this->createMock(\Neuron\Cli\Console\Output::class);
+ $output->expects($this->once())
+ ->method('error')
+ ->with('Application not initialized: Settings not found in Registry');
+ $output->expects($this->once())
+ ->method('writeln')
+ ->with('This is a configuration error - the application should load settings into the Registry');
+
+ $command->setOutput($output);
+
+ // Use reflection to test private getUserRepository method
+ $reflection = new \ReflectionClass($command);
+ $method = $reflection->getMethod('getUserRepository');
+ $method->setAccessible(true);
+
+ // Call the method and verify it returns null
+ $result = $method->invoke($command);
+ $this->assertNull($result);
+ }
+
+ public function testExecuteResetsPasswordSuccessfully(): void
+ {
+ // Set up test input responses
+ $this->inputReader->addResponses([
+ 'testuser', // username/email
+ 'yes', // confirm reset
+ 'NewSecurePass123!', // new password
+ 'NewSecurePass123!' // confirm password
+ ]);
+
+ // Create mock user
+ $user = new User();
+ $user->setId(1);
+ $user->setUsername('testuser');
+ $user->setEmail('test@example.com');
+ $user->setRole('admin');
+ $user->setPasswordHash('old_hash');
+
+ // Mock repository
+ $repository = $this->createMock(DatabaseUserRepository::class);
+ $repository->expects($this->once())
+ ->method('findByUsername')
+ ->with('testuser')
+ ->willReturn($user);
+
+ $repository->expects($this->once())
+ ->method('update')
+ ->willReturnCallback(function($updatedUser) {
+ $this->assertNotEquals('old_hash', $updatedUser->getPasswordHash());
+ return true;
+ });
+
+ // Set up Registry with settings
+ $settings = $this->createMock(SettingManager::class);
+ Registry::getInstance()->set('Settings', $settings);
+
+ // Create command with mocked repository
+ $command = $this->getMockBuilder(ResetPasswordCommand::class)
+ ->onlyMethods(['getUserRepository'])
+ ->getMock();
+
+ $command->expects($this->once())
+ ->method('getUserRepository')
+ ->willReturn($repository);
+
+ $command->setInput($this->input);
+ $command->setOutput($this->output);
+ $command->setInputReader($this->inputReader);
+
+ // Execute command
+ $exitCode = $command->execute();
+
+ // Verify success
+ $this->assertEquals(0, $exitCode);
+
+ // Verify all prompts were shown
+ $prompts = $this->inputReader->getPromptHistory();
+ $this->assertCount(4, $prompts);
+ $this->assertStringContainsString('username or email', $prompts[0]);
+ $this->assertStringContainsString('Reset password', $prompts[1]);
+ $this->assertStringContainsString('new password', $prompts[2]);
+ $this->assertStringContainsString('Confirm', $prompts[3]);
+ }
+
+ public function testExecuteResetsPasswordWithEmailSuccessfully(): void
+ {
+ // Set up test input responses
+ $this->inputReader->addResponses([
+ 'test@example.com', // email
+ 'yes', // confirm reset
+ 'NewSecurePass123!', // new password
+ 'NewSecurePass123!' // confirm password
+ ]);
+
+ // Create mock user
+ $user = new User();
+ $user->setId(1);
+ $user->setUsername('testuser');
+ $user->setEmail('test@example.com');
+ $user->setRole('admin');
+ $user->setPasswordHash('old_hash');
+
+ // Mock repository
+ $repository = $this->createMock(DatabaseUserRepository::class);
+ $repository->expects($this->once())
+ ->method('findByEmail')
+ ->with('test@example.com')
+ ->willReturn($user);
+
+ $repository->expects($this->once())
+ ->method('update')
+ ->willReturn(true);
+
+ // Set up Registry with settings
+ $settings = $this->createMock(SettingManager::class);
+ Registry::getInstance()->set('Settings', $settings);
+
+ // Create command with mocked repository
+ $command = $this->getMockBuilder(ResetPasswordCommand::class)
+ ->onlyMethods(['getUserRepository'])
+ ->getMock();
+
+ $command->expects($this->once())
+ ->method('getUserRepository')
+ ->willReturn($repository);
+
+ $command->setInput($this->input);
+ $command->setOutput($this->output);
+ $command->setInputReader($this->inputReader);
+
+ // Execute command
+ $exitCode = $command->execute();
+
+ // Verify success
+ $this->assertEquals(0, $exitCode);
+ }
+
+ public function testExecuteFailsWhenUserNotFound(): void
+ {
+ $this->inputReader->addResponse('nonexistent');
+
+ $repository = $this->createMock(DatabaseUserRepository::class);
+ $repository->expects($this->once())
+ ->method('findByUsername')
+ ->with('nonexistent')
+ ->willReturn(null);
+
+ $settings = $this->createMock(SettingManager::class);
+ Registry::getInstance()->set('Settings', $settings);
+
+ $command = $this->getMockBuilder(ResetPasswordCommand::class)
+ ->onlyMethods(['getUserRepository'])
+ ->getMock();
+ $command->expects($this->once())
+ ->method('getUserRepository')
+ ->willReturn($repository);
+
+ $command->setInput($this->input);
+ $command->setOutput($this->output);
+ $command->setInputReader($this->inputReader);
+
+ $exitCode = $command->execute();
+
+ $this->assertEquals(1, $exitCode);
+ }
+
+ public function testExecuteCancelsWhenUserDeclines(): void
+ {
+ $this->inputReader->addResponses([
+ 'testuser',
+ 'no' // Decline reset
+ ]);
+
+ $user = new User();
+ $user->setId(1);
+ $user->setUsername('testuser');
+ $user->setEmail('test@example.com');
+ $user->setRole('admin');
+
+ $repository = $this->createMock(DatabaseUserRepository::class);
+ $repository->expects($this->once())
+ ->method('findByUsername')
+ ->willReturn($user);
+
+ // Ensure update is never called
+ $repository->expects($this->never())
+ ->method('update');
+
+ $settings = $this->createMock(SettingManager::class);
+ Registry::getInstance()->set('Settings', $settings);
+
+ $command = $this->getMockBuilder(ResetPasswordCommand::class)
+ ->onlyMethods(['getUserRepository'])
+ ->getMock();
+ $command->expects($this->once())
+ ->method('getUserRepository')
+ ->willReturn($repository);
+
+ $command->setInput($this->input);
+ $command->setOutput($this->output);
+ $command->setInputReader($this->inputReader);
+
+ $exitCode = $command->execute();
+
+ $this->assertEquals(0, $exitCode);
+ }
+
+ public function testExecuteFailsWhenPasswordTooShort(): void
+ {
+ $this->inputReader->addResponses([
+ 'testuser',
+ 'yes',
+ 'short' // Password too short
+ ]);
+
+ $user = new User();
+ $user->setId(1);
+ $user->setUsername('testuser');
+ $user->setEmail('test@example.com');
+ $user->setRole('admin');
+
+ $repository = $this->createMock(DatabaseUserRepository::class);
+ $repository->expects($this->once())
+ ->method('findByUsername')
+ ->willReturn($user);
+
+ $settings = $this->createMock(SettingManager::class);
+ Registry::getInstance()->set('Settings', $settings);
+
+ $command = $this->getMockBuilder(ResetPasswordCommand::class)
+ ->onlyMethods(['getUserRepository'])
+ ->getMock();
+ $command->expects($this->once())
+ ->method('getUserRepository')
+ ->willReturn($repository);
+
+ $command->setInput($this->input);
+ $command->setOutput($this->output);
+ $command->setInputReader($this->inputReader);
+
+ $exitCode = $command->execute();
+
+ $this->assertEquals(1, $exitCode);
+ }
+
+ public function testExecuteFailsWhenPasswordsDoNotMatch(): void
+ {
+ $this->inputReader->addResponses([
+ 'testuser',
+ 'yes',
+ 'NewSecurePass123!',
+ 'DifferentPassword123!' // Different password
+ ]);
+
+ $user = new User();
+ $user->setId(1);
+ $user->setUsername('testuser');
+ $user->setEmail('test@example.com');
+ $user->setRole('admin');
+
+ $repository = $this->createMock(DatabaseUserRepository::class);
+ $repository->expects($this->once())
+ ->method('findByUsername')
+ ->willReturn($user);
+
+ $settings = $this->createMock(SettingManager::class);
+ Registry::getInstance()->set('Settings', $settings);
+
+ $command = $this->getMockBuilder(ResetPasswordCommand::class)
+ ->onlyMethods(['getUserRepository'])
+ ->getMock();
+ $command->expects($this->once())
+ ->method('getUserRepository')
+ ->willReturn($repository);
+
+ $command->setInput($this->input);
+ $command->setOutput($this->output);
+ $command->setInputReader($this->inputReader);
+
+ $exitCode = $command->execute();
+
+ $this->assertEquals(1, $exitCode);
+ }
+
+ public function testExecuteWithUsernameOption(): void
+ {
+ // Create input with --username option
+ $input = new Input(['--username=testuser']);
+
+ $this->inputReader->addResponses([
+ 'yes', // confirm reset
+ 'NewSecurePass123!', // new password
+ 'NewSecurePass123!' // confirm password
+ ]);
+
+ $user = new User();
+ $user->setId(1);
+ $user->setUsername('testuser');
+ $user->setEmail('test@example.com');
+ $user->setRole('admin');
+
+ $repository = $this->createMock(DatabaseUserRepository::class);
+ $repository->expects($this->once())
+ ->method('findByUsername')
+ ->with('testuser')
+ ->willReturn($user);
+
+ $repository->expects($this->once())
+ ->method('update')
+ ->willReturn(true);
+
+ $settings = $this->createMock(SettingManager::class);
+ Registry::getInstance()->set('Settings', $settings);
+
+ $command = $this->getMockBuilder(ResetPasswordCommand::class)
+ ->onlyMethods(['getUserRepository'])
+ ->getMock();
+ $command->expects($this->once())
+ ->method('getUserRepository')
+ ->willReturn($repository);
+
+ $command->setInput($input);
+ $command->setOutput($this->output);
+ $command->setInputReader($this->inputReader);
+
+ $exitCode = $command->execute();
+
+ $this->assertEquals(0, $exitCode);
+
+ // Verify username prompt was not shown (only 3 prompts)
+ $prompts = $this->inputReader->getPromptHistory();
+ $this->assertCount(3, $prompts);
+ }
+
+ public function testExecuteWithEmailOption(): void
+ {
+ // Create input with --email option
+ $input = new Input(['--email=test@example.com']);
+
+ $this->inputReader->addResponses([
+ 'yes', // confirm reset
+ 'NewSecurePass123!', // new password
+ 'NewSecurePass123!' // confirm password
+ ]);
+
+ $user = new User();
+ $user->setId(1);
+ $user->setUsername('testuser');
+ $user->setEmail('test@example.com');
+ $user->setRole('admin');
+
+ $repository = $this->createMock(DatabaseUserRepository::class);
+ $repository->expects($this->once())
+ ->method('findByEmail')
+ ->with('test@example.com')
+ ->willReturn($user);
+
+ $repository->expects($this->once())
+ ->method('update')
+ ->willReturn(true);
+
+ $settings = $this->createMock(SettingManager::class);
+ Registry::getInstance()->set('Settings', $settings);
+
+ $command = $this->getMockBuilder(ResetPasswordCommand::class)
+ ->onlyMethods(['getUserRepository'])
+ ->getMock();
+ $command->expects($this->once())
+ ->method('getUserRepository')
+ ->willReturn($repository);
+
+ $command->setInput($input);
+ $command->setOutput($this->output);
+ $command->setInputReader($this->inputReader);
+
+ $exitCode = $command->execute();
+
+ $this->assertEquals(0, $exitCode);
+
+ // Verify email prompt was not shown (only 3 prompts)
+ $prompts = $this->inputReader->getPromptHistory();
+ $this->assertCount(3, $prompts);
+ }
+
+ public function testExecuteFailsWhenDatabaseUpdateFails(): void
+ {
+ $this->inputReader->addResponses([
+ 'testuser',
+ 'yes',
+ 'NewSecurePass123!',
+ 'NewSecurePass123!'
+ ]);
+
+ $user = new User();
+ $user->setId(1);
+ $user->setUsername('testuser');
+ $user->setEmail('test@example.com');
+ $user->setRole('admin');
+
+ $repository = $this->createMock(DatabaseUserRepository::class);
+ $repository->expects($this->once())
+ ->method('findByUsername')
+ ->willReturn($user);
+
+ // update() returns false, simulating database failure
+ $repository->expects($this->once())
+ ->method('update')
+ ->willReturn(false);
+
+ $settings = $this->createMock(SettingManager::class);
+ Registry::getInstance()->set('Settings', $settings);
+
+ $command = $this->getMockBuilder(ResetPasswordCommand::class)
+ ->onlyMethods(['getUserRepository'])
+ ->getMock();
+ $command->expects($this->once())
+ ->method('getUserRepository')
+ ->willReturn($repository);
+
+ $command->setInput($this->input);
+ $command->setOutput($this->output);
+ $command->setInputReader($this->inputReader);
+
+ $exitCode = $command->execute();
+
+ // Should fail with exit code 1
+ $this->assertEquals(1, $exitCode);
+ }
+
+ public function testExecuteLoadsPasswordPolicyFromConfiguration(): void
+ {
+ $this->inputReader->addResponses([
+ 'testuser',
+ 'yes',
+ 'abcdefghij', // 10 chars, all lowercase, no numbers - should fail with default policy
+ 'abcdefghij'
+ ]);
+
+ $user = new User();
+ $user->setId(1);
+ $user->setUsername('testuser');
+ $user->setEmail('test@example.com');
+ $user->setRole('admin');
+
+ $repository = $this->createMock(DatabaseUserRepository::class);
+ $repository->expects($this->once())
+ ->method('findByUsername')
+ ->willReturn($user);
+
+ // Mock settings to return custom password policy
+ $settings = $this->createMock(SettingManager::class);
+ $settings->method('get')
+ ->willReturnCallback(function(...$args) {
+ if ($args === ['auth', 'passwords', 'min_length']) {
+ return 8;
+ }
+ if ($args === ['auth', 'passwords', 'require_uppercase']) {
+ return true; // Require uppercase - should fail
+ }
+ if ($args === ['auth', 'passwords', 'require_lowercase']) {
+ return true;
+ }
+ if ($args === ['auth', 'passwords', 'require_numbers']) {
+ return true; // Require numbers - should fail
+ }
+ if ($args === ['auth', 'passwords', 'require_special_chars']) {
+ return false;
+ }
+ return null;
+ });
+
+ Registry::getInstance()->set('Settings', $settings);
+
+ $command = $this->getMockBuilder(ResetPasswordCommand::class)
+ ->onlyMethods(['getUserRepository'])
+ ->getMock();
+ $command->expects($this->once())
+ ->method('getUserRepository')
+ ->willReturn($repository);
+
+ $command->setInput($this->input);
+ $command->setOutput($this->output);
+ $command->setInputReader($this->inputReader);
+
+ $exitCode = $command->execute();
+
+ // Should fail because password doesn't meet configured requirements
+ $this->assertEquals(1, $exitCode);
+ }
+}
diff --git a/versionlog.md b/versionlog.md
index 5a7c40e..a248f34 100644
--- a/versionlog.md
+++ b/versionlog.md
@@ -1,4 +1,5 @@
## 0.8.42
+* Adds cms:user:reset-passwordLe command to reset a user's password from the CLI.
## 0.8.41 2026-01-14
* Fix for site name in error pages.