Skip to content

fix: consistent param validation, index hygiene, async cleanup, a11y, and test coverage - #334

Open
ida-jemi wants to merge 4 commits into
kunalverma2512:mainfrom
ida-jemi:fix/contest-validation-index-a11y-tests
Open

fix: consistent param validation, index hygiene, async cleanup, a11y, and test coverage#334
ida-jemi wants to merge 4 commits into
kunalverma2512:mainfrom
ida-jemi:fix/contest-validation-index-a11y-tests

Conversation

@ida-jemi

@ida-jemi ida-jemi commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📌 Pull Request Summary

🔗 Related Issue

Closes #279


📝 Description

Provide a clear and concise summary of the changes made in this pull request.

Changes Made

  • Added a validateParams Zod middleware and contestIdParamSchema, applied to DELETE /reminders/:contestId and POST /reminders/:contestId/notified, both now enforce the same positive-integer contract as POST /reminders at the route boundary, replacing a manual parseInt(...) that only rejected NaN and would silently accept permissive strings like "12abc".
  • Removed the redundant standalone contestId index on the Contest model, the existing compound unique index { platform: 1, contestId: 1 } already covers every current query pattern via its leftmost-prefix behavior. Documented the full rationale in server/modules/contests/INDEXES.md and added server/scripts/dropRedundantContestIndex.js, since a schema change alone never drops an already-existing index from a deployed database.
  • useContests now guards against stale/superseded async responses using a per-fetch sequence token (incremented on every new fetch, and again on unmount), so a slower earlier request can never overwrite a newer one's state, and no response can write state after unmount, matching the cancellation pattern the bell and notifier already used.
  • ContestReminderBell now has a dynamic aria-label (including the due-soon count when applicable) instead of relying on title alone, and marks its decorative icon and count badge aria-hidden so they aren't redundantly announced.
  • Added 28 new tests across the project's existing test tooling (node:test for backend, vitest for frontend, no new testing stack introduced): the validation contract (valid/zero/negative/decimal/non-numeric/"12abc" cases), reminder add/remove/mark-notified lifecycle edge cases (missing contest, non-upcoming phase), repository idempotency/sorting/joins/stale-cleanup, the useContests stale-response race condition and unmount safety, and bell accessibility (accessible name, due-soon count, keyboard focus).

Motivation

This resolves the 5 Low-severity findings consolidated from the production review of #269. None of these individually blocks a single-instance release, but together they close real gaps: split validation logic between layers with inconsistent error contracts, a database index adding write overhead without ever being selected by the query planner, a React hook that could apply stale data after a slower request resolves late, an icon-only control with no reliable accessible name for screen readers, and a substantial feature area (contest tracker + reminders) shipped without any automated coverage protecting its lifecycle/async/validation behavior.


🚀 Type of Change

Select all that apply:

  • Bug Fix
  • New Feature
  • Enhancement
  • Documentation Update
  • Refactoring
  • Performance Improvement
  • DevOps / Tooling
  • Other

🧪 Testing

Verification

  • Tested Locally
  • Existing Tests Passed
  • New Tests Added
  • No Testing Required

Test Details

  • Validation: validation.test.js runs both addReminderSchema and contestIdParamSchema through a shared table of 9 cases each, valid integers, valid numeric strings, zero, negatives, decimals, non-numeric strings, the specific "12abc" permissive-parsing case, missing values, and empty strings.
  • Reminder lifecycle: Extended service.test.js with 6 new tests covering addReminder throwing 404 for a missing contest, 400 for a FINISHED contest, 400 for a SYSTEM_TEST contest (not just FINISHED), successful add persisting correctly, plus removeReminder/markReminderNotified delegating with correct arguments.
  • Repository: New repository.test.js covers addReminder's upsert-with-$setOnInsert idempotency, getUpcomingContests's filter/sort shape, getActiveReminderContests's join behavior (including dropping reminders for contests that are no longer upcoming), and pruneStaleReminders's deletion scope.
  • Frontend async safety: New useContests.test.js includes a genuine race-condition test, a slow initial fetch is made to resolve after a fast refetch() call, and asserts the late response does not overwrite the newer data, plus an unmount test confirming a late-resolving response doesn't throw or apply.
  • Accessibility: New ContestReminderBell.test.jsx queries by accessible role/name (getByRole("link", { name: ... })), the same mechanism assistive tech uses, verifies the due-soon count is included and correctly filtered (excludes >24h-out and already-started contests), confirms decorative elements are aria-hidden, and verifies real keyboard tab-focus reaches the control.
  • Result: All 46 backend tests (npm test in server/) and all 28 frontend tests (npm test in frontend/) pass locally.

