feat: Divi adapters for roles and library with a live compatibility check
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,14 +4,21 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP\Integrations\Core;
|
namespace LIT\MCP\Integrations\Core;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\DiviCompatibility;
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects the themes/plugins LIT MCP integrates with.
|
* Detects the themes/plugins LIT MCP integrates with; for Divi also the internals that are missing
|
||||||
|
* (DiviCompatibility).
|
||||||
*/
|
*/
|
||||||
class EnvironmentProbe
|
class EnvironmentProbe
|
||||||
{
|
{
|
||||||
/** @return list<array{id: string, active: bool, version: ?string}> */
|
public function __construct(private ?DiviCompatibility $divi = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{id: string, active: bool, version: ?string, problems: list<string>}> */
|
||||||
public function components(): array
|
public function components(): array
|
||||||
{
|
{
|
||||||
$theme = wp_get_theme();
|
$theme = wp_get_theme();
|
||||||
@@ -22,16 +29,23 @@ class EnvironmentProbe
|
|||||||
$tecActive = class_exists('Tribe__Events__Main');
|
$tecActive = class_exists('Tribe__Events__Main');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
['id' => 'divi', 'active' => $isDivi, 'version' => $diviVersion],
|
[
|
||||||
|
'id' => 'divi',
|
||||||
|
'active' => $isDivi,
|
||||||
|
'version' => $diviVersion,
|
||||||
|
'problems' => $isDivi ? ($this->divi ?? new DiviCompatibility())->problems() : [],
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'id' => 'pods',
|
'id' => 'pods',
|
||||||
'active' => defined('PODS_VERSION'),
|
'active' => defined('PODS_VERSION'),
|
||||||
'version' => defined('PODS_VERSION') ? (string) PODS_VERSION : null,
|
'version' => defined('PODS_VERSION') ? (string) PODS_VERSION : null,
|
||||||
|
'problems' => [],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'id' => 'the-events-calendar',
|
'id' => 'the-events-calendar',
|
||||||
'active' => $tecActive,
|
'active' => $tecActive,
|
||||||
'version' => $tecActive ? (string) constant('Tribe__Events__Main::VERSION') : null,
|
'version' => $tecActive ? (string) constant('Tribe__Events__Main::VERSION') : null,
|
||||||
|
'problems' => [],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ final class SiteStatusAbility implements Ability
|
|||||||
public function description(): string
|
public function description(): string
|
||||||
{
|
{
|
||||||
return 'Überblick: Website, WordPress-Version, eigener Benutzer und Rollen, '
|
return 'Überblick: Website, WordPress-Version, eigener Benutzer und Rollen, '
|
||||||
. 'erkannte Integrationen (Divi, Pods, The Events Calendar).';
|
. 'erkannte Integrationen (Divi, Pods, The Events Calendar). '
|
||||||
|
. 'Bei Divi nennt problems Divi-Funktionen, die nach einem Update fehlen.';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function inputSchema(): array
|
public function inputSchema(): array
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||||
|
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
||||||
|
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live check of the Divi internals LIT MCP relies on (decisions note §5, E15): classes,
|
||||||
|
* functions, the library post type and taxonomies. lit-site-status shows the failed checks, so a
|
||||||
|
* Divi update that moves one of them is visible before a tool reports "unsupported".
|
||||||
|
*/
|
||||||
|
final class DiviCompatibility
|
||||||
|
{
|
||||||
|
/** @param array<string, callable(): bool>|null $checks Label => check; null = defaults(). */
|
||||||
|
public function __construct(private ?array $checks = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> Labels of the checks that failed or threw. */
|
||||||
|
public function problems(): array
|
||||||
|
{
|
||||||
|
$problems = [];
|
||||||
|
foreach ($this->checks ?? self::defaults() as $label => $check) {
|
||||||
|
try {
|
||||||
|
$ok = (bool) $check();
|
||||||
|
} catch (Throwable) {
|
||||||
|
$ok = false;
|
||||||
|
}
|
||||||
|
if (! $ok) {
|
||||||
|
$problems[] = (string) $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $problems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, callable(): bool> */
|
||||||
|
public static function defaults(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ModuleRegistration (Modul-Katalog)' => static fn (): bool => DiviModules::isAvailable(),
|
||||||
|
'DynamicContentOptions (Dynamic Content)' =>
|
||||||
|
static fn (): bool => class_exists(DynamicContentMap::REGISTRY),
|
||||||
|
'et_pb_is_allowed (Divi-Rollen)' => static fn (): bool => function_exists('et_pb_is_allowed'),
|
||||||
|
'Post-Typ et_pb_layout (Bibliothek)' =>
|
||||||
|
static fn (): bool => post_type_exists(DiviLibraryStore::POST_TYPE),
|
||||||
|
'Taxonomien scope, layout_type, module_width (Bibliothek)' =>
|
||||||
|
static fn (): bool => (new DiviLibraryStore())->missingTaxonomies() === [],
|
||||||
|
'GlobalLayout::is_global_layout_template (globale Module)' =>
|
||||||
|
static fn (): bool => method_exists(DiviLibraryStore::GLOBAL_LAYOUT, 'is_global_layout_template'),
|
||||||
|
'ET_Core_PageResource (CSS-Cache)' => static fn (): bool => class_exists('ET_Core_PageResource'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Library;
|
||||||
|
|
||||||
|
use WP_Post;
|
||||||
|
use WP_Term;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for the Divi Library (E4, E15): post type et_pb_layout, the taxonomies scope,
|
||||||
|
* layout_type, module_width, layout_category, layout_tag and the meta Divi's et_pb_submit_layout()
|
||||||
|
* writes (F1, F2, fixtures stage3). "Global" is Divi's own definition
|
||||||
|
* GlobalLayout::is_global_layout_template() (F5) with the scope term as fallback when the class is
|
||||||
|
* gone. find() filters by the scope term only, so an entry Divi counts as global through its D4
|
||||||
|
* meta fallback can show up under scope "normal" (preflight M-12, accepted). Library entries are
|
||||||
|
* always "publish" (F2). Not final so abilities can be tested with a mock.
|
||||||
|
*/
|
||||||
|
class DiviLibraryStore
|
||||||
|
{
|
||||||
|
public const POST_TYPE = 'et_pb_layout';
|
||||||
|
|
||||||
|
public const GLOBAL_LAYOUT = 'ET\Builder\Packages\GlobalLayout\GlobalLayout';
|
||||||
|
|
||||||
|
public const LAYOUT_TYPES = ['layout', 'section', 'row', 'module'];
|
||||||
|
|
||||||
|
/** Taxonomies every new entry needs (et_pb_submit_layout). */
|
||||||
|
public const TAXONOMIES = ['scope', 'layout_type', 'module_width'];
|
||||||
|
|
||||||
|
public function isGlobal(int $id): bool
|
||||||
|
{
|
||||||
|
if (method_exists(self::GLOBAL_LAYOUT, 'is_global_layout_template')) {
|
||||||
|
return (bool) call_user_func([self::GLOBAL_LAYOUT, 'is_global_layout_template'], $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array('global', $this->termSlugs($id, 'scope'), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "layout", "section", "row" or "module"; null when the entry has no known type. */
|
||||||
|
public function layoutType(int $id): ?string
|
||||||
|
{
|
||||||
|
foreach ($this->termSlugs($id, 'layout_type') as $slug) {
|
||||||
|
if (in_array($slug, self::LAYOUT_TYPES, true)) {
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
public function termSlugs(int $id, string $taxonomy): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (WP_Term $term): string => (string) $term->slug, $this->terms($id, $taxonomy));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
public function termNames(int $id, string $taxonomy): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (WP_Term $term): string => (string) $term->name, $this->terms($id, $taxonomy));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Published entries, $limit + 1 of them (Cursor::page() tells whether there are more).
|
||||||
|
*
|
||||||
|
* @param array{layout_type?: string, scope?: string, search?: string, category?: string} $filters
|
||||||
|
* scope: "global" or "normal"; category: name of a layout_category term.
|
||||||
|
* @return list<WP_Post>
|
||||||
|
*/
|
||||||
|
public function find(array $filters, int $limit, int $offset): array
|
||||||
|
{
|
||||||
|
$taxQuery = [];
|
||||||
|
if (in_array($filters['layout_type'] ?? '', self::LAYOUT_TYPES, true)) {
|
||||||
|
$taxQuery[] = ['taxonomy' => 'layout_type', 'field' => 'slug', 'terms' => [$filters['layout_type']]];
|
||||||
|
}
|
||||||
|
if (($filters['scope'] ?? '') === 'global') {
|
||||||
|
$taxQuery[] = ['taxonomy' => 'scope', 'field' => 'slug', 'terms' => ['global']];
|
||||||
|
} elseif (($filters['scope'] ?? '') === 'normal') {
|
||||||
|
$taxQuery[] = ['taxonomy' => 'scope', 'field' => 'slug', 'terms' => ['global'], 'operator' => 'NOT IN'];
|
||||||
|
}
|
||||||
|
if (($filters['category'] ?? '') !== '') {
|
||||||
|
$taxQuery[] = ['taxonomy' => 'layout_category', 'field' => 'name', 'terms' => [$filters['category']]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
'post_type' => self::POST_TYPE,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'posts_per_page' => $limit + 1,
|
||||||
|
'offset' => $offset,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
'no_found_rows' => true,
|
||||||
|
'suppress_filters' => false,
|
||||||
|
];
|
||||||
|
if (($filters['search'] ?? '') !== '') {
|
||||||
|
$args['s'] = $filters['search'];
|
||||||
|
}
|
||||||
|
if ($taxQuery !== []) {
|
||||||
|
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- the library is small.
|
||||||
|
$args['tax_query'] = array_merge(['relation' => 'AND'], $taxQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter(get_posts($args), static fn (mixed $post): bool => $post instanceof WP_Post));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{id: int, title: string, layout_type: ?string, scope: string, module_type: ?string,
|
||||||
|
* categories: list<string>, tags: list<string>, modified: string}
|
||||||
|
*/
|
||||||
|
public function describe(WP_Post $post): array
|
||||||
|
{
|
||||||
|
$id = (int) $post->ID;
|
||||||
|
$moduleType = (string) get_post_meta($id, '_et_pb_module_type', true);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $id,
|
||||||
|
'title' => (string) $post->post_title,
|
||||||
|
'layout_type' => $this->layoutType($id),
|
||||||
|
'scope' => $this->isGlobal($id) ? 'global' : 'normal',
|
||||||
|
'module_type' => $moduleType === '' ? null : $moduleType,
|
||||||
|
'categories' => $this->termNames($id, 'layout_category'),
|
||||||
|
'tags' => $this->termNames($id, 'layout_tag'),
|
||||||
|
'modified' => (string) $post->post_modified_gmt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Library taxonomies not registered in this request (F2: Divi registers them in
|
||||||
|
* ET_Builder_Library; fixture finding 6: not in every context).
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function missingTaxonomies(): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(
|
||||||
|
self::TAXONOMIES,
|
||||||
|
static fn (string $taxonomy): bool => ! taxonomy_exists($taxonomy)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<WP_Term> */
|
||||||
|
private function terms(int $id, string $taxonomy): array
|
||||||
|
{
|
||||||
|
$terms = get_the_terms($id, $taxonomy);
|
||||||
|
|
||||||
|
return is_array($terms)
|
||||||
|
? array_values(array_filter($terms, static fn (mixed $term): bool => $term instanceof WP_Term))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Library;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use LIT\MCP\Abilities\Errors;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for Divi's role editor (E3, E15): et_pb_is_allowed() (Divi 5.2.1 builder/core.php:1128)
|
||||||
|
* answers for the current user's role, "dangerous" options (theme_builder,
|
||||||
|
* read_dynamic_content_custom_fields) default to off for every role but administrator. When
|
||||||
|
* Divi no longer has the function, nothing is allowed – DiviCompatibility reports it.
|
||||||
|
* Not final so tests can pass a check.
|
||||||
|
*/
|
||||||
|
class DiviRoles
|
||||||
|
{
|
||||||
|
private const LABELS = [
|
||||||
|
'divi_library' => 'Divi-Bibliothek',
|
||||||
|
'save_library' => 'In der Bibliothek speichern',
|
||||||
|
'add_library' => 'Aus der Bibliothek hinzufügen',
|
||||||
|
'edit_global_library' => 'Globale Bibliothekselemente bearbeiten',
|
||||||
|
'read_dynamic_content_custom_fields' => 'Dynamic Content: benutzerdefinierte Felder lesen',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @param (Closure(string): bool)|null $check Test seam; null = Divi's et_pb_is_allowed(). */
|
||||||
|
public function __construct(private ?Closure $check = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allows(string $capability): bool
|
||||||
|
{
|
||||||
|
if ($this->check !== null) {
|
||||||
|
return ($this->check)($capability);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed($capability);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Null when the role allows $capability, else a forbidden error naming the option. */
|
||||||
|
public function refusal(string $capability): ?WP_Error
|
||||||
|
{
|
||||||
|
if ($this->allows($capability)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Errors::forbidden(
|
||||||
|
sprintf('Die Divi-Rolle dieses Kontos erlaubt „%s“ nicht.', self::LABELS[$capability] ?? $capability),
|
||||||
|
'Einen Administrator bitten, das Recht unter Divi → Rollen-Editor freizugeben.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,8 @@ defined('ABSPATH') || exit;
|
|||||||
*/
|
*/
|
||||||
class DynamicContentMap
|
class DynamicContentMap
|
||||||
{
|
{
|
||||||
private const REGISTRY = 'ET\Builder\Packages\Module\Layout\Components\DynamicContent\DynamicContentOptions';
|
/** Divi's dynamic-content registry class; also checked by DiviCompatibility. */
|
||||||
|
public const REGISTRY = 'ET\Builder\Packages\Module\Layout\Components\DynamicContent\DynamicContentOptions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param (Closure(int): mixed)|null $registry Registry lookup by post ID (tests); null = Divi's.
|
* @param (Closure(int): mixed)|null $registry Registry lookup by post ID (tests); null = Divi's.
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Core;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\Core\EnvironmentProbe;
|
||||||
|
use LIT\MCP\Integrations\Divi\DiviCompatibility;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
|
||||||
|
final class EnvironmentProbeTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDiviComponentCarriesTheCompatibilityFindings(): void
|
||||||
|
{
|
||||||
|
$theme = Mockery::mock();
|
||||||
|
$theme->allows('get_template')->andReturn('Divi');
|
||||||
|
$theme->allows('parent')->andReturn(false);
|
||||||
|
$theme->allows('get')->with('Version')->andReturn('5.2.1');
|
||||||
|
Functions\when('wp_get_theme')->justReturn($theme);
|
||||||
|
|
||||||
|
$probe = new EnvironmentProbe(new DiviCompatibility(['GlobalLayout' => static fn (): bool => false]));
|
||||||
|
$components = array_column($probe->components(), null, 'id');
|
||||||
|
|
||||||
|
self::assertSame(['GlobalLayout'], $components['divi']['problems']);
|
||||||
|
self::assertSame([], $components['pods']['problems']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\DiviCompatibility;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
final class DiviCompatibilityTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testListsFailedAndThrowingChecks(): void
|
||||||
|
{
|
||||||
|
$compatibility = new DiviCompatibility([
|
||||||
|
'A' => static fn (): bool => true,
|
||||||
|
'B' => static fn (): bool => false,
|
||||||
|
'C' => static function (): bool {
|
||||||
|
throw new RuntimeException('Divi changed');
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame(['B', 'C'], $compatibility->problems());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultsCoverTheDiviInternalsInUse(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'ModuleRegistration (Modul-Katalog)',
|
||||||
|
'DynamicContentOptions (Dynamic Content)',
|
||||||
|
'et_pb_is_allowed (Divi-Rollen)',
|
||||||
|
'Post-Typ et_pb_layout (Bibliothek)',
|
||||||
|
'Taxonomien scope, layout_type, module_width (Bibliothek)',
|
||||||
|
'GlobalLayout::is_global_layout_template (globale Module)',
|
||||||
|
'ET_Core_PageResource (CSS-Cache)',
|
||||||
|
],
|
||||||
|
array_keys(DiviCompatibility::defaults())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use WP_Post;
|
||||||
|
use WP_Term;
|
||||||
|
|
||||||
|
final class DiviLibraryStoreTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$terms = [
|
||||||
|
1058 => [
|
||||||
|
'scope' => [new WP_Term(['slug' => 'global', 'name' => 'global'])],
|
||||||
|
'layout_type' => [new WP_Term(['slug' => 'section', 'name' => 'section'])],
|
||||||
|
'layout_category' => [new WP_Term(['slug' => 'kopf', 'name' => 'Kopfbereiche'])],
|
||||||
|
],
|
||||||
|
1055 => [
|
||||||
|
'scope' => [new WP_Term(['slug' => 'non_global', 'name' => 'non_global'])],
|
||||||
|
'layout_type' => [new WP_Term(['slug' => 'section', 'name' => 'section'])],
|
||||||
|
],
|
||||||
|
1057 => [],
|
||||||
|
];
|
||||||
|
Functions\when('get_the_terms')->alias(
|
||||||
|
static fn (int $id, string $taxonomy): array|false => $terms[$id][$taxonomy] ?? false
|
||||||
|
);
|
||||||
|
Functions\when('get_post_meta')->alias(
|
||||||
|
static fn (int $id, string $key): string => $id === 1057 && $key === '_et_pb_module_type' ? 'divi/blurb' : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGlobalScopeAndLayoutTypeFromTheTerms(): void
|
||||||
|
{
|
||||||
|
$store = new DiviLibraryStore();
|
||||||
|
|
||||||
|
self::assertTrue($store->isGlobal(1058));
|
||||||
|
self::assertFalse($store->isGlobal(1055));
|
||||||
|
self::assertSame('section', $store->layoutType(1058));
|
||||||
|
self::assertNull($store->layoutType(1057));
|
||||||
|
self::assertSame(['Kopfbereiche'], $store->termNames(1058, 'layout_category'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDescribesAnEntry(): void
|
||||||
|
{
|
||||||
|
$post = new WP_Post([
|
||||||
|
'ID' => 1057, 'post_type' => 'et_pb_layout', 'post_title' => '[Fixture] Library-Modul',
|
||||||
|
'post_modified_gmt' => '2026-09-26 20:56:29',
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'id' => 1057, 'title' => '[Fixture] Library-Modul', 'layout_type' => null, 'scope' => 'normal',
|
||||||
|
'module_type' => 'divi/blurb', 'categories' => [], 'tags' => [], 'modified' => '2026-09-26 20:56:29',
|
||||||
|
],
|
||||||
|
(new DiviLibraryStore())->describe($post)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindQueriesPublishedEntriesWithFilters(): void
|
||||||
|
{
|
||||||
|
$captured = [];
|
||||||
|
Functions\when('get_posts')->alias(static function (array $args) use (&$captured): array {
|
||||||
|
$captured = $args;
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
(new DiviLibraryStore())->find(
|
||||||
|
['layout_type' => 'section', 'scope' => 'normal', 'search' => 'Kopf', 'category' => 'Kopfbereiche'],
|
||||||
|
20,
|
||||||
|
40
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame('et_pb_layout', $captured['post_type']);
|
||||||
|
self::assertSame('publish', $captured['post_status']);
|
||||||
|
self::assertSame(21, $captured['posts_per_page']);
|
||||||
|
self::assertSame(40, $captured['offset']);
|
||||||
|
self::assertSame('Kopf', $captured['s']);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'relation' => 'AND',
|
||||||
|
['taxonomy' => 'layout_type', 'field' => 'slug', 'terms' => ['section']],
|
||||||
|
['taxonomy' => 'scope', 'field' => 'slug', 'terms' => ['global'], 'operator' => 'NOT IN'],
|
||||||
|
['taxonomy' => 'layout_category', 'field' => 'name', 'terms' => ['Kopfbereiche']],
|
||||||
|
],
|
||||||
|
$captured['tax_query']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReportsMissingTaxonomies(): void
|
||||||
|
{
|
||||||
|
Functions\when('taxonomy_exists')->alias(static fn (string $taxonomy): bool => $taxonomy !== 'scope');
|
||||||
|
|
||||||
|
self::assertSame(['scope'], (new DiviLibraryStore())->missingTaxonomies());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
|
final class DiviRolesTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testAsksTheCheckAndExplainsARefusal(): void
|
||||||
|
{
|
||||||
|
$roles = new DiviRoles(static fn (string $cap): bool => $cap !== 'edit_global_library');
|
||||||
|
|
||||||
|
self::assertTrue($roles->allows('divi_library'));
|
||||||
|
self::assertNull($roles->refusal('divi_library'));
|
||||||
|
$refusal = $roles->refusal('edit_global_library');
|
||||||
|
self::assertToolError($refusal, 'forbidden');
|
||||||
|
self::assertStringContainsString('Globale Bibliothekselemente bearbeiten', $refusal->get_error_message());
|
||||||
|
self::assertStringContainsString('Rollen-Editor', $refusal->get_error_message());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithoutDiviNothingIsAllowed(): void
|
||||||
|
{
|
||||||
|
// Unit tests have no et_pb_is_allowed(): a missing Divi function never means "allowed".
|
||||||
|
self::assertFalse((new DiviRoles())->allows('divi_library'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user