Files
WP-LIT-MCP/tests/Unit/Integrations/WordPress/Render/PageRendererTest.php
T
janniclas.loosenandClaude Opus 5.5 b3cd0eee62 fix: bound render-summary strings, restore postdata globals, drain buffers
Review fix round 1 for lit/page-render-summary: extract shared Text::shorten()
(reused by NodeSummary), replace data: URIs with a short marker, prefer
data-src/data-lazy-src over placeholder src, dedup nested p/li/blockquote
text and ignore script/style/noscript content, restore every setup_postdata
global (not just $post), drain output buffers robustly around the_content,
and disclose that rendering can trigger oEmbed/network requests.

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

132 lines
4.6 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Render;
use Brain\Monkey\Filters;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\WordPress\Render\PageRenderer;
use LIT\MCP\Tests\Unit\TestCase;
use RuntimeException;
use WP_Post;
final class PageRendererTest extends TestCase
{
private const POSTDATA_GLOBALS = ['id', 'authordata', 'pages', 'page', 'more', 'numpages', 'multipage'];
protected function tearDown(): void
{
unset($GLOBALS['post']);
foreach (self::POSTDATA_GLOBALS as $name) {
unset($GLOBALS[$name]);
}
parent::tearDown();
}
public function testRendersTheContentFilterAndRestoresGlobalPostAndPostdata(): void
{
$previousPost = new WP_Post(['ID' => 1]);
$GLOBALS['post'] = $previousPost;
$GLOBALS['id'] = 1;
$GLOBALS['authordata'] = 'previous-author';
$GLOBALS['pages'] = ['previous'];
$GLOBALS['page'] = 1;
$GLOBALS['more'] = 1;
$GLOBALS['numpages'] = 1;
$GLOBALS['multipage'] = 0;
$post = new WP_Post(['ID' => 7, 'post_content' => '<!-- wp:divi/section /-->']);
$setup = [];
Functions\when('setup_postdata')->alias(static function (WP_Post $p) use (&$setup): bool {
$setup[] = $p->ID;
$GLOBALS['id'] = $p->ID;
$GLOBALS['authordata'] = 'author-of-' . $p->ID;
$GLOBALS['pages'] = ['page-of-' . $p->ID];
$GLOBALS['page'] = 2;
$GLOBALS['more'] = 0;
$GLOBALS['numpages'] = 3;
$GLOBALS['multipage'] = 1;
return true;
});
Filters\expectApplied('the_content')->once()->with('<!-- wp:divi/section /-->')->andReturnUsing(
static function () use ($post): string {
echo '<p>echoed</p>';
return $GLOBALS['post'] === $post ? '<div class="et_pb_section"></div>' : 'wrong post';
}
);
$html = (new PageRenderer())->render($post);
self::assertSame('<p>echoed</p><div class="et_pb_section"></div>', $html);
self::assertSame([7], $setup);
self::assertSame($previousPost, $GLOBALS['post']);
self::assertSame(1, $GLOBALS['id']);
self::assertSame('previous-author', $GLOBALS['authordata']);
self::assertSame(['previous'], $GLOBALS['pages']);
self::assertSame(1, $GLOBALS['page']);
self::assertSame(1, $GLOBALS['more']);
self::assertSame(1, $GLOBALS['numpages']);
self::assertSame(0, $GLOBALS['multipage']);
}
public function testRestoresEvenWhenRenderingThrows(): void
{
Functions\when('setup_postdata')->justReturn(true);
Filters\expectApplied('the_content')->once()->andReturnUsing(static function (): string {
throw new RuntimeException('module crashed');
});
try {
(new PageRenderer())->render(new WP_Post(['ID' => 7]));
self::fail('exception expected');
} catch (RuntimeException) {
self::assertArrayHasKey('post', $GLOBALS);
self::assertNull($GLOBALS['post']);
foreach (self::POSTDATA_GLOBALS as $name) {
self::assertArrayHasKey($name, $GLOBALS);
self::assertNull($GLOBALS[$name]);
}
}
}
public function testDrainsAnExtraOutputBufferAShortcodeLeavesOpen(): void
{
Functions\when('setup_postdata')->justReturn(true);
Filters\expectApplied('the_content')->once()->andReturnUsing(static function (): string {
echo 'before-';
ob_start(); // a buggy shortcode opens a buffer and never closes it
echo 'leaked';
return 'html';
});
$levelBefore = ob_get_level();
$html = (new PageRenderer())->render(new WP_Post(['ID' => 7]));
self::assertSame('before-leakedhtml', $html);
self::assertSame($levelBefore, ob_get_level());
}
public function testToleratesAShortcodeThatClosesOneOutputBufferTooMany(): void
{
Functions\when('setup_postdata')->justReturn(true);
Filters\expectApplied('the_content')->once()->andReturnUsing(static function (): string {
ob_start(); // shortcode's own nested buffer
echo 'own';
ob_end_clean(); // correct: closes its own buffer
ob_end_clean(); // bug: also closes PageRenderer's buffer
return 'html';
});
$levelBefore = ob_get_level();
$html = (new PageRenderer())->render(new WP_Post(['ID' => 7]));
self::assertSame('html', $html);
self::assertSame($levelBefore, ob_get_level());
}
}