Files
WP-LIT-MCP/tests/Unit/Integrations/WordPress/Content/PostPresenterTest.php
T
janniclas.loosenandClaude Opus 5.5 251934ce53 feat: deferred 1a items - menu_rev lock, lit_mcp_guides filter, warnings, schedule guard precision
Task 14 (rulings P17/P18, Q10):
- lit/menu-get returns menu_rev (Rev::ofMenu over id, parent, position,
  title, url, object, object_id); lit/menu-set-items requires it, refuses
  a stale one with rev_mismatch (hint: lit/menu-get) and returns the new
  menu_rev. Errors::revMismatch takes an optional message/hint.
- GuideRepository: filter lit_mcp_guides adds/overrides topics; invalid
  topic names and non-string content are ignored, files stay the default.
- lit/post-trash names front page, posts page and linking menus in the
  confirmation and as warnings; lit/menu-assign-location needs confirm
  to remove an assigned menu and names it; lit/post-schedule warns that
  a private entry becomes public; Divi hints no longer point to
  non-existing lit-divi-* tools.
- PostGuard::missedSchedule: 60 s publish window, zero GMT date falls
  back to get_gmt_from_date(post_date); post-schedule refuses dates
  within that window.
- write_failed after a clean rollback that keeps a revision/backup says
  "Die Aenderung wurde nicht gespeichert (... bleibt erhalten)" via the
  new state parameter (Errors::writeFailed / WriteFailed rolledBackState).
- wp-basis-smoke: menu_rev flow, unassign confirm, trash/menu and
  private-schedule warnings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-26 04:33:15 +02:00

126 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Content;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Rev;
use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use stdClass;
use WP_Post;
use WP_Term;
final class PostPresenterTest extends TestCase
{
private function stubLinks(): void
{
Functions\when('get_permalink')->justReturn('https://kunde.de/team/');
Functions\when('get_edit_post_link')->justReturn('https://kunde.de/wp-admin/post.php?post=5&action=edit');
Functions\when('get_preview_post_link')->justReturn('https://kunde.de/?p=5&preview=true');
Functions\when('get_the_author_meta')->justReturn('Jan');
Functions\when('get_page_template_slug')->justReturn('');
}
public function testSummaryContainsCoreDataAndUrls(): void
{
$this->stubLinks();
Functions\when('get_post_meta')->justReturn('');
$post = new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_title' => 'Team', 'post_name' => 'team']);
$summary = (new PostPresenter(Mockery::mock(ContentSchema::class)))->summary($post);
self::assertSame(5, $summary['id']);
self::assertSame('Team', $summary['title']);
self::assertSame('team', $summary['slug']);
self::assertSame('https://kunde.de/team/', $summary['url']);
self::assertFalse($summary['divi']);
}
public function testFullReadsTermsFieldsAndFeaturedImage(): void
{
$this->stubLinks();
Functions\when('get_post_meta')->alias(static fn (int $id, string $key) => match ($key) {
'_wp_attachment_image_alt' => 'Porträt',
default => '',
});
Functions\when('wp_get_post_terms')->justReturn([new WP_Term(['term_id' => 4, 'name' => 'News', 'slug' => 'news'])]);
Functions\when('get_post_thumbnail_id')->justReturn(33);
Functions\when('wp_get_attachment_url')->justReturn('https://kunde.de/wp-content/uploads/foto.jpg');
$role = new FieldDefinition('role', 'Funktion', 'string', 'pods', 'writable');
$source = Mockery::mock(FieldSource::class);
$source->expects('read')->with(5, $role)->andReturn('Leitung');
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('taxonomies')->andReturn([['name' => 'category', 'label' => 'Kategorien', 'hierarchical' => true]]);
$schema->allows('fields')->andReturn(['role' => $role]);
$schema->allows('sourceOf')->andReturn($source);
$post = new WP_Post(['ID' => 5, 'post_type' => 'lit_person', 'post_content' => '<p>Hallo</p>', 'post_author' => '2']);
$full = (new PostPresenter($schema))->full($post);
self::assertSame('<p>Hallo</p>', $full['content']);
self::assertSame(['category' => [['id' => 4, 'name' => 'News', 'slug' => 'news']]], $full['terms']);
self::assertSame(['role' => 'Leitung'], $full['fields']);
self::assertSame(['id' => 33, 'url' => 'https://kunde.de/wp-content/uploads/foto.jpg', 'alt' => 'Porträt'], $full['featured_image']);
self::assertSame(['id' => 2, 'name' => 'Jan'], $full['author']);
self::assertSame(Rev::ofPost($post), $full['rev']);
self::assertSame([], $full['hints']);
}
public function testDiviContentIsWithheldAndPointsToTheDiviOutline(): void
{
$this->stubLinks();
Functions\when('get_post_meta')->alias(static fn (int $id, string $key) => $key === '_et_pb_use_builder' ? 'on' : '');
Functions\when('get_post_thumbnail_id')->justReturn(0);
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('taxonomies')->andReturn([]);
$schema->allows('fields')->andReturn([]);
$post = new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_content' => '<!-- wp:divi/section /-->']);
$full = (new PostPresenter($schema))->full($post);
self::assertTrue($full['divi']);
self::assertNull($full['content']);
self::assertNull($full['featured_image']);
self::assertStringNotContainsString('lit-divi', $full['hints'][0]);
self::assertStringContainsString('Divi-Werkzeuge folgen in einer späteren Version', $full['hints'][0]);
self::assertInstanceOf(stdClass::class, $full['terms']);
self::assertInstanceOf(stdClass::class, $full['fields']);
self::assertSame(Rev::ofPost($post), $full['rev']);
}
public function testPasswordAndProtectedReadonlyMetaFieldsAreOmitted(): void
{
$this->stubLinks();
Functions\when('get_post_meta')->justReturn('');
Functions\when('get_post_thumbnail_id')->justReturn(0);
$password = new FieldDefinition('secret', 'Secret', 'password', 'pods', 'writable');
$protected = new FieldDefinition('_internal', 'Internal', 'string', 'meta', 'readonly');
$visible = new FieldDefinition('role', 'Funktion', 'string', 'pods', 'writable');
$source = Mockery::mock(FieldSource::class);
$source->expects('read')->once()->with(5, $visible)->andReturn('Leitung');
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('taxonomies')->andReturn([]);
$schema->allows('fields')->andReturn(['secret' => $password, '_internal' => $protected, 'role' => $visible]);
$schema->allows('sourceOf')->andReturn($source);
$post = new WP_Post(['ID' => 5, 'post_type' => 'lit_person']);
$full = (new PostPresenter($schema))->full($post);
self::assertSame(['role' => 'Leitung'], $full['fields']);
}
public function testDiviIsAlsoDetectedFromBlockMarkup(): void
{
Functions\when('get_post_meta')->justReturn('');
self::assertTrue(PostPresenter::usesDivi(new WP_Post(['post_content' => 'x <!-- wp:divi/text {} /--> y'])));
self::assertFalse(PostPresenter::usesDivi(new WP_Post(['post_content' => '<!-- wp:paragraph -->'])));
}
}