Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Pages/DiviDynamicSourcesAbilityTest.php
T
2026-09-27 03:22:13 +02:00

164 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Pages\DiviDynamicSourcesAbility;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Support\LibraryFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class DiviDynamicSourcesAbilityTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Functions\when('get_post_type_object')->justReturn(
(object) ['name' => 'page', 'cap' => (object) ['edit_posts' => 'edit_pages']]
);
Functions\when('current_user_can')->justReturn(true);
}
private function schema(): ContentSchema
{
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('isManaged')->andReturn(true);
$schema->allows('fields')->with('page')->andReturn([
'telefon' => new FieldDefinition('telefon', 'Telefon', 'string', 'meta', FieldDefinition::WRITABLE),
'notiz' => new FieldDefinition('notiz', 'Notiz', 'text', 'pods', FieldDefinition::WRITABLE),
'foto' => new FieldDefinition('foto', 'Foto', 'file', 'pods', FieldDefinition::WRITABLE),
'projekte' => new FieldDefinition('projekte', 'Projekte', 'relation', 'pods', FieldDefinition::WRITABLE),
'_intern' => new FieldDefinition('_intern', '_intern', 'string', 'meta', FieldDefinition::WRITABLE),
'geheim' => new FieldDefinition('geheim', 'Geheim', 'string', 'pods', FieldDefinition::HIDDEN),
'gefunden' => new FieldDefinition('gefunden', 'gefunden', 'unknown', 'sample', FieldDefinition::UNKNOWN),
'aktiv' => new FieldDefinition('aktiv', 'Aktiv', 'boolean', 'pods', FieldDefinition::WRITABLE),
'kennwort' => new FieldDefinition('kennwort', 'Kennwort', 'password', 'pods', FieldDefinition::WRITABLE),
]);
return $schema;
}
/**
* @param list<string> $deniedRoles
* @param array<array-key, mixed>|null $registry
*/
private function ability(
array $deniedRoles = [],
?string $storage = 'meta',
?array $registry = null
): DiviDynamicSourcesAbility {
$registry ??= DiviFixtures::dynamicContentRegistry();
return new DiviDynamicSourcesAbility(
$this->schema(),
new DynamicContentMap(static fn (int $id): array => $registry),
LibraryFixtures::roles($deniedRoles),
static fn (string $postType): int => 831,
static fn (string $postType): ?string => $storage
);
}
public function testListsDiviSourcesWithTheirPlaceholders(): void
{
$result = $this->ability()->execute(['post_type' => 'page']);
$sources = array_column($result['sources'], null, 'name');
self::assertSame(831, $result['sample_post_id']);
self::assertSame('{{post:title}}', $sources['post_title']['placeholder']);
self::assertSame('{{link:<ID>}}', $sources['post_link_url_page']['placeholder']);
self::assertNull($sources['site_title']['placeholder']);
self::assertArrayNotHasKey('loop_post_title', $sources);
self::assertEquals(['post_id' => 'select_post'], (array) $sources['post_link_url_page']['settings']);
self::assertFalse($this->ability()->isDirect());
self::assertTrue($this->ability()->annotations()['readonly']);
self::assertSame('lit/divi-dynamic-sources', $this->ability()->name());
}
public function testOffersTextLikeFieldsAndNamesTheOthers(): void
{
$result = $this->ability()->execute(['post_type' => 'page']);
self::assertSame(['telefon', 'notiz'], array_column($result['fields'], 'key'));
self::assertSame('{{field:notiz}}', $result['fields'][1]['placeholder']);
$omitted = array_column($result['omitted'], 'reason', 'key');
self::assertSame(['foto', 'projekte', '_intern', 'gefunden', 'aktiv', 'kennwort'], array_keys($omitted));
self::assertStringContainsString('nur IDs', $omitted['foto']);
self::assertStringContainsString('nur IDs', $omitted['projekte']);
self::assertStringContainsString('„_“', $omitted['_intern']);
// Ruling P9: unknown field types are not offered.
self::assertStringContainsString('unbekannt', $omitted['gefunden']);
self::assertStringContainsString('nicht textartig', $omitted['aktiv']);
self::assertStringContainsString('nicht textartig', $omitted['kennwort']);
}
public function testProtectedAndHiddenFieldsLeakNothing(): void
{
$result = $this->ability()->execute(['post_type' => 'page']);
$json = (string) json_encode($result);
// Hidden fields are neither offered nor named (security audit M4).
self::assertStringNotContainsString('geheim', $json);
self::assertStringNotContainsString('Geheim', $json);
// The protected key is named (so the client knows why) but never offered with a placeholder.
self::assertStringNotContainsString('{{field:_intern}}', $json);
self::assertSame(['key', 'label', 'reason'], array_keys($result['omitted'][2]));
}
public function testSourcesNeverNameProtectedMetaOptions(): void
{
$registry = DiviFixtures::dynamicContentRegistry() + [
'custom_meta__edit_lock' => ['label' => '_edit_lock', 'group' => 'Custom', 'fields' => []],
];
$result = $this->ability([], 'meta', $registry)->execute(['post_type' => 'page']);
self::assertStringNotContainsString('_edit_lock', (string) json_encode($result));
}
public function testPodsTableStorageIsNotReadableByDivi(): void
{
$result = $this->ability([], 'table')->execute(['post_type' => 'page']);
self::assertSame(['telefon'], array_column($result['fields'], 'key'));
self::assertStringContainsString('Tabellen', array_column($result['omitted'], 'reason', 'key')['notiz']);
}
public function testFieldListNeedsTheDiviRole(): void
{
$result = $this->ability(['read_dynamic_content_custom_fields'])->execute(['post_type' => 'page']);
self::assertSame([], $result['fields']);
self::assertSame([], $result['omitted']);
self::assertStringContainsString('Rollen-Editor', implode(' ', $result['notes']));
}
public function testWithoutDivisRegistryThePlaceholderSourcesAreShown(): void
{
$result = $this->ability([], 'meta', [])->execute(['post_type' => 'page']);
self::assertContains('post_title', array_column($result['sources'], 'name'));
self::assertStringContainsString('nicht verfügbar', implode(' ', $result['notes']));
}
public function testUnknownPostTypeIsNotFound(): void
{
Functions\when('get_post_type_object')->justReturn(null);
self::assertToolError($this->ability()->permission(['post_type' => 'nix']), 'not_found');
self::assertToolError($this->ability()->execute(['post_type' => 'nix']), 'not_found');
}
public function testNeedsTheTypesEditCapability(): void
{
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap !== 'edit_pages');
self::assertToolError($this->ability()->permission(['post_type' => 'page']), 'forbidden');
}
}