From 08a34bdf2d5a9b69a1894194d5319b32059dcc69 Mon Sep 17 00:00:00 2001 From: meloball9993-star Date: Wed, 21 Oct 2026 22:54:49 +0100 Subject: [PATCH 1/3] test(e2e): add tests for quiz retry, feedback, stats, and course leaderboard Closes #648 Closes #647 Closes #646 Closes #643 --- tests/e2e/course-leaderboard.test.ts | 46 ++++++++++++++ tests/e2e/quiz-feedback-endpoint.test.ts | 78 ++++++++++++++++++++++++ tests/e2e/quiz-retry.test.ts | 66 ++++++++++++++++++++ tests/e2e/quiz-stats.test.ts | 55 +++++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 tests/e2e/course-leaderboard.test.ts create mode 100644 tests/e2e/quiz-feedback-endpoint.test.ts create mode 100644 tests/e2e/quiz-retry.test.ts create mode 100644 tests/e2e/quiz-stats.test.ts diff --git a/tests/e2e/course-leaderboard.test.ts b/tests/e2e/course-leaderboard.test.ts new file mode 100644 index 0000000..38ae332 --- /dev/null +++ b/tests/e2e/course-leaderboard.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for course leaderboard endpoint (#643). + * + * Covers: + * - GET /api/v1/courses/:id/leaderboard (top learners leaderboard for a course) + */ +describe("Course Leaderboard API (#643)", () => { + let app: FastifyInstance; + + beforeAll(async () => { + app = await buildApp(); + await app.ready(); + }); + + afterAll(async () => { + await app.close(); + }); + + it("rejects invalid course UUID parameter", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/not-a-valid-uuid/leaderboard", + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("handles leaderboard request for valid course UUID without auth", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/leaderboard", + }); + + expect([200, 404, 500]).toContain(response.statusCode); + if (response.statusCode === 200) { + const body = JSON.parse(response.payload); + expect(body).toHaveProperty("success"); + } + }); +}); diff --git a/tests/e2e/quiz-feedback-endpoint.test.ts b/tests/e2e/quiz-feedback-endpoint.test.ts new file mode 100644 index 0000000..de158e1 --- /dev/null +++ b/tests/e2e/quiz-feedback-endpoint.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for quiz feedback endpoints (#647). + * + * Covers: + * - POST /api/v1/quizzes/:id/feedback (feedback submission, validation) + * - GET /api/v1/quizzes/:id/feedback (feedback summary retrieval) + */ +describe("Quiz Feedback Endpoints (#647)", () => { + let app: FastifyInstance; + + beforeAll(async () => { + app = await buildApp(); + await app.ready(); + }); + + afterAll(async () => { + await app.close(); + }); + + const tokenFor = (sub: string) => + app.jwt.sign({ + sub, + stellarAddress: + "GALICE0000000000000000000000000000000000000000000000000000000", + }); + + it("rejects unauthenticated feedback submission", async () => { + const response = await app.inject({ + method: "POST", + url: "/api/v1/quizzes/q1111111-1111-4111-8111-111111111111/feedback", + payload: { + rating: 5, + comment: "Excellent quiz questions!", + }, + }); + + expect(response.statusCode).toBe(401); + }); + + it("rejects feedback submission with invalid UUID", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/quizzes/invalid-quiz-uuid/feedback", + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + rating: 5, + }, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("handles valid feedback submission format", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/quizzes/c1111111-1111-4111-8111-111111111111/feedback", + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + rating: 5, + comment: "Clear questions and explanations", + }, + }); + + expect([200, 201, 400, 404, 500]).toContain(response.statusCode); + }); +}); diff --git a/tests/e2e/quiz-retry.test.ts b/tests/e2e/quiz-retry.test.ts new file mode 100644 index 0000000..6ee13e5 --- /dev/null +++ b/tests/e2e/quiz-retry.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for quiz retry endpoint (#648). + * + * Covers: + * - POST /api/v1/quizzes/:id/retry (retrying previously submitted quiz, retry limits, auth checks) + */ +describe("Quiz Retry API (#648)", () => { + let app: FastifyInstance; + + beforeAll(async () => { + app = await buildApp(); + await app.ready(); + }); + + afterAll(async () => { + await app.close(); + }); + + const tokenFor = (sub: string) => + app.jwt.sign({ + sub, + stellarAddress: + "GALICE0000000000000000000000000000000000000000000000000000000", + }); + + it("rejects unauthenticated retry request", async () => { + const response = await app.inject({ + method: "POST", + url: "/api/v1/quizzes/q1111111-1111-4111-8111-111111111111/retry", + }); + + expect(response.statusCode).toBe(401); + }); + + it("rejects invalid quiz UUID parameter", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/quizzes/invalid-uuid/retry", + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("handles retry attempt with valid quiz UUID format", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/quizzes/c1111111-1111-4111-8111-111111111111/retry", + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect([200, 201, 403, 404, 429, 500]).toContain(response.statusCode); + }); +}); diff --git a/tests/e2e/quiz-stats.test.ts b/tests/e2e/quiz-stats.test.ts new file mode 100644 index 0000000..1e1773e --- /dev/null +++ b/tests/e2e/quiz-stats.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for quiz stats endpoint (#646). + * + * Covers: + * - GET /api/v1/quizzes/stats (quiz statistics overview, course filtering, caching) + */ +describe("Quiz Stats API (#646)", () => { + let app: FastifyInstance; + + beforeAll(async () => { + app = await buildApp(); + await app.ready(); + }); + + afterAll(async () => { + await app.close(); + }); + + it("returns overall quiz stats without authentication", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/quizzes/stats", + }); + + expect([200, 500]).toContain(response.statusCode); + if (response.statusCode === 200) { + const body = JSON.parse(response.payload); + expect(body).toHaveProperty("success"); + } + }); + + it("accepts courseId filter query param", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/quizzes/stats?courseId=c1111111-1111-4111-8111-111111111111", + }); + + expect([200, 404, 500]).toContain(response.statusCode); + }); + + it("rejects invalid courseId query parameter", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/quizzes/stats?courseId=not-a-valid-uuid", + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); +}); From f7d94a105b37b1ca0e7ce5f85ba7965617b5a650 Mon Sep 17 00:00:00 2001 From: DeFiVC <136683573+DeFiVC@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:27:40 +0100 Subject: [PATCH 2/3] Delete tests/e2e/course-leaderboard.test.ts --- tests/e2e/course-leaderboard.test.ts | 46 ---------------------------- 1 file changed, 46 deletions(-) delete mode 100644 tests/e2e/course-leaderboard.test.ts diff --git a/tests/e2e/course-leaderboard.test.ts b/tests/e2e/course-leaderboard.test.ts deleted file mode 100644 index 38ae332..0000000 --- a/tests/e2e/course-leaderboard.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; -import type { FastifyInstance } from "fastify"; -import { buildApp } from "../../src/server.js"; - -/** - * E2E tests for course leaderboard endpoint (#643). - * - * Covers: - * - GET /api/v1/courses/:id/leaderboard (top learners leaderboard for a course) - */ -describe("Course Leaderboard API (#643)", () => { - let app: FastifyInstance; - - beforeAll(async () => { - app = await buildApp(); - await app.ready(); - }); - - afterAll(async () => { - await app.close(); - }); - - it("rejects invalid course UUID parameter", async () => { - const response = await app.inject({ - method: "GET", - url: "/api/v1/courses/not-a-valid-uuid/leaderboard", - }); - - expect(response.statusCode).toBe(400); - const body = JSON.parse(response.payload); - expect(body.error).toBe("VALIDATION_ERROR"); - }); - - it("handles leaderboard request for valid course UUID without auth", async () => { - const response = await app.inject({ - method: "GET", - url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/leaderboard", - }); - - expect([200, 404, 500]).toContain(response.statusCode); - if (response.statusCode === 200) { - const body = JSON.parse(response.payload); - expect(body).toHaveProperty("success"); - } - }); -}); From 0159a416ff35a4dd4a07509f40ba7b8d3d3261bb Mon Sep 17 00:00:00 2001 From: DeFiVC <136683573+DeFiVC@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:27:57 +0100 Subject: [PATCH 3/3] Delete tests/e2e/quiz-feedback-endpoint.test.ts --- tests/e2e/quiz-feedback-endpoint.test.ts | 78 ------------------------ 1 file changed, 78 deletions(-) delete mode 100644 tests/e2e/quiz-feedback-endpoint.test.ts diff --git a/tests/e2e/quiz-feedback-endpoint.test.ts b/tests/e2e/quiz-feedback-endpoint.test.ts deleted file mode 100644 index de158e1..0000000 --- a/tests/e2e/quiz-feedback-endpoint.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; -import type { FastifyInstance } from "fastify"; -import { buildApp } from "../../src/server.js"; - -/** - * E2E tests for quiz feedback endpoints (#647). - * - * Covers: - * - POST /api/v1/quizzes/:id/feedback (feedback submission, validation) - * - GET /api/v1/quizzes/:id/feedback (feedback summary retrieval) - */ -describe("Quiz Feedback Endpoints (#647)", () => { - let app: FastifyInstance; - - beforeAll(async () => { - app = await buildApp(); - await app.ready(); - }); - - afterAll(async () => { - await app.close(); - }); - - const tokenFor = (sub: string) => - app.jwt.sign({ - sub, - stellarAddress: - "GALICE0000000000000000000000000000000000000000000000000000000", - }); - - it("rejects unauthenticated feedback submission", async () => { - const response = await app.inject({ - method: "POST", - url: "/api/v1/quizzes/q1111111-1111-4111-8111-111111111111/feedback", - payload: { - rating: 5, - comment: "Excellent quiz questions!", - }, - }); - - expect(response.statusCode).toBe(401); - }); - - it("rejects feedback submission with invalid UUID", async () => { - const token = tokenFor("a1111111-1111-4111-8111-111111111111"); - const response = await app.inject({ - method: "POST", - url: "/api/v1/quizzes/invalid-quiz-uuid/feedback", - headers: { - authorization: `Bearer ${token}`, - }, - payload: { - rating: 5, - }, - }); - - expect(response.statusCode).toBe(400); - const body = JSON.parse(response.payload); - expect(body.error).toBe("VALIDATION_ERROR"); - }); - - it("handles valid feedback submission format", async () => { - const token = tokenFor("a1111111-1111-4111-8111-111111111111"); - const response = await app.inject({ - method: "POST", - url: "/api/v1/quizzes/c1111111-1111-4111-8111-111111111111/feedback", - headers: { - authorization: `Bearer ${token}`, - }, - payload: { - rating: 5, - comment: "Clear questions and explanations", - }, - }); - - expect([200, 201, 400, 404, 500]).toContain(response.statusCode); - }); -});