Files
WP-LIT-MCP/tests/Unit/Integrations/Media/MediaUsageFinderTest.php
T
janniclas.loosenandClaude Opus 5.5 b197d8171f fix: media usage search never reports a capped scan as unused
Adds widgets and header/background theme mods, integer galleryIds,
wider pre-filter, regex failures as possible usages, newest-first
candidates, and usages_complete/usages_truncated in media-get.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-26 02:02:21 +02:00

320 lines
13 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\Media;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Media\MediaUsageFinder;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use Mockery\MockInterface;
final class MediaUsageFinderTest extends TestCase
{
/** @var list<string> */
private array $queries = [];
/**
* @param array<int, string> $contents Candidate post ID => post_content.
* @param list<object> $featured
* @param list<object> $fields
* @param array<string, mixed> $options Option name => value.
* @param array<string, mixed> $themeMods Theme mod name => value.
*/
private function finder(
array $contents = [],
array $featured = [],
array $fields = [],
array $options = [],
array $themeMods = [],
array $mediaKeys = [],
string $file = '2026/09/foto-scaled.jpg'
): MediaUsageFinder {
Functions\when('get_post_meta')->justReturn($file);
Functions\when('get_option')->alias(static fn (string $name) => $options[$name] ?? false);
Functions\when('get_theme_mod')->alias(static fn (string $name) => $themeMods[$name] ?? false);
Functions\when('post_type_exists')->alias(static fn (string $type): bool => $type !== 'ghost');
Functions\when('current_user_can')->alias(static fn (string $cap, int $id): bool => $cap === 'read_post' && $id !== 13);
/** @var MockInterface&\wpdb $db */
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->allows('get_results')->andReturnUsing(function (string $sql) use ($contents, $featured, $fields): array {
$this->queries[] = $sql;
if (str_contains($sql, '_thumbnail_id')) {
return $featured;
}
if (str_contains($sql, 'pm.meta_key IN')) {
return $fields;
}
if (str_contains($sql, 'post_content LIKE')) {
return array_map(
static fn (int $id): object => (object) ['ID' => $id, 'post_title' => 'Seite ' . $id, 'post_type' => 'page'],
array_keys($contents)
);
}
if (str_contains($sql, 'p.ID IN')) {
// No regex here: the PCRE limit test runs this with a backtrack limit of 1.
$list = substr($sql, (int) strpos($sql, 'p.ID IN (') + strlen('p.ID IN ('));
$ids = array_map('intval', explode(',', substr($list, 0, (int) strpos($list, ')'))));
$rows = [];
foreach ($ids as $id) {
if (isset($contents[$id])) {
$rows[] = (object) ['ID' => $id, 'post_content' => $contents[$id]];
}
}
return $rows;
}
return [];
});
return new MediaUsageFinder(static fn (): array => $mediaKeys, $db);
}
/** @return array<int|string, string> post_id (or title for site usages) => certainty */
private static function certainties(array $usages): array
{
$result = [];
foreach ($usages as $usage) {
$result[$usage['post_id'] === 0 ? $usage['title'] : $usage['post_id']] = $usage['certainty'];
}
return $result;
}
private static function post(int $id, string $title, string $type = 'page'): object
{
return (object) ['ID' => $id, 'post_title' => $title, 'post_type' => $type];
}
public function testCombinesFeaturedFieldSiteAndContentUsages(): void
{
$usages = $this->finder(
[5 => '<img class="wp-image-33">', 9 => '<img class="wp-image-33 size-full">'],
[self::post(5, 'Team')],
[self::post(12, 'Anna', 'lit_person')],
['site_icon' => '33'],
['custom_logo' => 33],
['photo']
)->usages(33);
self::assertSame(
[
['post_id' => 5, 'title' => 'Team', 'post_type' => 'page', 'where' => 'featured', 'certainty' => 'sicher'],
['post_id' => 12, 'title' => 'Anna', 'post_type' => 'lit_person', 'where' => 'field', 'certainty' => 'sicher'],
['post_id' => 0, 'title' => 'Website-Icon', 'post_type' => 'option', 'where' => 'site_icon', 'certainty' => 'sicher'],
['post_id' => 0, 'title' => 'Logo (Customizer)', 'post_type' => 'theme_mod', 'where' => 'custom_logo', 'certainty' => 'sicher'],
['post_id' => 9, 'title' => 'Seite 9', 'post_type' => 'page', 'where' => 'content', 'certainty' => 'sicher'],
],
$usages
);
}
public function testQueriesArePreparedBoundedAndNewestFirst(): void
{
$this->finder([5 => 'x'], [], [], [], [], ['photo'])->usages(33, 7);
[$featured, $fields, $candidates] = $this->queries;
self::assertStringContainsString("pm.meta_value = '33'", $featured);
self::assertStringContainsString('ORDER BY p.ID DESC LIMIT 8', $featured);
self::assertStringContainsString("pm.meta_key IN ('photo')", $fields);
self::assertStringContainsString('ORDER BY p.ID DESC LIMIT 8', $fields);
$likes = [
'%wp-image-33%', '%"id":33%', '%"id":"33"%', '%"id": 33%', '%"id": "33"%', '%"mediaId":33%',
'%"mediaId":"33"%', '%"mediaId": 33%', '%ids=%33%', '%"ids":%33%', '%galleryIds":%33%',
'%/2026/09/foto%', '%\\\\/2026\\\\/09\\\\/foto%',
];
foreach ($likes as $like) {
self::assertStringContainsString("'" . $like . "'", $candidates, $like);
}
self::assertStringContainsString('ORDER BY p.ID DESC LIMIT 200', $candidates);
self::assertStringNotContainsString('post_content,', $candidates, 'candidates must not load content');
}
public function testCertainContentReferencesByAttachmentId(): void
{
$contents = [
1 => '[gallery ids="4,33,7"]',
2 => '[et_pb_gallery gallery_ids="33,8"][/et_pb_gallery]',
3 => '<!-- wp:image {"id":33,"sizeSlug":"large"} --><figure></figure><!-- /wp:image -->',
4 => '<!-- wp:gallery {"ids":[2,33]} /-->',
5 => '<!-- wp:media-text {"mediaId":"33","mediaType":"image"} --><div></div><!-- /wp:media-text -->',
6 => '<!-- wp:cover {"url":"x", "id": 33} --><div></div><!-- /wp:cover -->',
7 => '<!-- wp:divi/image {"image":{"innerContent":{"desktop":{"value":{"src":"https://k.de/a.jpg","id":"33"}}}}} /-->',
8 => '<!-- wp:divi/gallery {"image":{"advanced":{"galleryIds":{"desktop":{"value":"12,33"}}}}} /-->',
9 => '<!-- wp:divi/text {"content":{"innerContent":{"desktop":{"value":"[et_pb_gallery gallery_ids=\\u002233\\u0022]"}}}} /-->',
10 => '<!-- wp:divi/gallery {"image":{"advanced":{"galleryIds":{"desktop":{"value":[12,33]}}}}} /-->',
];
$usages = $this->finder($contents)->usages(33);
self::assertSame(array_fill_keys(range(1, 10), 'sicher'), self::certainties($usages));
}
public function testFileUrlMatchesAreOnlyPossibleUsages(): void
{
$contents = [
1 => '<img src="https://k.de/wp-content/uploads/2026/09/foto-300x200.jpg">',
2 => '[et_pb_image src="https://k.de/wp-content/uploads/2026/09/foto-scaled.jpg"]',
3 => '<!-- wp:divi/image {"image":{"innerContent":{"desktop":{"value":{"src":"https:\/\/k.de\/wp-content\/uploads\/2026\/09\/foto.webp"}}}}} /-->',
];
self::assertSame(
[1 => 'möglicherweise', 2 => 'möglicherweise', 3 => 'möglicherweise'],
self::certainties($this->finder($contents)->usages(33))
);
}
public function testLookalikesAreNoUsage(): void
{
$contents = [
1 => '<img class="wp-image-330">',
2 => '<!-- wp:image {"id":330} /--> <!-- wp:gallery {"ids":[133,334]} /-->',
3 => '[gallery ids="330,1334"] <!-- wp:paragraph {"id":33} /-->',
4 => '<img src="https://k.de/wp-content/uploads/2026/09/foto-2.jpg"> /2025/09/foto.jpg',
5 => '<!-- wp:divi/gallery {"image":{"advanced":{"galleryIds":{"desktop":{"value":[330,3]}}}}} /-->',
];
$finder = $this->finder($contents);
self::assertSame([], $finder->usages(33));
self::assertFalse($finder->isUsed(33));
}
public function testCappedContentSearchIsNeverReportedAsUnused(): void
{
$finder = $this->finder(array_fill_keys(range(1, 200), 'nichts'));
self::assertTrue($finder->isUsed(33));
$scan = $finder->scan(33);
self::assertFalse($scan['complete']);
self::assertFalse($scan['truncated']);
self::assertSame(
[[
'post_id' => 0,
'title' => 'Inhaltssuche unvollständig – weitere Verwendungen möglich',
'post_type' => 'content',
'where' => 'content',
'certainty' => 'möglicherweise',
]],
$scan['usages']
);
}
public function testScanReportsTruncationBeyondMax(): void
{
$finder = $this->finder([], [self::post(1, 'A'), self::post(2, 'B'), self::post(3, 'C')]);
$scan = $finder->scan(33, 2);
self::assertCount(2, $scan['usages']);
self::assertTrue($scan['truncated']);
self::assertTrue($scan['complete']);
self::assertFalse($finder->scan(33, 3)['truncated']);
}
public function testRegexFailureCountsAsPossibleUsage(): void
{
$jit = ini_get('pcre.jit');
$backtrack = ini_get('pcre.backtrack_limit');
$finder = $this->finder([7 => str_repeat('wp-image-3 ', 200)]);
ini_set('pcre.jit', '0');
ini_set('pcre.backtrack_limit', '1');
try {
$usages = $finder->usages(33);
} finally {
ini_set('pcre.jit', (string) $jit);
ini_set('pcre.backtrack_limit', (string) $backtrack);
}
self::assertSame([7 => 'möglicherweise'], self::certainties($usages));
}
public function testWidgetsAndThemeMods(): void
{
$usages = $this->finder(
[],
[],
[],
[
'widget_media_image' => [3 => ['attachment_id' => 33, 'url' => 'x'], 4 => ['attachment_id' => 330], '_multiwidget' => 1],
'widget_media_gallery' => [2 => ['ids' => [5, 33]], 3 => ['ids' => [330]], '_multiwidget' => 1],
'widget_block' => [
4 => ['content' => '<!-- wp:image {"id":33} --><figure></figure><!-- /wp:image -->'],
5 => ['content' => '<img src="https://k.de/wp-content/uploads/2026/09/foto-150x150.jpg">'],
6 => ['content' => '<p>nichts</p>'],
'_multiwidget' => 1,
],
],
[
'header_image_data' => (object) ['attachment_id' => 33, 'url' => 'x'],
'background_image' => 'https://k.de/wp-content/uploads/2026/09/foto.jpg',
]
)->usages(33);
self::assertSame(
[
'Header-Bild' => 'sicher',
'Hintergrundbild' => 'möglicherweise',
'Widget media_image-3' => 'sicher',
'Widget media_gallery-2' => 'sicher',
'Widget block-4' => 'sicher',
'Widget block-5' => 'möglicherweise',
],
self::certainties($usages)
);
self::assertSame(['header_image', 'background_image', 'widget', 'widget', 'widget', 'widget'], array_column($usages, 'where'));
}
public function testUnreadableEntriesHideTheirTitleAndDiviLogoIsPossible(): void
{
$usages = $this->finder(
[13 => '<img class="wp-image-33">'],
[self::post(20, 'Geist', 'ghost')],
[],
['et_divi' => ['divi_logo' => 'https://k.de/wp-content/uploads/2026/09/foto.jpg']]
)->usages(33);
self::assertSame(
[
['post_id' => 20, 'title' => '(nicht lesbar)', 'post_type' => 'ghost', 'where' => 'featured', 'certainty' => 'sicher'],
['post_id' => 0, 'title' => 'Divi-Logo', 'post_type' => 'option', 'where' => 'divi_logo', 'certainty' => 'möglicherweise'],
['post_id' => 13, 'title' => '(nicht lesbar)', 'post_type' => 'page', 'where' => 'content', 'certainty' => 'sicher'],
],
$usages
);
}
public function testIdReferenceWinsOverAFileMatchInTheSameContent(): void
{
$usages = $this->finder([5 => '/2026/09/foto.jpg <img class="wp-image-33">'])->usages(33);
self::assertSame([5 => 'sicher'], self::certainties($usages));
}
public function testNoFieldQueryWithoutMediaFieldsAndUnusedMedia(): void
{
self::assertFalse($this->finder()->isUsed(33));
self::assertCount(2, $this->queries);
}
public function testIsUsedStopsAfterTheFirstHit(): void
{
$finder = $this->finder([5 => 'wp-image-33'], [self::post(5, 'Team')], [], [], [], ['photo']);
self::assertTrue($finder->isUsed(33));
self::assertCount(1, $this->queries);
}
public function testWithoutAttachedFileNoFilePatterns(): void
{
$this->finder([], [], [], [], [], [], '')->usages(33);
self::assertStringNotContainsString('foto', $this->queries[1]);
self::assertStringNotContainsString("'%/%'", $this->queries[1]);
}
}