roles = ['administrator' => [], 'editor' => []];
$roles->allows('get_names')->andReturn(['administrator' => 'Administrator', 'editor' => 'Redakteur']);
Functions\when('wp_roles')->justReturn($roles);
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://kunde.de/wp-admin/' . $path);
Functions\when('add_query_arg')->alias(static fn (array $args, string $url): string => $url . '?' . http_build_query($args));
Functions\when('get_settings_errors')->justReturn([]);
Functions\when('wp_get_current_user')->justReturn(new WP_User(1, ['editor']));
Functions\when('is_multisite')->justReturn(false);
}
protected function tearDown(): void
{
$_POST = [];
parent::tearDown();
}
public function testRegistersTheRolesOptionWithSanitizer(): void
{
Functions\expect('register_setting')->once()->with(
'lit_mcp_access',
'lit_mcp_allowed_roles',
Mockery::on(static fn (array $args): bool => $args['sanitize_callback'] === [AccessTab::class, 'sanitizeRoles'])
);
(new AccessTab(new Access(), Mockery::mock(ConnectionRepository::class)))->registerSettings();
}
public function testRegistersTheRevokeHandler(): void
{
$tab = new AccessTab(new Access(), Mockery::mock(ConnectionRepository::class));
Functions\expect('add_action')->once()->with('admin_post_lit_mcp_revoke', [$tab, 'handleRevoke']);
$tab->registerActions();
self::assertSame('zugriff', $tab->slug());
}
public function testSanitizeKeepsExistingRolesOnlyAndWarnsWhenEmpty(): void
{
Functions\expect('add_settings_error')->once()->with(
'lit_mcp_allowed_roles',
'lit_mcp_no_roles',
Mockery::type('string'),
'warning'
);
self::assertSame(['editor'], AccessTab::sanitizeRoles(['editor', 'nope', '', 5, 'editor']));
self::assertSame([], AccessTab::sanitizeRoles(null));
}
public function testSanitizeDoesNotRepeatAWarningOnTheSecondPassOfAFirstSave(): void
{
Functions\when('get_settings_errors')->justReturn([
['setting' => 'lit_mcp_allowed_roles', 'code' => 'lit_mcp_no_roles', 'message' => 'x', 'type' => 'warning'],
]);
Functions\expect('add_settings_error')->never();
self::assertSame([], AccessTab::sanitizeRoles(['']));
}
public function testSanitizeWarnsWhenTheCurrentUserLosesMcpAccess(): void
{
Functions\when('wp_get_current_user')->justReturn(new WP_User(1, ['administrator']));
Functions\expect('add_settings_error')->once()->with(
'lit_mcp_allowed_roles',
'lit_mcp_own_role',
Mockery::type('string'),
'warning'
);
self::assertSame(['editor'], AccessTab::sanitizeRoles(['editor']));
}
public function testSanitizeDoesNotWarnWhenTheCurrentUserKeepsAccess(): void
{
Functions\when('wp_get_current_user')->justReturn(new WP_User(1, ['administrator']));
Functions\expect('add_settings_error')->never();
self::assertSame(['administrator', 'editor'], AccessTab::sanitizeRoles(['administrator', 'editor']));
}
public function testSanitizeDoesNotWarnMultisiteSuperAdmins(): void
{
Functions\when('wp_get_current_user')->justReturn(new WP_User(1, []));
Functions\when('is_multisite')->justReturn(true);
Functions\when('is_super_admin')->justReturn(true);
Functions\expect('add_settings_error')->never();
self::assertSame(['editor'], AccessTab::sanitizeRoles(['editor']));
}
public function testRevokeChecksNonceAndRedirectsWithNotice(): void
{
$_POST = ['user_id' => '3', 'uuid' => 'uuid-1'];
Functions\when('current_user_can')->justReturn(true);
Functions\expect('check_admin_referer')->once()->with('lit_mcp_revoke_uuid-1');
$connections = Mockery::mock(ConnectionRepository::class);
$connections->expects('revoke')->with(3, 'uuid-1')->andReturn(true);
Functions\expect('wp_safe_redirect')->once()->with(Mockery::on(
static fn (string $url): bool => str_contains($url, 'tab=zugriff') && str_contains($url, 'lit_mcp_notice=revoked')
));
$terminated = false;
(new AccessTab(new Access(), $connections, static function () use (&$terminated): void {
$terminated = true;
}))->handleRevoke();
self::assertTrue($terminated);
}
public function testRevokeFailureRedirectsWithErrorNotice(): void
{
$_POST = ['user_id' => '3', 'uuid' => 'uuid-other'];
Functions\when('current_user_can')->justReturn(true);
Functions\when('check_admin_referer')->justReturn(1);
$connections = Mockery::mock(ConnectionRepository::class);
$connections->expects('revoke')->with(3, 'uuid-other')->andReturn(false);
Functions\expect('wp_safe_redirect')->once()->with(Mockery::on(
static fn (string $url): bool => str_contains($url, 'lit_mcp_notice=revoke_failed')
));
(new AccessTab(new Access(), $connections, static function (): void {
}))->handleRevoke();
}
public function testRevokeTreatsANonStringUuidAsMissing(): void
{
$_POST = ['user_id' => '3', 'uuid' => ['uuid-1']];
Functions\when('current_user_can')->justReturn(true);
Functions\expect('check_admin_referer')->once()->with('lit_mcp_revoke_');
$connections = Mockery::mock(ConnectionRepository::class);
$connections->expects('revoke')->with(3, '')->andReturn(false);
Functions\when('wp_safe_redirect')->justReturn(true);
(new AccessTab(new Access(), $connections, static function (): void {
}))->handleRevoke();
}
public function testRevokeRequiresManageOptions(): void
{
$_POST = ['user_id' => '3', 'uuid' => 'uuid-1'];
Functions\expect('current_user_can')->once()->with('manage_options')->andReturn(false);
Functions\expect('wp_die')->once()->andThrow(new RuntimeException('died'));
Functions\expect('check_admin_referer')->never();
$connections = Mockery::mock(ConnectionRepository::class);
$connections->expects('revoke')->never();
$this->expectException(RuntimeException::class);
(new AccessTab(new Access(), $connections))->handleRevoke();
}
/** @param mixed $roles value of the roles option */
private function stubRender(mixed $roles): void
{
Functions\when('get_option')->alias(static fn (string $key, $default = false) => match ($key) {
'lit_mcp_allowed_roles' => $roles,
'date_format' => 'd.m.Y',
'time_format' => 'H:i',
default => $default,
});
Functions\when('translate_user_role')->returnArg();
Functions\when('checked')->alias(static fn ($a, $b = true, $echo = true): string => $a === $b ? ' checked="checked"' : '');
Functions\when('settings_fields')->justReturn(null);
Functions\when('submit_button')->alias(static function ($text = null, $type = 'primary', $name = 'submit', $wrap = true, $other = ''): void {
$attrs = is_array($other) ? implode(' ', array_map(
static fn ($k, $v): string => sprintf('%s="%s"', $k, $v),
array_keys($other),
$other
)) : '';
echo '';
});
Functions\when('wp_nonce_field')->alias(static function (string $action): void {
echo '';
});
Functions\when('wp_date')->alias(static fn (string $format, int $ts): string => $format . '@' . $ts);
}
/** @param array $overrides */
private function connection(array $overrides = []): array
{
return $overrides + [
'user_id' => 3,
'user_login' => 'redaktion',
'uuid' => 'uuid-1',
'name' => 'Claude