Skip to content

Feat/react posts emoji#556

Open
VongoSanDi wants to merge 8 commits into
Arcadia-Solutions:mainfrom
VongoSanDi:feat/react-posts-emoji
Open

Feat/react posts emoji#556
VongoSanDi wants to merge 8 commits into
Arcadia-Solutions:mainfrom
VongoSanDi:feat/react-posts-emoji

Conversation

@VongoSanDi
Copy link
Copy Markdown
Contributor

@VongoSanDi VongoSanDi commented May 7, 2026

[] DB:
[x] Create table forum_post_reactions

[] BACK:
[] Creation tests
[x] Test get the post with a reaction
[x] Test add in forum_post
[x] Test delete in in forum_post
[x] Test edit in forum_post
[] Creation handlers set_forum_post_reaction & delete_forum_post_reaction
[x] PUT /api/forum/post/{post_id}/reaction → add or replace the reaction to the post
[x] DELETE /api/forum/post/{post_id}/reaction → delete the user reaction from the post

Summary by CodeRabbit

Release Notes

  • New Features
    • Forum post reactions: Users can now add emoji reactions to forum posts and manage them (create, update, or delete). Each user can maintain one reaction per post.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds forum post reaction functionality (emoji reactions to posts). It introduces database tables, storage repository methods, HTTP PUT/DELETE handlers, error handling, integration tests, and SQLx query metadata for managing user reactions on forum posts with permission checks.

Changes

Forum Post Reaction Feature

