Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions src/modules/quizzes/quiz.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ export const MAX_RETRIES_PER_MODULE_PER_DAY = 3;
// calls).
export const MAX_QUIZ_GENERATIONS_PER_MODULE_PER_HOUR = 5;

/** Smallest workable number of choices for a multiple-choice question. */
export const MIN_QUIZ_OPTIONS = 2;

/**
* Upper bound on choices per question (#388, #538).
* Aligns both authoredQuestionSchema and submitQuizSchema so a user cannot
* submit answer indices beyond the allowable options range.
*/
export const MAX_QUIZ_OPTIONS = 10;

// ─── Request Schemas ────────────────────────────────────────────────────────

export const generateQuizSchema = z.object({
Expand Down Expand Up @@ -53,8 +63,8 @@ export const submitQuizSchema = z.object({
.array(
z.object({
questionId: z.string().min(1).max(100),
// Bound the index so out-of-range values can't be submitted.
selectedIndex: z.number().int().min(0).max(20),
// Bound the index so out-of-range values can't be submitted (#538).
selectedIndex: z.number().int().min(0).max(MAX_QUIZ_OPTIONS - 1),
})
)
.min(1, "At least one answer is required")
Expand Down Expand Up @@ -83,13 +93,6 @@ export const quizFeedbackSummaryQuerySchema = z.object({

// ─── Admin: Manual Quiz Authoring (#388) ─────────────────────────────────────

/** Smallest workable number of choices for a multiple-choice question. */
export const MIN_QUIZ_OPTIONS = 2;
/** Upper bound on choices per question. Capped well below
* submitQuizSchema's static max(20) so a hand-authored question can never
* exceed what a submitted answer index can address. */
export const MAX_QUIZ_OPTIONS = 10;

/**
* One hand-authored question (#388). Matches the shape QuizService stores
* after AI generation, minus the shuffle bookkeeping (see the note on
Expand Down
14 changes: 11 additions & 3 deletions tests/unit/schemas/input-validation.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import { updateProfileSchema } from "../../../src/modules/users/user.types.js";
import { submitQuizSchema } from "../../../src/modules/quizzes/quiz.types.js";
import { submitQuizSchema, MAX_QUIZ_OPTIONS } from "../../../src/modules/quizzes/quiz.types.js";
import {
createCourseSchema,
listCoursesSchema,
Expand Down Expand Up @@ -54,9 +54,17 @@ describe("submitQuizSchema", () => {
expect(submitQuizSchema.safeParse({ answers: [] }).success).toBe(false);
});

it("rejects selectedIndex above the max bound", () => {
it("accepts selectedIndex within bounds (up to MAX_QUIZ_OPTIONS - 1)", () => {
expect(
submitQuizSchema.safeParse({
answers: [{ questionId: "q1", selectedIndex: MAX_QUIZ_OPTIONS - 1 }],
}).success
).toBe(true);
});

it("rejects selectedIndex at or above the MAX_QUIZ_OPTIONS bound", () => {
const result = submitQuizSchema.safeParse({
answers: [{ questionId: "q1", selectedIndex: 21 }],
answers: [{ questionId: "q1", selectedIndex: MAX_QUIZ_OPTIONS }],
});
expect(result.success).toBe(false);
});
Expand Down