Skip to content

Fixed Riddle issues #1027#1031

Open
BarnaPuppi wants to merge 1 commit intostagingfrom
fix/1027-riddle-issues
Open

Fixed Riddle issues #1027#1031
BarnaPuppi wants to merge 1 commit intostagingfrom
fix/1027-riddle-issues

Conversation

@BarnaPuppi
Copy link
Copy Markdown

@BarnaPuppi BarnaPuppi commented Apr 25, 2026

Implemented checks for solved and unavailable riddles when submitting a riddle.

Summary by CodeRabbit

  • Bug Fixes

    • Riddle submissions with empty solutions are now properly rejected with a validation error.
  • Refactor

    • Enhanced eligibility verification for riddle access and submission; the system now enforces stricter checks to ensure users and groups can only solve riddles they are eligible for at any given time.

@BarnaPuppi BarnaPuppi linked an issue Apr 25, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

📝 Walkthrough

Walkthrough

The changes add empty solution validation in the API layer, refactor riddle eligibility checks in the business logic to use a "nextRiddles" set-based approach, and introduce a new NOT_SOLVABLE submission status enum value.

Changes

Cohort / File(s) Summary
API Input Validation
backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleApiController.kt
Added rejection of requests with empty solution strings, returning 400 BAD_REQUEST before service invocation.
Eligibility Check Refactoring
backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt
Replaced completion-mapping logic with early "nextRiddles" membership guards in both getRiddle* and submitRiddle* methods; rejections now return NOT_SOLVABLE status.
Submission Status Extension
backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleSubmissionView.kt
Added NOT_SOLVABLE enum value to RiddleSubmissionStatus.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


🐰 Empty solutions? Hop away, we say!
Riddles guarded by nextRiddles' way,
NOT_SOLVABLE flags when you can't play,
Eligibility checks keep seekers at bay!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fixed Riddle issues #1027' is related to the changeset, which implements checks for solved and unavailable riddles as described in the PR objectives.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1027-riddle-issues

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.

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: 2

🧹 Nitpick comments (3)
backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt (2)

202-206: Eligibility check ordering is inconsistent with submitRiddleForGroup.

Here the submission is fetched on line 202 before the NOT_SOLVABLE check on lines 204–205, so a request for a non-eligible riddle still incurs a DB lookup. submitRiddleForGroup already does the check first (lines 290–291) and only queries findMappingByOwnerGroupIdAndRiddleId afterwards on line 293. Move the check above the fetch to match.

