Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Library/DiviLibraryStoreTest.php
T
janniclas.loosenandClaude Opus 5.5 5035092805 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>
2026-09-27 02:12:50 +02:00

233 lines
9.3 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\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;
final class DiviLibraryStoreTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$terms = [
1058 => [
'scope' => [new WP_Term(['slug' => 'global', 'name' => 'global'])],
'layout_type' => [new WP_Term(['slug' => 'section', 'name' => 'section'])],
'layout_category' => [new WP_Term(['slug' => 'kopf', 'name' => 'Kopfbereiche'])],
],
1055 => [
'scope' => [new WP_Term(['slug' => 'non_global', 'name' => 'non_global'])],
'layout_type' => [new WP_Term(['slug' => 'section', 'name' => 'section'])],
],
1057 => [],
];
Functions\when('get_the_terms')->alias(
static fn (int $id, string $taxonomy): array|false => $terms[$id][$taxonomy] ?? false
);
Functions\when('get_post_meta')->alias(
static fn (int $id, string $key): string => $id === 1057 && $key === '_et_pb_module_type' ? 'divi/blurb' : ''
);
}
public function testGlobalScopeAndLayoutTypeFromTheTerms(): void
{
$store = new DiviLibraryStore();
self::assertTrue($store->isGlobal(1058));
self::assertFalse($store->isGlobal(1055));
self::assertSame('section', $store->layoutType(1058));
self::assertNull($store->layoutType(1057));
self::assertSame(['Kopfbereiche'], $store->termNames(1058, 'layout_category'));
}
public function testDescribesAnEntry(): void
{
$post = new WP_Post([
'ID' => 1057, 'post_type' => 'et_pb_layout', 'post_title' => '[Fixture] Library-Modul',
'post_modified_gmt' => '2026-09-26 20:56:29',
]);
self::assertSame(
[
'id' => 1057, 'title' => '[Fixture] Library-Modul', 'layout_type' => null, 'scope' => 'normal',
'module_type' => 'divi/blurb', 'categories' => [], 'tags' => [], 'modified' => '2026-09-26 20:56:29',
],
(new DiviLibraryStore())->describe($post)
);
}
public function testFindQueriesPublishedEntriesWithFilters(): void
{
$captured = [];
Functions\when('get_posts')->alias(static function (array $args) use (&$captured): array {
$captured = $args;
return [];
});
(new DiviLibraryStore())->find(
['layout_type' => 'section', 'scope' => 'normal', 'search' => 'Kopf', 'category' => 'Kopfbereiche'],
20,
40
);
self::assertSame('et_pb_layout', $captured['post_type']);
self::assertSame('publish', $captured['post_status']);
self::assertSame(21, $captured['posts_per_page']);
self::assertSame(40, $captured['offset']);
self::assertSame('Kopf', $captured['s']);
self::assertSame(
[
'relation' => 'AND',
['taxonomy' => 'layout_type', 'field' => 'slug', 'terms' => ['section']],
['taxonomy' => 'scope', 'field' => 'slug', 'terms' => ['global'], 'operator' => 'NOT IN'],
['taxonomy' => 'layout_category', 'field' => 'name', 'terms' => ['Kopfbereiche']],
],
$captured['tax_query']
);
}
public function testReportsMissingTaxonomies(): void
{
Functions\when('taxonomy_exists')->alias(static fn (string $taxonomy): bool => $taxonomy !== 'scope');
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());
}
}