Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
304 lines
12 KiB
PHP
304 lines
12 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
|
||
|
||
use Brain\Monkey\Functions;
|
||
use LIT\MCP\Abilities\WriteFailed;
|
||
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||
use LIT\MCP\Integrations\Divi\Pages\SaveScope;
|
||
use LIT\MCP\Tests\Unit\TestCase;
|
||
use RuntimeException;
|
||
use WP_Error;
|
||
use WP_Post;
|
||
|
||
final class DiviSaverTest extends TestCase
|
||
{
|
||
private WP_Post $post;
|
||
|
||
/** @var list<string> */
|
||
private array $calls = [];
|
||
|
||
/** @var array<string, string> */
|
||
private array $meta = [];
|
||
|
||
private string $log = '';
|
||
|
||
private string|false $previousLog = false;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
$this->post = new WP_Post(['ID' => 7, 'post_type' => 'page', 'post_content' => 'ALT']);
|
||
Functions\when('wp_save_post_revision')->alias(function (): int {
|
||
$this->calls[] = 'revision';
|
||
|
||
return 811;
|
||
});
|
||
Functions\when('wp_get_post_revisions')->justReturn([811 => new WP_Post(['ID' => 811])]);
|
||
Functions\when('update_post_meta')->alias(function (int $id, string $key, string $value): bool {
|
||
$this->calls[] = "meta:$key=$value";
|
||
$this->meta[$key] = $value;
|
||
|
||
return true;
|
||
});
|
||
Functions\when('get_post_meta')->alias(fn (int $id, string $key): string => $this->meta[$key] ?? '');
|
||
Functions\when('kses_remove_filters')->alias(function (): void {
|
||
$this->calls[] = 'kses off';
|
||
});
|
||
Functions\when('kses_init_filters')->alias(function (): void {
|
||
$this->calls[] = 'kses on';
|
||
});
|
||
// FollowUp logs failures with error_log() (a PHP internal): keep the test output clean.
|
||
$this->log = (string) tempnam(sys_get_temp_dir(), 'lit-log');
|
||
$this->previousLog = ini_set('error_log', $this->log);
|
||
}
|
||
|
||
protected function tearDown(): void
|
||
{
|
||
ini_set('error_log', (string) $this->previousLog);
|
||
@unlink($this->log);
|
||
parent::tearDown();
|
||
}
|
||
|
||
public function testSavesWithoutKsesForFilteredUsersAndSetsDiviFlags(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(false);
|
||
Functions\when('has_filter')->justReturn(10);
|
||
Functions\expect('wp_update_post')->once()->with(['ID' => 7, 'post_content' => 'NEU'], true)
|
||
->andReturnUsing(function (): int {
|
||
$this->calls[] = 'update';
|
||
|
||
return 7;
|
||
});
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_content' => 'NEU']));
|
||
|
||
$saved = (new DiviSaver())->save($this->post, 'NEU');
|
||
|
||
self::assertSame(['revision_id' => 811, 'warnings' => []], $saved);
|
||
self::assertSame(
|
||
// Ruling S37: the restore-point revision is captured inside the same kses-off window –
|
||
// it copies the stored content, and kses would strip an admin's embeds from it.
|
||
[
|
||
'kses off', 'revision', 'update', 'kses on',
|
||
'meta:_et_pb_use_divi_5=on', 'meta:_et_pb_use_builder=on', 'meta:_et_pb_show_page_creation=off',
|
||
],
|
||
$this->calls
|
||
);
|
||
}
|
||
|
||
public function testKeepsKsesUntouchedForUnfilteredUsersAndWarnsWhenAFilterChangedTheContent(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
Functions\when('wp_update_post')->justReturn(7);
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_content' => 'NEU, gefiltert']));
|
||
|
||
$saved = (new DiviSaver())->save($this->post, 'NEU');
|
||
|
||
self::assertNotContains('kses off', $this->calls);
|
||
self::assertSame(['revision'], array_slice($this->calls, 0, 1));
|
||
self::assertSame([DiviSaver::CHANGED_ON_SAVE], $saved['warnings']);
|
||
}
|
||
|
||
public function testKsesStaysUntouchedWhenItIsNotActive(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(false);
|
||
Functions\when('has_filter')->justReturn(false);
|
||
Functions\when('wp_update_post')->justReturn(7);
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_content' => 'NEU']));
|
||
|
||
(new DiviSaver())->save($this->post, 'NEU');
|
||
|
||
self::assertNotContains('kses off', $this->calls);
|
||
self::assertNotContains('kses on', $this->calls);
|
||
}
|
||
|
||
public function testRefusedUpdateIsACleanFailure(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
Functions\when('wp_update_post')->justReturn(new WP_Error('db_update_error', 'DB kaputt'));
|
||
|
||
try {
|
||
(new DiviSaver())->save($this->post, 'NEU');
|
||
self::fail('WriteFailed expected');
|
||
} catch (WriteFailed $e) {
|
||
self::assertTrue($e->rolledBack);
|
||
self::assertSame(811, $e->revisionId);
|
||
self::assertStringContainsString('Revision 811 bleibt erhalten', $e->toError()->get_error_message());
|
||
}
|
||
}
|
||
|
||
public function testAThrowingHookRestoresThePreviousContentAndKsesComesBack(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(false);
|
||
Functions\when('has_filter')->justReturn(10);
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_status' => 'draft']));
|
||
$written = [];
|
||
Functions\when('wp_update_post')->alias(function (array $data) use (&$written): int {
|
||
$written[] = $data['post_content'];
|
||
$this->calls[] = 'update';
|
||
if ($data['post_content'] === 'NEU') {
|
||
throw new RuntimeException('save_post hook failed');
|
||
}
|
||
|
||
return 7;
|
||
});
|
||
|
||
try {
|
||
(new DiviSaver())->save($this->post, 'NEU');
|
||
self::fail('WriteFailed expected');
|
||
} catch (WriteFailed $e) {
|
||
self::assertTrue($e->rolledBack);
|
||
self::assertSame(['NEU', 'ALT'], $written);
|
||
// The kses bypass is restored in finally – after the failed save and after the restore.
|
||
self::assertSame(
|
||
['kses off', 'revision', 'update', 'kses on', 'kses off', 'update', 'kses on'],
|
||
$this->calls
|
||
);
|
||
}
|
||
}
|
||
|
||
public function testFailedRestoreIsAPartialWrite(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_status' => 'draft']));
|
||
Functions\when('wp_update_post')->alias(static function (): int {
|
||
throw new RuntimeException('database gone');
|
||
});
|
||
|
||
try {
|
||
(new DiviSaver())->save($this->post, 'NEU');
|
||
self::fail('WriteFailed expected');
|
||
} catch (WriteFailed $e) {
|
||
self::assertFalse($e->rolledBack);
|
||
self::assertToolError($e->toError(), 'partial_write');
|
||
}
|
||
}
|
||
|
||
public function testRestoreNeverPublishesAMissedSchedule(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
Functions\when('get_post')->justReturn(new WP_Post([
|
||
'ID' => 7,
|
||
'post_status' => 'future',
|
||
'post_date_gmt' => '2020-01-01 00:00:00',
|
||
]));
|
||
$written = [];
|
||
Functions\when('wp_update_post')->alias(static function (array $data) use (&$written): int {
|
||
$written[] = $data['post_content'];
|
||
throw new RuntimeException('save_post hook failed');
|
||
});
|
||
|
||
try {
|
||
(new DiviSaver())->save($this->post, 'NEU');
|
||
self::fail('WriteFailed expected');
|
||
} catch (WriteFailed $e) {
|
||
self::assertFalse($e->rolledBack);
|
||
self::assertSame(['NEU'], $written);
|
||
}
|
||
}
|
||
|
||
public function testFlagFailureIsOnlyAWarning(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
Functions\when('wp_update_post')->justReturn(7);
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_content' => 'NEU']));
|
||
Functions\when('get_post_meta')->justReturn('');
|
||
|
||
$saved = (new DiviSaver())->save($this->post, 'NEU');
|
||
|
||
self::assertSame(811, $saved['revision_id']);
|
||
self::assertCount(1, $saved['warnings']);
|
||
self::assertStringContainsString('Divi-Kennzeichen', $saved['warnings'][0]);
|
||
self::assertStringContainsString('divi builder flags failed', (string) file_get_contents($this->log));
|
||
}
|
||
|
||
public function testSaveNewWritesTheFirstContentWithoutKsesAndSetsTheFlagsStrictly(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(false);
|
||
Functions\when('has_filter')->justReturn(10);
|
||
Functions\expect('wp_update_post')->once()->with(['ID' => 42, 'post_content' => 'NEU'], true)
|
||
->andReturnUsing(function (): int {
|
||
$this->calls[] = 'update';
|
||
|
||
return 42;
|
||
});
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 42, 'post_content' => 'NEU']));
|
||
Functions\expect('wp_save_post_revision')->never();
|
||
|
||
self::assertSame([], (new DiviSaver())->saveNew(42, 'NEU'));
|
||
self::assertSame(
|
||
// Same kses window as save(), but no restore point: the new draft has no earlier state.
|
||
[
|
||
'kses off', 'update', 'kses on',
|
||
'meta:_et_pb_use_divi_5=on', 'meta:_et_pb_use_builder=on', 'meta:_et_pb_show_page_creation=off',
|
||
],
|
||
$this->calls
|
||
);
|
||
}
|
||
|
||
public function testSaveNewReportsAContentChangedOnSaveAndKeepsKsesForUnfilteredUsers(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
Functions\when('has_filter')->justReturn(10);
|
||
Functions\when('wp_update_post')->justReturn(42);
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 42, 'post_content' => 'ANDERS']));
|
||
|
||
self::assertSame([DiviSaver::CHANGED_ON_SAVE], (new DiviSaver())->saveNew(42, 'NEU'));
|
||
self::assertNotContains('kses off', $this->calls);
|
||
}
|
||
|
||
public function testSaveNewThrowsWhenTheUpdateIsRefusedOrAFlagIsMissing(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(false);
|
||
Functions\when('has_filter')->justReturn(10);
|
||
Functions\when('wp_update_post')->justReturn(new WP_Error('db', 'kaputt'));
|
||
|
||
try {
|
||
(new DiviSaver())->saveNew(42, 'NEU');
|
||
self::fail('RuntimeException expected');
|
||
} catch (RuntimeException $e) {
|
||
self::assertStringContainsString('kaputt', $e->getMessage());
|
||
}
|
||
self::assertSame(['kses off', 'kses on'], $this->calls, 'kses comes back, no flags on a failed write');
|
||
|
||
Functions\when('wp_update_post')->justReturn(42);
|
||
Functions\when('get_post_meta')->justReturn('');
|
||
$this->expectException(RuntimeException::class);
|
||
(new DiviSaver())->saveNew(42, 'NEU');
|
||
}
|
||
|
||
public function testLibraryItemsGetNoDiviFlagsAndGlobalOnesClearEveryPage(): void
|
||
{
|
||
\ET_Core_PageResource::$removed = [];
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
Functions\when('wp_update_post')->justReturn(7);
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_content' => 'NEU']));
|
||
$cleared = [];
|
||
Functions\when('et_theme_builder_clear_wp_cache')->alias(static function (string $what) use (&$cleared): void {
|
||
$cleared[] = $what;
|
||
});
|
||
|
||
(new DiviSaver())->save($this->post, 'NEU', SaveScope::LibraryItem);
|
||
self::assertSame([], array_filter($this->calls, static fn (string $call): bool => str_starts_with($call, 'meta:')));
|
||
self::assertSame([[7, 'all', true]], \ET_Core_PageResource::$removed);
|
||
self::assertSame([], $cleared);
|
||
|
||
\ET_Core_PageResource::$removed = [];
|
||
(new DiviSaver())->save($this->post, 'NEU', SaveScope::GlobalItem);
|
||
self::assertSame([[7, 'all', true], ['all', 'all', true]], \ET_Core_PageResource::$removed);
|
||
self::assertSame(['all'], $cleared);
|
||
}
|
||
|
||
public function testSaveNewOfALibraryItemSetsNoFlags(): void
|
||
{
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
Functions\when('wp_update_post')->justReturn(42);
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 42, 'post_content' => 'NEU']));
|
||
|
||
self::assertSame([], (new DiviSaver())->saveNew(42, 'NEU', SaveScope::LibraryItem));
|
||
self::assertSame([], array_filter($this->calls, static fn (string $call): bool => str_starts_with($call, 'meta:')));
|
||
}
|
||
}
|