Files
WP-LIT-MCP/tests/Unit/Admin/AccessTabTest.php
T

281 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 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 '<button ' . $attrs . '>' . ($text ?? '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')->alias(static fn (string $format, int $ts): string => $format . '@' . $ts);
}
/** @param array<string, mixed> $overrides */
private function connection(array $overrides = []): array
{
return $overrides + [
'user_id' => 3,
'user_login' => 'redaktion<b>',
'uuid' => 'uuid-1',
'name' => 'Claude <script>',
'app_id' => 'https://claude.ai/x',
'created' => 1790000000,
'last_used' => null,
];
}
private function renderTab(array $connections): string
{
$repository = Mockery::mock(ConnectionRepository::class);
$repository->allows('all')->andReturn($connections);
ob_start();
(new AccessTab(new Access(), $repository))->render();
return (string) ob_get_clean();
}
public function testRenderShowsRolesAndConnections(): void
{
$this->stubRender(['editor']);
$html = $this->renderTab([$this->connection()]);
self::assertStringContainsString('value="editor" checked="checked"', $html);
self::assertStringNotContainsString('value="administrator" checked', $html);
self::assertStringContainsString('<strong>Claude &lt;script&gt;</strong>', $html);
self::assertStringContainsString('redaktion&lt;b&gt;', $html);
self::assertStringNotContainsString('<script>', $html);
self::assertStringNotContainsString('<code>', $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);
self::assertStringContainsString('confirm(', $html);
}
public function testRolesAreAFormTableFieldsetWithLegendAndDescription(): void
{
$this->stubRender(['editor']);
$html = $this->renderTab([]);
self::assertStringContainsString('<table class="form-table" role="presentation">', $html);
self::assertStringContainsString('<th scope="row">Rollen mit Zugriff</th>', $html);
self::assertStringContainsString('<fieldset><legend class="screen-reader-text"><span>Rollen mit Zugriff</span></legend>', $html);
self::assertStringContainsString('<p class="description">Nur Benutzer dieser Rollen können einen KI-Assistenten verbinden.', $html);
}
public function testDatesUseTheSiteDateAndTimeFormat(): void
{
$this->stubRender(['editor']);
$html = $this->renderTab([
$this->connection(['uuid' => 'a', 'last_used' => null]),
$this->connection(['uuid' => 'b', 'last_used' => 1790050000]),
]);
self::assertStringContainsString('d.m.Y H:i@1790000000', $html);
self::assertStringContainsString('d.m.Y@1790050000', $html);
self::assertStringContainsString('<td class="column-last-used">–</td>', $html);
}
public function testRenderWithoutConnectionsShowsAnEmptyStateRowLinkingToTheConnectionTab(): void
{
$this->stubRender(['administrator']);
$html = $this->renderTab([]);
self::assertStringContainsString('<tr class="no-items"><td class="colspanchange" colspan="5">', $html);
self::assertStringContainsString('Noch keine Verbindung.', $html);
self::assertStringContainsString('href="https://kunde.de/wp-admin/options-general.php?page=lit-mcp&tab=verbindung"', $html);
}
}