Skip to content

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

Open
ida-jemi wants to merge 1 commit 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 1 commit 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.
    • Added stronger validation for contest reminder routes and lifecycle actions.
  • Tests

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

    • Removed a redundant contest database index 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

📝 Walkthrough

Walkthrough

This PR hardens the contest tracker with route-level ID validation, stale-request protection, reminder accessibility updates, contest index cleanup, and frontend and backend automated tests.

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 states and reminder 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 reminder 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 now 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
The standalone contestId index was removed from the schema. Index rationale and migration instructions were documented, with a script to remove the deployed index.

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

Possibly related PRs

Suggested labels: Frontend, backend, type:bug, type:testing, documentation

Suggested reviewers: kunalverma2512

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the pull request's validation, index, async cleanup, accessibility, and testing changes.
Description check ✅ Passed The description follows the template and provides issue linkage, scope, motivation, testing details, checklist status, and deployment notes.
Linked Issues check ✅ Passed The changes address all five objectives in issue #279 with shared validation, index cleanup, async guards, accessibility improvements, and focused tests.
Out of Scope Changes check ✅ Passed All reviewed changes are directly related to the validation, index hygiene, async safety, accessibility, and test coverage objectives in issue #279.
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 on lines +9 to +13
| Index | Backs |
|---|---|
| `{ platform: 1, contestId: 1 }` (unique) | `findByContestId` (repository.js), `bulkUpsertContests`'s upsert filter — both always filter on `platform` + `contestId` together. Also enforces the one-document-per-(platform, contestId) invariant. Its leftmost prefix (`platform`) also serves any query that filters on `platform` alone. |
| `{ phase: 1 }` | `getUpcomingContests`, `getNonFinishedContestIds`, `pruneStaleReminders`, `getActiveReminderContests` all filter on `phase` (combined with `platform`, which the compound index above already indexes as a prefix — see note below). |
| `{ startTimeSeconds: 1 }` | `getUpcomingContests`'s sort, and range queries if added later (e.g. "contests starting in the next N hours"). |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Make the index inventory match Contest.js.

server/models/Contest.js Line 15 still declares a standalone { platform: 1 } index, but this table omits it. Also, { platform: 1, contestId: 1 } contains no phase key; its leftmost prefix covers platform only. List and justify { platform: 1 }, or remove it from the schema and deployed databases. Rewrite the Line 12 explanation to describe the actual index support.

🤖 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/INDEXES.md` around lines 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.

Comment on lines 39 to +50
export const addReminderSchema = z.object({
contestId: z.coerce.number().int().positive(),
});

// Same positive-integer contract as addReminderSchema, applied at the
// route boundary for :contestId params instead of a request body. Keeping
// this separate from addReminderSchema (rather than reusing it directly)
// makes the two independently evolvable if param- and body-level rules
// ever need to diverge (e.g. bounds specific to one context).
export const contestIdParamSchema = z.object({
contestId: z.coerce.number().int().positive(),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For Zod 4.3.6, does z.coerce.number() use JavaScript Number() coercion and accept inputs such as true and [42]?

💡 Result:

Yes, in Zod 4.3.6, z.coerce.number relies on the JavaScript Number constructor for coercion [1][2]. Because it uses Number internally, it accepts the same inputs that JavaScript's native Number function converts [1]. The behavior for the requested inputs is: 1. true: Coerces to 1 [3][1]. 2.: Coerces to 42. In JavaScript, an array containing a single number is coerced to a string "42" and then to the number 42 by the Number constructor. Official test cases for Zod 4.3.6 confirm these behaviors [3]. You can verify this by running: const schema = z.coerce.number; schema.parse(true); // 1 schema.parse; // 42

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ -x "$(command -v gh)" ]; then
  echo "GitHub CLI: available"
  gh --version | head -1
else
  echo "GitHub CLI: unavailable"
fi

echo "--- package manifests with zod versions ---"
for f in package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lockb; do
  [ -e "$f" ] && echo "### $f" && rg -n '"?zod"?|zod@|version:' "$f" | head -80 || true
done

echo "--- target file excerpt ---"
if [ -f server/modules/contests/validation.js ]; then
  cat -n server/modules/contests/validation.js | sed -n '1,120p'
else
  echo "validation.js not found"
fi

echo "--- route/body usage references ---"
rg -n "addReminderSchema|contestIdParamSchema|z\.coerce\.number|addReminder" server -g '*.{js,ts,mjs,cjs}' || true

echo "--- local zod behavior probe (if package is present) ---"
node - <<'JS'
try {
  const z = require('./node_modules/zod');
  let schemaV4, schemaV3, coerce3;
  try {
    schemaV4 = z.v4.z.coerce.number;
    console.log("zod version:", require('./node_modules/zod/package.json').version);
    console.log("v4 true parse:", schemaV4.parse(true));
    console.log("v4 [42] parse:", schemaV4.parse([42]));
  } catch (err) {
    console.log("No zod 4 available at require path", err && err.message);
    schemaV3 = z.coerce.number;
    console.log("v3 true parse:", schemaV3.safeParse(true));
    console.log("v3 [42] parse:", schemaV3.safeParse([42]));
  }
} catch (e) {
  console.log("Could not run local zod probe:", e && e.message);
}
JS

Repository: kunalverma2512/CodeLens

Length of output: 5693


Reject non-numeric input types before coercion.

z.coerce.number() uses JavaScript coercion, so the add-reminder body can accept true as 1 and [42] as 42. Constrain contestId to strings or numbers before applying int().positive(), and add regression tests for boolean and array values.

🤖 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.js` around lines 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.