📸 Screenshots / Demo (If Applicable)

N/A - this PR is backend validation/indexing logic, a React hook's async-safety internals, and accessibility attributes, not new UI. No visible design changes.


✅ Checklist

  • I have read and followed the contribution guidelines.
  • I have self-reviewed my changes.
  • My changes are limited to the scope of this issue.
  • Documentation has been updated where necessary.
  • No unnecessary files or unrelated changes have been included.
  • The related issue has been linked correctly.
  • All applicable testing and validation steps have been completed.

📚 Additional Notes

This is the 4th and final issue from the #269 production review - #276/#280, #277/#284, and #278/#286 are already merged. The index removal requires an explicit one-time run of node server/scripts/dropRedundantContestIndex.js against any already-deployed database (staging/production) after this merges, since Mongoose's autoIndex only ever adds missing indexes, never drops ones no longer declared in the schema, flagging this for whoever handles the deploy. Test coverage here is intentionally proportionate rather than exhaustive per the issue's own guidance ("avoid introducing a second testing stack," "prioritize behavior over implementation details"), full Playwright E2E flows and an exhaustive UpcomingContestsList state matrix were left as natural follow-ups for the existing Playwright setup at the repo root, rather than duplicated here.

Summary by CodeRabbit

  • Accessibility

    • Improved reminder bell labels, keyboard focus styling, and screen-reader behavior.
  • Bug Fixes

    • Prevented outdated contest and reminder requests from overwriting newer data.
    • Improved safety when requests finish after the page is closed.
    • Strengthened validation for contest reminder actions.
  • Tests

    • Expanded coverage for countdowns, reminders, validation, asynchronous requests, and contest data handling.
  • Maintenance

    • Removed redundant contest database indexes and documented the index configuration.

… and test coverage for contest module

- Added validateParams middleware + contestIdParamSchema, applied to
  DELETE /reminders/:contestId and POST /reminders/:contestId/notified.
  Both now enforce the same positive-integer contract as POST
  /reminders instead of a manual parseInt(...) that permissively
  accepted strings like '12abc'.
- Removed the redundant standalone contestId index on Contest — the
  compound unique {platform, contestId} index already covers every
  query pattern in the codebase. Documented the rationale in
  server/modules/contests/INDEXES.md and added a migration script
  (dropRedundantContestIndex.js) since a schema change alone never
  drops an existing production index.
- useContests now guards against stale/superseded async responses via
  a per-fetch sequence token, and invalidates in-flight requests on
  unmount, matching the cancellation pattern already used elsewhere
  (bell, notifier).
- ContestReminderBell now has a dynamic aria-label (including the
  due-soon count) instead of relying on title alone, and marks its
  decorative icon/badge aria-hidden.
- Added 28 new tests (46 backend total, 28 frontend total) covering
  the validation contract, reminder lifecycle edge cases, repository
  idempotency/joins/cleanup, the stale-response race condition, and
  bell accessibility (accessible name, count, keyboard focus) — using
  the project's existing test tooling (node:test, vitest), no new
  testing stack introduced.

Closes kunalverma2512#279
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

@ida-jemi is attempting to deploy a commit to the Kunal Verma's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🎉 Welcome to CodeLens — Thank You for Your Contribution!

