Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Library/GlobalUsagesTest.php
T
janniclas.loosenandClaude Opus 5.5 e8f69f67ea fix: bounded usage scan and count-only unreadable posts in global edits (L1, L4)
GlobalUsages runs one query per request (ID, status, type – no content),
checks read_post/edit_post in chunks of 200 with a primed and released post
cache, lists at most 50 readable posts and refuses a confirmed global edit
above 2000 live references (unsupported, edit in the Divi builder). Posts the
caller may not read appear only as hidden_count / blocked_hidden_count;
blocked_ids names readable posts only.

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

139 lines
5.5 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("LIKE '%\"globalModule\":\"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']);
}
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()));
}
}