Layer / File(s) Summary
Data Models
backend/storage/src/models/forum.rs
ForumPostHierarchy adds optional reaction field. New structs: ForumPostReaction (id, forum_post_id, user_id, emoji) and UserCreatedForumPostReaction (emoji).
Database Schema
backend/storage/migrations/20250312215600_initdb.sql
New forum_post_reactions table with FK to forum_posts and users, unique constraint on (forum_post_id, user_id). New permission enum value set_forum_post_reaction.
Repository Imports & Mapping
backend/storage/src/repositories/forum_repository.rs
Imports reaction models. DBImportForumPost extended with optional reaction columns (id, forum_post_id, user_id, emoji) for join mapping.
Repository Query Extension
backend/storage/src/repositories/forum_repository.rs
find_forum_thread_posts adds LEFT JOIN forum_post_reactions filtered by user_id; maps joined reaction data into ForumPostHierarchy.reaction.
Repository Methods
backend/storage/src/repositories/forum_repository.rs
set_forum_post_reaction_and_get_post() upserts reaction, returns updated post. delete_forum_post_reaction_and_get_post() deletes reaction, returns post without reaction.
Storage Queries
backend/storage/.sqlx/query-*.json
SQLx metadata for thread posts with reactions, upsert reaction with conflict handling, and delete reaction queries.
API Handlers
backend/api/src/handlers/forum/set_forum_post_reaction.rs, delete_forum_post_reaction.rs
PUT handler creates/updates reaction. DELETE handler removes reaction. Both enforce CreateForumPost permission, return ForumPostHierarchy as JSON.
API Route Registration
backend/api/src/handlers/forum/mod.rs, backend/api/src/api_doc.rs
Module exports for handlers. Routes under /post/{id}/reaction for PUT and DELETE. Handlers added to OpenAPI paths.
Error Handling
backend/common/src/error/mod.rs
CouldNotUpdateForumPostReaction (HTTP 400), CouldNotFindForumPostReaction (HTTP 404) error variants.
Integration Tests
backend/api/tests/test_forum_post.rs
Five tests: fetch posts with reactions, create reaction, update reaction, invalid emoji error, delete reaction.
Test Fixtures & Seed Data
backend/api/tests/fixtures/with_test_forum_post.sql, with_test_forum_reaction.sql, backend/storage/migrations/fixtures/fixtures.sql
Forum posts and reactions fixture rows, sequence initialization for post/reaction IDs.
Configuration
.gitignore
Added .idea to ignored paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A forum burns bright with emoji cheer,
PUT and DELETE reactions here!
Upsert, join, and map with glee,
Reactions stored for all to see.
From schemas deep to handlers high,
Our little bunny hops on by! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feat/react posts emoji' partially relates to the changeset but is vague and uses non-descriptive phrasing that doesn't clearly convey the specific features implemented. Consider revising the title to be more descriptive, such as 'Add forum post reaction endpoints (PUT/DELETE)' or 'Implement emoji reactions for forum posts' for clarity.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread backend/storage/migrations/20250312215600_initdb.sql Outdated
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
emoji TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that updated_at isn't needed here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me it's part of the combo created_at / updated_at, I agree update_at isn't really need and so is created_at no ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created_at makes sense, it's just that we don't update reactions, we only create new ones (or remove them)

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/common/src/error/mod.rs`:
- Around line 347-348: The enum variant CouldNotFindForumPostReaction has the
wrong error text and isn't handled as a 404 in the HTTP-status mapping; update
the enum variant definition to #[error("could not find forum post reaction")]
CouldNotFindForumPostReaction(#[source] sqlx::Error) and then add that variant
to the match that maps errors to 404 (the error-to-status mapping function/match
around the 678+ region) so that CouldNotFindForumPostReaction returns 404
instead of falling through to 500.

In
`@backend/storage/.sqlx/query-fe8367af19eab2a0b0d0be3c828c0c9ee871b4a05e63de8d770a6d8050b1bdd8.json`:
- Line 3: The upsert CTE `upserted` can return zero rows when the emoji is
unchanged, causing the outer LEFT JOIN against `upserted up` to yield NULL
reaction fields; change the outer SELECT to join directly to
`forum_post_reactions` filtered by user_id = $2 (instead of LEFT JOIN upserted)
so the response always reflects the persisted reaction, and update the SELECT
projection to use `forum_post_reactions.id`, `.forum_post_id`, `.user_id`, and
`.emoji` (rather than `up.reaction_*`) while keeping the `upserted` CTE in place
for the write side-effect.

In `@backend/storage/migrations/fixtures/fixtures.sql`:
- Around line 232-233: The sequence public.forum_posts_id_seq must be advanced
to avoid collisions with seeded IDs (the fixtures insert rows up to id 10);
update the sequence value for forum_posts (public.forum_posts_id_seq) to at
least 10 (preferably set next value to 11) after the INSERTs so subsequent
inserts use unique IDs — locate the fixture INSERTs for forum_posts and
add/adjust a SETVAL/ALTER SEQUENCE call referencing public.forum_posts_id_seq to
set its next value accordingly.

In `@backend/storage/src/models/forum.rs`:
- Around line 335-338: The UserCreatedForumPostReaction struct's emoji field
currently accepts arbitrary input; add server-side validation by introducing
either a newtype (e.g., Emoji) with TryFrom<String> or a validate() method on
UserCreatedForumPostReaction that is invoked wherever instances are
created/parsed, enforcing: trim and reject empty/whitespace-only strings,
enforce a reasonable max length (e.g., limit to ~4–8 Unicode grapheme clusters
or a byte cap such as 64 bytes), and optionally run a simple emoji-only
check/allowlist or Unicode emoji detection; return a clear validation error
instead of persisting invalid emoji. Ensure the validation is used during
deserialization/creation paths so invalid payloads are rejected before DB
storage.

In `@backend/storage/src/repositories/forum_repository.rs`:
- Around line 1657-1659: The query is fetching a forum post but maps failures to
Error::CouldNotFindForumThread which is misleading; change the error mapping on
the .fetch_one(self.borrow()).await call to a post-specific variant (e.g.
Error::CouldNotFindForumPost) and if that variant doesn't exist, add a new
Error::CouldNotFindForumPost variant and use it in place of
Error::CouldNotFindForumThread so failures from this fetch are reported as post
lookup errors.
- Around line 1531-1565: The upsert CTE (upserted) can return no row when the ON
CONFLICT uses "DO UPDATE ... WHERE forum_post_reactions.emoji IS DISTINCT FROM
EXCLUDED.emoji", causing existing reactions to disappear from the result; remove
the WHERE clause so the UPDATE always returns a row and instead make updated_at
conditional so it only changes on real emoji changes (e.g., replace "DO UPDATE
SET emoji = EXCLUDED.emoji, updated_at = NOW() WHERE forum_post_reactions.emoji
IS DISTINCT FROM EXCLUDED.emoji" with an unconditional "DO UPDATE SET emoji =
EXCLUDED.emoji, updated_at = CASE WHEN forum_post_reactions.emoji IS DISTINCT
FROM EXCLUDED.emoji THEN NOW() ELSE forum_post_reactions.updated_at END" so the
upserted CTE always RETURNING the reaction row).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 81f2df4e-1ffc-433d-810c-a6c2e7c165f0

📥 Commits

Reviewing files that changed from the base of the PR and between 3942cd1 and 9b0a2cc.

📒 Files selected for processing (28)
  • .gitignore
  • backend/api/src/api_doc.rs
  • backend/api/src/handlers/forum/delete_forum_post_reaction.rs
  • backend/api/src/handlers/forum/mod.rs
  • backend/api/src/handlers/forum/set_forum_post_reaction.rs
  • backend/api/tests/fixtures/with_test_forum_post.sql
  • backend/api/tests/fixtures/with_test_forum_reaction.sql
  • backend/api/tests/test_forum_post.rs
  • backend/common/src/error/mod.rs
  • backend/storage/.sqlx/query-07435792b9b25324453fb323c5edc0f0a3765f2b32969f229d8675f08ac0a006.json
  • backend/storage/.sqlx/query-43ca35fea526b25dffb69e86700c0d3a1bdebdb973df5059ff7eb3e9a933eb33.json
  • backend/storage/.sqlx/query-46d7eee133e0653d9f11ab67f5d6faec7050c9b4c6a8c78e2097015d3e0fb7fb.json
  • backend/storage/.sqlx/query-4edda78ffd766d9ec15eb015fe5b985755924b0f0b44d5cf9411059cfbc5c757.json
  • backend/storage/.sqlx/query-599587c7ce69b090843274603171c411af859ae256fc01eaf66af2aa2a922900.json
  • backend/storage/.sqlx/query-6c4a5f875d86d837493c8bf2a8e4f6fb93f4e50dce73ca6d07d42df32a80308a.json
  • backend/storage/.sqlx/query-7da73662a96a68e239d011598ace3bc5b287a82c5b0c34ce9543842a1bed0ea4.json
  • backend/storage/.sqlx/query-9b42210d7c01c72c57238e1b0984cbfee280f3d5dcc6592a58eec33562760504.json
  • backend/storage/.sqlx/query-af3da3d706d7dfb3a66c7bb2a3f9747f2717ea16a0a01de22630c87f42bec1b6.json
  • backend/storage/.sqlx/query-c45f235654a1b2aa8c849c5644443fe34ea7a4dd976fe6b4405e7b4a585a1325.json
  • backend/storage/.sqlx/query-c4e55538610671cfcf982e49c58425223e356c29912d19d4e62cf7b576637c0a.json
  • backend/storage/.sqlx/query-d94c7cf9c02a4f060345d02ac4bd2434069fc46d43e6f3e7e3618737c2dcd547.json
  • backend/storage/.sqlx/query-f4de9d3dad0a4229f75304798debf8ab4a602081f6e429658262bc9e58f7320b.json
  • backend/storage/.sqlx/query-f6d849721ff84614c129c14455d9a6adbe0ad29b7876963d5bd9015c0f73ba9d.json
  • backend/storage/.sqlx/query-fe8367af19eab2a0b0d0be3c828c0c9ee871b4a05e63de8d770a6d8050b1bdd8.json
  • backend/storage/migrations/20250312215600_initdb.sql
  • backend/storage/migrations/fixtures/fixtures.sql
  • backend/storage/src/models/forum.rs
  • backend/storage/src/repositories/forum_repository.rs
💤 Files with no reviewable changes (11)
  • backend/storage/.sqlx/query-c45f235654a1b2aa8c849c5644443fe34ea7a4dd976fe6b4405e7b4a585a1325.json
  • backend/storage/.sqlx/query-43ca35fea526b25dffb69e86700c0d3a1bdebdb973df5059ff7eb3e9a933eb33.json
  • backend/storage/.sqlx/query-46d7eee133e0653d9f11ab67f5d6faec7050c9b4c6a8c78e2097015d3e0fb7fb.json
  • backend/storage/.sqlx/query-7da73662a96a68e239d011598ace3bc5b287a82c5b0c34ce9543842a1bed0ea4.json
  • backend/storage/.sqlx/query-599587c7ce69b090843274603171c411af859ae256fc01eaf66af2aa2a922900.json
  • backend/storage/.sqlx/query-c4e55538610671cfcf982e49c58425223e356c29912d19d4e62cf7b576637c0a.json
  • backend/storage/.sqlx/query-4edda78ffd766d9ec15eb015fe5b985755924b0f0b44d5cf9411059cfbc5c757.json
  • backend/storage/.sqlx/query-6c4a5f875d86d837493c8bf2a8e4f6fb93f4e50dce73ca6d07d42df32a80308a.json
  • backend/storage/.sqlx/query-f4de9d3dad0a4229f75304798debf8ab4a602081f6e429658262bc9e58f7320b.json
  • backend/storage/.sqlx/query-d94c7cf9c02a4f060345d02ac4bd2434069fc46d43e6f3e7e3618737c2dcd547.json
  • backend/storage/.sqlx/query-f6d849721ff84614c129c14455d9a6adbe0ad29b7876963d5bd9015c0f73ba9d.json

Comment thread backend/common/src/error/mod.rs
@@ -0,0 +1,126 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH upserted AS (\n INSERT INTO forum_post_reactions (forum_post_id, user_id, emoji)\n VALUES ($1, $2, $3)\n ON CONFLICT (forum_post_id, user_id)\n DO UPDATE SET\n emoji = EXCLUDED.emoji,\n updated_at = NOW()\n WHERE forum_post_reactions.emoji IS DISTINCT FROM EXCLUDED.emoji\n RETURNING\n id AS reaction_id,\n forum_post_id AS reaction_forum_post_id,\n user_id AS reaction_user_id,\n emoji AS reaction_emoji\n )\n SELECT\n fp.id,\n fp.content,\n fp.created_at,\n fp.updated_at,\n fp.sticky,\n fp.locked,\n fp.forum_thread_id,\n u.id AS created_by_user_id,\n u.username AS created_by_user_username,\n u.class_name AS created_by_user_class_name,\n u.avatar AS created_by_user_avatar,\n u.banned AS created_by_user_banned,\n u.warned AS created_by_user_warned,\n u.custom_title AS created_by_user_custom_title,\n up.reaction_id AS \"reaction_id?\",\n up.reaction_forum_post_id AS \"reaction_forum_post_id?\",\n up.reaction_user_id AS \"reaction_user_id?\",\n up.reaction_emoji AS \"reaction_emoji?\"\n FROM forum_posts fp\n JOIN users u ON fp.created_by_id = u.id\n LEFT JOIN upserted up\n ON up.reaction_forum_post_id = fp.id\n WHERE fp.id = $1\n ",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Upsert returns reaction: None when the same emoji is re-submitted.

The PostgreSQL ON CONFLICT DO UPDATE WHERE <condition> clause behaves as DO NOTHING when the condition is false (i.e., the incoming emoji is the same as the stored one). In that case, the RETURNING clause emits zero rows. The CTE upserted is empty, the LEFT JOIN upserted up ON ... produces NULLs for all up.* columns, and ForumPostHierarchy.reaction is deserialized as None — even though the reaction still exists in the database.

Reproduce: call PUT with emoji "👍" twice; the second response has "reaction": null.

The fix is to have the outer SELECT join directly against the forum_post_reactions table for the reaction data (using $2 as the user filter), keeping the CTE purely as the write side-effect:

🐛 Proposed fix (change the LEFT JOIN in the outer SELECT)
-        LEFT JOIN upserted up
-            ON up.reaction_forum_post_id = fp.id
+        LEFT JOIN forum_post_reactions r
+            ON r.forum_post_id = fp.id AND r.user_id = $2
         WHERE fp.id = $1

and update the SELECT projection accordingly:

-            up.reaction_id AS "reaction_id?",
-            up.reaction_forum_post_id AS "reaction_forum_post_id?",
-            up.reaction_user_id AS "reaction_user_id?",
-            up.reaction_emoji AS "reaction_emoji?"
+            r.id AS "reaction_id?",
+            r.forum_post_id AS "reaction_forum_post_id?",
+            r.user_id AS "reaction_user_id?",
+            r.emoji AS "reaction_emoji?"

The CTE still runs (write-side-effect CTEs execute even when unreferenced in PG 12+), updated_at is only touched when the emoji actually changes, and the response always reflects the current persisted reaction.

Note: This fix must be applied in backend/storage/src/repositories/forum_repository.rs (the SQL source); the SQLx JSON artefact will be regenerated by cargo sqlx prepare.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/storage/.sqlx/query-fe8367af19eab2a0b0d0be3c828c0c9ee871b4a05e63de8d770a6d8050b1bdd8.json`
at line 3, The upsert CTE `upserted` can return zero rows when the emoji is
unchanged, causing the outer LEFT JOIN against `upserted up` to yield NULL
reaction fields; change the outer SELECT to join directly to
`forum_post_reactions` filtered by user_id = $2 (instead of LEFT JOIN upserted)
so the response always reflects the persisted reaction, and update the SELECT
projection to use `forum_post_reactions.id`, `.forum_post_id`, `.user_id`, and
`.emoji` (rather than `up.reaction_*`) while keeping the `upserted` CTE in place
for the write side-effect.

