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, htmlspecialchars((string) $v, ENT_QUOTES)), 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