StoredNodeGuard accepts a zero-padded library id (Divi intval()s it) but refuses instances whose opener holds more than one or an escaped globalModule, so the entry Divi renders is the one checked. GlobalUsages matches "globalModule":"0*<id>" with REGEXP behind a cheap LIKE prefilter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
160 lines
6.6 KiB
PHP
160 lines
6.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||
|
||
use Brain\Monkey\Functions;
|
||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||
use LIT\MCP\Tests\Unit\TestCase;
|
||
use Mockery;
|
||
use WP_Post;
|
||
|
||
final class GlobalUsagesTest extends TestCase
|
||
{
|
||
/** Audit L1: one query, ID, status and type only – never the content, no COUNT, no second scan. */
|
||
public function testFindsInstancesLikeDiviWithOneQueryWithoutContent(): void
|
||
{
|
||
$db = Mockery::mock(\wpdb::class)->makePartial();
|
||
$queries = [];
|
||
$db->allows('get_results')->andReturnUsing(static function (string $sql) use (&$queries): array {
|
||
$queries[] = $sql;
|
||
|
||
return [
|
||
['ID' => '1053', 'post_status' => 'draft', 'post_type' => 'page'],
|
||
['ID' => '1061', 'post_status' => 'private', 'post_type' => 'et_header_layout'],
|
||
];
|
||
});
|
||
|
||
$rows = (new GlobalUsages($db))->rows(1058);
|
||
|
||
self::assertSame(
|
||
[
|
||
['id' => 1053, 'status' => 'draft', 'type' => 'page'],
|
||
['id' => 1061, 'status' => 'private', 'type' => 'et_header_layout'],
|
||
],
|
||
$rows
|
||
);
|
||
self::assertCount(1, $queries);
|
||
self::assertStringStartsWith('SELECT ID, post_status, post_type FROM', $queries[0]);
|
||
self::assertStringContainsString("post_content LIKE '%\"globalModule\":\"%'", $queries[0]);
|
||
self::assertStringContainsString("post_content REGEXP '\"globalModule\":\"0*1058\"'", $queries[0]);
|
||
self::assertStringContainsString("post_status NOT IN ('trash', 'auto-draft', 'inherit')", $queries[0]);
|
||
self::assertStringContainsString("post_type <> 'revision'", $queries[0]);
|
||
self::assertStringNotContainsString('LIMIT', $queries[0]);
|
||
self::assertStringNotContainsString('post_content,', $queries[0]);
|
||
}
|
||
|
||
/** Audit L4: readable posts with title, unreadable ones only counted. */
|
||
public function testListsReadablePostsAndCountsTheOthers(): void
|
||
{
|
||
$usages = LibraryFixtures::usages([
|
||
['id' => 1053, 'status' => 'draft', 'type' => 'page'],
|
||
['id' => 1061, 'status' => 'private', 'type' => 'et_header_layout'],
|
||
]);
|
||
Functions\when('get_post')->alias(
|
||
static fn (int $id): WP_Post => new WP_Post(['ID' => $id, 'post_title' => 'Titel ' . $id])
|
||
);
|
||
Functions\when('current_user_can')->alias(
|
||
static fn (string $cap, int $id): bool => $id !== 1061
|
||
);
|
||
|
||
$report = $usages->report(1058);
|
||
|
||
self::assertSame(2, $report->count);
|
||
self::assertSame(
|
||
[['post_id' => 1053, 'title' => 'Titel 1053', 'status' => 'draft', 'type' => 'page']],
|
||
$report->items
|
||
);
|
||
self::assertSame(1, $report->hidden);
|
||
self::assertSame(1, $report->live);
|
||
self::assertSame([], $report->blocked);
|
||
self::assertSame(1, $report->blockedHidden);
|
||
self::assertSame(
|
||
['affected' => $report->items, 'affected_count' => 2, 'hidden_count' => 1, 'blocked_hidden_count' => 1],
|
||
$report->details()
|
||
);
|
||
}
|
||
|
||
/** Audit L1: rights are checked in chunks, each with its own primed cache that is dropped afterwards. */
|
||
public function testChecksLargeSetsInChunksAndReleasesTheCache(): void
|
||
{
|
||
$usages = LibraryFixtures::usages(LibraryFixtures::rows(range(1, 450)));
|
||
$primed = [];
|
||
Functions\when('_prime_post_caches')->alias(static function (array $ids) use (&$primed): void {
|
||
$primed[] = count($ids);
|
||
});
|
||
$deleted = 0;
|
||
Functions\when('wp_cache_delete')->alias(static function () use (&$deleted): bool {
|
||
++$deleted;
|
||
|
||
return true;
|
||
});
|
||
Functions\when('get_post')->justReturn(null);
|
||
Functions\when('current_user_can')->alias(static fn (string $cap, int $id): bool => $id % 100 !== 0);
|
||
|
||
$report = $usages->report(1058);
|
||
|
||
self::assertSame([GlobalUsages::CHUNK, GlobalUsages::CHUNK, 50], $primed);
|
||
self::assertSame(450, $deleted);
|
||
self::assertSame(450, $report->count);
|
||
self::assertCount(GlobalUsages::LIMIT, $report->items);
|
||
self::assertSame(4, $report->hidden);
|
||
self::assertSame(4, $report->blockedHidden);
|
||
self::assertTrue($report->blocks());
|
||
}
|
||
|
||
/** Audit L1: above the hard cap nothing is checked for edit_post, and the report blocks. */
|
||
public function testAboveTheHardCapNoPostIsCheckedForEditing(): void
|
||
{
|
||
$usages = LibraryFixtures::usages(LibraryFixtures::rows(range(1, GlobalUsages::HARD_CAP + 1)));
|
||
Functions\when('get_post')->justReturn(null);
|
||
$asked = [];
|
||
Functions\when('current_user_can')->alias(static function (string $cap) use (&$asked): bool {
|
||
$asked[$cap] = ($asked[$cap] ?? 0) + 1;
|
||
|
||
return true;
|
||
});
|
||
|
||
$report = $usages->report(1058);
|
||
|
||
self::assertTrue($report->tooMany);
|
||
self::assertTrue($report->blocks());
|
||
self::assertArrayNotHasKey('edit_post', $asked);
|
||
self::assertSame(GlobalUsages::CHECK_CAP, $asked['read_post']);
|
||
}
|
||
|
||
/** Audit L6: Theme Builder layouts additionally need Divi's role option theme_builder. */
|
||
public function testThemeBuilderLayoutsNeedTheThemeBuilderRole(): void
|
||
{
|
||
$rows = [
|
||
['id' => 1053, 'status' => 'publish', 'type' => 'page'],
|
||
['id' => 1061, 'status' => 'publish', 'type' => 'et_header_layout'],
|
||
['id' => 1062, 'status' => 'publish', 'type' => 'et_body_layout'],
|
||
['id' => 1063, 'status' => 'publish', 'type' => 'et_footer_layout'],
|
||
['id' => 1064, 'status' => 'publish', 'type' => 'et_template'],
|
||
];
|
||
Functions\when('get_post')->justReturn(null);
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
|
||
$without = LibraryFixtures::usages($rows, LibraryFixtures::roles(['theme_builder']))->report(1058);
|
||
$with = LibraryFixtures::usages($rows, LibraryFixtures::roles())->report(1058);
|
||
|
||
self::assertSame([1061, 1062, 1063, 1064], $without->blocked);
|
||
self::assertSame([], $with->blocked);
|
||
}
|
||
|
||
public function testAListingSkipsTheEditChecks(): void
|
||
{
|
||
$usages = LibraryFixtures::usages(LibraryFixtures::rows([1053]));
|
||
Functions\when('get_post')->justReturn(null);
|
||
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap !== 'edit_post');
|
||
|
||
$report = $usages->report(1058, false);
|
||
|
||
self::assertFalse($report->blocks());
|
||
self::assertSame(['affected', 'affected_count'], array_keys($report->listing()));
|
||
}
|
||
}
|