Hey @ida-jemi! 👋

We are genuinely excited to have you here. Every single PR — big or small — makes CodeLens better, and yours is no exception. Take a moment to review the checklist below to help us merge your work quickly and smoothly.

✅ Before Requesting a Review

  • Keep code clean, readable, and consistent with the existing codebase
  • Avoid unrelated or unnecessary file changes
  • Make sure the UI is fully responsive across all device sizes
  • Attach screenshots or a short screen recording for any UI changes
  • Resolve all merge conflicts before marking the PR as ready
  • Do not submit AI-generated, copy-pasted, or low-effort implementations

💬 Join Our Community Channel — This is Mandatory

Being part of our communication channel is compulsory for all contributors, not optional.

📡 Join the CodeLens Matrix Channel

Why join? This is where all important announcements, PR review updates, contribution discussions, and maintainer decisions happen in real time. Contributors who are not in the channel regularly miss critical context and updates, which often leads to duplicated or misaligned work. Staying connected here is what keeps the community strong and your contributions impactful.


We are rooting for you! If you have any questions, drop them in the channel or comment right here on this PR. Let's build something great together. 🚀✨

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ida-jemi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc04b67f-128e-4c1b-a101-23277e4a3a13

📥 Commits

Reviewing files that changed from the base of the PR and between b51838a and c9c75ba.

📒 Files selected for processing (2)
  • frontend/src/components/contests/ContestReminderBell.jsx
  • frontend/src/pages/LandingPage.jsx
📝 Walkthrough

Walkthrough

This PR adds route-level contest ID validation, stale-request protection, reminder accessibility updates, redundant contest index cleanup, and frontend and backend test coverage.

Changes

Frontend contest behavior

Layer / File(s) Summary
Contest UI accessibility and display coverage
frontend/src/components/contests/ContestReminderBell.jsx, frontend/src/components/contests/ContestReminderBell.test.jsx, frontend/src/components/contests/ContestCountdown.test.jsx, frontend/package.json
The reminder bell now exposes a dynamic accessible name, hides decorative elements, and shows keyboard focus styling. Countdown and accessibility behavior are tested.
Contest request lifecycle handling
frontend/src/hooks/useContests.js, frontend/src/hooks/useContests.test.js
Contest and reminder responses now apply only for the latest mounted request. Optimistic updates, rollback behavior, stale responses, and unmount completion are tested.

Backend contest validation and index management

Layer / File(s) Summary
Reminder route validation and lifecycle contracts
server/modules/contests/validation.js, server/modules/contests/validation.test.js, server/modules/contests/routes.js, server/modules/contests/controller.js, server/modules/contests/service.test.js
Reminder route parameters use a shared positive-integer Zod contract. Controllers consume validated values. Validation and reminder lifecycle behavior are tested.
Reminder repository behavior coverage
server/modules/contests/repository.test.js
Repository tests cover idempotent reminder creation, upcoming contest queries, active reminder joins, sorting, and stale-contest pruning.
Contest index migration and documentation
server/models/Contest.js, server/modules/contests/INDEXES.md, server/scripts/dropRedundantContestIndex.js
Standalone platform and contestId indexes were removed from the schema. Index rationale and migration instructions were documented. A guarded migration removes deployed redundant indexes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: Frontend, backend, type:bug, type:testing, documentation, type:ui/ux

Suggested reviewers: kunalverma2512

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the PR's primary changes across validation, indexes, async cleanup, accessibility, and test coverage.
Description check ✅ Passed The description follows the template and provides complete scope, motivation, testing details, checklist status, and deployment notes.
Linked Issues check ✅ Passed The changes address all five objectives in issue #279, including validation, index cleanup, async safety, accessibility, and focused tests.
Out of Scope Changes check ✅ Passed The changed files and dependency update support the objectives in issue #279, with no unrelated code changes identified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
server/modules/contests/validation.test.js (1)

13-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add direct validateParams middleware tests.

