diff --git a/src/Integrations/WordPress/Status/PostPublishAbility.php b/src/Integrations/WordPress/Status/PostPublishAbility.php new file mode 100644 index 0000000..67b0c46 --- /dev/null +++ b/src/Integrations/WordPress/Status/PostPublishAbility.php @@ -0,0 +1,164 @@ + 'object', + 'properties' => ['post_id' => ['type' => 'integer', 'minimum' => 1]], + 'required' => ['post_id'], + 'additionalProperties' => false, + ]; + } + + public function outputSchema(): ?array + { + return ToolResult::schema(); + } + + public function annotations(): array + { + return ['readonly' => false, 'destructive' => false, 'idempotent' => true]; + } + + public function isDirect(): bool + { + return true; + } + + public function permission(array $input): bool|WP_Error + { + $post = $this->load($input); + + return $post instanceof WP_Error ? $post : true; + } + + public function execute(array $input): mixed + { + $post = $this->load($input); + if ($post instanceof WP_Error) { + return $post; + } + + $postId = (int) $post->ID; + $title = (string) $post->post_title; + $editUrl = (string) get_edit_post_link($postId, 'raw'); + if ($post->post_status === 'publish') { + return ToolResult::ok(sprintf('"%s" ist bereits veröffentlicht.', $title)) + ->rev(Rev::ofPost($post)) + ->urls((string) get_permalink($postId), $editUrl) + ->toArray(); + } + + $data = ['ID' => $postId, 'post_status' => 'publish']; + if ($post->post_status === 'future' || $this->isFutureDated($post)) { + $data['post_date'] = current_time('mysql'); + $data['post_date_gmt'] = current_time('mysql', true); + $data['edit_date'] = true; + } + + $result = wp_update_post($data, true); + if ($result instanceof WP_Error) { + return Errors::invalidInput( + 'WordPress hat das Veröffentlichen abgelehnt: ' . $result->get_error_message() + ); + } + + $status = get_post_status($postId); + if ($status !== 'publish') { + return Errors::unsupported( + sprintf('Nach dem Veröffentlichen ist der Status "%s".', (string) $status), + 'Eintrag in WordPress prüfen (z. B. Freigabe-Workflow eines anderen Plugins).' + ); + } + + // The status change is saved – failures from here on are logged and reported as warnings only. + $warnings = []; + FollowUp::run( + fn () => do_action('lit_mcp_after_save', $postId, $this->name(), $input), + $this->name() . ' lit_mcp_after_save', + 'Eine Erweiterung ist nach dem Veröffentlichen auf einen Fehler gestoßen; die Änderung ist gespeichert.', + $warnings + ); + + $summary = sprintf('"%s" veröffentlicht', $title); + $logId = FollowUp::run( + fn () => $this->log->record($this->name(), (string) $post->post_type, (string) $postId, $summary), + $this->name() . ' change log', + 'Änderungsprotokoll konnte nicht geschrieben werden; die Änderung ist gespeichert.', + $warnings + ); + + $updated = get_post($postId); + $updated = $updated instanceof WP_Post ? $updated : $post; + + return ToolResult::ok(sprintf('"%s" ist jetzt veröffentlicht.', $title)) + ->changed((string) $postId, (string) $post->post_type, 'published', $title) + ->rev(Rev::ofPost($updated)) + ->logId(is_int($logId) ? $logId : null) + ->urls((string) get_permalink($postId), $editUrl) + ->warnings($warnings) + ->toArray(); + } + + /** @param array $input */ + private function load(array $input): WP_Post|WP_Error + { + $post = PostGuard::editable($input['post_id'] ?? null, $this->schema); + if ($post instanceof WP_Error) { + return $post; + } + + return current_user_can('publish_post', (int) $post->ID) + ? $post + : Errors::forbidden('Keine Berechtigung, diesen Eintrag zu veröffentlichen.'); + } + + private function isFutureDated(WP_Post $post): bool + { + $gmt = (string) $post->post_date_gmt; + + return $gmt !== '' && $gmt !== '0000-00-00 00:00:00' && strtotime($gmt . ' UTC') > time(); + } +} diff --git a/src/Integrations/WordPress/Status/PostScheduleAbility.php b/src/Integrations/WordPress/Status/PostScheduleAbility.php new file mode 100644 index 0000000..d6ce674 --- /dev/null +++ b/src/Integrations/WordPress/Status/PostScheduleAbility.php @@ -0,0 +1,173 @@ + 'object', + 'properties' => [ + 'post_id' => ['type' => 'integer', 'minimum' => 1], + 'date' => [ + 'type' => 'string', + 'pattern' => '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$', + 'description' => 'JJJJ-MM-TT HH:MM in der Zeitzone der Website.', + ], + ], + 'required' => ['post_id', 'date'], + 'additionalProperties' => false, + ]; + } + + public function outputSchema(): ?array + { + return ToolResult::schema(); + } + + public function annotations(): array + { + return ['readonly' => false, 'destructive' => false, 'idempotent' => true]; + } + + public function isDirect(): bool + { + return false; + } + + public function permission(array $input): bool|WP_Error + { + $post = $this->load($input); + + return $post instanceof WP_Error ? $post : true; + } + + public function execute(array $input): mixed + { + $post = $this->load($input); + if ($post instanceof WP_Error) { + return $post; + } + + $raw = (string) ($input['date'] ?? ''); + $date = DateTimeImmutable::createFromFormat('!' . self::FORMAT, $raw, wp_timezone()); + if ($date === false || $date->format(self::FORMAT) !== $raw) { + return Errors::invalidInput('Ungültiges Datum.', ['date' => 'Format JJJJ-MM-TT HH:MM erwartet']); + } + + if ($date->getTimestamp() <= time()) { + return Errors::invalidInput( + 'Der Zeitpunkt muss in der Zukunft liegen.', + ['date' => 'liegt nicht in der Zukunft'], + 'Zum sofortigen Veröffentlichen lit-post-publish verwenden.' + ); + } + + $postId = (int) $post->ID; + $result = wp_update_post([ + 'ID' => $postId, + 'post_status' => 'future', + 'post_date' => $date->format('Y-m-d H:i:s'), + 'post_date_gmt' => $date->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s'), + 'edit_date' => true, + ], true); + if ($result instanceof WP_Error) { + return Errors::invalidInput('WordPress hat das Planen abgelehnt: ' . $result->get_error_message()); + } + + if (get_post_status($postId) !== 'future') { + return Errors::unsupported( + 'WordPress hat den Eintrag nicht als geplant gespeichert.', + 'Eintrag in WordPress prüfen (z. B. Freigabe-Workflow eines anderen Plugins).' + ); + } + + // The status change is saved – failures from here on are logged and reported as warnings only. + $warnings = []; + FollowUp::run( + fn () => do_action('lit_mcp_after_save', $postId, $this->name(), $input), + $this->name() . ' lit_mcp_after_save', + 'Eine Erweiterung ist nach dem Planen auf einen Fehler gestoßen; die Änderung ist gespeichert.', + $warnings + ); + + $title = (string) $post->post_title; + $summary = sprintf('"%s" geplant für %s', $title, $raw) + . ($post->post_status === 'publish' ? ' (war veröffentlicht und ist bis dahin offline)' : ''); + $logId = FollowUp::run( + fn () => $this->log->record($this->name(), (string) $post->post_type, (string) $postId, $summary), + $this->name() . ' change log', + 'Änderungsprotokoll konnte nicht geschrieben werden; die Änderung ist gespeichert.', + $warnings + ); + + $updated = get_post($postId); + $updated = $updated instanceof WP_Post ? $updated : $post; + + return ToolResult::ok($summary . '.') + ->changed((string) $postId, (string) $post->post_type, 'scheduled', $title) + ->rev(Rev::ofPost($updated)) + ->logId(is_int($logId) ? $logId : null) + ->urls((string) get_preview_post_link($updated), (string) get_edit_post_link($postId, 'raw')) + ->warnings($warnings) + ->toArray(); + } + + /** @param array $input */ + private function load(array $input): WP_Post|WP_Error + { + $post = PostGuard::editable($input['post_id'] ?? null, $this->schema); + if ($post instanceof WP_Error) { + return $post; + } + + return current_user_can('publish_post', (int) $post->ID) + ? $post + : Errors::forbidden('Keine Berechtigung, diesen Eintrag zu veröffentlichen oder zu planen.'); + } +} diff --git a/src/Integrations/WordPress/Status/PostTrashAbility.php b/src/Integrations/WordPress/Status/PostTrashAbility.php new file mode 100644 index 0000000..4933da6 --- /dev/null +++ b/src/Integrations/WordPress/Status/PostTrashAbility.php @@ -0,0 +1,156 @@ +trashEnabled = $trashEnabled + ?? static fn (): bool => ! defined('EMPTY_TRASH_DAYS') || (bool) EMPTY_TRASH_DAYS; + } + + public function name(): string + { + return 'lit/post-trash'; + } + + public function label(): string + { + return 'In den Papierkorb'; + } + + public function description(): string + { + return 'Legt einen Eintrag in den Papierkorb (nie endgültig löschen; wiederherstellbar in WordPress). ' + . 'Erst nach Rückfrage beim Nutzer mit confirm: true aufrufen.'; + } + + public function inputSchema(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'post_id' => ['type' => 'integer', 'minimum' => 1], + 'confirm' => ['type' => 'boolean', 'description' => 'true nach Bestätigung durch den Nutzer.'], + ], + 'required' => ['post_id'], + 'additionalProperties' => false, + ]; + } + + public function outputSchema(): ?array + { + return ToolResult::schema(); + } + + public function annotations(): array + { + return ['readonly' => false, 'destructive' => true, 'idempotent' => true]; + } + + public function isDirect(): bool + { + return false; + } + + public function permission(array $input): bool|WP_Error + { + $post = $this->load($input); + + return $post instanceof WP_Error ? $post : true; + } + + public function execute(array $input): mixed + { + $post = $this->load($input); + if ($post instanceof WP_Error) { + return $post; + } + + $postId = (int) $post->ID; + $title = (string) $post->post_title; + if (($input['confirm'] ?? false) !== true) { + return Errors::confirmRequired(sprintf('"%s" (ID %d) in den Papierkorb legen?', $title, $postId)); + } + + if ($post->post_status === 'trash') { + return ToolResult::ok(sprintf('"%s" liegt bereits im Papierkorb.', $title)) + ->rev(Rev::ofPost($post)) + ->toArray(); + } + + if (! ($this->trashEnabled)()) { + return Errors::unsupported( + 'Der Papierkorb ist auf dieser Website deaktiviert (EMPTY_TRASH_DAYS = 0); ' + . 'WordPress würde endgültig löschen. Nichts wurde geändert.', + 'Endgültiges Löschen ist über LIT MCP nicht möglich; der Nutzer kann es in WordPress selbst tun.' + ); + } + + if (! wp_trash_post($postId)) { + return Errors::unsupported( + 'WordPress hat das Verschieben in den Papierkorb abgelehnt.', + 'Eintrag in WordPress prüfen (ein anderes Plugin kann das Verschieben verhindern).' + ); + } + + // The change is saved – a failure of the change log from here on is a warning only. + $summary = sprintf('"%s" in den Papierkorb gelegt', $title); + $warnings = []; + $logId = FollowUp::run( + fn () => $this->log->record($this->name(), (string) $post->post_type, (string) $postId, $summary), + $this->name() . ' change log', + 'Änderungsprotokoll konnte nicht geschrieben werden; die Änderung ist gespeichert.', + $warnings + ); + + $updated = get_post($postId); + $updated = $updated instanceof WP_Post ? $updated : $post; + + return ToolResult::ok(sprintf('"%s" liegt im Papierkorb und ist dort wiederherstellbar.', $title)) + ->changed((string) $postId, (string) $post->post_type, 'trashed', $title) + ->rev(Rev::ofPost($updated)) + ->logId(is_int($logId) ? $logId : null) + ->warnings($warnings) + ->toArray(); + } + + /** @param array $input */ + private function load(array $input): WP_Post|WP_Error + { + $post = PostGuard::editable($input['post_id'] ?? null, $this->schema); + if ($post instanceof WP_Error) { + return $post; + } + + return current_user_can('delete_post', (int) $post->ID) + ? $post + : Errors::forbidden('Keine Berechtigung, diesen Eintrag zu löschen.'); + } +} diff --git a/src/Integrations/WordPress/Status/PostUnpublishAbility.php b/src/Integrations/WordPress/Status/PostUnpublishAbility.php new file mode 100644 index 0000000..d82ffa9 --- /dev/null +++ b/src/Integrations/WordPress/Status/PostUnpublishAbility.php @@ -0,0 +1,128 @@ + 'object', + 'properties' => ['post_id' => ['type' => 'integer', 'minimum' => 1]], + 'required' => ['post_id'], + 'additionalProperties' => false, + ]; + } + + public function outputSchema(): ?array + { + return ToolResult::schema(); + } + + public function annotations(): array + { + return ['readonly' => false, 'destructive' => false, 'idempotent' => true]; + } + + public function isDirect(): bool + { + return false; + } + + public function permission(array $input): bool|WP_Error + { + $post = PostGuard::editable($input['post_id'] ?? null, $this->schema); + + return $post instanceof WP_Error ? $post : true; + } + + public function execute(array $input): mixed + { + $post = PostGuard::editable($input['post_id'] ?? null, $this->schema); + if ($post instanceof WP_Error) { + return $post; + } + + $postId = (int) $post->ID; + $title = (string) $post->post_title; + $editUrl = (string) get_edit_post_link($postId, 'raw'); + if (in_array($post->post_status, self::NOT_PUBLIC, true)) { + return ToolResult::ok(sprintf('"%s" ist nicht veröffentlicht (Status: %s).', $title, $post->post_status)) + ->rev(Rev::ofPost($post)) + ->urls((string) get_preview_post_link($post), $editUrl) + ->toArray(); + } + + $result = wp_update_post(['ID' => $postId, 'post_status' => 'draft'], true); + if ($result instanceof WP_Error) { + return Errors::invalidInput('WordPress hat das Zurückziehen abgelehnt: ' . $result->get_error_message()); + } + + // The status change is saved – failures from here on are logged and reported as warnings only. + $warnings = []; + FollowUp::run( + fn () => do_action('lit_mcp_after_save', $postId, $this->name(), $input), + $this->name() . ' lit_mcp_after_save', + 'Eine Erweiterung ist nach dem Zurückziehen auf einen Fehler gestoßen; die Änderung ist gespeichert.', + $warnings + ); + + $summary = sprintf('"%s" zurückgezogen (vorher %s, jetzt Entwurf)', $title, $post->post_status); + $logId = FollowUp::run( + fn () => $this->log->record($this->name(), (string) $post->post_type, (string) $postId, $summary), + $this->name() . ' change log', + 'Änderungsprotokoll konnte nicht geschrieben werden; die Änderung ist gespeichert.', + $warnings + ); + + $updated = get_post($postId); + $updated = $updated instanceof WP_Post ? $updated : $post; + + return ToolResult::ok($summary . '.') + ->changed((string) $postId, (string) $post->post_type, 'unpublished', $title) + ->rev(Rev::ofPost($updated)) + ->logId(is_int($logId) ? $logId : null) + ->urls((string) get_preview_post_link($updated), $editUrl) + ->warnings($warnings) + ->toArray(); + } +} diff --git a/src/Integrations/WordPress/WordPressIntegration.php b/src/Integrations/WordPress/WordPressIntegration.php index 3450a79..6fa6447 100644 --- a/src/Integrations/WordPress/WordPressIntegration.php +++ b/src/Integrations/WordPress/WordPressIntegration.php @@ -16,6 +16,10 @@ use LIT\MCP\Integrations\WordPress\Content\PostWriter; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Schema\FieldValidator; use LIT\MCP\Integrations\WordPress\Schema\TermAssigner; +use LIT\MCP\Integrations\WordPress\Status\PostPublishAbility; +use LIT\MCP\Integrations\WordPress\Status\PostScheduleAbility; +use LIT\MCP\Integrations\WordPress\Status\PostTrashAbility; +use LIT\MCP\Integrations\WordPress\Status\PostUnpublishAbility; /** * WordPress basics (spec §6): content, status, copies, revisions, menus, settings. @@ -48,6 +52,10 @@ final class WordPressIntegration implements Integration new ContentGetAbility($this->schema, $presenter), new ContentCreateAbility($this->schema, $writer, $this->log), new ContentUpdateAbility($this->schema, $writer, $this->log), + new PostPublishAbility($this->schema, $this->log), + new PostUnpublishAbility($this->schema, $this->log), + new PostScheduleAbility($this->schema, $this->log), + new PostTrashAbility($this->schema, $this->log), ]; } } diff --git a/tests/Unit/Integrations/WordPress/Status/PostPublishAbilityTest.php b/tests/Unit/Integrations/WordPress/Status/PostPublishAbilityTest.php new file mode 100644 index 0000000..54a1884 --- /dev/null +++ b/tests/Unit/Integrations/WordPress/Status/PostPublishAbilityTest.php @@ -0,0 +1,84 @@ +allows('isManaged')->andReturn(true); + + return new PostPublishAbility($schema, $log); + } + + private function stubPost(array $fields): void + { + Functions\when('get_post')->justReturn(new WP_Post(array_merge(['ID' => 5, 'post_type' => 'page', 'post_title' => 'Team'], $fields))); + Functions\when('current_user_can')->justReturn(true); + 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'); + } + + public function testPublishesADraft(): void + { + $this->stubPost(['post_status' => 'draft', 'post_date_gmt' => '0000-00-00 00:00:00']); + Functions\expect('wp_update_post')->once()->with(['ID' => 5, 'post_status' => 'publish'], true)->andReturn(5); + Functions\when('get_post_status')->justReturn('publish'); + $log = Mockery::mock(ChangeLog::class); + $log->expects('record')->with('lit/post-publish', 'page', '5', '"Team" veröffentlicht')->andReturn(9); + + $result = $this->ability($log)->execute(['post_id' => 5]); + + self::assertSame('published', $result['changed'][0]['action']); + self::assertSame(9, $result['log_id']); + self::assertSame('https://kunde.de/team/', $result['preview_url']); + } + + public function testScheduledPostIsPublishedNowInsteadOfStayingFuture(): void + { + $this->stubPost(['post_status' => 'future', 'post_date_gmt' => '2099-01-01 10:00:00']); + Functions\when('current_time')->alias(static fn (string $type, $gmt = 0) => $gmt ? '2026-09-25 08:00:00' : '2026-09-25 10:00:00'); + Functions\expect('wp_update_post')->once()->with([ + 'ID' => 5, + 'post_status' => 'publish', + 'post_date' => '2026-09-25 10:00:00', + 'post_date_gmt' => '2026-09-25 08:00:00', + 'edit_date' => true, + ], true)->andReturn(5); + Functions\when('get_post_status')->justReturn('publish'); + $log = Mockery::mock(ChangeLog::class); + $log->allows('record')->andReturn(1); + + $this->ability($log)->execute(['post_id' => 5]); + } + + public function testAlreadyPublishedIsANoOp(): void + { + $this->stubPost(['post_status' => 'publish']); + Functions\expect('wp_update_post')->never(); + + $result = $this->ability(Mockery::mock(ChangeLog::class))->execute(['post_id' => 5]); + + self::assertSame([], $result['changed']); + self::assertStringContainsString('bereits veröffentlicht', $result['summary']); + } + + public function testPermissionNeedsPublishRights(): void + { + $this->stubPost(['post_status' => 'draft']); + Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap !== 'publish_post'); + + self::assertSame('lit_forbidden', $this->ability(Mockery::mock(ChangeLog::class))->permission(['post_id' => 5])->get_error_code()); + } +} diff --git a/tests/Unit/Integrations/WordPress/Status/PostScheduleAbilityTest.php b/tests/Unit/Integrations/WordPress/Status/PostScheduleAbilityTest.php new file mode 100644 index 0000000..8bc6214 --- /dev/null +++ b/tests/Unit/Integrations/WordPress/Status/PostScheduleAbilityTest.php @@ -0,0 +1,67 @@ +justReturn(new WP_Post(['ID' => 5, 'post_type' => 'post', 'post_title' => 'News', 'post_status' => 'draft'])); + Functions\when('current_user_can')->justReturn(true); + Functions\when('wp_timezone')->justReturn(new DateTimeZone('Europe/Berlin')); + Functions\when('get_preview_post_link')->justReturn('https://kunde.de/?p=5&preview=true'); + Functions\when('get_edit_post_link')->justReturn('https://kunde.de/wp-admin/post.php?post=5&action=edit'); + } + + private function ability(ChangeLog $log): PostScheduleAbility + { + $schema = Mockery::mock(ContentSchema::class); + $schema->allows('isManaged')->andReturn(true); + + return new PostScheduleAbility($schema, $log); + } + + public function testSchedulesWithLocalAndGmtDates(): void + { + Functions\expect('wp_update_post')->once()->with([ + 'ID' => 5, + 'post_status' => 'future', + 'post_date' => '2099-06-01 09:30:00', + 'post_date_gmt' => '2099-06-01 07:30:00', + 'edit_date' => true, + ], true)->andReturn(5); + Functions\when('get_post_status')->justReturn('future'); + $log = Mockery::mock(ChangeLog::class); + $log->expects('record')->andReturn(4); + + $result = $this->ability($log)->execute(['post_id' => 5, 'date' => '2099-06-01 09:30']); + + self::assertSame('scheduled', $result['changed'][0]['action']); + self::assertStringContainsString('2099-06-01 09:30', $result['summary']); + } + + public function testPastAndImpossibleDatesAreRejected(): void + { + Functions\expect('wp_update_post')->never(); + $ability = $this->ability(Mockery::mock(ChangeLog::class)); + + $past = $ability->execute(['post_id' => 5, 'date' => '2020-01-01 10:00']); + $impossible = $ability->execute(['post_id' => 5, 'date' => '2099-13-45 10:00']); + + self::assertSame('lit_validation_failed', $past->get_error_code()); + self::assertStringContainsString('Zukunft', $past->get_error_message()); + self::assertSame('lit_validation_failed', $impossible->get_error_code()); + } +} diff --git a/tests/Unit/Integrations/WordPress/Status/PostTrashAbilityTest.php b/tests/Unit/Integrations/WordPress/Status/PostTrashAbilityTest.php new file mode 100644 index 0000000..e19e9ea --- /dev/null +++ b/tests/Unit/Integrations/WordPress/Status/PostTrashAbilityTest.php @@ -0,0 +1,70 @@ +justReturn(new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_title' => 'Alt', 'post_status' => 'publish'])); + Functions\when('current_user_can')->justReturn(true); + $schema = Mockery::mock(ContentSchema::class); + $schema->allows('isManaged')->andReturn(true); + + return new PostTrashAbility($schema, $log); + } + + public function testRequiresConfirmation(): void + { + Functions\expect('wp_trash_post')->never(); + + $result = $this->ability(Mockery::mock(ChangeLog::class))->execute(['post_id' => 5]); + + self::assertSame('lit_confirm_required', $result->get_error_code()); + } + + public function testMovesToTrashNeverDeletes(): void + { + Functions\expect('wp_trash_post')->once()->with(5)->andReturn(new WP_Post(['ID' => 5])); + Functions\expect('wp_delete_post')->never(); + $log = Mockery::mock(ChangeLog::class); + $log->expects('record')->with('lit/post-trash', 'page', '5', '"Alt" in den Papierkorb gelegt')->andReturn(2); + + $result = $this->ability($log)->execute(['post_id' => 5, 'confirm' => true]); + + self::assertSame('trashed', $result['changed'][0]['action']); + self::assertStringContainsString('Papierkorb', $result['summary']); + } + + public function testRefusesWhenTheTrashIsDisabledInsteadOfDeleting(): void + { + Functions\when('get_post')->justReturn(new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_status' => 'publish'])); + Functions\when('current_user_can')->justReturn(true); + Functions\expect('wp_trash_post')->never(); + $schema = Mockery::mock(ContentSchema::class); + $schema->allows('isManaged')->andReturn(true); + $ability = new PostTrashAbility($schema, Mockery::mock(ChangeLog::class), static fn (): bool => false); + + $result = $ability->execute(['post_id' => 5, 'confirm' => true]); + + self::assertSame('lit_unsupported', $result->get_error_code()); + } + + public function testIsDestructiveAndIndirect(): void + { + $ability = $this->ability(Mockery::mock(ChangeLog::class)); + + self::assertTrue($ability->annotations()['destructive']); + self::assertFalse($ability->isDirect()); + } +} diff --git a/tests/Unit/Integrations/WordPress/Status/PostUnpublishAbilityTest.php b/tests/Unit/Integrations/WordPress/Status/PostUnpublishAbilityTest.php new file mode 100644 index 0000000..955d6c4 --- /dev/null +++ b/tests/Unit/Integrations/WordPress/Status/PostUnpublishAbilityTest.php @@ -0,0 +1,48 @@ +justReturn(new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_title' => 'Team', 'post_status' => $status])); + Functions\when('current_user_can')->justReturn(true); + Functions\when('get_preview_post_link')->justReturn('https://kunde.de/?p=5&preview=true'); + Functions\when('get_edit_post_link')->justReturn('https://kunde.de/wp-admin/post.php?post=5&action=edit'); + $schema = Mockery::mock(ContentSchema::class); + $schema->allows('isManaged')->andReturn(true); + + return new PostUnpublishAbility($schema, $log); + } + + public function testPublishedPostBecomesDraft(): void + { + Functions\expect('wp_update_post')->once()->with(['ID' => 5, 'post_status' => 'draft'], true)->andReturn(5); + $log = Mockery::mock(ChangeLog::class); + $log->expects('record')->andReturn(3); + + $result = $this->ability($log, 'publish')->execute(['post_id' => 5]); + + self::assertSame('unpublished', $result['changed'][0]['action']); + } + + public function testDraftIsANoOp(): void + { + Functions\expect('wp_update_post')->never(); + + $result = $this->ability(Mockery::mock(ChangeLog::class), 'draft')->execute(['post_id' => 5]); + + self::assertSame([], $result['changed']); + } +} diff --git a/tests/Unit/Integrations/WordPress/WordPressIntegrationTest.php b/tests/Unit/Integrations/WordPress/WordPressIntegrationTest.php index e9a6529..c5ac069 100644 --- a/tests/Unit/Integrations/WordPress/WordPressIntegrationTest.php +++ b/tests/Unit/Integrations/WordPress/WordPressIntegrationTest.php @@ -20,6 +20,10 @@ final class WordPressIntegrationTest extends TestCase 'lit/content-get', 'lit/content-create', 'lit/content-update', + 'lit/post-publish', + 'lit/post-unpublish', + 'lit/post-schedule', + 'lit/post-trash', ]; public function testProvidesTheWordPressAbilitiesInOrder(): void