Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
151 lines
7.2 KiB
PHP
151 lines
7.2 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||
|
||
use InvalidArgumentException;
|
||
use LIT\MCP\Integrations\Divi\Compose\DynamicContentRules;
|
||
use LIT\MCP\Integrations\Divi\Compose\LoopScope;
|
||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
/** Rulings X2/X5 and the security boundary "Typabgleich": loop_* names only in a loop of their kind. */
|
||
final class DynamicContentRulesTest extends TestCase
|
||
{
|
||
/** The map of a composition inside a loop: forPost() plus loopFields() of every kind (worst case). */
|
||
private const MAP = [
|
||
'post_title' => ['before' => 'richtext', 'after' => 'richtext'],
|
||
'loop_post_title' => ['before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text'],
|
||
'loop_post_meta_key_manual_custom_field' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'],
|
||
'loop_term_name' => ['before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text'],
|
||
'loop_term_meta_key_manual_custom_field' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'],
|
||
'loop_user_meta_key_manual_custom_field' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'],
|
||
'loop_menu_text' => ['before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text'],
|
||
'loop_acf_team|||name' => ['before' => 'richtext', 'after' => 'richtext'],
|
||
];
|
||
|
||
private static function rules(?LoopScope $loop = null): DynamicContentRules
|
||
{
|
||
return new DynamicContentRules(DiviFixtures::designSystem(), self::MAP, $loop);
|
||
}
|
||
|
||
private static function assertRefused(DynamicContentRules $rules, string $name, string $message): void
|
||
{
|
||
try {
|
||
$rules->fieldsFor($name);
|
||
self::fail('Accepted: ' . $name);
|
||
} catch (InvalidArgumentException $e) {
|
||
self::assertSame($message, $e->getMessage(), $name);
|
||
}
|
||
}
|
||
|
||
/** Spec §9 / X5: without a loop every loop_* name is refused – also one the map names. */
|
||
public function testWithoutALoopEveryLoopOptionIsRefused(): void
|
||
{
|
||
foreach ([self::rules(), self::rules(LoopScope::none())] as $rules) {
|
||
foreach (array_keys(self::MAP) as $name) {
|
||
if (! str_starts_with($name, 'loop_')) {
|
||
continue;
|
||
}
|
||
self::assertRefused(
|
||
$rules,
|
||
$name,
|
||
sprintf(
|
||
'Schleifen-Inhalt "%s" braucht eine Divi-Schleife in diesem Modul oder einem übergeordneten '
|
||
. 'Element (Abschnitt, Zeile, Spalte, Gruppe).',
|
||
$name
|
||
)
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
public function testALoopOptionOfTheLoopsKindHasItsMapSettings(): void
|
||
{
|
||
self::assertSame(self::MAP['loop_post_title'], self::rules(LoopScope::ofQueryType('post_types'))
|
||
->fieldsFor('loop_post_title'));
|
||
self::assertSame(self::MAP['loop_post_title'], self::rules(LoopScope::ofQueryType('current_page'))
|
||
->fieldsFor('loop_post_title'));
|
||
self::assertSame(self::MAP['loop_term_name'], self::rules(LoopScope::ofQueryType('post_taxonomies'))
|
||
->fieldsFor('loop_term_name'));
|
||
self::assertSame(
|
||
self::MAP['loop_user_meta_key_manual_custom_field'],
|
||
self::rules(LoopScope::ofQueryType('user_roles'))->fieldsFor('loop_user_meta_key_manual_custom_field')
|
||
);
|
||
self::assertSame(self::MAP['loop_menu_text'], self::rules(LoopScope::ofQueryType('menus'))
|
||
->fieldsFor('loop_menu_text'));
|
||
}
|
||
|
||
/**
|
||
* Security boundary: Divi reads the meta of the loop object's ID whatever the object is – post meta
|
||
* of the post with the term's ID (DynamicContentLoopOptions.php:145-158). A hard error, not a hint.
|
||
*
|
||
* @return array<string, array{string, string, string}>
|
||
*/
|
||
public static function mismatches(): array
|
||
{
|
||
return [
|
||
'post meta in a term loop' => ['post_taxonomies', 'loop_post_meta_key_manual_custom_field', 'Terme'],
|
||
'post meta in a user loop' => ['user_roles', 'loop_post_meta_key_manual_custom_field', 'Benutzer'],
|
||
'post meta in a menu loop' => ['menus', 'loop_post_meta_key_manual_custom_field', 'Menü'],
|
||
'user meta in a post loop' => ['post_types', 'loop_user_meta_key_manual_custom_field', 'Beiträge'],
|
||
'term meta in a post loop' => ['current_page', 'loop_term_meta_key_manual_custom_field', 'Beiträge'],
|
||
'term meta in a user loop' => ['user_roles', 'loop_term_meta_key_manual_custom_field', 'Benutzer'],
|
||
'term name in a post loop' => ['post_types', 'loop_term_name', 'Beiträge'],
|
||
'post title in a menu loop' => ['menus', 'loop_post_title', 'Menü'],
|
||
'menu text in a term loop' => ['post_taxonomies', 'loop_menu_text', 'Terme'],
|
||
'acf row in a post loop' => ['post_types', 'loop_acf_team|||name', 'Beiträge'],
|
||
'post title in an acf repeater loop' => ['repeater_team', 'loop_post_title', 'repeater_team'],
|
||
'acf row in an acf repeater loop' => ['repeater_team', 'loop_acf_team|||name', 'repeater_team'],
|
||
];
|
||
}
|
||
|
||
/** @dataProvider mismatches */
|
||
public function testAnOptionOfAnotherKindIsRefused(string $queryType, string $name, string $label): void
|
||
{
|
||
self::assertRefused(
|
||
self::rules(LoopScope::ofQueryType($queryType)),
|
||
$name,
|
||
sprintf('Schleifen-Inhalt "%s" passt nicht zur Schleife dieses Elements (Typ: %s).', $name, $label)
|
||
);
|
||
}
|
||
|
||
public function testALoopOptionTheMapDoesNotNameIsUnknown(): void
|
||
{
|
||
$rules = self::rules(LoopScope::ofQueryType('post_types'));
|
||
|
||
foreach (['loop_post_date', 'loop_post_meta_key_telefon', 'loop_'] as $name) {
|
||
try {
|
||
$rules->fieldsFor($name);
|
||
self::fail('Accepted: ' . $name);
|
||
} catch (InvalidArgumentException $e) {
|
||
self::assertStringContainsString('Unbekannter Name "' . $name . '"', $e->getMessage());
|
||
}
|
||
}
|
||
// The default map (registry unavailable) names no loop option.
|
||
$this->expectExceptionMessage('Unbekannter Name "loop_post_title"');
|
||
(new DynamicContentRules(DiviFixtures::designSystem(), null, LoopScope::ofQueryType('post_types')))
|
||
->fieldsFor('loop_post_title');
|
||
}
|
||
|
||
public function testOtherNamesAreTheSameInsideALoop(): void
|
||
{
|
||
foreach ([null, LoopScope::ofQueryType('post_taxonomies')] as $loop) {
|
||
$rules = self::rules($loop);
|
||
|
||
self::assertSame(self::MAP['post_title'], $rules->fieldsFor('post_title'));
|
||
self::assertSame(DynamicContentRules::META_SETTINGS, $rules->fieldsFor('custom_meta_telefon'));
|
||
self::assertSame(DynamicContentRules::DESIGN_SETTINGS, $rules->fieldsFor('gvid-s1'));
|
||
foreach (['custom_meta__edit_lock', 'site_title'] as $name) {
|
||
try {
|
||
$rules->fieldsFor($name);
|
||
self::fail('Accepted: ' . $name);
|
||
} catch (InvalidArgumentException $e) {
|
||
self::assertStringContainsString('Unbekannter Name "' . $name . '"', $e->getMessage());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|