5, 'post_type' => 'page', 'post_title' => 'Team', 'post_content' => '', 'post_excerpt' => 'Kurz', 'post_parent' => 2, 'menu_order' => 3, ]); } private function copy(): WP_Post { return new WP_Post([ 'ID' => 77, 'post_type' => 'page', 'post_title' => 'Team (Kopie)', 'post_content' => 'neu', 'post_excerpt' => 'Neu kurz', ]); } public function testDuplicateCopiesContentMetaAndTermsAsDraft(): void { Functions\when('get_current_user_id')->justReturn(4); Functions\when('maybe_unserialize')->returnArg(); Functions\expect('wp_insert_post')->once()->with($this->draftMatcher(), true)->andReturn(77); Functions\when('get_post_meta')->alias(static fn (int $id) => $id === 5 ? [ '_edit_lock' => ['123:1'], '_edit_last' => ['1'], '_wp_old_slug' => ['team-alt'], '_et_pb_use_builder' => ['on'], '_et_pb_use_divi_5' => ['on'], '_wp_page_template' => ['page-template-blank.php'], '_thumbnail_id' => ['31'], 'role' => ['Leitung'], ] : ['_edit_lock' => ['999:4'], '_et_pb_use_builder' => ['off']]); // Meta a save hook put on the fresh copy is replaced by the original's. Functions\expect('delete_post_meta')->once()->with(77, '_et_pb_use_builder'); Functions\expect('add_post_meta')->once()->with(77, '_et_pb_use_builder', 'on'); Functions\expect('add_post_meta')->once()->with(77, '_et_pb_use_divi_5', 'on'); Functions\expect('add_post_meta')->once()->with(77, '_wp_page_template', 'page-template-blank.php'); Functions\expect('add_post_meta')->once()->with(77, '_thumbnail_id', '31'); Functions\expect('add_post_meta')->once()->with(77, 'role', 'Leitung'); Functions\when('get_object_taxonomies')->justReturn(['category']); Functions\when('wp_get_object_terms')->justReturn([4, 9]); Functions\expect('wp_set_object_terms')->once()->with(77, [4, 9], 'category', false); Functions\expect('update_post_meta')->once()->with(77, '_lit_mcp_copy_of', 5); self::assertSame(77, (new PostCloner())->duplicate($this->original())); } public function testDuplicateFailureThrows(): void { Functions\when('get_current_user_id')->justReturn(4); Functions\when('wp_insert_post')->justReturn(new WP_Error('x', 'kaputt')); $this->expectException(RuntimeException::class); (new PostCloner())->duplicate($this->original()); } public function testDuplicateFailingAfterInsertDeletesTheNewDraft(): void { Functions\when('get_current_user_id')->justReturn(4); Functions\when('maybe_unserialize')->returnArg(); Functions\when('wp_insert_post')->justReturn(77); Functions\when('get_post_meta')->alias(static fn (int $id) => $id === 5 ? ['role' => ['Leitung']] : []); Functions\when('add_post_meta')->justReturn(false); Functions\expect('wp_delete_post')->once()->with(77, true)->andReturn(new WP_Post(['ID' => 77])); try { (new PostCloner())->duplicate($this->original()); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertTrue($e->rolledBack); } } public function testDuplicateReportsALeftoverDraftWhenItCannotBeDeleted(): void { Functions\when('get_current_user_id')->justReturn(4); Functions\when('maybe_unserialize')->returnArg(); Functions\when('wp_insert_post')->justReturn(77); Functions\when('get_post_meta')->justReturn([]); Functions\when('get_object_taxonomies')->justReturn(['category']); Functions\when('wp_get_object_terms')->justReturn(new WP_Error('x', 'kaputt')); Functions\when('wp_delete_post')->justReturn(false); try { (new PostCloner())->duplicate($this->original()); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertFalse($e->rolledBack); self::assertSame(77, $e->postId); } } public function testReplaceOverwritesContentMetaAndTermsButKeepsTheOriginalsIdentity(): void { Functions\when('maybe_unserialize')->returnArg(); Functions\expect('wp_update_post')->once()->with([ 'ID' => 5, 'post_title' => 'Team', 'post_content' => 'neu', 'post_excerpt' => 'Neu kurz', ], true)->andReturn(5); Functions\when('get_post_meta')->alias(static fn (int $id) => $id === 5 ? ['_edit_lock' => ['1'], '_wp_old_slug' => ['alt'], 'role' => ['Alt'], 'city' => ['Trier']] : ['_lit_mcp_copy_of' => ['5'], '_edit_last' => ['4'], 'role' => ['Neu']]); Functions\expect('delete_post_meta')->once()->with(5, 'role'); Functions\expect('delete_post_meta')->once()->with(5, 'city'); Functions\expect('add_post_meta')->once()->with(5, 'role', 'Neu'); Functions\when('get_object_taxonomies')->justReturn(['category']); Functions\when('wp_get_object_terms')->justReturn([12]); Functions\expect('wp_set_object_terms')->once()->with(5, [12], 'category', false); (new PostCloner())->replace($this->original(), $this->copy()); } public function testReplaceThrowsWhenWordPressRejectsTheUpdate(): void { Functions\when('wp_update_post')->justReturn(new WP_Error('x', 'kaputt')); Functions\expect('delete_post_meta')->never(); $this->expectException(RuntimeException::class); (new PostCloner())->replace($this->original(), $this->copy()); } public function testReplaceThrowsWhenTermsCannotBeSet(): void { Functions\when('wp_update_post')->justReturn(5); Functions\when('get_post_meta')->justReturn([]); Functions\when('get_object_taxonomies')->justReturn(['category']); Functions\when('wp_get_object_terms')->justReturn([12]); Functions\when('wp_set_object_terms')->justReturn(new WP_Error('x', 'kaputt')); $this->expectException(RuntimeException::class); (new PostCloner())->replace($this->original(), $this->copy()); } public function testSnapshotAndRestoreBringBackRowMetaAndTerms(): void { $meta = ['_edit_lock' => ['1'], 'role' => ['Alt']]; Functions\when('get_post_meta')->alias(static function () use (&$meta): array { return $meta; }); Functions\when('get_object_taxonomies')->justReturn(['category']); Functions\when('wp_get_object_terms')->justReturn([4]); $cloner = new PostCloner(); $snapshot = $cloner->snapshot($this->original()); self::assertSame(['post_title' => 'Team', 'post_content' => '', 'post_excerpt' => 'Kurz'], $snapshot['post']); self::assertSame(['role' => ['Alt']], $snapshot['meta']); self::assertSame(['category' => [4]], $snapshot['terms']); $meta = ['_edit_lock' => ['1'], 'role' => ['Neu'], 'city' => ['Trier']]; Functions\when('maybe_unserialize')->returnArg(); Functions\expect('wp_update_post')->once() ->with(array_merge(['ID' => 5], $snapshot['post']), true)->andReturn(5); Functions\expect('delete_post_meta')->once()->with(5, 'role'); Functions\expect('delete_post_meta')->once()->with(5, 'city'); Functions\expect('add_post_meta')->once()->with(5, 'role', 'Alt'); Functions\expect('wp_set_object_terms')->once()->with(5, [4], 'category', false); $cloner->restore($this->original(), $snapshot); } public function testRestoreTriesEveryPartAndReportsFailures(): void { Functions\when('wp_update_post')->justReturn(new WP_Error('x', 'kaputt')); Functions\when('get_post_meta')->justReturn([]); Functions\expect('wp_set_object_terms')->once()->with(5, [4], 'category', false); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Restore incomplete'); (new PostCloner())->restore($this->original(), [ 'post' => ['post_title' => 'Team', 'post_content' => '', 'post_excerpt' => ''], 'meta' => [], 'terms' => ['category' => [4]], ]); } private function draftMatcher(): \Mockery\Matcher\Closure { return \Mockery::on(static fn (array $data): bool => $data['post_status'] === 'draft' && $data['post_type'] === 'page' && $data['post_title'] === 'Team (Kopie)' && $data['post_content'] === '' && $data['post_excerpt'] === 'Kurz' && $data['post_parent'] === 2 && $data['menu_order'] === 3 && $data['post_author'] === 4); } }