Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
218 lines
8.9 KiB
PHP
218 lines
8.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Admin;
|
|
|
|
use Brain\Monkey\Functions;
|
|
use LIT\MCP\Access;
|
|
use LIT\MCP\Admin\AccessTab;
|
|
use LIT\MCP\Admin\ConnectionRepository;
|
|
use LIT\MCP\Tests\Unit\TestCase;
|
|
use Mockery;
|
|
use RuntimeException;
|
|
use WP_User;
|
|
|
|
final class AccessTabTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Functions\stubEscapeFunctions();
|
|
Functions\stubTranslationFunctions();
|
|
$roles = Mockery::mock();
|
|
$roles->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 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();
|
|
}
|
|
|
|
public function testRenderShowsRolesAndConnections(): void
|
|
{
|
|
Functions\when('get_option')->justReturn(['editor']);
|
|
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 (): void {
|
|
echo '<button>Speichern</button>';
|
|
});
|
|
Functions\when('wp_nonce_field')->alias(static function (string $action): void {
|
|
echo '<input type="hidden" name="_wpnonce" value="nonce-' . $action . '">';
|
|
});
|
|
Functions\when('wp_date')->justReturn('25.09.2026 10:00');
|
|
$connections = Mockery::mock(ConnectionRepository::class);
|
|
$connections->allows('all')->andReturn([[
|
|
'user_id' => 3,
|
|
'user_login' => 'redaktion<b>',
|
|
'uuid' => 'uuid-1',
|
|
'name' => 'Claude <script>',
|
|
'app_id' => 'https://claude.ai/x',
|
|
'created' => 1790000000,
|
|
'last_used' => null,
|
|
]]);
|
|
|
|
ob_start();
|
|
(new AccessTab(new Access(), $connections))->render();
|
|
$html = (string) ob_get_clean();
|
|
|
|
self::assertStringContainsString('value="editor" checked="checked"', $html);
|
|
self::assertStringNotContainsString('value="administrator" checked', $html);
|
|
self::assertStringContainsString('Claude <script>', $html);
|
|
self::assertStringContainsString('redaktion<b>', $html);
|
|
self::assertStringNotContainsString('<script>', $html);
|
|
self::assertStringContainsString('value="uuid-1"', $html);
|
|
self::assertStringContainsString('name="user_id" value="3"', $html);
|
|
self::assertStringContainsString('value="lit_mcp_revoke"', $html);
|
|
self::assertStringContainsString('nonce-lit_mcp_revoke_uuid-1', $html);
|
|
}
|
|
|
|
public function testRenderWithoutConnections(): void
|
|
{
|
|
Functions\when('get_option')->justReturn(['administrator']);
|
|
Functions\when('translate_user_role')->returnArg();
|
|
Functions\when('checked')->justReturn('');
|
|
Functions\when('settings_fields')->justReturn(null);
|
|
Functions\when('submit_button')->justReturn(null);
|
|
$connections = Mockery::mock(ConnectionRepository::class);
|
|
$connections->allows('all')->andReturn([]);
|
|
|
|
ob_start();
|
|
(new AccessTab(new Access(), $connections))->render();
|
|
$html = (string) ob_get_clean();
|
|
|
|
self::assertStringContainsString('Keine aktiven Verbindungen.', $html);
|
|
self::assertStringNotContainsString('<table', $html);
|
|
}
|
|
}
|