These tests validate schemas only. Add tests that verify an invalid parameter returns 400 without calling next, and that a valid string parameter reaches next as a numeric req.params.contestId.

🤖 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 `@server/modules/contests/validation.test.js` around lines 13 - 65, Add direct
validateParams middleware tests alongside the existing schema tests: verify an
invalid contestId produces a 400 response without invoking next, and verify a
valid string contestId invokes next with req.params.contestId converted to a
number. Reuse the existing contest validation schemas and middleware setup
symbols rather than testing schema.safeParse alone.
frontend/src/hooks/useContests.test.js (1)

114-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen or reframe the post-unmount test; it cannot currently detect a regression.

This test unmounts the hook, then resolves the pending fetch, and asserts nothing about result.current or about any spy. React 18 removed the "Can't perform a React state update on an unmounted component" warning: We've removed a warning when you call setState on an unmounted component. An update to an unmounted component is now a silent no-op with no re-render, no warning, and no thrown error, whether or not the contestsRequestId/remindersRequestId guard in useContests.js (Lines 72-80) exists.

Since result.current only reflects the last re-render, and no re-render occurs for a truly unmounted tree, this test passes identically whether or not the requestId invalidation logic is removed. It does not verify the guard it claims to test.

Consider one of these:

  • Add a console.error spy assertion to make the "no warning/no crash" claim explicit in the test, even though it can't discriminate the guard's presence.
  • Rely on the existing stale-response race test (Lines 86-112) as the actual regression check for this guard, and reword this test's description to state it only verifies "no crash on late resolution after unmount," not "the response was ignored."
🧪 Example of making the smoke-test intent explicit
   it("does not apply a fetch response that resolves after unmount", async () => {
     let resolveFetch;
     mockGetUpcomingCodeforcesContests.mockReturnValue(
       new Promise((resolve) => { resolveFetch = resolve; })
     );
 
+    const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
     const { unmount } = renderHook(() => useContests());
     unmount();
 
     // Should not throw / cause an act() warning even though state would
     // otherwise be updated after unmount.
     await act(async () => {
       resolveFetch({ data: { data: [CONTEST_A] } });
     });
+
+    expect(errorSpy).not.toHaveBeenCalled();
+    errorSpy.mockRestore();
   });

Since React 18+ no-ops these updates unconditionally, this doesn't restore the test's ability to catch a removed guard; it only documents intent. The real regression coverage for the requestId contract is the stale-response race test above it.

🤖 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 `@frontend/src/hooks/useContests.test.js` around lines 114 - 128, Reframe the
test description and comments around the unmount flow in useContests to state
that it only verifies late resolution causes no crash or warning, rather than
claiming it validates response suppression. Keep the existing stale-response
race test as coverage for the contestsRequestId/remindersRequestId guard, and
optionally add an explicit console.error spy assertion if retaining the
no-warning claim.
🤖 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 `@server/modules/contests/INDEXES.md`:
- Around line 9-13: Update the index inventory to include Contest.js’s
standalone { platform: 1 } index and justify its platform-only query coverage,
or remove that index from both the schema and deployed databases. Correct the {
platform: 1, contestId: 1 } entry so its explanation only claims support for
platform-prefixed queries, not phase-filtered queries, and revise the phase
index description accordingly.

In `@server/modules/contests/validation.js`:
- Around line 39-50: Update the contestId validation in addReminderSchema and
contestIdParamSchema to reject booleans, arrays, and other non-string/non-number
inputs before numeric coercion, while preserving integer and positive-value
checks. Add regression tests covering boolean and array contestId values in the
add-reminder body.

In `@server/scripts/dropRedundantContestIndex.js`:
- Around line 5-15: Update the migration documentation and the script’s Usage
comment to consistently use the repository-root command `node
server/scripts/dropRedundantContestIndex.js`, or explicitly state that the
command must be run from the server directory; ensure both documented locations
follow the same convention.
- Around line 19-27: Update the migration around the standalone index lookup to
first verify that a unique compound index on `{ platform: 1, contestId: 1 }`
exists. Abort before `collection.dropIndex(standalone.name)` when the
replacement index is missing, while preserving the existing drop behavior when
both indexes are present.

