From d312fc673c47aa1a6ea45061130ac7f4e3c9e886 Mon Sep 17 00:00:00 2001 From: Grimur Vid Neyst Date: Thu, 16 Apr 2026 17:50:16 +0200 Subject: [PATCH] fix: unread/read disucssion bug --- composer.json | 8 + src/Api/Commands/SplitDiscussionHandler.php | 12 ++ tests/integration/api/SplitReadStateTest.php | 165 +++++++++++++++++++ tests/integration/setup.php | 9 + tests/phpunit.integration.xml | 11 ++ 5 files changed, 205 insertions(+) create mode 100644 tests/integration/api/SplitReadStateTest.php create mode 100644 tests/integration/setup.php create mode 100644 tests/phpunit.integration.xml diff --git a/composer.json b/composer.json index 686e3fb..3dbc6fc 100644 --- a/composer.json +++ b/composer.json @@ -60,10 +60,18 @@ "flarum/flags": "*" }, "scripts": { + "test": [ + "@php -r \"echo 'Available test commands:'.PHP_EOL.' composer test:setup'.PHP_EOL.' composer test:integration'.PHP_EOL;\"" + ], + "test:setup": "@php tests/integration/setup.php", + "test:integration": "phpunit -c tests/phpunit.integration.xml", "analyse:phpstan": "phpstan analyse", "clear-cache:phpstan": "phpstan clear-result-cache" }, "scripts-descriptions": { + "test": "Show available test commands", + "test:setup": "Set up integration test environment", + "test:integration": "Run integration tests", "analyse:phpstan": "Run static analysis" }, "minimum-stability": "beta", diff --git a/src/Api/Commands/SplitDiscussionHandler.php b/src/Api/Commands/SplitDiscussionHandler.php index 77edf9a..d713e7f 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\UserState; use Flarum\Extension\ExtensionManager; use Flarum\Post\Post; use Flarum\Post\PostRepository; @@ -83,6 +84,9 @@ public function handle(SplitDiscussion $command) new DiscussionWasSplit($command->actor, $affectedPosts, $originalDiscussion, $discussion) ); + $originalDiscussion = $this->refreshDiscussion($originalDiscussion); + $this->clampReadStates($originalDiscussion); + return $discussion; } @@ -156,6 +160,14 @@ protected function refreshDiscussion(Discussion $discussion) return Discussion::find($discussion->id); } + protected function clampReadStates(Discussion $discussion): void + { + UserState::query() + ->where('discussion_id', $discussion->id) + ->where('last_read_post_number', '>', $discussion->last_post_number) + ->update(['last_read_post_number' => $discussion->last_post_number]); + } + /** * Sets the tags for the new discussion based on the old one. * diff --git a/tests/integration/api/SplitReadStateTest.php b/tests/integration/api/SplitReadStateTest.php new file mode 100644 index 0000000..1272b2a --- /dev/null +++ b/tests/integration/api/SplitReadStateTest.php @@ -0,0 +1,165 @@ +extension('fof-split'); + + $this->prepareDatabase([ + 'users' => [ + [ + 'id' => 1, + 'username' => 'admin', + 'email' => 'admin@example.test', + 'is_email_confirmed' => true, + 'password' => 'password', + 'joined_at' => '2026-01-01 00:00:00', + ], + [ + 'id' => 2, + 'username' => 'moderator', + 'email' => 'moderator@example.test', + 'is_email_confirmed' => true, + 'password' => 'password', + 'joined_at' => '2026-01-01 00:00:00', + ], + ], + 'group_user' => [ + ['user_id' => 1, 'group_id' => 1], + ['user_id' => 2, 'group_id' => 4], + ], + 'discussions' => [ + [ + 'id' => 1, + 'title' => 'Source Discussion', + 'slug' => 'source-discussion', + 'user_id' => 1, + 'comment_count' => 5, + 'participant_count' => 1, + 'created_at' => '2026-01-01 00:00:00', + 'last_posted_at' => '2026-01-01 00:04:00', + 'last_post_number' => 5, + 'first_post_id' => 10, + 'last_post_id' => 14, + ], + ], + 'posts' => [ + $this->commentPost(10, 1, 1, 1, '2026-01-01 00:00:00', '

Post 1

'), + $this->commentPost(11, 1, 1, 2, '2026-01-01 00:01:00', '

Post 2

'), + $this->commentPost(12, 1, 1, 3, '2026-01-01 00:02:00', '

Post 3

'), + $this->commentPost(13, 1, 1, 4, '2026-01-01 00:03:00', '

Post 4

'), + $this->commentPost(14, 1, 1, 5, '2026-01-01 00:04:00', '

Post 5

'), + ], + 'discussion_user' => [ + [ + 'discussion_id' => 1, + 'user_id' => 1, + 'last_read_post_number' => 5, + 'last_read_at' => '2026-01-01 00:04:00', + ], + ], + ]); + } + + #[Test] + public function it_clamps_stale_read_state_after_split_and_keeps_the_source_unread_after_a_new_reply(): void + { + $splitResponse = $this->send( + $this->request('POST', '/api/split', [ + 'authenticatedAs' => 1, + 'json' => [ + 'title' => 'Split Child', + 'start_post_id' => 12, + 'end_post_number' => 5, + ], + ]) + ); + + $this->assertSame(200, $splitResponse->getStatusCode()); + + /** @var Discussion $sourceDiscussion */ + $sourceDiscussion = Discussion::query()->findOrFail(1); + /** @var UserState $userState */ + $userState = UserState::query() + ->where('discussion_id', 1) + ->where('user_id', 1) + ->firstOrFail(); + + $this->assertSame(2, $sourceDiscussion->last_post_number); + $this->assertSame(2, $userState->last_read_post_number); + + $replyResponse = $this->send( + $this->request('POST', '/api/posts', [ + 'authenticatedAs' => 2, + 'json' => [ + 'data' => [ + 'type' => 'posts', + 'attributes' => [ + 'content' => 'Moderator reply after split', + ], + 'relationships' => [ + 'discussion' => [ + 'data' => [ + 'type' => 'discussions', + 'id' => '1', + ], + ], + ], + ], + ], + ]) + ); + + $this->assertSame(201, $replyResponse->getStatusCode()); + + $sourceDiscussion->refresh(); + $userState = UserState::query() + ->where('discussion_id', 1) + ->where('user_id', 1) + ->firstOrFail(); + + $this->assertSame(4, $sourceDiscussion->last_post_number); + $this->assertSame(2, $userState->last_read_post_number); + + $showResponse = $this->send( + $this->request('GET', '/api/discussions/1', ['authenticatedAs' => 1]) + ); + + $this->assertSame(200, $showResponse->getStatusCode()); + + $showJson = json_decode($showResponse->getBody()->getContents(), true); + + $this->assertSame(4, $showJson['data']['attributes']['lastPostNumber']); + $this->assertSame(2, $showJson['data']['attributes']['lastReadPostNumber']); + } + + protected function commentPost( + int $id, + int $discussionId, + int $userId, + int $number, + string $createdAt, + string $content + ): array { + return [ + 'id' => $id, + 'discussion_id' => $discussionId, + 'user_id' => $userId, + 'type' => 'comment', + 'number' => $number, + 'created_at' => $createdAt, + 'content' => $content, + ]; + } +} diff --git a/tests/integration/setup.php b/tests/integration/setup.php new file mode 100644 index 0000000..5f24462 --- /dev/null +++ b/tests/integration/setup.php @@ -0,0 +1,9 @@ +run(); diff --git a/tests/phpunit.integration.xml b/tests/phpunit.integration.xml new file mode 100644 index 0000000..3343d5d --- /dev/null +++ b/tests/phpunit.integration.xml @@ -0,0 +1,11 @@ + + + + + ./integration + + +