All-or-nothing writes: validation (fields, terms, featured image, parent, template) before any write; failed extras delete the new draft (create) or restore revision, old columns, fields, terms and featured image (update). content-update requires rev; Rev::ofPost covers content, title, excerpt, status and post_modified_gmt. MetaFieldSource::write() now throws on failure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
115 lines
5.4 KiB
PHP
115 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Content;
|
|
|
|
use Brain\Monkey\Actions;
|
|
use Brain\Monkey\Functions;
|
|
use LIT\MCP\Abilities\Errors;
|
|
use LIT\MCP\Abilities\Rev;
|
|
use LIT\MCP\ChangeLog\ChangeLog;
|
|
use LIT\MCP\Integrations\WordPress\Content\ContentCreateAbility;
|
|
use LIT\MCP\Integrations\WordPress\Content\PostWriter;
|
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
|
use LIT\MCP\Tests\Unit\TestCase;
|
|
use Mockery;
|
|
use RuntimeException;
|
|
use WP_Post;
|
|
|
|
final class ContentCreateAbilityTest extends TestCase
|
|
{
|
|
private function schema(): ContentSchema
|
|
{
|
|
Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['create_posts' => 'edit_pages']]);
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
$schema = Mockery::mock(ContentSchema::class);
|
|
$schema->allows('isManaged')->andReturn(true);
|
|
|
|
return $schema;
|
|
}
|
|
|
|
public function testInvalidInputWritesNothing(): void
|
|
{
|
|
$writer = Mockery::mock(PostWriter::class);
|
|
$writer->allows('prepare')->andReturn(Errors::invalidInput('ungültig', ['fields.role' => 'Pflichtfeld fehlt.']));
|
|
Functions\expect('wp_insert_post')->never();
|
|
|
|
$result = (new ContentCreateAbility($this->schema(), $writer, Mockery::mock(ChangeLog::class)))
|
|
->execute(['post_type' => 'lit_person', 'title' => 'Anna']);
|
|
|
|
self::assertSame('lit_validation_failed', $result->get_error_code());
|
|
}
|
|
|
|
public function testCreatesADraftWritesExtrasLogsAndReturnsAToolResult(): void
|
|
{
|
|
$schema = $this->schema();
|
|
$prepared = ['fields' => ['role' => 'Leitung'], 'terms' => [], 'warnings' => ['w']];
|
|
$input = ['post_type' => 'lit_person', 'title' => 'Anna', 'fields' => ['role' => 'Leitung']];
|
|
$writer = Mockery::mock(PostWriter::class);
|
|
$writer->allows('prepare')->with('lit_person', $input, true)->andReturn($prepared);
|
|
$writer->allows('postData')->andReturn(['post_title' => 'Anna']);
|
|
$writer->expects('writeExtras')->with(42, 'lit_person', $prepared, $input);
|
|
Functions\when('get_current_user_id')->justReturn(3);
|
|
Functions\expect('wp_insert_post')->once()->with([
|
|
'post_title' => 'Anna',
|
|
'post_type' => 'lit_person',
|
|
'post_status' => 'draft',
|
|
'post_author' => 3,
|
|
], true)->andReturn(42);
|
|
Actions\expectDone('lit_mcp_after_save')->once()->with(42, 'lit/content-create', $input);
|
|
$post = new WP_Post(['ID' => 42, 'post_title' => 'Anna', 'post_type' => 'lit_person']);
|
|
Functions\when('get_post')->justReturn($post);
|
|
Functions\when('get_preview_post_link')->justReturn('https://kunde.de/?p=42&preview=true');
|
|
Functions\when('get_edit_post_link')->justReturn('https://kunde.de/wp-admin/post.php?post=42&action=edit');
|
|
$log = Mockery::mock(ChangeLog::class);
|
|
$log->expects('record')->with('lit/content-create', 'lit_person', '42', 'Entwurf "Anna" angelegt')->andReturn(7);
|
|
|
|
$result = (new ContentCreateAbility($schema, $writer, $log))->execute($input);
|
|
|
|
self::assertTrue($result['ok']);
|
|
self::assertSame([['id' => '42', 'type' => 'lit_person', 'action' => 'created', 'label' => 'Anna']], $result['changed']);
|
|
self::assertSame(7, $result['log_id']);
|
|
self::assertSame(['w'], $result['warnings']);
|
|
self::assertStringContainsString('Entwurf', $result['summary']);
|
|
self::assertSame(Rev::ofPost($post), $result['rev']);
|
|
}
|
|
|
|
public function testFailingExtrasDeleteTheNewDraftAndNothingIsLogged(): void
|
|
{
|
|
$schema = $this->schema();
|
|
$input = ['post_type' => 'lit_person', 'title' => 'Anna', 'fields' => ['role' => 'Leitung']];
|
|
$writer = Mockery::mock(PostWriter::class);
|
|
$writer->allows('prepare')->andReturn(['fields' => ['role' => 'Leitung'], 'terms' => [], 'warnings' => []]);
|
|
$writer->allows('postData')->andReturn(['post_title' => 'Anna']);
|
|
$writer->expects('writeExtras')->andThrow(new RuntimeException('Pods could not save the fields.'));
|
|
Functions\when('get_current_user_id')->justReturn(3);
|
|
Functions\when('wp_insert_post')->justReturn(42);
|
|
Functions\expect('wp_delete_post')->once()->with(42, true)->andReturn(new WP_Post(['ID' => 42]));
|
|
Actions\expectDone('lit_mcp_after_save')->never();
|
|
$log = Mockery::mock(ChangeLog::class);
|
|
$log->expects('record')->never();
|
|
|
|
try {
|
|
(new ContentCreateAbility($schema, $writer, $log))->execute($input);
|
|
self::fail('RuntimeException expected');
|
|
} catch (RuntimeException $e) {
|
|
self::assertStringContainsString('Pods could not save the fields.', $e->getMessage());
|
|
self::assertStringContainsString('deleted', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function testStatusIsNotAnInputAndPermissionNeedsCreateRights(): void
|
|
{
|
|
$schema = $this->schema();
|
|
$ability = new ContentCreateAbility($schema, Mockery::mock(PostWriter::class), Mockery::mock(ChangeLog::class));
|
|
|
|
self::assertArrayNotHasKey('status', $ability->inputSchema()['properties']);
|
|
self::assertFalse($ability->inputSchema()['additionalProperties']);
|
|
self::assertTrue($ability->permission(['post_type' => 'page']));
|
|
|
|
Functions\when('current_user_can')->justReturn(false);
|
|
self::assertSame('lit_forbidden', $ability->permission(['post_type' => 'page'])->get_error_code());
|
|
}
|
|
}
|