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:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user