Comment on lines +5 to +15
/**
* One-off migration: drops the standalone `contestId_1` index on the
* contests collection, if present. Mongoose schema changes never
* automatically drop existing indexes in production — this must be run
* explicitly against each environment (staging, production) after the
* schema change deploys.
*
* Safe to run multiple times — it's a no-op if the index is already gone.
*
* Usage: node scripts/dropRedundantContestIndex.js
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use one documented working directory.

INDEXES.md Line 35 uses node server/scripts/dropRedundantContestIndex.js, while Line 14 uses node scripts/dropRedundantContestIndex.js. The second command works only from the server directory. State the working directory or use the repository-root command in both locations.

🤖 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/scripts/dropRedundantContestIndex.js` around lines 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.

Comment on lines +19 to +27
const indexes = await collection.indexes();

const standalone = indexes.find(
(idx) => JSON.stringify(idx.key) === JSON.stringify({ contestId: 1 })
);

if (standalone) {
await collection.dropIndex(standalone.name);
console.log(`Dropped redundant index: ${standalone.name}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

: "${MONGO_URI:?Set MONGO_URI for the target environment}"

mongosh "$MONGO_URI" --quiet --eval '
const contests = db.getSiblingDB("CodeLens").contests;
printjson(contests.getIndexes());
'

Repository: kunalverma2512/CodeLens

Length of output: 227


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== locate target file =="
fd -a 'dropRedundantContestIndex\.js$' . || true

echo "== target file =="
file="$(fd 'dropRedundantContestIndex\.js$' . | head -n 1)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  cat -n "$file"
fi

echo "== related index references =="
rg -n "dropRedundantContestIndex|REDFund|contestId|platform|INDEXES|createIndex|unique" -S . --glob '!node_modules' --glob '!dist' --glob '!build' | head -n 200

Repository: kunalverma2512/CodeLens

Length of output: 24917


Require the compound unique index before dropping contestId_1.

The migration only checks for { contestId: 1 }; it can drop that index even when the replacement { platform: 1, contestId: 1 } unique index is missing. A drifted production database would then lose the one-document-per-(platform, contestId) invariant. Abort before dropIndex unless that unique index exists.

Guard the destructive operation
 const indexes = await collection.indexes();
+const compoundUnique = indexes.find(
+  (idx) =>
+    JSON.stringify(idx.key) ===
+      JSON.stringify({ platform: 1, contestId: 1 }) &&
+    idx.unique === true
+  );
 
 if (standalone) {
+  if (!compoundUnique) {
+    throw new Error(
+      "Refusing to drop contestId_1: compound unique index is missing"
+    );
+  }
   await collection.dropIndex(standalone.name);
📝 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
const indexes = await collection.indexes();
const standalone = indexes.find(
(idx) => JSON.stringify(idx.key) === JSON.stringify({ contestId: 1 })
);
if (standalone) {
await collection.dropIndex(standalone.name);
console.log(`Dropped redundant index: ${standalone.name}`);
const indexes = await collection.indexes();
const compoundUnique = indexes.find(
(idx) =>
JSON.stringify(idx.key) ===
JSON.stringify({ platform: 1, contestId: 1 }) &&
idx.unique === true
);
const standalone = indexes.find(
(idx) => JSON.stringify(idx.key) === JSON.stringify({ contestId: 1 })
);
if (standalone) {
if (!compoundUnique) {
throw new Error(
"Refusing to drop contestId_1: compound unique index is missing"
);
}
await collection.dropIndex(standalone.name);
console.log(`Dropped redundant index: ${standalone.name}`);
🤖 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/scripts/dropRedundantContestIndex.js` around lines 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.

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