feat: lit/library-save creates library entries like Divi (E4)
Reuses NewDraft for insert and rollback (P6), requires edit_global_library for scope global (P7), refuses opaque and global roots before wrapping (P11) and checks the node with StoredNodeGuard. The source page is never written. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,7 @@ final class DiviIntegrationTest extends TestCase
|
||||
self::assertSame('Divi-Bibliothek', $library->label());
|
||||
self::assertFalse($library->isAvailable(), 'no Divi classes in unit tests');
|
||||
self::assertSame(
|
||||
['lit/library-list'],
|
||||
['lit/library-list', 'lit/library-save'],
|
||||
array_map(static fn (Ability $ability): string => $ability->name(), $library->abilities())
|
||||
);
|
||||
foreach ($library->abilities() as $ability) {
|
||||
|
||||
@@ -5,7 +5,11 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryItemSpec;
|
||||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
@@ -99,4 +103,130 @@ final class DiviLibraryStoreTest extends TestCase
|
||||
|
||||
self::assertSame(['scope'], (new DiviLibraryStore())->missingTaxonomies());
|
||||
}
|
||||
|
||||
private static function draft(?FakeChangeLog $log = null): NewDraft
|
||||
{
|
||||
return new NewDraft(null, $log ?? FakeChangeLog::forbidden(), 'lit/library-save', 'et_pb_layout', 'Bibliothekseintrag');
|
||||
}
|
||||
|
||||
public function testCreatesAPublishedEntryWithTermsAndMeta(): void
|
||||
{
|
||||
Functions\when('taxonomy_exists')->justReturn(true);
|
||||
$inserted = [];
|
||||
Functions\when('wp_insert_post')->alias(static function (array $data) use (&$inserted): int {
|
||||
$inserted = $data;
|
||||
|
||||
return 2001;
|
||||
});
|
||||
$terms = [];
|
||||
Functions\when('wp_set_object_terms')->alias(
|
||||
static function (int $id, array $values, string $taxonomy) use (&$terms): array {
|
||||
$terms[$taxonomy] = $values;
|
||||
|
||||
return [1];
|
||||
}
|
||||
);
|
||||
$meta = [];
|
||||
Functions\when('update_post_meta')->alias(static function (int $id, string $key, string $value) use (&$meta): bool {
|
||||
$meta[$key] = $value;
|
||||
|
||||
return true;
|
||||
});
|
||||
// By reference: an arrow function would capture the still empty $meta.
|
||||
Functions\when('get_post_meta')->alias(static function (int $id, string $key) use (&$meta): string {
|
||||
return $meta[$key] ?? '';
|
||||
});
|
||||
$spec = new LibraryItemSpec('Öffnungszeiten', 'module', true, 'regular', 'divi/text', 'page');
|
||||
|
||||
self::assertSame(2001, (new DiviLibraryStore())->create($spec, self::draft()));
|
||||
self::assertSame(
|
||||
['post_type' => 'et_pb_layout', 'post_status' => 'publish', 'post_title' => 'Öffnungszeiten', 'post_content' => ''],
|
||||
$inserted
|
||||
);
|
||||
self::assertSame(['scope' => ['global'], 'layout_type' => ['module'], 'module_width' => ['regular']], $terms);
|
||||
self::assertSame('[]', $meta['_et_pb_excluded_global_options']);
|
||||
}
|
||||
|
||||
public function testAFailedStepDeletesTheNewEntry(): void
|
||||
{
|
||||
Functions\when('taxonomy_exists')->justReturn(true);
|
||||
Functions\when('wp_insert_post')->justReturn(2001);
|
||||
Functions\when('wp_set_object_terms')->justReturn(new \WP_Error('invalid_taxonomy', 'Ungültige Taxonomie.'));
|
||||
Functions\expect('wp_delete_post')->once()->with(2001, true)->andReturn(new WP_Post(['ID' => 2001]));
|
||||
|
||||
try {
|
||||
(new DiviLibraryStore())->create(new LibraryItemSpec('X', 'section', false, 'regular', null, 'page'), self::draft());
|
||||
self::fail('WriteFailed expected');
|
||||
} catch (WriteFailed $e) {
|
||||
self::assertTrue($e->rolledBack);
|
||||
}
|
||||
}
|
||||
|
||||
/** Ruling P6: a failed rollback is an honest partial write, logged as partial (NewDraft). */
|
||||
public function testAFailedRollbackIsAPartialWrite(): void
|
||||
{
|
||||
Functions\when('taxonomy_exists')->justReturn(true);
|
||||
Functions\when('wp_insert_post')->justReturn(2001);
|
||||
Functions\when('wp_set_object_terms')->justReturn([1]);
|
||||
Functions\when('update_post_meta')->justReturn(false);
|
||||
Functions\when('get_post_meta')->justReturn('');
|
||||
Functions\when('wp_delete_post')->justReturn(false);
|
||||
$log = FakeChangeLog::partialsOnly();
|
||||
|
||||
try {
|
||||
(new DiviLibraryStore())->create(new LibraryItemSpec('X', 'section', false, 'regular', null, 'page'), self::draft($log));
|
||||
self::fail('WriteFailed expected');
|
||||
} catch (WriteFailed $e) {
|
||||
self::assertFalse($e->rolledBack);
|
||||
self::assertSame(2001, $e->postId);
|
||||
}
|
||||
$log->assertPartials([
|
||||
'ability' => 'lit/library-save',
|
||||
'object_type' => 'et_pb_layout',
|
||||
'object_id' => '2001',
|
||||
'summary' => 'Bibliothekseintrag 2001: Anlegen nur teilweise zurückgenommen',
|
||||
]);
|
||||
}
|
||||
|
||||
/** Ruling P6: a throwing insert hook leaves the entry's existence unknown – partial write without id. */
|
||||
public function testAThrowingInsertHookIsAPartialWrite(): void
|
||||
{
|
||||
Functions\when('taxonomy_exists')->justReturn(true);
|
||||
Functions\when('wp_insert_post')->alias(static function (): never {
|
||||
throw new \RuntimeException('hook kaputt');
|
||||
});
|
||||
$log = FakeChangeLog::partialsOnly();
|
||||
|
||||
try {
|
||||
(new DiviLibraryStore())->create(new LibraryItemSpec('X', 'section', false, 'regular', null, 'page'), self::draft($log));
|
||||
self::fail('WriteFailed expected');
|
||||
} catch (WriteFailed $e) {
|
||||
self::assertFalse($e->rolledBack);
|
||||
self::assertNull($e->postId);
|
||||
}
|
||||
self::assertCount(1, $log->partials);
|
||||
}
|
||||
|
||||
public function testARefusedInsertCreatesNothing(): void
|
||||
{
|
||||
Functions\when('taxonomy_exists')->justReturn(true);
|
||||
Functions\when('wp_insert_post')->justReturn(new \WP_Error('db_insert_error', 'Datenbankfehler.'));
|
||||
Functions\expect('wp_delete_post')->never();
|
||||
|
||||
try {
|
||||
(new DiviLibraryStore())->create(new LibraryItemSpec('X', 'section', false, 'regular', null, 'page'), self::draft());
|
||||
self::fail('WriteFailed expected');
|
||||
} catch (WriteFailed $e) {
|
||||
self::assertTrue($e->rolledBack);
|
||||
}
|
||||
}
|
||||
|
||||
public function testRefusesWhenTheLibraryTaxonomiesAreMissing(): void
|
||||
{
|
||||
Functions\when('taxonomy_exists')->justReturn(false);
|
||||
Functions\expect('wp_insert_post')->never();
|
||||
|
||||
$this->expectException(WriteFailed::class);
|
||||
(new DiviLibraryStore())->create(new LibraryItemSpec('X', 'section', false, 'regular', null, 'page'), self::draft());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,4 +142,28 @@ final class LibraryAccessTest extends TestCase
|
||||
self::assertInstanceOf(WP_Post::class, LibraryFixtures::access([], false)->item(1057, false));
|
||||
self::assertNull(LibraryFixtures::access()->writableGate());
|
||||
}
|
||||
|
||||
public function testSavingNeedsPublishPostsSaveLibraryAndAWritableGroup(): void
|
||||
{
|
||||
LibraryFixtures::given([]);
|
||||
self::assertNull(LibraryFixtures::access()->canSave());
|
||||
|
||||
LibraryFixtures::given([], ['publish_posts']);
|
||||
self::assertToolError(LibraryFixtures::access()->canSave(), 'forbidden');
|
||||
|
||||
LibraryFixtures::given([]);
|
||||
self::assertToolError(LibraryFixtures::access(['save_library'])->canSave(), 'forbidden');
|
||||
self::assertToolError(LibraryFixtures::access([], false)->canSave(), 'forbidden');
|
||||
self::assertToolError(LibraryFixtures::access(['divi_library'])->canSave(), 'forbidden');
|
||||
}
|
||||
|
||||
/** Ruling P7: a global entry additionally needs Divi's edit_global_library. */
|
||||
public function testSavingAGlobalEntryNeedsEditGlobalLibrary(): void
|
||||
{
|
||||
LibraryFixtures::given([]);
|
||||
|
||||
self::assertNull(LibraryFixtures::access(['edit_global_library'])->canSave());
|
||||
self::assertToolError(LibraryFixtures::access(['edit_global_library'])->canSave(true), 'forbidden');
|
||||
self::assertNull(LibraryFixtures::access()->canSave(true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\LibraryContent;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryItemSpec;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class LibraryItemSpecTest extends TestCase
|
||||
{
|
||||
private static function pageNode(string $id): Block
|
||||
{
|
||||
return NodeIndex::build((new Parser())->parse(DiviFixtures::markup('stage3/page--globale-instanzen.html')))
|
||||
->find($id);
|
||||
}
|
||||
|
||||
private static function libraryRoot(string $file): Block
|
||||
{
|
||||
return LibraryContent::roots(LibraryContent::load(new Parser(), DiviFixtures::markup('stage3/' . $file)))[0];
|
||||
}
|
||||
|
||||
/** @return array<string, array{string, bool}> Fixture role => [node source, global]. */
|
||||
public static function builderEntries(): array
|
||||
{
|
||||
return [
|
||||
'library--library-abschnitt' => ['page:s1', false],
|
||||
'library--library-zeile' => ['page:s1.r0', false],
|
||||
'library--library-modul' => ['page:s1.r0.c0.m0', false],
|
||||
'library-global--globaler-abschnitt' => ['library:library-global--globaler-abschnitt.html', true],
|
||||
'library-global--globales-modul' => ['library:library-global--globales-modul.html', true],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Terms and meta as Divi's own "add to library" wrote them (fixtures stage3, JSON of each entry),
|
||||
* without Divi's broken _et_pb_row_layout ("[object Object]", finding 5).
|
||||
*
|
||||
* @dataProvider builderEntries
|
||||
*/
|
||||
public function testTermsAndMetaMatchTheBuilderEntries(string $source, bool $global): void
|
||||
{
|
||||
[$kind, $ref] = explode(':', $source, 2);
|
||||
$node = $kind === 'page' ? self::pageNode($ref) : self::libraryRoot($ref);
|
||||
$role = (string) $this->dataName();
|
||||
$json = json_decode(DiviFixtures::markup('stage3/' . $role . '.json'), true);
|
||||
|
||||
$spec = LibraryItemSpec::forNode($node, DiviFixtures::catalog(), 'Titel', $global, 'page');
|
||||
|
||||
self::assertInstanceOf(LibraryItemSpec::class, $spec);
|
||||
$expectedTerms = [];
|
||||
foreach (['scope', 'layout_type', 'module_width'] as $taxonomy) {
|
||||
$expectedTerms[$taxonomy] = array_column($json['terms'][$taxonomy], 'slug');
|
||||
}
|
||||
self::assertEquals($expectedTerms, $spec->terms());
|
||||
$expectedMeta = array_map(static fn (array $values): string => $values[0], $json['meta']);
|
||||
unset($expectedMeta['_et_pb_row_layout']);
|
||||
self::assertEquals($expectedMeta, $spec->meta());
|
||||
}
|
||||
|
||||
public function testRefusesInstancesColumnsAndGlobalsInsideGlobals(): void
|
||||
{
|
||||
$catalog = DiviFixtures::catalog();
|
||||
|
||||
self::assertStringContainsString(
|
||||
'bereits ein globales Modul (Bibliothekseintrag 1058)',
|
||||
(string) LibraryItemSpec::forNode(self::pageNode('g0'), $catalog, 'T', false, 'page')
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
'nur Abschnitte, Zeilen und Module',
|
||||
(string) LibraryItemSpec::forNode(self::pageNode('s1.r0.c0'), $catalog, 'T', false, 'page')
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
'keine globalen Module',
|
||||
(string) LibraryItemSpec::forNode(self::pageNode('s0'), $catalog, 'T', true, 'page')
|
||||
);
|
||||
self::assertInstanceOf(LibraryItemSpec::class, LibraryItemSpec::forNode(self::pageNode('s0'), $catalog, 'T', false, 'page'));
|
||||
}
|
||||
|
||||
/** Ruling P11: opaque roots and instances without a readable id are refused before wrapping. */
|
||||
public function testRefusesOpaqueRootsAndUnreadableInstances(): void
|
||||
{
|
||||
$catalog = DiviFixtures::catalog();
|
||||
$document = (new Parser())->parse(
|
||||
'<!-- wp:divi/section {"module":kaputt} --><!-- /wp:divi/section -->'
|
||||
. '<!-- wp:divi/global-layout {"globalModule":["x"],"blockName":"divi/section"} /-->'
|
||||
);
|
||||
[$opaque, $instance] = $document->children();
|
||||
self::assertTrue($opaque->isOpaque());
|
||||
|
||||
self::assertStringContainsString('nicht lesbar', (string) LibraryItemSpec::forNode($opaque, $catalog, 'T', false, 'page'));
|
||||
self::assertStringContainsString(
|
||||
'globales Modul',
|
||||
(string) LibraryItemSpec::forNode($instance, $catalog, 'T', false, 'page')
|
||||
);
|
||||
}
|
||||
|
||||
public function testFullwidthAndTerms(): void
|
||||
{
|
||||
$section = Block::create('divi/section', ['module' => ['advanced' => ['type' => ['desktop' => ['value' => 'fullwidth']]]]]);
|
||||
|
||||
$spec = LibraryItemSpec::forNode($section, DiviFixtures::catalog(), 'Kopf', false, 'page');
|
||||
|
||||
self::assertSame(['fullwidth'], $spec->terms()['module_width']);
|
||||
self::assertSame([26], $spec->withTerms([26], [])->terms()['layout_category']);
|
||||
self::assertArrayNotHasKey('layout_tag', $spec->withTerms([26], [])->terms());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibraryItemSpec;
|
||||
use LIT\MCP\Integrations\Divi\Library\LibrarySaveAbility;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||||
use LIT\MCP\Integrations\Divi\Pages\SaveScope;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use LIT\MCP\Tests\Support\DiviWriterFixture;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use RuntimeException;
|
||||
use WP_Post;
|
||||
use WP_Term;
|
||||
|
||||
final class LibrarySaveAbilityTest extends TestCase
|
||||
{
|
||||
private WP_Post $page;
|
||||
|
||||
/** @var list<LibraryItemSpec> */
|
||||
private array $specs = [];
|
||||
|
||||
/** @var list<array{int, string, SaveScope}> */
|
||||
private array $contents = [];
|
||||
|
||||
/** @var list<string> Contents DiviWriter saved (the source page must never be written). */
|
||||
private array $saved = [];
|
||||
|
||||
private FakeChangeLog $log;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->page = LibraryFixtures::page(DiviFixtures::markup('stage3/page--globale-instanzen.html'));
|
||||
LibraryFixtures::given([$this->page]);
|
||||
Functions\when('get_term_by')->alias(static fn (string $field, string $name, string $taxonomy): WP_Term|false => $name === 'Fixture'
|
||||
? new WP_Term(['term_id' => 26, 'name' => 'Fixture', 'taxonomy' => $taxonomy])
|
||||
: false);
|
||||
$this->log = new FakeChangeLog(4711);
|
||||
}
|
||||
|
||||
/** @param list<string> $deniedRoles */
|
||||
private function ability(array $deniedRoles = [], bool $writable = true, ?\Throwable $layoutFailure = null): LibrarySaveAbility
|
||||
{
|
||||
$store = LibraryFixtures::store();
|
||||
$store->allows('create')->andReturnUsing(function (LibraryItemSpec $spec): int {
|
||||
$this->specs[] = $spec;
|
||||
|
||||
return 2001;
|
||||
});
|
||||
$saver = Mockery::mock(DiviSaver::class);
|
||||
$saver->allows('saveNew')->andReturnUsing(function (int $id, string $content, SaveScope $scope) use ($layoutFailure): array {
|
||||
if ($layoutFailure !== null) {
|
||||
throw $layoutFailure;
|
||||
}
|
||||
$this->contents[] = [$id, $content, $scope];
|
||||
LibraryFixtures::given([$this->page, LibraryFixtures::item(1055, ['ID' => 2001, 'post_content' => $content])]);
|
||||
|
||||
return [];
|
||||
});
|
||||
$access = LibraryFixtures::access($deniedRoles, $writable, $store);
|
||||
$failures = [];
|
||||
|
||||
return new LibrarySaveAbility(
|
||||
$access,
|
||||
LibraryFixtures::loader($access),
|
||||
DiviWriterFixture::writer($this->saved, $failures, false, LibraryFixtures::loader($access)),
|
||||
$saver,
|
||||
$this->log,
|
||||
DiviWriterFixture::modules()
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function input(array $extra = []): array
|
||||
{
|
||||
return array_merge(
|
||||
['post_id' => 7, 'tree_rev' => Rev::of((string) $this->page->post_content), 'id' => 's1', 'title' => 'Leistungen'],
|
||||
$extra
|
||||
);
|
||||
}
|
||||
|
||||
public function testSavesASectionLikeTheBuilder(): void
|
||||
{
|
||||
$result = $this->ability()->execute($this->input(['categories' => ['Fixture']]));
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertSame(2001, $result['library_id']);
|
||||
self::assertSame('section', $this->specs[0]->layoutType);
|
||||
self::assertSame([26], $this->specs[0]->terms()['layout_category']);
|
||||
self::assertSame(
|
||||
str_replace("\r\n", "\n", DiviFixtures::markup('stage3/library--library-abschnitt.html')),
|
||||
$this->contents[0][1]
|
||||
);
|
||||
self::assertSame(SaveScope::LibraryItem, $this->contents[0][2]);
|
||||
self::assertSame(Rev::of($this->contents[0][1]), $result['tree_rev']);
|
||||
self::assertSame('et_pb_layout', $this->log->records[0]['object_type']);
|
||||
self::assertSame('2001', $this->log->records[0]['object_id']);
|
||||
}
|
||||
|
||||
public function testSavesAGlobalModule(): void
|
||||
{
|
||||
$result = $this->ability()->execute($this->input(['id' => 's0.r0.c0.m0', 'title' => 'Kontakt-Button', 'scope' => 'global']));
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertTrue($this->specs[0]->global);
|
||||
self::assertSame('divi/button', $this->specs[0]->meta()['_et_pb_module_type']);
|
||||
self::assertSame('[]', $this->specs[0]->meta()['_et_pb_excluded_global_options']);
|
||||
}
|
||||
|
||||
public function testRefusesAGlobalInstanceAndColumnsAndCreatesNothing(): void
|
||||
{
|
||||
self::assertToolError($this->ability()->execute($this->input(['id' => 'g0'])), 'validation_failed');
|
||||
self::assertToolError($this->ability()->execute($this->input(['id' => 's1.r0.c0'])), 'validation_failed');
|
||||
self::assertSame([], $this->specs);
|
||||
}
|
||||
|
||||
public function testStaleSourceTreeRevCreatesNothing(): void
|
||||
{
|
||||
self::assertToolError($this->ability()->execute($this->input(['tree_rev' => 'abcdef012345'])), 'rev_mismatch');
|
||||
self::assertSame([], $this->specs);
|
||||
}
|
||||
|
||||
public function testNeedsPublishPostsAndTheDiviRole(): void
|
||||
{
|
||||
self::assertToolError($this->ability(['save_library'])->permission($this->input()), 'forbidden');
|
||||
self::assertToolError($this->ability([], false)->permission($this->input()), 'forbidden');
|
||||
LibraryFixtures::given([$this->page], ['publish_posts']);
|
||||
self::assertToolError($this->ability()->permission($this->input()), 'forbidden');
|
||||
self::assertSame([], $this->specs);
|
||||
}
|
||||
|
||||
public function testEditorCannotSaveACodeModule(): void
|
||||
{
|
||||
$this->page = LibraryFixtures::page(
|
||||
'<!-- wp:divi/placeholder --><!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->'
|
||||
. '<!-- wp:divi/code {"content":{"innerContent":{"desktop":{"value":"<b>x</b>"}}}} /-->'
|
||||
. '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->'
|
||||
);
|
||||
LibraryFixtures::given([$this->page]);
|
||||
|
||||
$result = $this->ability()->execute($this->input(['id' => 's0']));
|
||||
|
||||
self::assertToolError($result, 'validation_failed');
|
||||
self::assertStringContainsString('Code-Modul', implode(' ', \LIT\MCP\Abilities\Errors::fieldsOf($result)));
|
||||
self::assertSame([], $this->specs);
|
||||
}
|
||||
|
||||
public function testUnknownCategoryCreatesNothing(): void
|
||||
{
|
||||
$result = $this->ability()->execute($this->input(['categories' => ['Gibt es nicht']]));
|
||||
|
||||
self::assertToolError($result, 'validation_failed');
|
||||
self::assertSame([], $this->specs);
|
||||
}
|
||||
|
||||
/** Ruling P6: the layout write failing rolls the new entry back through NewDraft (write_failed). */
|
||||
public function testFailedLayoutWriteDeletesTheEntry(): void
|
||||
{
|
||||
Functions\expect('wp_delete_post')->once()->with(2001, true)->andReturn(true);
|
||||
|
||||
try {
|
||||
$this->ability([], true, new RuntimeException('hook failed'))->execute($this->input());
|
||||
self::fail('WriteFailed expected');
|
||||
} catch (WriteFailed $e) {
|
||||
self::assertTrue($e->rolledBack);
|
||||
self::assertToolError($e->toError(), 'write_failed');
|
||||
}
|
||||
self::assertSame([], $this->log->records);
|
||||
}
|
||||
|
||||
/** Ruling P6: when the rollback fails too, the answer is partial_write and the change log says so. */
|
||||
public function testFailedRollbackIsAPartialWrite(): void
|
||||
{
|
||||
Functions\when('wp_delete_post')->justReturn(false);
|
||||
$this->log = FakeChangeLog::any(4711);
|
||||
|
||||
try {
|
||||
$this->ability([], true, new RuntimeException('hook failed'))->execute($this->input());
|
||||
self::fail('WriteFailed expected');
|
||||
} catch (WriteFailed $e) {
|
||||
self::assertFalse($e->rolledBack);
|
||||
self::assertSame(2001, $e->postId);
|
||||
self::assertToolError($e->toError(), 'partial_write');
|
||||
}
|
||||
self::assertSame('et_pb_layout', $this->log->partials[0]['object_type']);
|
||||
self::assertSame('2001', $this->log->partials[0]['object_id']);
|
||||
}
|
||||
|
||||
/** Ruling P7: scope "global" also needs Divi's edit_global_library. */
|
||||
public function testGlobalScopeNeedsEditGlobalLibrary(): void
|
||||
{
|
||||
$input = $this->input(['id' => 's0.r0.c0.m0', 'scope' => 'global']);
|
||||
|
||||
self::assertToolError($this->ability(['edit_global_library'])->permission($input), 'forbidden');
|
||||
self::assertToolError($this->ability(['edit_global_library'])->execute($input), 'forbidden');
|
||||
self::assertTrue($this->ability(['edit_global_library'])->permission($this->input()));
|
||||
self::assertSame([], $this->specs);
|
||||
}
|
||||
|
||||
public function testTheSourcePageIsNeverWritten(): void
|
||||
{
|
||||
$this->ability()->execute($this->input(['scope' => 'normal']));
|
||||
|
||||
self::assertSame([], $this->saved);
|
||||
self::assertCount(1, $this->contents);
|
||||
self::assertSame(2001, $this->contents[0][0]);
|
||||
}
|
||||
|
||||
/** Line breaks between blocks are unified like LibraryContent::load() (fixture finding 9). */
|
||||
public function testLineBreaksBetweenBlocksAreUnified(): void
|
||||
{
|
||||
$this->page = LibraryFixtures::page(
|
||||
"<!-- wp:divi/placeholder --><!-- wp:divi/section -->\r\n<!-- wp:divi/row -->\r\n<!-- wp:divi/column -->"
|
||||
. '<!-- wp:divi/text {"content":{"innerContent":{"desktop":{"value":"<p>a</p>"}}}} /-->'
|
||||
. "<!-- /wp:divi/column -->\r\n<!-- /wp:divi/row -->\r\n<!-- /wp:divi/section --><!-- /wp:divi/placeholder -->"
|
||||
);
|
||||
LibraryFixtures::given([$this->page]);
|
||||
|
||||
$result = $this->ability()->execute($this->input(['id' => 's0']));
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertStringNotContainsString("\r", $this->contents[0][1]);
|
||||
self::assertStringContainsString("<!-- wp:divi/section -->\n<!-- wp:divi/row -->", $this->contents[0][1]);
|
||||
}
|
||||
|
||||
public function testUnknownScopeCreatesNothing(): void
|
||||
{
|
||||
self::assertToolError($this->ability()->execute($this->input(['scope' => 'site'])), 'validation_failed');
|
||||
self::assertSame([], $this->specs);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user