Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Pages/WpLinkTargetsTest.php
T
janniclas.loosenandClaude Opus 5.5 82ef69a99c fix: link targets never reveal library entries by type (ruling Q14)
WpLinkTargets answers the library, internal "_" types and every type
without a Divi post_link_url option like a missing ID; replace-text strips
library IDs only from global subtrees.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 15:13:40 +02:00

122 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
use Brain\Monkey\Functions;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Pages\WpLinkTargets;
use LIT\MCP\Integrations\Divi\Pages\WpValueSanitizer;
use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use WP_Post;
final class WpLinkTargetsTest extends TestCase
{
public function testOnlyReadableLivingPostsCanBeLinked(): void
{
$posts = [
839 => new WP_Post(['ID' => 839, 'post_type' => 'page', 'post_status' => 'draft']),
841 => new WP_Post(['ID' => 841, 'post_type' => 'page', 'post_status' => 'trash']),
842 => new WP_Post(['ID' => 842, 'post_type' => 'page', 'post_status' => 'private']),
843 => new WP_Post(['ID' => 843, 'post_type' => 'revision', 'post_status' => 'inherit']),
844 => new WP_Post(['ID' => 844, 'post_type' => 'page', 'post_status' => 'auto-draft']),
845 => new WP_Post(['ID' => 845, 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_parent' => 0]),
846 => new WP_Post(['ID' => 846, 'post_type' => 'attachment', 'post_status' => 'trash']),
847 => new WP_Post(['ID' => 847, 'post_type' => 'attachment', 'post_status' => 'private']),
];
Functions\when('get_post')->alias(static fn (int $id): ?WP_Post => $posts[$id] ?? null);
Functions\when('current_user_can')->alias(
static fn (string $cap, int $id): bool => $cap === 'read_post' && $id !== 842
);
$links = new WpLinkTargets(self::linkable());
self::assertSame('page', $links->postTypeOf(839));
self::assertNull($links->postTypeOf(841), 'trashed');
self::assertNull($links->postTypeOf(842), 'not readable');
self::assertNull($links->postTypeOf(843), 'revision');
self::assertNull($links->postTypeOf(844), 'auto-draft');
self::assertNull($links->postTypeOf(900), 'missing');
self::assertNull($links->postTypeOf(845), 'unattached attachment');
self::assertNull($links->postTypeOf(846), 'trashed attachment');
self::assertNull($links->postTypeOf(847), 'attachment with an own status (ruling P10: no attachment)');
}
/** Post types Divi offers post_link_url_<type> for in these tests (et_builder_get_public_post_types). */
private static function linkable(): \Closure
{
return static fn (string $type): bool => in_array($type, ['page', 'post', 'attachment', 'lit_person'], true);
}
/**
* Ruling Q14: a type without a Divi link option (the library, internal "_" types, any other) is no
* link target – answered like a missing ID, so {{link:}} tells nobody which IDs are library entries.
*/
public function testTypesWithoutADiviLinkOptionAreNotFound(): void
{
$posts = [
1058 => new WP_Post(['ID' => 1058, 'post_type' => 'et_pb_layout', 'post_status' => 'publish']),
1070 => new WP_Post(['ID' => 1070, 'post_type' => '_internal', 'post_status' => 'publish']),
1071 => new WP_Post(['ID' => 1071, 'post_type' => 'wp_template', 'post_status' => 'publish']),
1072 => new WP_Post(['ID' => 1072, 'post_type' => 'lit_person', 'post_status' => 'publish']),
];
Functions\when('get_post')->alias(static fn (int $id): ?WP_Post => $posts[$id] ?? null);
Functions\when('current_user_can')->justReturn(true);
$everything = new WpLinkTargets(static fn (string $type): bool => true);
$links = new WpLinkTargets(self::linkable());
self::assertNull($links->postTypeOf(1058), 'library entry');
self::assertNull($links->postTypeOf(1070), 'internal type');
self::assertNull($links->postTypeOf(1071), 'type without link option');
self::assertSame('lit_person', $links->postTypeOf(1072));
self::assertNull($everything->postTypeOf(1058), 'the library never, whatever Divi lists');
self::assertNull($everything->postTypeOf(1070), 'internal types never, whatever Divi lists');
self::assertNull((new WpLinkTargets())->postTypeOf(1072), 'without Divi no type has a link option');
}
/**
* Ruling Q14: {{link:}} and the full form answer a library ID exactly like a missing ID – with and
* without the library right (read_post on the entry), so no guessed ID reveals a library entry.
*/
public function testALibraryIdIsAnsweredLikeAMissingId(): void
{
Functions\when('get_post')->alias(static fn (int $id): ?WP_Post => $id === 1058
? new WP_Post(['ID' => 1058, 'post_type' => 'et_pb_layout', 'post_status' => 'publish'])
: null);
$links = new WpLinkTargets(self::linkable());
$placeholders = new Placeholders(DiviFixtures::designSystem(), $links);
$ref = new VariableRef(DiviFixtures::designSystem(), ['post_link_url_page' => ['post_id' => 'select_post']]);
$sanitizer = new WpValueSanitizer($links);
$answer = static function (callable $call, string $id): string {
try {
$call($id);
return 'accepted';
} catch (InvalidArgumentException $e) {
return str_replace($id, '<ID>', $e->getMessage());
}
};
$short = static fn (string $id): string => $placeholders->resolve('{{link:' . $id . '}}');
$full = static fn (string $id): string => $ref->canonical(
'$variable({"type":"content","value":{"name":"post_link_url_page","settings":{"post_id":"' . $id . '"}}})$',
$sanitizer,
true
);
foreach (['with the library right' => true, 'without it' => false] as $case => $right) {
Functions\when('current_user_can')->justReturn($right);
foreach (['{{link:}}' => $short, 'post_link_url_page' => $full] as $form => $call) {
$library = $answer($call, '1058');
self::assertNotSame('accepted', $library, $case . ' ' . $form);
self::assertSame($answer($call, '1066'), $library, $case . ' ' . $form);
self::assertStringNotContainsString('et_pb_layout', $library, $case . ' ' . $form);
}
}
}
}