Skip to content

fix: validate question input and keep question order unique - #219

Merged
TheCodeHeist-Coder merged 1 commit into
TheCodeHeist-Coder:mainfrom
Anshuu2004:fix/question-validation-and-order
Oct 26, 2026
Merged

TheCodeHeist-Coder merged 1 commit into
TheCodeHeist-Coder:mainfrom
Anshuu2004:fix/question-validation-and-order

Conversation

@Anshuu2004

Copy link
Copy Markdown
Contributor

What does this PR do?

Adding a question used to trust whatever the request body said. This covers the three things in the issue:

  • POST /api/v1/quizzes/:quizId/questions now validates the body with zod (new utils/questionInput.ts) and returns a 400 that starts with the field name, e.g. timeLimit must be a whole number from 5 to 300. Rules: text non-empty string, timeLimit integer 5 to 300 (same bounds as the AI generator on the frontend), difficulty Low/Medium/High (default Medium), answers an array of 2 to 6 { text, isCorrect } with at least one correct. I kept "at least one" correct (not exactly one) so nothing that works today breaks.
  • order is now max(order) + 1, computed in a transaction that locks the quiz row. Deleting a question renumbers the rest to 1..n in the same transaction (utils/questionOrder.ts), which also cleans up quizzes that already have ties.
  • Add Question form in QuizBuilder.tsx: Secs is checked before sending (so -5 is no longer saved) and the server error is shown in the form with role="alert", the same way deleteError is shown. Before it was only a console.error.

I did not add the @@unique([quizId, order]) constraint on purpose. It needs a migration (and a check that existing databases have no ties first), so I think it is better as a follow up.

http-server had no test setup, so I added vitest the same way ws-server has it, a test script, and a "Test http-server" step in ci.yml. The lockfile only gets the one new vitest entry for http-server.

Typing -5 in Secs:

negative seconds blocked in the form

Server message shown in the form (I forced a bad value through with Playwright, the normal form can't send one anymore, which is why the box says 20):

server error shown in the add question form

Related issue

Closes #213

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactoring
  • Performance
  • Tests
  • Build / CI / deployment

Which services does this touch?

  • apps/frontend
  • apps/http-server
  • apps/ws-server
  • apps/genAI
  • packages/db (schema or Prisma changes)
  • Docker / CI

How did you test this?

Unit tests:

  • pnpm --filter http-server run test: 23 tests pass (validation schema and the renumber helper).
  • pnpm --filter frontend run test: 81 tests pass, including new ones for bad Secs values not being sent, the empty field falling back to 15, and the server error showing in the form.
  • pnpm --filter ws-server run test: 42 pass (untouched, just checking).

Against a real Postgres (throwaway PG 18 with prisma migrate deploy applied) and the built http-server, I ran the curl cases from the issue before and after:

case before after
no correct answer 201 400 answers needs at least one correct option
answers: [] 201 400 answers must be an array of 2 to 6 options
timeLimit: -5 201 400 timeLimit must be a whole number from 5 to 300
answers: "nope" 500 400 answers must be an array of 2 to 6 options
timeLimit: 99999999999 500 400 timeLimit must be a whole number from 5 to 300
add One, Two, Three, delete Two, add Four orders 4,6,6 (Three and Four tie) One 1, Three 2, Four 3
6 questions added at the same time orders had repeats (6,6,7,7,7,9,9) 1..9 with no repeats

In the browser (vite + http-server + that database) I opened /quiz/<id>/edit, clicked Add Question and typed -5 in Secs: the form shows "Seconds must be a whole number from 5 to 300" and no request is sent. To see the server message I let a bad value through by rewriting the request body in Playwright (timeLimit: 99999999999), and the form showed timeLimit must be a whole number from 5 to 300.

I only tested with one user. I did not run the live quiz (ws-server) against the renumbered questions, only checked the order values in the database.

Checklist

  • I ran pnpm run lint (it ran, but the frontend already has ~19 eslint errors on main and exits non-zero; QuizBuilder.tsx has the same 6 findings as before my change)
  • I ran pnpm run check-types
  • I ran pnpm run build
  • The app still starts with docker-compose up
  • I updated documentation if this changes behaviour or setup
  • I did not commit any .env file, API key or other secret

First pull request here? CONTRIBUTING.md walks through the
whole process, and a checkbox left unticked is not a problem — say so and we
will help.

POST /quizzes/:quizId/questions now validates the body with zod and returns
a 400 naming the field instead of saving unplayable questions or crashing
with a 500. order is max(order) + 1 inside a transaction that locks the quiz
row, and deleting a question renumbers the rest. The Add Question form checks
Secs (5 to 300) before sending and shows the server error.
@TheCodeHeist-Coder
TheCodeHeist-Coder merged commit 00694fd into TheCodeHeist-Coder:main Oct 26, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants