Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Library/DiviLibraryStoreTest.php
T
janniclas.loosenandClaude Opus 5.5 d8d7bb67be fix: lit/library-list search by title, own entries for authors, cursor without hidden rows (L3)
search uses search_columns post_title; users without edit_others_posts on
et_pb_layout query their own entries only (PostGuard::ownPostsOnly); the
cursor counts the entries shown, never the rows left out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 04:18:04 +02:00

250 lines
9.9 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(['post_title'], $captured['search_columns'], 'audit L3: the title only');
self::assertArrayNotHasKey('author', $captured);
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']
);
}
/** Audit L3: users without edit_others_posts query their own entries only. */
public function testFindRestrictsToAnAuthor(): void
{
$captured = [];
Functions\when('get_posts')->alias(static function (array $args) use (&$captured): array {
$captured = $args;
return [];
});
(new DiviLibraryStore())->find(['author' => 12], 20, 0);
self::assertSame(12, $captured['author']);
}
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());
}
}