♻️ Proposed reorder
-        val submission = riddleCacheManager.findMappingByOwnerUserIdAndRiddleId(user.id, riddleId)
-
-        if (riddle.id !in listRiddlesForUser(user).flatMap { it.nextRiddles }.map { it.id })
-            return RiddleSubmissionView(status = RiddleSubmissionStatus.NOT_SOLVABLE)
-
+        if (riddle.id !in listRiddlesForUser(user).flatMap { it.nextRiddles }.map { it.id })
+            return RiddleSubmissionView(status = RiddleSubmissionStatus.NOT_SOLVABLE)
+
+        val submission = riddleCacheManager.findMappingByOwnerUserIdAndRiddleId(user.id, riddleId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt`
around lines 202 - 206, The eligibility check is done after fetching the
submission which causes an unnecessary lookup; move the riddle eligibility check
(the if that uses listRiddlesForUser(user).flatMap { it.nextRiddles }...) to run
before calling riddleCacheManager.findMappingByOwnerUserIdAndRiddleId(user.id,
riddleId) so you only query when the riddle is eligible—mirror the ordering used
in submitRiddleForGroup (check eligibility first, then call
findMappingByOwnerGroupIdAndRiddleId).

204-206: Scope the eligibility lookup to the riddle's category; the inner null-return becomes dead code.

Same concern applies to submitRiddleForGroup lines 290–291. Two points:

  1. Use the single-category helpers. listRiddlesForUser(user) recomputes findNextTo for every category just to flatten and check one id. getNextRiddlesUser(user, riddle) already exists and does exactly the right query for the riddle's category. Same for getNextRiddlesGroup(groupId, riddle) in the group variant. This shrinks the work from O(categories × riddles_per_category) to O(riddles_in_one_category) on every submit.

  2. Now-redundant inner check. Once the new guard passes, the else branch's if (nextRiddles.find { it.id == riddle.id } == null) return null (line 232–233 for user, 319–320 for group) can never fire — the riddle is guaranteed eligible for its category. You can drop it (and reuse the freshly computed list) to avoid the duplicate work.

♻️ Suggested shape (user variant; mirror for group)
-        if (riddle.id !in listRiddlesForUser(user).flatMap { it.nextRiddles }.map { it.id })
-            return RiddleSubmissionView(status = RiddleSubmissionStatus.NOT_SOLVABLE)
+        val nextRiddlesForCategory = getNextRiddlesUser(user, riddle)
+        if (nextRiddlesForCategory.none { it.id == riddle.id })
+            return RiddleSubmissionView(status = RiddleSubmissionStatus.NOT_SOLVABLE)
@@
-        } else {
-            val nextRiddles = getNextRiddlesUser(user, riddle)
-            if (nextRiddles.find { it.id == riddle.id } == null)
-                return null
-            val userEntity = userService.getById(user.internalId)
+        } else {
+            val userEntity = userService.getById(user.internalId)

Note: this also implicitly rejects re-submissions to already-solved riddles (since findNextTo excludes completed ones) — which matches the PR's stated intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt`
around lines 204 - 206, Replace the broad eligibility check that flattens
listRiddlesForUser(user) with a category-scoped check using
getNextRiddlesUser(user, riddle) (and mirror the change in submitRiddleForGroup
to use getNextRiddlesGroup(groupId, riddle)); specifically, call
getNextRiddlesUser/getNextRiddlesGroup to compute nextRiddles for the riddle's
category, verify riddle.id is present there (return NOT_SOLVABLE if not), then
reuse that nextRiddles list for the subsequent logic and remove the redundant
inner null-check (the if nextRiddles.find { it.id == riddle.id } == null) since
it can no longer occur.
backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleApiController.kt (1)

106-108: Consider isBlank() to also reject whitespace-only solutions.

body.solution == "" lets whitespace-only payloads (e.g., " ", "\t") through. Since checkSolutionIsWrong strips whitespace before comparison (RiddleBusinessLogicService.kt lines 365–378), such submissions effectively become empty downstream and would just be counted as a WRONG attempt rather than rejected at the API edge. Using isBlank() keeps the controller guard consistent with the service-side normalization.

♻️ Proposed change
-        if (body.solution == "")
-            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build()
+        if (body.solution.isBlank())
+            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleApiController.kt`
around lines 106 - 108, In RiddleApiController update the input validation to
reject whitespace-only solutions by using Kotlin's isBlank(): replace the
current check that compares body.solution == "" with body.solution.isBlank() so
the controller rejects empty or whitespace-only payloads (keeping the same
ResponseEntity.status(HttpStatus.BAD_REQUEST).build() behavior); this keeps the
controller consistent with the normalization performed in
RiddleBusinessLogicService.checkSolutionIsWrong.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt`:
- Around line 70-76: Remove the redundant multi-category precheck that calls
listRiddlesForUser(user) inside getRiddleForUser and likewise in
getRiddleForGroup; instead rely solely on the local eligibility check already
done by computing submissions via
riddleCacheManager.findAllMappingByOwnerUserIdAndRiddleCategoryId(...) and using
findNextTo(riddle.categoryId, submissions). Concretely, delete the conditional
that checks riddle.id against listRiddlesForUser(...).flatMap { it.nextRiddles
}... so the method simply computes submissions, calls findNextTo, and returns
nextRiddles.find { it.id == riddle.id }; apply the same removal in
getRiddleForGroup to avoid the expensive multi-category traversal and to
preserve access behavior for already-solved riddles.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleSubmissionView.kt`:
- Around line 10-11: The frontend RiddleSubmissionStatus union is missing
NOT_SOLVABLE which the backend (submitRiddleForUser / submitRiddleForGroup) can
return; add NOT_SOLVABLE to the TypeScript union in RiddleSubmissionStatus (in
riddle.view.ts) and update the submission handler and skip handler in
riddle.page.tsx to handle the NOT_SOLVABLE case by showing a localized message
(e.g., "this riddle is not currently available") via the same toast/localization
mechanism used for other statuses and ensuring any navigation/flow is consistent
with other non-success outcomes.

---

Nitpick comments:
In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleApiController.kt`:
- Around line 106-108: In RiddleApiController update the input validation to
reject whitespace-only solutions by using Kotlin's isBlank(): replace the
current check that compares body.solution == "" with body.solution.isBlank() so
the controller rejects empty or whitespace-only payloads (keeping the same
ResponseEntity.status(HttpStatus.BAD_REQUEST).build() behavior); this keeps the
controller consistent with the normalization performed in
RiddleBusinessLogicService.checkSolutionIsWrong.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt`:
- Around line 202-206: The eligibility check is done after fetching the
submission which causes an unnecessary lookup; move the riddle eligibility check
(the if that uses listRiddlesForUser(user).flatMap { it.nextRiddles }...) to run
before calling riddleCacheManager.findMappingByOwnerUserIdAndRiddleId(user.id,
riddleId) so you only query when the riddle is eligible—mirror the ordering used
in submitRiddleForGroup (check eligibility first, then call
findMappingByOwnerGroupIdAndRiddleId).
- Around line 204-206: Replace the broad eligibility check that flattens
listRiddlesForUser(user) with a category-scoped check using
getNextRiddlesUser(user, riddle) (and mirror the change in submitRiddleForGroup
to use getNextRiddlesGroup(groupId, riddle)); specifically, call
getNextRiddlesUser/getNextRiddlesGroup to compute nextRiddles for the riddle's
category, verify riddle.id is present there (return NOT_SOLVABLE if not), then
reuse that nextRiddles list for the subsequent logic and remove the redundant
inner null-check (the if nextRiddles.find { it.id == riddle.id } == null) since
it can no longer occur.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57d95f9d-2e6c-4255-b7ae-1cc68674f4d4

📥 Commits

Reviewing files that changed from the base of the PR and between 6be8ff1 and e191c0a.

📒 Files selected for processing (3)
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleApiController.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt
  • backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleSubmissionView.kt

Comment on lines +70 to 76
if (riddle.id !in listRiddlesForUser(user).flatMap { it.nextRiddles }.map { it.id })
return null

val submissions = riddleCacheManager.findAllMappingByOwnerUserIdAndRiddleCategoryId(user.id, riddle.categoryId)
return getRiddleIfAllowedToRead(riddle, submissions)
val nextRiddles = findNextTo(riddle.categoryId, submissions)
return nextRiddles.find { it.id == riddle.id }
}
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the frontend ever opens /riddle/solve/{id} for a riddle that may already be completed
fd -t f -e ts -e tsx | xargs rg -nP -C3 "/riddle/solve/"
# And whether the history view links into the solve route
rg -nP -C2 'riddle.*history|history.*riddle' --type=ts --type=tsx

Repository: kir-dev/cmsch

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Search for /riddle/solve/ references in TypeScript files (without tsx type specifier)
fd -t f -e ts | xargs rg -nP -C3 "/riddle/solve/" 2>/dev/null || echo "No matches found with .ts extension"

# Also search more broadly for riddle/solve patterns
rg -nP "riddle.*solve" --type=typescript --type=javascript 2>/dev/null || echo "Pattern search complete"

# Check for history-related views
rg -nP "history|completion" --type=typescript --type=javascript -i | grep -i riddle | head -20

Repository: kir-dev/cmsch

Length of output: 170


🏁 Script executed:

#!/bin/bash
# List main directories to understand repo structure
ls -la | head -20

# Find all frontend/client code directories
find . -maxdepth 2 -type d -name "frontend" -o -name "client" -o -name "src" 2>/dev/null | head -10

# Search for any file containing /riddle/solve pattern regardless of extension
rg "/riddle/solve" --no-type-list 2>/dev/null | head -30

# Search for history endpoint usage patterns
rg "riddle.*history|solve.*history" --no-type-list -i 2>/dev/null | head -20

Repository: kir-dev/cmsch

Length of output: 866


🏁 Script executed:

#!/bin/bash
# Search within frontend directory for /riddle/solve route patterns
rg "/riddle/solve" ./frontend/ 2>/dev/null | head -30

# Search for history-related components/views
rg "history.*riddle|riddle.*history" ./frontend/ -i 2>/dev/null | head -20

# Search for route definitions
rg "path.*riddle|riddle.*path|Route.*riddle" ./frontend/ -i -C2 2>/dev/null | head -50

# Look for navigation after completion
rg "complete|solved|finished" ./frontend/ -i -C2 2>/dev/null | grep -i "riddle\|navigate\|redirect" | head -20

Repository: kir-dev/cmsch

Length of output: 7825


🏁 Script executed:

#!/bin/bash
# Check the getRiddleForUser and getRiddleForGroup methods
rg -A 15 "fun getRiddleForUser\|fun getRiddleForGroup" ./backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt

# Check the findNextTo implementation at line 420
rg -B 5 -A 10 "fun findNextTo" ./backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt

# Check what listRiddlesForUser does
rg -A 20 "fun listRiddlesForUser" ./backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt | head -40

Repository: kir-dev/cmsch

Length of output: 1738


Redundant precheck with performance overhead; also prevents viewing already-solved riddles.

Two concerns with the guard on lines 70–76 (also applies to getRiddleForGroup lines 87–93):

  1. Redundant and expensive. The precheck calls listRiddlesForUser(user) to compute nextRiddles for every category, then filters the result to a single id—pure overhead. Lines 73–75 already perform the same eligibility check locally for just the relevant category via findNextTo(riddle.categoryId, submissions) and nextRiddles.find { it.id == riddle.id } returns null if the riddle isn't in the result. The precheck adds a multi-category traversal for no functional change.

  2. Blocks access to completed riddles. findNextTo (line 85) filters out riddles where it.completed == true, so after solving a riddle, any direct GET to /riddle/solve/{riddleId} will return null/404. Current frontend flows navigate to the next riddle or main list on completion, so this doesn't break normal UX. However, if users ever bookmark, deep-link, or history-nav to a solved riddle's solve view, it will 404. If blocking solve-view post-completion is intentional, this is fine; otherwise it's a usability regression.

♻️ Proposed simplification (drop the redundant precheck)
     override fun getRiddleForUser(user: CmschUser, riddleId: Int): RiddleView? {
         val riddle = riddleCacheManager.getRiddleById(riddleId) ?: return null
         riddleCacheManager.findCategoryByCategoryIdAndVisibleTrueAndMinRoleAtMost(riddle.categoryId, user.role)
             ?: return null

-        if (riddle.id !in listRiddlesForUser(user).flatMap { it.nextRiddles }.map { it.id })
-            return null
-
         val submissions = riddleCacheManager.findAllMappingByOwnerUserIdAndRiddleCategoryId(user.id, riddle.categoryId)
         val nextRiddles = findNextTo(riddle.categoryId, submissions)
         return nextRiddles.find { it.id == riddle.id }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleBusinessLogicService.kt`
around lines 70 - 76, Remove the redundant multi-category precheck that calls
listRiddlesForUser(user) inside getRiddleForUser and likewise in
getRiddleForGroup; instead rely solely on the local eligibility check already
done by computing submissions via
riddleCacheManager.findAllMappingByOwnerUserIdAndRiddleCategoryId(...) and using
findNextTo(riddle.categoryId, submissions). Concretely, delete the conditional
that checks riddle.id against listRiddlesForUser(...).flatMap { it.nextRiddles
}... so the method simply computes submissions, calls findNextTo, and returns
nextRiddles.find { it.id == riddle.id }; apply the same removal in
getRiddleForGroup to avoid the expensive multi-category traversal and to
preserve access behavior for already-solved riddles.

Comment on lines +10 to +11
SUBMITTER_BANNED,
NOT_SOLVABLE
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the frontend enum and any pattern-matching against riddle submission statuses
fd -t f -e ts -e tsx | xargs rg -nP -C2 '\bRiddleSubmissionStatus\b'
# Find places that switch on status values
rg -nP -C2 "'(CORRECT|WRONG|CANNOT_SKIP|SUBMITTER_BANNED)'"

Repository: kir-dev/cmsch

Length of output: 5127


🏁 Script executed:

#!/bin/bash
# Verify backend returns NOT_SOLVABLE status
rg -nP -C3 'NOT_SOLVABLE' backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/

# Check full riddle.page.tsx for any catch-all or error handling
fd -t f -name 'riddle.page.tsx' | xargs cat -n | head -200

Repository: kir-dev/cmsch

Length of output: 3017


🏁 Script executed:

#!/bin/bash
# Get full riddle.page.tsx to check for error handling of unknown statuses
wc -l frontend/src/pages/riddle/riddle.page.tsx
cat -n frontend/src/pages/riddle/riddle.page.tsx

Repository: kir-dev/cmsch

Length of output: 10356


Frontend RiddleSubmissionStatus is out of sync with backend enum — NOT_SOLVABLE not handled.

Backend returns NOT_SOLVABLE from submitRiddleForUser and submitRiddleForGroup (lines 205, 291 in RiddleBusinessLogicService.kt), but the frontend TypeScript union in frontend/src/util/views/riddle.view.ts only defines CORRECT, WRONG, CANNOT_SKIP, SUBMITTER_BANNED.

The riddle.page.tsx submission handler (lines 54–87) and skip handler (lines 102–120) have no case for NOT_SOLVABLE. When this status is received, all conditional checks fail and the user sees no toast, no navigation, and no feedback — a silent failure with confusing UX.

Add NOT_SOLVABLE to the TS union in riddle.view.ts and provide a localized message in both submission and skip handlers (e.g., "this riddle is not currently available").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/kotlin/hu/bme/sch/cmsch/component/riddle/RiddleSubmissionView.kt`
around lines 10 - 11, The frontend RiddleSubmissionStatus union is missing
NOT_SOLVABLE which the backend (submitRiddleForUser / submitRiddleForGroup) can
return; add NOT_SOLVABLE to the TypeScript union in RiddleSubmissionStatus (in
riddle.view.ts) and update the submission handler and skip handler in
riddle.page.tsx to handle the NOT_SOLVABLE case by showing a localized message
(e.g., "this riddle is not currently available") via the same toast/localization
mechanism used for other statuses and ensuring any navigation/flow is consistent
with other non-success outcomes.

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.

Riddle issues

1 participant