---

Nitpick comments:
In `@frontend/src/hooks/useContests.test.js`:
- Around line 114-128: Reframe the test description and comments around the
unmount flow in useContests to state that it only verifies late resolution
causes no crash or warning, rather than claiming it validates response
suppression. Keep the existing stale-response race test as coverage for the
contestsRequestId/remindersRequestId guard, and optionally add an explicit
console.error spy assertion if retaining the no-warning claim.

In `@server/modules/contests/validation.test.js`:
- Around line 13-65: Add direct validateParams middleware tests alongside the
existing schema tests: verify an invalid contestId produces a 400 response
without invoking next, and verify a valid string contestId invokes next with
req.params.contestId converted to a number. Reuse the existing contest
validation schemas and middleware setup symbols rather than testing
schema.safeParse alone.
🪄 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 Plus

Run ID: 02b9bdba-dcc9-4873-93e4-c8a194767806

📥 Commits

Reviewing files that changed from the base of the PR and between 7ad67ff and a8f25cc.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • frontend/package.json
  • frontend/src/components/contests/ContestCountdown.test.jsx
  • frontend/src/components/contests/ContestReminderBell.jsx
  • frontend/src/components/contests/ContestReminderBell.test.jsx
  • frontend/src/hooks/useContests.js
  • frontend/src/hooks/useContests.test.js
  • server/models/Contest.js
  • server/modules/contests/INDEXES.md
  • server/modules/contests/controller.js
  • server/modules/contests/repository.test.js
  • server/modules/contests/routes.js
  • server/modules/contests/service.test.js
  • server/modules/contests/validation.js
  • server/modules/contests/validation.test.js
  • server/scripts/dropRedundantContestIndex.js

Comment thread server/modules/contests/INDEXES.md
Comment thread server/modules/contests/validation.js
Comment thread server/scripts/dropRedundantContestIndex.js
Comment thread server/scripts/dropRedundantContestIndex.js Outdated
…guard destructive index migration, fix docs

- contestId validation now rejects booleans and arrays before numeric
  coercion (z.coerce.number() alone treats true as 1 and [42] as 42
  via JS Number() semantics).
- dropRedundantContestIndex.js now refuses to drop any standalone
  index unless the compound unique {platform, contestId} replacement
  index is confirmed present, preventing a drifted database from
  losing lookup coverage and the uniqueness guarantee.
- Removed the also-redundant standalone platform index (no query
  filters on platform alone), and corrected INDEXES.md to accurately
  describe what each index covers and use one consistent invocation
  path for the migration script.
- Added direct validateParams middleware tests, not just schema-level
  ones.
- Reframed the useContests unmount test to accurately state what it
  verifies, since React 18 already silently no-ops post-unmount
  setState regardless of the requestId guard's presence.

Addresses CodeRabbit review feedback on this PR
…solve react-hooks/set-state-in-effect

Unrelated to issue kunalverma2512#279's scope, but blocking the required Frontend
Lint & Test CI check on this PR. Matches the setTimeout-deferral
pattern already used by the other two branches in the same effect —
no behavior change, just no longer calls setState synchronously
during the effect body.
Date.now() was called directly in the render body, which the new
react-hooks/purity lint rule flags as an impure call. Moved to
useState's lazy initializer + a periodic refresh via useEffect,
matching the existing 'now' pattern already used in useContests.js.
This was the actual blocking error in the Frontend Lint & Test CI
check — the other similarly-worded 'Error: Calling setState...' log
lines are warning-severity, not the build-failing error.
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.23810% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
frontend/src/hooks/useContests.js 91.66% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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.

Low: Improve contest validation, index hygiene, async cleanup, accessibility, and test coverage

1 participant