fix: check WordPress meta capabilities per field key

Fields from the meta/sample sources need edit_post_meta (update, checked in
PostWriter::prepare) or add_post_meta (create, checked right after the insert;
a refusal deletes the new draft again) for their key; a refusal is a
fields.<key> validation error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 01:24:20 +02:00
co-authored by Claude Opus 5.5
parent 563d9e8d15
commit 106a099483
4 changed files with 160 additions and 0 deletions
@@ -39,6 +39,7 @@ final class ContentCreateAbilityTest extends TestCase
$writer = Mockery::mock(PostWriter::class);
$writer->allows('prepare')->andReturn(['fields' => ['role' => 'Leitung'], 'terms' => [], 'warnings' => $warnings]);
$writer->allows('postData')->andReturn(['post_title' => 'Anna']);
$writer->allows('deniedMeta')->andReturn([]);
Functions\when('get_current_user_id')->justReturn(3);
return $writer;
@@ -85,6 +86,7 @@ final class ContentCreateAbilityTest extends TestCase
$writer = Mockery::mock(PostWriter::class);
$writer->allows('prepare')->with('lit_person', $input, true)->andReturn($prepared);
$writer->allows('postData')->andReturn(['post_title' => 'Anna']);
$writer->expects('deniedMeta')->with(42, 'lit_person', ['role' => 'Leitung'], 'add_post_meta')->andReturn([]);
$writer->expects('writeExtras')->with(42, 'lit_person', $prepared, $input);
Functions\when('get_current_user_id')->justReturn(3);
Functions\expect('wp_insert_post')->once()->with([
@@ -126,6 +128,47 @@ final class ContentCreateAbilityTest extends TestCase
self::assertSame('lit_write_failed', $failure->toError()->get_error_code());
}
public function testMetaFieldsTheUserMayNotAddAreRefusedAndTheDraftIsRemoved(): void
{
$writer = Mockery::mock(PostWriter::class);
$writer->allows('prepare')->andReturn(['fields' => ['city' => 'Trier'], 'terms' => [], 'warnings' => []]);
$writer->allows('postData')->andReturn(['post_title' => 'Anna']);
$writer->expects('deniedMeta')->with(42, 'lit_person', ['city' => 'Trier'], 'add_post_meta')
->andReturn(['fields.city' => 'Keine Berechtigung, dieses Feld zu ändern.']);
$writer->expects('writeExtras')->never();
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]));
$log = Mockery::mock(ChangeLog::class);
$log->expects('record')->never();
$result = (new ContentCreateAbility($this->schema(), $writer, $log))->execute(self::INPUT);
self::assertSame('lit_validation_failed', $result->get_error_code());
self::assertArrayHasKey('fields.city', $result->get_error_data()['fields']);
}
public function testARefusedDraftThatCannotBeDeletedIsAPartialWrite(): void
{
$writer = Mockery::mock(PostWriter::class);
$writer->allows('prepare')->andReturn(['fields' => ['city' => 'Trier'], 'terms' => [], 'warnings' => []]);
$writer->allows('postData')->andReturn(['post_title' => 'Anna']);
$writer->allows('deniedMeta')->andReturn(['fields.city' => 'Keine Berechtigung, dieses Feld zu ändern.']);
Functions\when('get_current_user_id')->justReturn(3);
Functions\when('wp_insert_post')->justReturn(42);
Functions\when('wp_delete_post')->justReturn(false);
$log = Mockery::mock(ChangeLog::class);
$log->expects('recordPartial');
try {
(new ContentCreateAbility($this->schema(), $writer, $log))->execute(self::INPUT);
self::fail('WriteFailed expected');
} catch (WriteFailed $e) {
self::assertFalse($e->rolledBack);
self::assertSame(42, $e->postId);
}
}
public function testADraftThatCannotBeDeletedIsAPartialWriteAndLogged(): void
{
$writer = $this->writer();