Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Library/GlobalUsagesTest.php
T
janniclas.loosenandClaude Opus 5.5 ce4dc776e5 fix: read globalModule like Divi's render in the guard and usage search (hardening)
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>
2026-09-27 04:16:18 +02:00

160 lines
6.6 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\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()));
}
}