From 0fd5311f0987bc49a0c0321fa0e61d57b6dd88cf Mon Sep 17 00:00:00 2001 From: Grimur Vid Neyst Date: Fri, 10 Apr 2026 17:29:25 +0200 Subject: [PATCH 1/2] fix: splitting discussion when user has been deleted --- composer.json | 14 +- src/Api/Commands/SplitDiscussionHandler.php | 16 +- tests/.phpunit.result.cache | 1 + tests/integration/api/SplitDiscussionTest.php | 217 ++++++++++++++++++ tests/integration/setup.php | 19 ++ tests/phpunit.integration.xml | 24 ++ 6 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 tests/.phpunit.result.cache create mode 100644 tests/integration/api/SplitDiscussionTest.php create mode 100644 tests/integration/setup.php create mode 100644 tests/phpunit.integration.xml diff --git a/composer.json b/composer.json index 458f4f9..2c2d29c 100644 --- a/composer.json +++ b/composer.json @@ -53,14 +53,24 @@ "FoF\\Split\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "FoF\\Split\\Tests\\": "tests/" + } + }, "require-dev": { + "flarum/testing": "^1.0.0", "flarum/phpstan": "*" }, "scripts": { "analyse:phpstan": "phpstan analyse", - "clear-cache:phpstan": "phpstan clear-result-cache" + "clear-cache:phpstan": "phpstan clear-result-cache", + "test:integration": "phpunit -c tests/phpunit.integration.xml", + "test:setup": "@php tests/integration/setup.php" }, "scripts-descriptions": { - "analyse:phpstan": "Run static analysis" + "analyse:phpstan": "Run static analysis", + "test:integration": "Runs integration tests.", + "test:setup": "Sets up a database for use with integration tests. Execute this only once." } } diff --git a/src/Api/Commands/SplitDiscussionHandler.php b/src/Api/Commands/SplitDiscussionHandler.php index d3cbe9f..2ea83cf 100644 --- a/src/Api/Commands/SplitDiscussionHandler.php +++ b/src/Api/Commands/SplitDiscussionHandler.php @@ -13,6 +13,7 @@ namespace FoF\Split\Api\Commands; use Flarum\Discussion\Discussion; +use Flarum\Discussion\Event\Started; use Flarum\Post\Post; use Flarum\Post\PostRepository; use Flarum\Settings\SettingsRepositoryInterface; @@ -94,7 +95,7 @@ public function handle(SplitDiscussion $command) $originalDiscussion = $startPost->discussion; // create a new discussion for the user of the first splitted reply. - $discussion = Discussion::start($command->title, $startPost->user); + $discussion = $this->createDiscussionFromStartPost($command, $startPost); $discussion->setFirstPost($startPost); // persist the new discussion. @@ -121,6 +122,19 @@ public function handle(SplitDiscussion $command) return $discussion; } + protected function createDiscussionFromStartPost(SplitDiscussion $command, Post $startPost): Discussion + { + if ($startPost->user) { + return Discussion::start($command->title, $startPost->user); + } + + $discussion = new Discussion(); + $discussion->title = $command->title; + $discussion->raise(new Started($discussion)); + + return $discussion; + } + /** * Assign the specific range to a new Discussion without resetting post numbers. * diff --git a/tests/.phpunit.result.cache b/tests/.phpunit.result.cache new file mode 100644 index 0000000..8fa2a02 --- /dev/null +++ b/tests/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":{"FoF\\Split\\Tests\\integration\\api\\SplitDiscussionTest::it_can_split_posts_when_the_selected_start_post_author_has_been_deleted":3,"FoF\\Split\\Tests\\integration\\api\\SplitDiscussionTest::it_still_uses_the_normal_creation_path_when_the_start_post_author_exists":3,"FoF\\Split\\Tests\\integration\\api\\SplitDiscussionTest::it_refreshes_original_discussion_metadata_and_creates_split_event_posts":3},"times":{"FoF\\Split\\Tests\\integration\\api\\SplitDiscussionTest::it_can_split_posts_when_the_selected_start_post_author_has_been_deleted":0.529,"FoF\\Split\\Tests\\integration\\api\\SplitDiscussionTest::it_still_uses_the_normal_creation_path_when_the_start_post_author_exists":0.44,"FoF\\Split\\Tests\\integration\\api\\SplitDiscussionTest::it_refreshes_original_discussion_metadata_and_creates_split_event_posts":0.436}} \ No newline at end of file diff --git a/tests/integration/api/SplitDiscussionTest.php b/tests/integration/api/SplitDiscussionTest.php new file mode 100644 index 0000000..26a0d5f --- /dev/null +++ b/tests/integration/api/SplitDiscussionTest.php @@ -0,0 +1,217 @@ +extension('fof-split'); + + $this->prepareDatabase([ + 'discussions' => [ + [ + 'id' => 1, + 'title' => 'Original Discussion', + 'slug' => 'original-discussion', + 'comment_count' => 5, + 'participant_count' => 1, + 'created_at' => '2024-01-01 00:00:00', + 'user_id' => 1, + 'first_post_id' => 1, + 'last_posted_at' => '2024-01-01 00:04:00', + 'last_posted_user_id' => 1, + 'last_post_id' => 5, + 'last_post_number' => 5, + ], + ], + 'posts' => [ + [ + 'id' => 1, + 'discussion_id' => 1, + 'number' => 1, + 'created_at' => '2024-01-01 00:00:00', + 'user_id' => 1, + 'type' => 'comment', + 'content' => '

Opening post

', + 'ip_address' => '127.0.0.1', + 'is_private' => 0, + ], + [ + 'id' => 2, + 'discussion_id' => 1, + 'number' => 2, + 'created_at' => '2024-01-01 00:01:00', + 'user_id' => null, + 'type' => 'comment', + 'content' => '

Deleted author reply

', + 'ip_address' => '127.0.0.1', + 'is_private' => 0, + ], + [ + 'id' => 3, + 'discussion_id' => 1, + 'number' => 3, + 'created_at' => '2024-01-01 00:02:00', + 'user_id' => null, + 'type' => 'comment', + 'content' => '

Deleted author follow-up

', + 'ip_address' => '127.0.0.1', + 'is_private' => 0, + ], + [ + 'id' => 4, + 'discussion_id' => 1, + 'number' => 4, + 'created_at' => '2024-01-01 00:03:00', + 'user_id' => 1, + 'type' => 'comment', + 'content' => '

Existing author reply

', + 'ip_address' => '127.0.0.1', + 'is_private' => 0, + ], + [ + 'id' => 5, + 'discussion_id' => 1, + 'number' => 5, + 'created_at' => '2024-01-01 00:04:00', + 'user_id' => 1, + 'type' => 'comment', + 'content' => '

Existing author follow-up

', + 'ip_address' => '127.0.0.1', + 'is_private' => 0, + ], + ], + ]); + } + + /** + * @test + */ + public function it_can_split_posts_when_the_selected_start_post_author_has_been_deleted(): void + { + $response = $this->sendSplitRequest('Split Discussion', 2, 3); + + $this->assertSame(200, $response->getStatusCode()); + + $payload = json_decode((string) $response->getBody(), true); + + $this->assertIsArray($payload); + $this->assertSame('Split Discussion', $payload['data']['attributes']['title']); + + $newDiscussionId = (int) $payload['data']['id']; + + $discussion = $this->database()->table('discussions')->where('id', $newDiscussionId)->first(); + + $this->assertNotNull($discussion); + $this->assertSame(2, $discussion->first_post_id); + $this->assertNull($discussion->user_id); + $this->assertSame('2024-01-01 00:01:00', $discussion->created_at); + + $splitPosts = $this->database()->table('posts') + ->where('discussion_id', $newDiscussionId) + ->where('type', 'comment') + ->orderBy('number') + ->pluck('id') + ->all(); + + $this->assertSame([2, 3], $splitPosts); + } + + /** + * @test + */ + public function it_still_uses_the_normal_creation_path_when_the_start_post_author_exists(): void + { + $response = $this->sendSplitRequest('Normal Split Discussion', 4, 5); + + $this->assertSame(200, $response->getStatusCode()); + + $payload = json_decode((string) $response->getBody(), true); + + $this->assertIsArray($payload); + + $newDiscussionId = (int) $payload['data']['id']; + + $discussion = $this->database()->table('discussions')->where('id', $newDiscussionId)->first(); + + $this->assertNotNull($discussion); + $this->assertSame(4, $discussion->first_post_id); + $this->assertSame(1, $discussion->user_id); + $this->assertSame('2024-01-01 00:03:00', $discussion->created_at); + + $renumberedPosts = $this->database()->table('posts') + ->where('discussion_id', $newDiscussionId) + ->where('type', 'comment') + ->orderBy('id') + ->pluck('number') + ->all(); + + $this->assertSame([1, 2], $renumberedPosts); + } + + /** + * @test + */ + public function it_refreshes_original_discussion_metadata_and_creates_split_event_posts(): void + { + $response = $this->sendSplitRequest('Split Discussion', 2, 3); + + $this->assertSame(200, $response->getStatusCode()); + + $payload = json_decode((string) $response->getBody(), true); + + $this->assertIsArray($payload); + + $newDiscussionId = (int) $payload['data']['id']; + + $originalDiscussion = $this->database()->table('discussions')->where('id', 1)->first(); + + $this->assertNotNull($originalDiscussion); + $this->assertSame(3, $originalDiscussion->comment_count); + $this->assertSame(1, $originalDiscussion->participant_count); + $this->assertSame(5, $originalDiscussion->last_post_id); + $this->assertSame(5, $originalDiscussion->last_post_number); + + $originalSplitPosts = $this->database()->table('posts') + ->where('discussion_id', 1) + ->where('type', 'discussionSplit') + ->count(); + + $newSplitPosts = $this->database()->table('posts') + ->where('discussion_id', $newDiscussionId) + ->where('type', 'discussionSplit') + ->count(); + + $this->assertSame(1, $originalSplitPosts); + $this->assertSame(1, $newSplitPosts); + } + + protected function sendSplitRequest(string $title, int $startPostId, int $endPostNumber) + { + $request = $this->request('POST', '/api/split', [ + 'authenticatedAs' => 1, + 'json' => [ + 'title' => $title, + 'start_post_id' => $startPostId, + 'end_post_number' => $endPostNumber, + ], + ]); + + return $this->send($this->requestWithCsrfToken($request)); + } +} diff --git a/tests/integration/setup.php b/tests/integration/setup.php new file mode 100644 index 0000000..3281430 --- /dev/null +++ b/tests/integration/setup.php @@ -0,0 +1,19 @@ +run(); diff --git a/tests/phpunit.integration.xml b/tests/phpunit.integration.xml new file mode 100644 index 0000000..23afc23 --- /dev/null +++ b/tests/phpunit.integration.xml @@ -0,0 +1,24 @@ + + + + + ../src/ + + + + + ./integration + + + From b25cd0ca4705688c888c2118ca8daf94ebf2bafc Mon Sep 17 00:00:00 2001 From: Grimur Vid Neyst Date: Fri, 10 Apr 2026 17:38:50 +0200 Subject: [PATCH 2/2] refactor: fix style issues --- tests/integration/api/SplitDiscussionTest.php | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/tests/integration/api/SplitDiscussionTest.php b/tests/integration/api/SplitDiscussionTest.php index 26a0d5f..61388c1 100644 --- a/tests/integration/api/SplitDiscussionTest.php +++ b/tests/integration/api/SplitDiscussionTest.php @@ -25,75 +25,75 @@ protected function setUp(): void $this->prepareDatabase([ 'discussions' => [ [ - 'id' => 1, - 'title' => 'Original Discussion', - 'slug' => 'original-discussion', - 'comment_count' => 5, - 'participant_count' => 1, - 'created_at' => '2024-01-01 00:00:00', - 'user_id' => 1, - 'first_post_id' => 1, - 'last_posted_at' => '2024-01-01 00:04:00', + 'id' => 1, + 'title' => 'Original Discussion', + 'slug' => 'original-discussion', + 'comment_count' => 5, + 'participant_count' => 1, + 'created_at' => '2024-01-01 00:00:00', + 'user_id' => 1, + 'first_post_id' => 1, + 'last_posted_at' => '2024-01-01 00:04:00', 'last_posted_user_id' => 1, - 'last_post_id' => 5, - 'last_post_number' => 5, + 'last_post_id' => 5, + 'last_post_number' => 5, ], ], 'posts' => [ [ - 'id' => 1, + 'id' => 1, 'discussion_id' => 1, - 'number' => 1, - 'created_at' => '2024-01-01 00:00:00', - 'user_id' => 1, - 'type' => 'comment', - 'content' => '

Opening post

', - 'ip_address' => '127.0.0.1', - 'is_private' => 0, + 'number' => 1, + 'created_at' => '2024-01-01 00:00:00', + 'user_id' => 1, + 'type' => 'comment', + 'content' => '

Opening post

', + 'ip_address' => '127.0.0.1', + 'is_private' => 0, ], [ - 'id' => 2, + 'id' => 2, 'discussion_id' => 1, - 'number' => 2, - 'created_at' => '2024-01-01 00:01:00', - 'user_id' => null, - 'type' => 'comment', - 'content' => '

Deleted author reply

', - 'ip_address' => '127.0.0.1', - 'is_private' => 0, + 'number' => 2, + 'created_at' => '2024-01-01 00:01:00', + 'user_id' => null, + 'type' => 'comment', + 'content' => '

Deleted author reply

', + 'ip_address' => '127.0.0.1', + 'is_private' => 0, ], [ - 'id' => 3, + 'id' => 3, 'discussion_id' => 1, - 'number' => 3, - 'created_at' => '2024-01-01 00:02:00', - 'user_id' => null, - 'type' => 'comment', - 'content' => '

Deleted author follow-up

', - 'ip_address' => '127.0.0.1', - 'is_private' => 0, + 'number' => 3, + 'created_at' => '2024-01-01 00:02:00', + 'user_id' => null, + 'type' => 'comment', + 'content' => '

Deleted author follow-up

', + 'ip_address' => '127.0.0.1', + 'is_private' => 0, ], [ - 'id' => 4, + 'id' => 4, 'discussion_id' => 1, - 'number' => 4, - 'created_at' => '2024-01-01 00:03:00', - 'user_id' => 1, - 'type' => 'comment', - 'content' => '

Existing author reply

', - 'ip_address' => '127.0.0.1', - 'is_private' => 0, + 'number' => 4, + 'created_at' => '2024-01-01 00:03:00', + 'user_id' => 1, + 'type' => 'comment', + 'content' => '

Existing author reply

', + 'ip_address' => '127.0.0.1', + 'is_private' => 0, ], [ - 'id' => 5, + 'id' => 5, 'discussion_id' => 1, - 'number' => 5, - 'created_at' => '2024-01-01 00:04:00', - 'user_id' => 1, - 'type' => 'comment', - 'content' => '

Existing author follow-up

', - 'ip_address' => '127.0.0.1', - 'is_private' => 0, + 'number' => 5, + 'created_at' => '2024-01-01 00:04:00', + 'user_id' => 1, + 'type' => 'comment', + 'content' => '

Existing author follow-up

', + 'ip_address' => '127.0.0.1', + 'is_private' => 0, ], ], ]); @@ -205,9 +205,9 @@ protected function sendSplitRequest(string $title, int $startPostId, int $endPos { $request = $this->request('POST', '/api/split', [ 'authenticatedAs' => 1, - 'json' => [ - 'title' => $title, - 'start_post_id' => $startPostId, + 'json' => [ + 'title' => $title, + 'start_post_id' => $startPostId, 'end_post_number' => $endPostNumber, ], ]);