diff --git a/src/Console/UserRolesCommand.php b/src/Console/UserRolesCommand.php index 0e3efba..efabbad 100644 --- a/src/Console/UserRolesCommand.php +++ b/src/Console/UserRolesCommand.php @@ -26,8 +26,20 @@ public function handle(): void { $this->info('Updating roles...'); - UserRoles::setRoles(); + $this->createRolesForSites(); $this->info('All done!'); } + + private function createRolesForSites(): void + { + if (true === is_multisite()) { + foreach (get_sites(['fields' => 'ids']) as $siteId) { + switch_to_blog($siteId); + UserRoles::createRoles(); + } + } else { + UserRoles::createRoles(); + } + } } diff --git a/src/Facades/UserRoles.php b/src/Facades/UserRoles.php index 3b1ab9d..e2f6a3d 100644 --- a/src/Facades/UserRoles.php +++ b/src/Facades/UserRoles.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Facade; /** - * @method static void setRoles() + * @method static void createRoles() */ class UserRoles extends Facade { diff --git a/src/UserRoles.php b/src/UserRoles.php index 3bc92e7..bcdd108 100644 --- a/src/UserRoles.php +++ b/src/UserRoles.php @@ -7,63 +7,99 @@ use Role_Command; use Webmozart\Assert\Assert; use WP_CLI; +use WP_CLI\ExitException; class UserRoles { - public function __construct(private Role_Command $roleCommand) + private string $prefix; + + /** + * @param array $config + * + * @throws ExitException + */ + public function __construct(private array $config, private Role_Command $roleCommand, private WP_CLI $wpCli) { + $this->prefix = $this->prefixValidate($config); } - public function setRoles(): void + /** + * (Re)creates roles based on the provided configuration. + */ + public function createRoles(): void { - if (is_multisite()) { - foreach (get_sites(['fields' => 'ids']) as $siteId) { - switch_to_blog($siteId); - $this->setRolesForSite(); - } - } else { - $this->setRolesForSite(); - } + $this->deleteCustomRoles(); + $this->resetCoreRoles(); + $this->addCustomRoles(); + $this->deleteCoreRoles(); } - private function setRolesForSite(): void + /** + * @param array $config + * + * @throws ExitException + */ + private function prefixValidate(array $config): string { - $prefix = config('user-roles.prefix'); - Assert::stringNotEmpty($prefix); + $prefix = $config['prefix'] ?? ''; - $this->removeCustomRoles(); - $this->resetCoreRoles(); - $this->addCustomRoles(); - $this->removeCoreRoles(); + if (! is_string($prefix) || '' === $prefix) { + $this->wpCli::error('No prefix found in configuration file. Aborting role creation.'); + } + + return $prefix . '_'; } - private function removeCustomRoles(): void + private function deleteCustomRoles(): void { - WP_CLI::log(WP_CLI::colorize('%MDelete custom roles:%n')); + $this->wpCli::log($this->wpCli::colorize('%MDelete custom roles:%n')); - $prefix = config('user-roles.prefix'); - $currentCustomRoles = wp_roles()->roles; - $currentCustomRoles = array_filter( - array_keys($currentCustomRoles), - fn (string $role): bool => str_starts_with($role, $prefix) - ); - foreach ($currentCustomRoles as $role) { + $customRoles = $this->getCurrentCustomRoles(); + + if (0 === count($customRoles)) { + $this->wpCli::warning("No custom roles with prefix '" . $this->prefix . "' found in database. Skipping custom role deletion."); + + return; + } + + foreach ($customRoles as $role) { $this->roleCommand->delete([$role]); } } + /** + * @return array + */ + private function getCurrentCustomRoles(): array + { + return array_filter( + array_keys(wp_roles()->roles), + fn (string $role): bool => str_starts_with($role, $this->prefix) + ); + } + private function addCustomRoles(): void { - WP_CLI::log(WP_CLI::colorize('%MCreate custom roles:%n')); + $this->wpCli::log($this->wpCli::colorize('%MCreate custom roles:%n')); + + if (false === $this->rolesValid()) { + $this->wpCli::warning('No roles found in config. Skipping custom role creation.'); + + return; + } - $prefix = config('user-roles.prefix'); - $roles = config('user-roles.roles'); - Assert::isArray($roles); + $roles = $this->config['roles']; foreach ($roles as $role => $properties) { + if (! isset($properties['display_name']) || ! is_string($properties['display_name'])) { + $this->wpCli::warning("No display name configured for role $role. Skipping role creation."); + + continue; + } + $capabilities = []; if (! empty($properties['cap_groups'])) { - $capGroups = config('user-roles.cap_groups'); + $capGroups = $this->config['cap_groups']; Assert::isArray($capGroups); Assert::isArray($properties['cap_groups']); foreach ($properties['cap_groups'] as $group) { @@ -81,7 +117,7 @@ private function addCustomRoles(): void foreach ($properties['post_type_caps'] as $postType) { $postTypeCaps = get_post_type_object($postType)?->cap; if (null === $postTypeCaps) { - WP_CLI::warning("Post type $postType does not exist. Skipping post type caps."); + $this->wpCli::warning("Post type '$postType' does not exist. Skipping post type caps."); continue; } @@ -104,11 +140,11 @@ private function addCustomRoles(): void } $this->roleCommand->create([ - $prefix . '_' . $role, + $this->prefix . $role, $properties['display_name'], ], $clone); - $role = get_role($prefix . '_' . $role); + $role = get_role($this->prefix . $role); Assert::notNull($role); @@ -118,18 +154,31 @@ private function addCustomRoles(): void } } + private function rolesValid(): bool + { + return isset($this->config['roles']) + && is_array($this->config['roles']) + && 0 !== count($this->config['roles']); + } + private function resetCoreRoles(): void { - WP_CLI::log(WP_CLI::colorize('%MReset core roles:%n')); + $this->wpCli::log($this->wpCli::colorize('%MReset core roles:%n')); $this->roleCommand->reset([], ['all' => true]); } - private function removeCoreRoles(): void + private function deleteCoreRoles(): void { - WP_CLI::log(WP_CLI::colorize('%MDelete core roles:%n')); + $this->wpCli::log($this->wpCli::colorize('%MDelete core roles:%n')); - $coreRoles = config('user-roles.core_roles'); + if (! $this->coreRolesValid()) { + $this->wpCli::warning('No core roles found in config. Skipping core role deletion.'); + + return; + } + + $coreRoles = $this->config['core_roles']; foreach ($coreRoles as $role => $shouldStay) { if (false === $shouldStay) { @@ -137,4 +186,11 @@ private function removeCoreRoles(): void } } } + + private function coreRolesValid(): bool + { + return isset($this->config['core_roles']) + && is_array($this->config['core_roles']) + && 0 !== count($this->config['core_roles']); + } } diff --git a/src/UserRolesServiceProvider.php b/src/UserRolesServiceProvider.php index 5a0c640..b246786 100644 --- a/src/UserRolesServiceProvider.php +++ b/src/UserRolesServiceProvider.php @@ -20,6 +20,10 @@ public function configurePackage(Package $package): void public function packageRegistered(): void { - $this->app->singleton(UserRoles::class, fn () => new UserRoles(new \Role_Command)); + $this->app->bind(UserRoles::class, fn () => new UserRoles( + config('user-roles'), + new \Role_Command(), + new \WP_CLI() + )); } } diff --git a/tests/Console/UserRolesCommandTest.php b/tests/Console/UserRolesCommandTest.php index 91d69d2..174daf9 100644 --- a/tests/Console/UserRolesCommandTest.php +++ b/tests/Console/UserRolesCommandTest.php @@ -2,10 +2,35 @@ declare(strict_types=1); -use Yard\UserRoles\Facades\UserRoles; +use Facades\Yard\UserRoles\UserRoles; -it('calls setRoles on UserRoles and outputs messages', function () { - UserRoles::shouldReceive('setRoles')->once(); +it('creates roles once when not in multisite', function () { + WP_Mock::userFunction('is_multisite', [ + 'return' => false, + ]); + + UserRoles::shouldReceive('createRoles')->once(); + + $this->artisan('roles:create') + ->expectsOutput('Updating roles...') + ->expectsOutput('All done!') + ->assertExitCode(0); +}); + +it('creates roles for each site when in multisite', function () { + WP_Mock::userFunction('is_multisite', [ + 'return' => true, + ]); + + WP_Mock::userFunction('get_sites', [ + 'return' => [1, 2, 3], + ]); + + WP_Mock::userFunction('switch_to_blog', [ + 'times' => 3, + ]); + + UserRoles::shouldReceive('createRoles')->times(3); $this->artisan('roles:create') ->expectsOutput('Updating roles...') diff --git a/tests/Facades/UserRolesFacadeTest.php b/tests/Facades/UserRolesFacadeTest.php new file mode 100644 index 0000000..b1a7407 --- /dev/null +++ b/tests/Facades/UserRolesFacadeTest.php @@ -0,0 +1,13 @@ +mock(\Yard\UserRoles\UserRoles::class, function (MockInterface $mock) { + $mock->shouldReceive('createRoles')->once(); + }); + + Yard\UserRoles\Facades\UserRoles::createRoles(); +}); diff --git a/tests/Facades/UserRolesTest.php b/tests/Facades/UserRolesTest.php deleted file mode 100644 index 174d7fd..0000000 --- a/tests/Facades/UserRolesTest.php +++ /dev/null @@ -1,3 +0,0 @@ - true, - 'editor' => false, - 'author' => false, - 'contributor' => false, - 'subscriber' => false, - ]; +function mock_WP_Roles(array $roles): WP_Roles +{ + $WP_Roles = Mockery::mock(WP_Roles::class); + + $WP_Roles->roles = $roles; + + return $WP_Roles; +} + +function mock_empty_WP_Roles(): void +{ + WP_Mock::userFunction('wp_roles', [ + 'times' => 1, + 'return' => mock_WP_Roles([]), + ]); +} + +it('aborts if no prefix is found in config', function () { + $WP_CLI = Mockery::mock(WP_CLI::class); + $WP_CLI->shouldReceive('error') + ->once() + ->with('No prefix found in configuration file. Aborting role creation.') + ->andThrow(new WP_CLI\ExitException('No prefix found in configuration file. Aborting role creation.')); + + new UserRoles([], Mockery::mock(Role_Command::class), $WP_CLI); +})->throws(WP_CLI\ExitException::class, 'No prefix found in configuration file. Aborting role creation.'); + +it('does not remove any custom roles when they are not prefixed', function () { + mock_empty_WP_Roles(); + + $Role_Command = Mockery::mock(Role_Command::class)->shouldIgnoreMissing(); + $WP_CLI = Mockery::mock(WP_CLI::class)->shouldIgnoreMissing(); + + $Role_Command->shouldNotReceive('delete'); + $WP_CLI->shouldReceive('warning') + ->once() + ->with("No custom roles with prefix 'yard_' found in database. Skipping custom role deletion."); + + $userRoles = new UserRoles(['prefix' => 'yard'], $Role_Command, $WP_CLI); + + $userRoles->createRoles(); +}); + +it('removes custom roles with the right prefix', function () { + $WP_Roles = mock_WP_Roles([ + 'yard_superuser' => [], + 'my_prefix_visitor' => [], + ]); + + WP_Mock::userFunction('wp_roles', [ + 'times' => 1, + 'return' => $WP_Roles, + ]); + + $Role_Command = Mockery::mock(Role_Command::class)->shouldIgnoreMissing(); + + $Role_Command->shouldReceive('delete') + ->once() + ->with(['yard_superuser']); + + $Role_Command->shouldNotReceive('delete') + ->with(['my_prefix_visitor']); - expect(config('user-roles.core_roles'))->toBeArray() - ->and(config('user-roles.core_roles'))->toBe($coreRoles); + $userRoles = new UserRoles(['prefix' => 'yard'], $Role_Command, new WP_CLI); + + $userRoles->createRoles(); }); it('removes core roles marked for deletion', function () { - $roleCommand = Mockery::mock(Role_Command::class); - $roleCommand->shouldReceive('delete')->times(4); + mock_empty_WP_Roles(); + + $Role_Command = Mockery::mock(Role_Command::class)->shouldIgnoreMissing(); + + $config = [ + 'prefix' => 'yard', + 'core_roles' => [ + 'administrator' => true, + 'editor' => false, + 'author' => false, + ], + ]; + + $Role_Command->shouldNotReceive('delete') + ->with(['administrator']); + + $Role_Command->shouldReceive('delete') + ->once() + ->with(['editor']); + + $Role_Command->shouldReceive('delete') + ->once() + ->with(['author']); + + $userRoles = new UserRoles($config, $Role_Command, new WP_CLI); + + $userRoles->createRoles(); +}); + +it('creates custom role from config', function () { + mock_empty_WP_Roles(); + + $Role_Command = Mockery::mock(Role_Command::class)->shouldIgnoreMissing(); + + $Role_Command->shouldReceive('create') + ->once() + ->with(['yard_superuser', 'Superuser'], []); + + $config = [ + 'prefix' => 'yard', + 'roles' => [ + 'superuser' => [ + 'display_name' => 'Superuser', + 'caps' => [ + 'my_custom_cap', + ], + ], + ], + ]; + + $userRoles = new UserRoles($config, $Role_Command, new WP_CLI); + + $WP_Role = Mockery::mock(WP_Role::class); + + $WP_Role->shouldReceive('add_cap') + ->once() + ->with('my_custom_cap', true); + + WP_Mock::userFunction('get_role', [ + 'times' => 1, + 'return' => $WP_Role, + ]); + + $userRoles->createRoles(); +}); + +it('skips roles without display name', function () { + mock_empty_WP_Roles(); + + $Role_Command = Mockery::mock(Role_Command::class)->shouldIgnoreMissing(); + + $Role_Command->shouldNotReceive('create'); + + $config = [ + 'prefix' => 'yard', + 'roles' => [ + 'superuser' => [], + ], + ]; + + $WP_CLI = Mockery::mock(WP_CLI::class)->shouldIgnoreMissing(); - $userRoles = new UserRoles($roleCommand); + $WP_CLI->shouldReceive('warning') + ->once() + ->with('No display name configured for role superuser. Skipping role creation.'); - $reflection = new ReflectionClass($userRoles); - $method = $reflection->getMethod('removeCoreRoles'); - $method->setAccessible(true); + $userRoles = new UserRoles($config, $Role_Command, $WP_CLI); - $method->invoke($userRoles); + $userRoles->createRoles(); });