Comment thread backend/storage/migrations/fixtures/fixtures.sql Outdated
Comment on lines +335 to +338
#[derive(Debug, Serialize, Deserialize, FromRow, ToSchema)]
pub struct UserCreatedForumPostReaction {
pub emoji: String,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

emoji field accepts arbitrary input — add validation.

emoji: String has no constraints: empty strings, whitespace-only values, multi-kilobyte text, or HTML payloads are all accepted. At minimum, enforce:

  1. Non-empty / non-whitespace-only content.
  2. A reasonable max length (e.g., a handful of bytes sufficient for a single multi-codepoint emoji).
  3. Optionally, restrict to an allowlist of emoji codepoints or a validated emoji string.

Without server-side validation, the API is exposed to storing unexpected content that could cause display issues or, if the frontend ever renders this field without escaping, XSS.

💡 Sketch of a validation approach in the handler
// In set_forum_post_reaction.rs, before calling the repository:
+    let emoji = reaction.into_inner();
+    let emoji_str = emoji.emoji.trim();
+    if emoji_str.is_empty() || emoji_str.len() > 64 {
+        return Err(arcadia_common::error::Error::BadRequest(
+            "emoji must be a non-empty string of at most 64 bytes".into(),
+        ));
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/storage/src/models/forum.rs` around lines 335 - 338, The
UserCreatedForumPostReaction struct's emoji field currently accepts arbitrary
input; add server-side validation by introducing either a newtype (e.g., Emoji)
with TryFrom<String> or a validate() method on UserCreatedForumPostReaction that
is invoked wherever instances are created/parsed, enforcing: trim and reject
empty/whitespace-only strings, enforce a reasonable max length (e.g., limit to
~4–8 Unicode grapheme clusters or a byte cap such as 64 bytes), and optionally
run a simple emoji-only check/allowlist or Unicode emoji detection; return a
clear validation error instead of persisting invalid emoji. Ensure the
validation is used during deserialization/creation paths so invalid payloads are
rejected before DB storage.

Comment thread backend/storage/src/repositories/forum_repository.rs
Comment thread backend/storage/src/repositories/forum_repository.rs Outdated
Comment thread backend/storage/migrations/20250312215600_initdb.sql Outdated
pub content: String,
pub sticky: bool,
pub locked: bool,
pub reaction: Option<ForumPostReaction>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would rather be a Vec<ForumPostReaction> since multiple users can react on the same post.


#[derive(Debug, Serialize, Deserialize, FromRow, ToSchema)]
pub struct UserCreatedForumPostReaction {
pub emoji: String,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if rust has some way of typing this as an emoji type, but otherwise we should do some validation to make sure only emojis can be used here

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/common/src/error/mod.rs (1)

350-351: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrong error message on CouldNotFindForumPostReaction — still unresolved.

The #[error] text is still "could not find forum post", which duplicates CouldNotFindForumPost and produces a misleading JSON error body for callers. The 404 status-mapping fix from the previous review round was applied correctly, but this text was not updated.

🐛 Proposed fix
-    #[error("could not find forum post")]
+    #[error("could not find forum post reaction")]
     CouldNotFindForumPostReaction(#[source] sqlx::Error),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/common/src/error/mod.rs` around lines 350 - 351, The error enum
variant CouldNotFindForumPostReaction has the wrong #[error(...)] text ("could
not find forum post"); update its error message to a distinct, accurate string
such as "could not find forum post reaction" so callers receive the correct JSON
body; locate the enum variant CouldNotFindForumPostReaction and change the
#[error("...")] attribute accordingly (keeping the #[source] sqlx::Error
intact).
🧹 Nitpick comments (2)
backend/api/tests/test_forum_post.rs (2)

42-53: ⚡ Quick win

Avoid order-coupled assertions in list responses.

These checks rely on fixed list ordering. Prefer selecting posts by identity (or reaction presence) so tests remain stable if default ordering changes.

Proposed refactor
-    assert_eq!(posts.total_items, 2);
-    assert_eq!(posts.results[0].created_by.id, 100);
-    assert!(posts.results[0].reaction.is_some());
-    assert_eq!(posts.results[0].reaction.as_ref().unwrap().emoji, "🥰");
-    assert_eq!(
-        posts.results[0].reaction.as_ref().unwrap().forum_post_id,
-        100
-    );
-    assert!(posts.results[1].reaction.is_none());
+    assert_eq!(posts.total_items, 2);
+    let reacted_post = posts
+        .results
+        .iter()
+        .find(|p| p.reaction.is_some())
+        .expect("expected one reacted post");
+    assert_eq!(reacted_post.created_by.id, 100);
+    assert_eq!(reacted_post.reaction.as_ref().unwrap().emoji, "🥰");
+    assert_eq!(reacted_post.reaction.as_ref().unwrap().forum_post_id, 100);
+
+    let non_reacted_post = posts
+        .results
+        .iter()
+        .find(|p| p.reaction.is_none())
+        .expect("expected one non-reacted post");
+    assert_ne!(non_reacted_post.id, reacted_post.id);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/api/tests/test_forum_post.rs` around lines 42 - 53, The test in
backend/api/tests/test_forum_post.rs currently assumes a fixed ordering of posts
(asserting posts.results[0] and posts.results[1]); change the assertions to
locate posts by identity or reaction instead: use posts.results to find the item
with created_by.id == 100 and assert that that found post has reaction.is_some()
and that its reaction.emoji == "🥰" and reaction.forum_post_id == 100, and
separately locate the other post (e.g., by a different id or by
reaction.is_none()) and assert reaction.is_none(); this removes order coupling
while keeping checks against posts.results, created_by.id, and reaction.

146-154: ⚡ Quick win

Assert DB state is unchanged after invalid reaction payload.

The test verifies 400, but it should also confirm no reaction was created/updated for the target post.

Proposed refactor
     let resp = test::call_service(&service, req).await;
 
     assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
 
     let body = test::read_body(resp).await;
     let body = String::from_utf8(body.to_vec()).unwrap();
 
     assert!(body.contains("could not update forum post reaction"));
+
+    let req_verify = test::TestRequest::get()
+        .uri("/api/forum/thread/posts?thread_id=103&page_size=10")
+        .insert_header(auth_header(&user.token))
+        .to_request();
+    let posts_after: PaginatedResults<ForumPostHierarchy> =
+        common::call_and_read_body_json_with_status(&service, req_verify, StatusCode::OK).await;
+    assert!(posts_after.results[0].reaction.is_none());
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/api/tests/test_forum_post.rs` around lines 146 - 154, The test
currently only asserts a 400 response (using test::call_service and
StatusCode::BAD_REQUEST) but must also verify the DB state is unchanged: after
the existing assertions (resp and body via test::read_body), query the reactions
for the target post (use the same test DB connection/pool fixture used elsewhere
in this test suite) and assert that no reaction record was created or modified
for that post/user (or that the reaction count remains the same / reaction value
unchanged). Locate the test function in test_forum_post.rs and add a DB check
using the repository/DAO function you already have (e.g., get_post_reactions,
find_reaction_by_post_and_user, or direct query against the post_reactions
table) to confirm no new/updated reaction exists for the target post.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/storage/.sqlx/query-428a678919b1a16a2614dee0d093279f3235e39a250ce5a3ca2ed260c8f71d54.json`:
- Line 3: The upsert uses a WHERE clause that prevents RETURNING from emitting a
row when the emoji is unchanged, causing the upserted CTE to be empty and
reaction_* fields to be NULL; fix by removing the WHERE clause from the ON
CONFLICT DO UPDATE in the SQL used by forum_repository (the INSERT INTO
forum_post_reactions ... ON CONFLICT (forum_post_id, user_id) DO UPDATE SET
emoji = EXCLUDED.emoji — remove the "WHERE forum_post_reactions.emoji IS
DISTINCT FROM EXCLUDED.emoji" guard so the UPDATE always runs and RETURNING
always yields a row), then regenerate the sqlx metadata (run sqlx prepare /
rebuild) so the JSON query file reflects the updated SQL.

---

Duplicate comments:
In `@backend/common/src/error/mod.rs`:
- Around line 350-351: The error enum variant CouldNotFindForumPostReaction has
the wrong #[error(...)] text ("could not find forum post"); update its error
message to a distinct, accurate string such as "could not find forum post
reaction" so callers receive the correct JSON body; locate the enum variant
CouldNotFindForumPostReaction and change the #[error("...")] attribute
accordingly (keeping the #[source] sqlx::Error intact).

---

Nitpick comments:
In `@backend/api/tests/test_forum_post.rs`:
- Around line 42-53: The test in backend/api/tests/test_forum_post.rs currently
assumes a fixed ordering of posts (asserting posts.results[0] and
posts.results[1]); change the assertions to locate posts by identity or reaction
instead: use posts.results to find the item with created_by.id == 100 and assert
that that found post has reaction.is_some() and that its reaction.emoji == "🥰"
and reaction.forum_post_id == 100, and separately locate the other post (e.g.,
by a different id or by reaction.is_none()) and assert reaction.is_none(); this
removes order coupling while keeping checks against posts.results,
created_by.id, and reaction.
- Around line 146-154: The test currently only asserts a 400 response (using
test::call_service and StatusCode::BAD_REQUEST) but must also verify the DB
state is unchanged: after the existing assertions (resp and body via
test::read_body), query the reactions for the target post (use the same test DB
connection/pool fixture used elsewhere in this test suite) and assert that no
reaction record was created or modified for that post/user (or that the reaction
count remains the same / reaction value unchanged). Locate the test function in
test_forum_post.rs and add a DB check using the repository/DAO function you
already have (e.g., get_post_reactions, find_reaction_by_post_and_user, or
direct query against the post_reactions table) to confirm no new/updated
reaction exists for the target post.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6f68fd00-4c4c-4ffc-a229-b188e6123d06

📥 Commits

Reviewing files that changed from the base of the PR and between 9b0a2cc and f07ed87.

📒 Files selected for processing (9)
  • backend/api/tests/fixtures/with_test_forum_reaction.sql
  • backend/api/tests/test_forum_post.rs
  • backend/common/src/error/mod.rs
  • backend/storage/.sqlx/query-428a678919b1a16a2614dee0d093279f3235e39a250ce5a3ca2ed260c8f71d54.json
  • backend/storage/.sqlx/query-9b42210d7c01c72c57238e1b0984cbfee280f3d5dcc6592a58eec33562760504.json
  • backend/storage/.sqlx/query-af3da3d706d7dfb3a66c7bb2a3f9747f2717ea16a0a01de22630c87f42bec1b6.json
  • backend/storage/migrations/20250312215600_initdb.sql
  • backend/storage/migrations/fixtures/fixtures.sql
  • backend/storage/src/repositories/forum_repository.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • backend/storage/.sqlx/query-9b42210d7c01c72c57238e1b0984cbfee280f3d5dcc6592a58eec33562760504.json
  • backend/storage/migrations/20250312215600_initdb.sql
  • backend/storage/.sqlx/query-af3da3d706d7dfb3a66c7bb2a3f9747f2717ea16a0a01de22630c87f42bec1b6.json
  • backend/storage/src/repositories/forum_repository.rs

@@ -0,0 +1,126 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH upserted AS (\n INSERT INTO forum_post_reactions (forum_post_id, user_id, emoji)\n VALUES ($1, $2, $3)\n ON CONFLICT (forum_post_id, user_id)\n DO UPDATE SET\n emoji = EXCLUDED.emoji\n WHERE forum_post_reactions.emoji IS DISTINCT FROM EXCLUDED.emoji\n RETURNING\n id AS reaction_id,\n forum_post_id AS reaction_forum_post_id,\n user_id AS reaction_user_id,\n emoji AS reaction_emoji\n )\n SELECT\n fp.id,\n fp.content,\n fp.created_at,\n fp.updated_at,\n fp.sticky,\n fp.locked,\n fp.forum_thread_id,\n u.id AS created_by_user_id,\n u.username AS created_by_user_username,\n u.class_name AS created_by_user_class_name,\n u.avatar AS created_by_user_avatar,\n u.banned AS created_by_user_banned,\n u.warned AS created_by_user_warned,\n u.custom_title AS created_by_user_custom_title,\n up.reaction_id AS \"reaction_id?\",\n up.reaction_forum_post_id AS \"reaction_forum_post_id?\",\n up.reaction_user_id AS \"reaction_user_id?\",\n up.reaction_emoji AS \"reaction_emoji?\"\n FROM forum_posts fp\n JOIN users u ON fp.created_by_id = u.id\n LEFT JOIN upserted up\n ON up.reaction_forum_post_id = fp.id\n WHERE fp.id = $1\n ",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

No-op upsert silently returns reaction: null when the same emoji is resubmitted.

The DO UPDATE ... WHERE forum_post_reactions.emoji IS DISTINCT FROM EXCLUDED.emoji guard is the problem. When an ON CONFLICT DO UPDATE ... WHERE condition is not satisfied, the row will not be returned. So when a user PUTs the same emoji they already have:

  1. The conflict fires.
  2. The WHERE ... IS DISTINCT FROM condition is false → no update runs.
  3. RETURNING emits no row → the upserted CTE is empty.
  4. The outer LEFT JOIN upserted up produces NULLs for all reaction_* columns.
  5. SQLx decodes those NULLs as Option::None, and the JSON response contains "reaction": null.

The DB state is correct (the reaction is preserved), but the API response misrepresents it as absent — any client that trusts the response body will think the reaction was removed.

Simplest fix: drop the WHERE guard so the DO UPDATE always fires and RETURNING always produces a row. The cost is one extra write when the emoji hasn't changed, which is negligible.

Since this JSON is auto-generated by sqlx prepare, the fix must be applied to the SQL in the Rust source file (backend/storage/src/repositories/forum_repository.rs) and then regenerated.

🐛 Proposed fix (SQL diff, to be applied in the Rust source)
             ON CONFLICT (forum_post_id, user_id)
             DO UPDATE SET
-                emoji = EXCLUDED.emoji
-            WHERE forum_post_reactions.emoji IS DISTINCT FROM EXCLUDED.emoji
+                emoji = EXCLUDED.emoji
             RETURNING
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"query": "\n WITH upserted AS (\n INSERT INTO forum_post_reactions (forum_post_id, user_id, emoji)\n VALUES ($1, $2, $3)\n ON CONFLICT (forum_post_id, user_id)\n DO UPDATE SET\n emoji = EXCLUDED.emoji\n WHERE forum_post_reactions.emoji IS DISTINCT FROM EXCLUDED.emoji\n RETURNING\n id AS reaction_id,\n forum_post_id AS reaction_forum_post_id,\n user_id AS reaction_user_id,\n emoji AS reaction_emoji\n )\n SELECT\n fp.id,\n fp.content,\n fp.created_at,\n fp.updated_at,\n fp.sticky,\n fp.locked,\n fp.forum_thread_id,\n u.id AS created_by_user_id,\n u.username AS created_by_user_username,\n u.class_name AS created_by_user_class_name,\n u.avatar AS created_by_user_avatar,\n u.banned AS created_by_user_banned,\n u.warned AS created_by_user_warned,\n u.custom_title AS created_by_user_custom_title,\n up.reaction_id AS \"reaction_id?\",\n up.reaction_forum_post_id AS \"reaction_forum_post_id?\",\n up.reaction_user_id AS \"reaction_user_id?\",\n up.reaction_emoji AS \"reaction_emoji?\"\n FROM forum_posts fp\n JOIN users u ON fp.created_by_id = u.id\n LEFT JOIN upserted up\n ON up.reaction_forum_post_id = fp.id\n WHERE fp.id = $1\n ",
"query": "\n WITH upserted AS (\n INSERT INTO forum_post_reactions (forum_post_id, user_id, emoji)\n VALUES ($1, $2, $3)\n ON CONFLICT (forum_post_id, user_id)\n DO UPDATE SET\n emoji = EXCLUDED.emoji\n RETURNING\n id AS reaction_id,\n forum_post_id AS reaction_forum_post_id,\n user_id AS reaction_user_id,\n emoji AS reaction_emoji\n )\n SELECT\n fp.id,\n fp.content,\n fp.created_at,\n fp.updated_at,\n fp.sticky,\n fp.locked,\n fp.forum_thread_id,\n u.id AS created_by_user_id,\n u.username AS created_by_user_username,\n u.class_name AS created_by_user_class_name,\n u.avatar AS created_by_user_avatar,\n u.banned AS created_by_user_banned,\n u.warned AS created_by_user_warned,\n u.custom_title AS created_by_user_custom_title,\n up.reaction_id AS \"reaction_id?\",\n up.reaction_forum_post_id AS \"reaction_forum_post_id?\",\n up.reaction_user_id AS \"reaction_user_id?\",\n up.reaction_emoji AS \"reaction_emoji?\"\n FROM forum_posts fp\n JOIN users u ON fp.created_by_id = u.id\n LEFT JOIN upserted up\n ON up.reaction_forum_post_id = fp.id\n WHERE fp.id = $1\n ",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/storage/.sqlx/query-428a678919b1a16a2614dee0d093279f3235e39a250ce5a3ca2ed260c8f71d54.json`
at line 3, The upsert uses a WHERE clause that prevents RETURNING from emitting
a row when the emoji is unchanged, causing the upserted CTE to be empty and
reaction_* fields to be NULL; fix by removing the WHERE clause from the ON
CONFLICT DO UPDATE in the SQL used by forum_repository (the INSERT INTO
forum_post_reactions ... ON CONFLICT (forum_post_id, user_id) DO UPDATE SET
emoji = EXCLUDED.emoji — remove the "WHERE forum_post_reactions.emoji IS
DISTINCT FROM EXCLUDED.emoji" guard so the UPDATE always runs and RETURNING
always yields a row), then regenerate the sqlx metadata (run sqlx prepare /
rebuild) so the JSON query file reflects the updated SQL.

@FrenchGithubUser FrenchGithubUser force-pushed the main branch 4 times, most recently from 09383f1 to c8d1750 Compare May 10, 2026 19:09
@FrenchGithubUser FrenchGithubUser force-pushed the main branch 2 times, most recently from 4c57b76 to bf0ed97 Compare May 16, 2026 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants