'lit_person', 'title' => 'Anna', 'fields' => ['role' => 'Leitung']]; 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; } /** @return PostWriter&MockInterface */ private function writer(array $warnings = []): PostWriter { $writer = Mockery::mock(PostWriter::class); $writer->allows('prepare')->andReturn(['fields' => ['role' => 'Leitung'], 'terms' => [], 'warnings' => $warnings]); $writer->allows('postData')->andReturn(['post_title' => 'Anna']); Functions\when('get_current_user_id')->justReturn(3); return $writer; } private function execute(PostWriter $writer, ChangeLog $log): WriteFailed { try { (new ContentCreateAbility($this->schema(), $writer, $log))->execute(self::INPUT); } catch (WriteFailed $e) { return $e; } self::fail('WriteFailed expected'); } private function stubResultLinks(): WP_Post { $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'); return $post; } 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 = self::INPUT; $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 = $this->stubResultLinks(); $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 testFailingExtrasDeleteCreatedTermsAndTheNewDraft(): void { $writer = $this->writer(); $writer->expects('writeExtras')->andThrow(new RuntimeException('Pods could not save the fields.')); $writer->expects('deleteCreatedTerms'); 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(); $failure = $this->execute($writer, $log); self::assertTrue($failure->rolledBack); self::assertStringContainsString('Pods could not save the fields.', $failure->getMessage()); self::assertSame('lit_write_failed', $failure->toError()->get_error_code()); } public function testADraftThatCannotBeDeletedIsAPartialWriteAndLogged(): void { $writer = $this->writer(); $writer->expects('writeExtras')->andThrow(new RuntimeException('meta kaputt')); $writer->expects('deleteCreatedTerms'); Functions\when('wp_insert_post')->justReturn(42); Functions\when('wp_delete_post')->justReturn(false); $log = Mockery::mock(ChangeLog::class); $log->expects('recordPartial') ->with('lit/content-create', 'lit_person', '42', Mockery::type('string')) ->andReturn(9); $failure = $this->execute($writer, $log); self::assertFalse($failure->rolledBack); self::assertSame(42, $failure->postId); self::assertStringContainsString('Eintrag 42', $failure->toError()->get_error_message()); } public function testLeftoverCreatedTermsMakeItAPartialWrite(): void { $writer = $this->writer(); $writer->expects('writeExtras')->andThrow(new RuntimeException('meta kaputt')); $writer->expects('deleteCreatedTerms')->andThrow(new RuntimeException('term 15 bleibt')); Functions\when('wp_insert_post')->justReturn(42); Functions\when('wp_delete_post')->justReturn(new WP_Post(['ID' => 42])); $log = Mockery::mock(ChangeLog::class); $log->expects('recordPartial'); $failure = $this->execute($writer, $log); self::assertFalse($failure->rolledBack); self::assertStringContainsString('term 15 bleibt', $failure->getMessage()); } public function testAnInsertThatThrowsIsAPartialWriteWithoutId(): void { $writer = $this->writer(); Functions\when('wp_insert_post')->alias(static function (): never { throw new RuntimeException('save_post hook kaputt'); }); $log = Mockery::mock(ChangeLog::class); $log->expects('recordPartial') ->with('lit/content-create', 'lit_person', '', Mockery::type('string')) ->andReturn(9); $failure = $this->execute($writer, $log); self::assertFalse($failure->rolledBack); self::assertNull($failure->postId); self::assertSame('lit_partial_write', $failure->toError()->get_error_code()); } public function testFailuresAfterTheWriteOnlyAddWarnings(): void { $writer = $this->writer(['w']); $writer->allows('writeExtras'); Functions\when('wp_insert_post')->justReturn(42); $this->stubResultLinks(); Actions\expectDone('lit_mcp_after_save')->once()->whenHappen(static function (): void { throw new RuntimeException('listener kaputt'); }); $log = Mockery::mock(ChangeLog::class); $log->expects('record')->andThrow(new RuntimeException('db weg')); $file = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $file); try { $result = (new ContentCreateAbility($this->schema(), $writer, $log))->execute(self::INPUT); } finally { ini_set('error_log', (string) $previous); } self::assertTrue($result['ok']); self::assertNull($result['log_id']); self::assertCount(3, $result['warnings']); self::assertStringContainsString('listener kaputt', (string) file_get_contents($file)); self::assertStringContainsString('db weg', (string) file_get_contents($file)); unlink($file); } 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()); } }