Skip to content

fix(reviews): stop an educator reviewing their own course - #122

Merged
udaycodespace merged 1 commit into
udaycodespace:mainfrom
MOHITKOURAV01:fix/117-self-review
Aug 30, 2026
Merged

fix(reviews): stop an educator reviewing their own course#122
udaycodespace merged 1 commit into
udaycodespace:mainfrom
MOHITKOURAV01:fix/117-self-review

Conversation

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor

Closes #117.

The defect

createReview gated on one thing — is the reviewer enrolled — and nothing stops
an author enrolling in their own course. Two requests:

[#5] enrol in own course -> 200 true "Enroll Successfully"
[#5] review own course   -> 201 true "Review submitted successfully."
[#5] verifiedEnrollment  -> true | summary: {"averageRating":5,"totalReviews":1}

The badge made it worse. serializeReview stamped verifiedEnrollment: true on
every row, and the review card rendered ✓ Verified enrollment without reading
the field at all.

It flows into everything built on the summary: CourseRatingBadge on the
catalogue card, buildSummaryPipeline's average, and the rating a shopper reads
before enrolling. A course with one self-review outranked a course with three
genuine four-star reviews.

What is here

utils/courseAuthorship.js owns the comparison, because courseModel.userId is
a String while courseReview.userId is an ObjectId, and comparing them
without coercing both sides is false — always, and silently.
courseDeletionController already does this by hand and cascadeDelete carries
a comment about the same trap.

  • createReview refuses the author before the enrolment check. The
    ordering is the point: the enrolment is not the thing that is wrong, so
    "you are not enrolled" would be the wrong answer to give them.
  • getMyReview returns reason, isAuthor and isEnrolled, so the client
    stops rendering a form that answers 403.
  • updateReview reads before writing. A row written before this guard is
    still owned by its author, and findOneAndUpdate({ _id, userId }) would have
    let them edit it — reintroducing by the back door what the create path
    refuses.
  • serializeReview computes verifiedEnrollment instead of asserting it,
    and CourseReviews.jsx reads it.
  • Deleting stays open in every case — that is the direction that fixes the
    problem.

Guarding the write leaves existing rows in the average, so
scripts/removeSelfReviews.js clears them, following the db:dedupe-emails
pattern already in the repo:

npm run db:remove-self-reviews -- --dry-run
npm run db:remove-self-reviews

On the client, lib/reviewEligibility.js replaces one sentence for every
denial. An author was being told "Enroll in this course before submitting a
verified review"
— advice that leads nowhere, since they can enrol and the
review is still refused.

Three things deliberately left alone

The enrolment. An author can still enrol in their own course, and should:
sendCourseContentController will not serve the sections without one, so
refusing it would take away the ability to check the videos play. Blocking the
review protects the rating; blocking the enrolment costs something real and
protects nothing extra.

The enrolled counter. An author who enrols still counts as one of their own
learners. That counter has a separate, older drift problem, and fixing one course
of it here would change a number admins have been reading, for an unrelated
reason.

Excluding legacy self-reviews at read time. It would put a $lookup in
buildSummaryPipeline — the pipeline #86 built specifically so a twelve-card
page costs one indexed pass. The script does it once instead of on every render.

Verifying

The original repro, re-run:

enrol in own course -> 200 true "Enroll Successfully"
review own course   -> 403 false "You cannot review a course you created." | reason: own-course
catalogue summary   -> {"averageRating":0,"totalReviews":0,...}
canReview / reason  -> false / own-course | enrolled: true

Checklist

  • cd backend && npm test — 433 pass (413 on main, 20 added)
  • cd frontend && npm test — 180 pass (169 on main, 11 added)
  • cd frontend && npm run build
  • An enrolled student reviewing normally is covered, and unaffected
  • An educator reviewing somebody else's course still works
  • npm run lint — does not pass on main and does not here.
    CourseReviews.jsx reports the same 3 problems before and after.
    lib/reviewEligibility.js lints clean.

Write-up: docs/issue-117-self-review.md.

createReview checked one thing — is the reviewer enrolled — and nothing
stops an author enrolling in their own course. Two requests put a
five-star review on your own listing, badged "Verified enrollment" and
averaged into the rating CourseRatingBadge renders on every catalogue
card. A course with one self-review outranked a course with three genuine
four-star ones.

The badge made it worse: serializeReview stamped verifiedEnrollment as
the literal true on every row, and the review card rendered it without
reading the field.

utils/courseAuthorship owns the comparison, because courseModel.userId is
a String while courseReview.userId is an ObjectId and comparing them
without coercing both sides is false, always, and silently.

createReview refuses the author before the enrolment check — the
enrolment is not the thing that is wrong, so "you are not enrolled" would
be the wrong answer. getMyReview returns a reason so the client stops
rendering a form that answers 403. updateReview reads before writing,
because a row written before this guard is still owned by its author and
findOneAndUpdate would have let them edit it back into existence.
verifiedEnrollment is computed now. Deleting stays open in every case —
that is the direction that fixes the problem.

Guarding the write leaves the existing rows in the average, so
scripts/removeSelfReviews.js clears them, following db:dedupe-emails:
one $lookup on the indexed courseId with a $toString on the review's id,
--dry-run first.

On the client, lib/reviewEligibility replaces one sentence for every
denial. An author was being told to enrol, which leads nowhere.

Three things deliberately left alone, with reasons in docs/: the
enrolment itself, since previewing a course requires one; the `enrolled`
counter, which has an older drift problem of its own; and excluding
legacy self-reviews at read time, which would put a $lookup in the
pipeline udaycodespace#86 built to be one indexed pass.
@udaycodespace
udaycodespace self-requested a review August 28, 2026 09:31
@udaycodespace udaycodespace added ECSoC26 Required label for a PR to be eligible for Sentinel scoring good-pr PA-awarded bonus for an exceptionally executed PR — +15 XP good-backend PA-awarded bonus for outstanding backend work — +50 XP in review PR is up and waiting on maintainer review and removed in review PR is up and waiting on maintainer review labels Aug 28, 2026
@udaycodespace

Copy link
Copy Markdown
Owner

Reviewed this, @MOHITKOURAV01 . The self-review case is handled cleanly at both the backend and frontend levels, with the author check happening before enrollment validation and existing self-reviews being addressed through the cleanup script.

The verifiedEnrollment handling, review eligibility flow, update-path protection, and regression coverage all look good. I also like that the enrolment and existing enrolled counter behaviour were deliberately left unchanged rather than mixing unrelated changes into this fix.

All backend and frontend tests are passing, and the build is clean.

Approved and merging.

@udaycodespace
udaycodespace merged commit a0af108 into udaycodespace:main Aug 30, 2026
1 check passed
@ecsoc-sentinel ecsoc-sentinel Bot added the ECSoC26-L3 Difficult, auto-assigned by Sentinel — 15 points label Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ECSoC26-L3 Difficult, auto-assigned by Sentinel — 15 points ECSoC26 Required label for a PR to be eligible for Sentinel scoring good-backend PA-awarded bonus for outstanding backend work — +50 XP good-pr PA-awarded bonus for an exceptionally executed PR — +15 XP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: An educator can enrol in and five-star their own course, and it counts as a verified student review

2 participants