Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Release/DiviToolFeaturesTest.php
T

381 lines
18 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\DeclaresRequirements;
use LIT\MCP\Integrations\Divi\DiviIntegration;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\WordPressIntegration;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
/**
* The table of the Divi capabilities every tool of the groups divi, divi-library and divi-theme-builder and
* lit/content-create declare (plan "Divi-Freigabe je Fähigkeit", Task 8). The table is the contract: a tool that
* needs more or less than listed here changes it here, on purpose.
*/
final class DiviToolFeaturesTest extends TestCase
{
private const PAGE = 5;
private const LIBRARY_ENTRY = 7;
private const LAYOUT = 9;
/** Post ID => post type for get_post_type(); every other ID names no entry. */
private const TYPES = [
self::PAGE => 'page',
self::LIBRARY_ENTRY => 'et_pb_layout',
self::LAYOUT => 'et_body_layout',
];
/** What a tool needs whatever its input names: tool => capabilities (DiviFeature values). */
private const EXPECTED = [
// Pages (divi)
'lit/divi-outline' => ['page-read', 'design-read'],
'lit/divi-node-get' => ['page-read', 'design-read'],
'lit/divi-catalog' => ['catalog'],
'lit/divi-design-system' => ['design-read'],
'lit/divi-insert' => ['page-read', 'page-write', 'catalog', 'design-read', 'dynamic-sources'],
'lit/divi-update' => ['page-read', 'page-write', 'catalog', 'design-read', 'dynamic-sources'],
'lit/divi-move' => ['page-read', 'page-write', 'catalog', 'design-read'],
'lit/divi-delete' => ['page-read', 'page-write', 'catalog', 'design-read'],
'lit/divi-duplicate-node' => ['page-read', 'page-write', 'catalog', 'design-read'],
'lit/divi-replace-text' => ['page-read', 'catalog', 'design-read', 'dynamic-sources'],
'lit/divi-node-copy' => ['page-read', 'page-write', 'catalog', 'design-read', 'dynamic-sources'],
'lit/divi-dynamic-sources' => ['dynamic-sources', 'roles'],
// Design system (divi)
'lit/divi-preset-get' => ['design-read', 'catalog'],
'lit/divi-presets-set' => ['design-read', 'preset-write', 'catalog'],
'lit/divi-presets-delete' => ['design-read', 'preset-write'],
'lit/divi-design-snapshots' => [],
'lit/divi-design-restore' => [
'design-read', 'preset-write', 'color-write', 'variable-write', 'theme-option-read', 'theme-option-write',
],
'lit/divi-presets-assign' => [
'page-read', 'page-write', 'catalog', 'design-read', 'preset-write', 'library-read', 'library-write',
'tb-write', 'roles',
],
'lit/divi-design-detach' => [
'page-read', 'page-write', 'catalog', 'design-read', 'preset-write', 'library-read', 'library-write',
'tb-write', 'roles',
],
'lit/divi-colors-set' => ['design-read', 'color-write'],
'lit/divi-variables-set' => ['design-read', 'variable-write'],
'lit/divi-theme-options-get' => ['theme-option-read'],
'lit/divi-theme-options-update' => ['theme-option-read', 'theme-option-write'],
'lit/divi-fonts-upload' => ['font-upload'],
// Library (divi-library)
'lit/library-list' => ['library-read', 'roles'],
'lit/library-find-similar' => ['page-read', 'catalog', 'roles'],
'lit/library-insert' => [
'page-read', 'page-write', 'catalog', 'design-read', 'dynamic-sources', 'library-read', 'roles',
],
'lit/library-make-global' => [
'page-read', 'page-write', 'catalog', 'design-read', 'dynamic-sources', 'library-read', 'library-write',
'roles',
],
'lit/library-save' => [
'page-read', 'page-write', 'catalog', 'design-read', 'dynamic-sources', 'library-write', 'roles',
],
// Theme Builder (divi-theme-builder)
'lit/tb-list' => ['tb-read', 'roles'],
'lit/tb-create-template' => ['tb-read', 'tb-write', 'roles'],
'lit/tb-assign' => ['tb-read', 'tb-write', 'roles'],
'lit/tb-activate' => ['tb-read', 'tb-write', 'roles'],
// WordPress: without the divi key it stays in the core
'lit/content-create' => [],
];
/** The compose set of a new or changed page. */
private const COMPOSE = ['page-read', 'page-write', 'catalog', 'design-read', 'dynamic-sources'];
/**
* Input-dependent additions: label => [tool, input, capabilities for that input].
*
* @return array<string, array{0: string, 1: array<string, mixed>, 2: list<string>}>
*/
public static function inputCases(): array
{
$page = self::PAGE;
$library = self::LIBRARY_ENTRY;
$layout = self::LAYOUT;
$libraryWrite = ['roles', 'library-read', 'library-write'];
$layoutWrite = ['roles', 'tb-read', 'tb-write'];
return [
'outline of a page' => ['lit/divi-outline', ['post_id' => $page], ['page-read', 'design-read']],
'outline of a library entry' => [
'lit/divi-outline', ['post_id' => $library], ['page-read', 'design-read', 'roles'],
],
'outline of a layout' => [
'lit/divi-outline', ['post_id' => $layout], ['page-read', 'design-read', 'roles'],
],
'node-get of a library entry' => [
'lit/divi-node-get', ['post_id' => $library, 'id' => 's0'], ['page-read', 'design-read', 'roles'],
],
'outline of an unknown entry' => [
'lit/divi-outline', ['post_id' => 999], ['page-read', 'design-read'],
],
'outline without post_id' => ['lit/divi-outline', [], ['page-read', 'design-read']],
'insert into a page' => ['lit/divi-insert', ['post_id' => $page], self::COMPOSE],
'insert into a library entry' => [
'lit/divi-insert', ['post_id' => $library], array_merge(self::COMPOSE, $libraryWrite),
],
'update of a layout' => [
'lit/divi-update', ['post_id' => $layout], array_merge(self::COMPOSE, $layoutWrite),
],
'move in a layout' => [
'lit/divi-move', ['post_id' => $layout],
array_merge(['page-read', 'page-write', 'catalog', 'design-read'], $layoutWrite),
],
'delete with a numeric string ID' => [
'lit/divi-delete', ['post_id' => (string) $library],
array_merge(['page-read', 'page-write', 'catalog', 'design-read'], $libraryWrite),
],
'node-copy of a page into a page' => [
'lit/divi-node-copy', ['post_id' => $page, 'source_post_id' => $page], self::COMPOSE,
],
'node-copy out of a library entry' => [
'lit/divi-node-copy', ['post_id' => $page, 'source_post_id' => $library],
array_merge(self::COMPOSE, ['roles']),
],
'node-copy into a library entry' => [
'lit/divi-node-copy', ['post_id' => $library, 'source_post_id' => $page],
array_merge(self::COMPOSE, $libraryWrite),
],
'replace-text dry run by default' => [
'lit/divi-replace-text', ['search' => 'a', 'replace' => 'b'],
['page-read', 'catalog', 'design-read', 'dynamic-sources'],
],
'replace-text dry run explicit' => [
'lit/divi-replace-text', ['dry_run' => true, 'targets' => [['post_id' => $library]]],
['page-read', 'catalog', 'design-read', 'dynamic-sources'],
],
'replace-text on pages' => [
'lit/divi-replace-text', ['dry_run' => false, 'targets' => [['post_id' => $page, 'tree_rev' => 'x']]],
array_merge(['page-read', 'catalog', 'design-read', 'dynamic-sources'], ['page-write']),
],
'replace-text on a library entry' => [
'lit/divi-replace-text',
['dry_run' => false, 'targets' => [['post_id' => $page], ['post_id' => $library], 'bad']],
array_merge(['page-read', 'catalog', 'design-read', 'dynamic-sources', 'page-write'], $libraryWrite),
],
'presets-set without from_node' => [
'lit/divi-presets-set', ['items' => [['module' => 'button', 'name' => 'x']]],
['design-read', 'preset-write', 'catalog'],
],
'presets-set from a page node' => [
'lit/divi-presets-set', ['items' => [['from_node' => ['post_id' => $page, 'id' => 's0']]]],
['design-read', 'preset-write', 'catalog', 'page-read'],
],
'presets-set from a library node' => [
'lit/divi-presets-set', ['items' => [1, ['from_node' => ['post_id' => $library, 'id' => 's0']]]],
['design-read', 'preset-write', 'catalog', 'page-read', 'roles'],
],
'design-detach of a variable' => [
'lit/divi-design-detach', ['variable' => 'Eckenradius'],
[
'page-read', 'page-write', 'catalog', 'design-read', 'preset-write', 'library-read',
'library-write', 'tb-write', 'roles', 'variable-write',
],
],
'design-restore of presets' => [
'lit/divi-design-restore', ['snapshot_id' => 'presets-20261016T100000Z-aaaaaa'],
['design-read', 'preset-write'],
],
'design-restore of colors' => [
'lit/divi-design-restore', ['snapshot_id' => 'colors-20261016T100000Z-aaaaaa'],
['design-read', 'color-write'],
],
'design-restore of variables' => [
'lit/divi-design-restore', ['snapshot_id' => 'variables-20261016T100000Z-aaaaaa'],
['design-read', 'variable-write'],
],
'design-restore of theme_options' => [
'lit/divi-design-restore', ['snapshot_id' => 'theme_options-20261016T100000Z-aaaaaa'],
['theme-option-read', 'theme-option-write'],
],
'library-list' => ['lit/library-list', [], ['library-read', 'roles']],
'library-list of one entry' => [
'lit/library-list', ['id' => $library], ['library-read', 'roles', 'page-read', 'design-read'],
],
'library-insert into a layout' => [
'lit/library-insert', ['post_id' => $layout, 'library_id' => $library],
array_merge(
['page-read', 'page-write', 'catalog', 'design-read', 'dynamic-sources', 'library-read'],
$layoutWrite
),
],
'tb-create-template without nodes' => [
'lit/tb-create-template', ['title' => 'T', 'header' => ['source' => 'new']],
['tb-read', 'tb-write', 'roles'],
],
'tb-create-template with nodes' => [
'lit/tb-create-template', ['title' => 'T', 'body' => ['source' => 'new', 'nodes' => [[]]]],
array_merge(['tb-read', 'tb-write', 'roles'], self::COMPOSE),
],
'tb-create-template with empty nodes' => [
'lit/tb-create-template', ['title' => 'T', 'footer' => ['source' => 'new', 'nodes' => []]],
['tb-read', 'tb-write', 'roles'],
],
'content-create without divi' => ['lit/content-create', ['post_type' => 'page'], []],
'content-create with divi' => [
'lit/content-create', ['post_type' => 'page', 'divi' => [[]]], self::COMPOSE,
],
];
}
protected function setUp(): void
{
parent::setUp();
Functions\when('__')->returnArg();
Functions\when('get_post_type')->alias(static fn (int $id): string|false => self::TYPES[$id] ?? false);
Functions\when('current_user_can')->justReturn(true);
}
/** @return array<string, Ability> name => ability of the three Divi groups and lit/content-create */
private function abilities(): array
{
$divi = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden());
$composer = Mockery::mock(ContentComposer::class);
$composer->allows('available')->andReturn(true);
$composer->allows('requirements')->andReturn(self::COMPOSE);
$wordPress = new WordPressIntegration(
Mockery::mock(ContentSchema::class),
FakeChangeLog::forbidden(),
$composer
);
$found = [];
foreach ([
...$divi->abilities(),
...$divi->library()->abilities(),
...$divi->themeBuilder()->abilities(),
] as $ability) {
$found[$ability->name()] = $ability;
}
foreach ($wordPress->abilities() as $ability) {
if ($ability->name() === 'lit/content-create') {
$found[$ability->name()] = $ability;
}
}
return $found;
}
/**
* @param list<DiviFeature> $features
* @return list<string> The values, sorted.
*/
private static function values(array $features): array
{
$values = array_map(static fn (DiviFeature $feature): string => $feature->value, $features);
sort($values);
return $values;
}
/** @param list<string> $values */
private static function sorted(array $values): array
{
sort($values);
return array_values(array_unique($values));
}
public function testEveryToolOfTheDiviGroupsDeclaresItsFeatures(): void
{
$abilities = $this->abilities();
foreach ($abilities as $name => $ability) {
if ($name === 'lit/content-create') {
self::assertInstanceOf(DeclaresRequirements::class, $ability, $name);
self::assertNotInstanceOf(RequiresDiviFeatures::class, $ability, 'the core imports no Divi class');
continue;
}
self::assertInstanceOf(RequiresDiviFeatures::class, $ability, $name);
}
}
public function testEveryRowOfTheTableHasAnAbilityAndTheOtherWayRound(): void
{
$abilities = array_keys($this->abilities());
self::assertSame([], array_values(array_diff(array_keys(self::EXPECTED), $abilities)), 'row without ability');
self::assertSame([], array_values(array_diff($abilities, array_keys(self::EXPECTED))), 'ability without row');
}
public function testEveryToolDeclaresExactlyTheTableForAnInputThatNamesNothing(): void
{
foreach ($this->abilities() as $name => $ability) {
self::assertSame(
self::sorted(self::EXPECTED[$name] ?? ['<no row>']),
self::values(FeatureSets::declaredBy($ability, [])),
$name
);
}
}
/**
* @param array<string, mixed> $input
* @param list<string> $expected
* @dataProvider inputCases
*/
public function testInputDependentFeatures(string $tool, array $input, array $expected): void
{
$ability = $this->abilities()[$tool];
self::assertSame(self::sorted($expected), self::values(FeatureSets::declaredBy($ability, $input)));
}
public function testNoToolDeclaresAFeatureTwiceAndEveryIdIsAFeature(): void
{
foreach ($this->abilities() as $name => $ability) {
$declared = $ability instanceof RequiresDiviFeatures
? array_map(static fn (DiviFeature $f): string => $f->value, $ability->diviFeatures([]))
: [];
self::assertSame($declared, array_values(array_unique($declared)), $name);
}
foreach (self::EXPECTED as $name => $ids) {
foreach ($ids as $id) {
self::assertNotNull(DiviFeature::tryFrom($id), $name . ': ' . $id);
}
}
}
public function testEveryWritingToolNeedsTheWriteFeatureOfEverythingItSaves(): void
{
$writes = [
'lit/divi-insert' => 'page-write', 'lit/divi-update' => 'page-write', 'lit/divi-move' => 'page-write',
'lit/divi-delete' => 'page-write', 'lit/divi-duplicate-node' => 'page-write',
'lit/divi-node-copy' => 'page-write', 'lit/library-insert' => 'page-write',
'lit/library-make-global' => 'library-write', 'lit/library-save' => 'library-write',
'lit/divi-presets-set' => 'preset-write', 'lit/divi-presets-delete' => 'preset-write',
'lit/divi-colors-set' => 'color-write', 'lit/divi-variables-set' => 'variable-write',
'lit/divi-theme-options-update' => 'theme-option-write', 'lit/divi-fonts-upload' => 'font-upload',
'lit/divi-design-restore' => 'preset-write', 'lit/divi-design-detach' => 'preset-write',
'lit/divi-presets-assign' => 'page-write', 'lit/tb-assign' => 'tb-write', 'lit/tb-activate' => 'tb-write',
'lit/tb-create-template' => 'tb-write',
];
foreach ($writes as $name => $feature) {
self::assertContains($feature, self::EXPECTED[$name], $name);
}
// Read-only tools never need a write capability.
foreach (['lit/divi-outline', 'lit/divi-node-get', 'lit/divi-catalog', 'lit/divi-design-system',
'lit/divi-dynamic-sources', 'lit/divi-preset-get', 'lit/library-list', 'lit/library-find-similar',
'lit/tb-list', 'lit/divi-design-snapshots'] as $name) {
foreach (self::EXPECTED[$name] as $id) {
self::assertFalse(DiviFeature::from($id)->isWrite(), $name . ': ' . $id);
}
}
}
}