From 65b8a3cf6b81277db9646c3506d423ba9dc93b4f Mon Sep 17 00:00:00 2001 From: emdevelopa Date: Wed, 21 Oct 2026 22:47:06 +0100 Subject: [PATCH] test(e2e): add tests for sessions, course report, share, and review endpoints Closes #633 Closes #632 Closes #631 Closes #630 --- tests/e2e/course-report.test.ts | 96 +++++++++++++++++++++++++ tests/e2e/course-reviews.test.ts | 116 +++++++++++++++++++++++++++++++ tests/e2e/course-share.test.ts | 84 ++++++++++++++++++++++ tests/e2e/sessions.test.ts | 111 +++++++++++++++++++++++++++++ 4 files changed, 407 insertions(+) create mode 100644 tests/e2e/course-report.test.ts create mode 100644 tests/e2e/course-reviews.test.ts create mode 100644 tests/e2e/course-share.test.ts create mode 100644 tests/e2e/sessions.test.ts diff --git a/tests/e2e/course-report.test.ts b/tests/e2e/course-report.test.ts new file mode 100644 index 0000000..4aab0da --- /dev/null +++ b/tests/e2e/course-report.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for course report endpoint (#632). + * + * Covers: + * - POST /api/v1/courses/:id/report (report creation, duplicate handling, input validation) + */ +describe("Course Report API (#632)", () => { + 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 report submissions", async () => { + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/report", + payload: { + reason: "inappropriate", + description: "Inappropriate language in module 2", + }, + }); + + expect(response.statusCode).toBe(401); + const body = JSON.parse(response.payload); + expect(body.error).toBe("UNAUTHORIZED"); + }); + + it("rejects requests with invalid course UUID parameter", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/invalid-uuid/report", + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + reason: "inappropriate", + description: "Report description", + }, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("rejects invalid report payload", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/report", + headers: { + authorization: `Bearer ${token}`, + }, + payload: {}, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("handles valid report submission structure", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/report", + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + reason: "inappropriate", + description: "Detailed description of the issue in course materials", + }, + }); + + expect([200, 201, 404, 409, 500]).toContain(response.statusCode); + }); +}); diff --git a/tests/e2e/course-reviews.test.ts b/tests/e2e/course-reviews.test.ts new file mode 100644 index 0000000..514c583 --- /dev/null +++ b/tests/e2e/course-reviews.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for course reviews endpoints (#630). + * + * Covers: + * - GET /api/v1/courses/:id/reviews (listing course reviews) + * - POST /api/v1/courses/:id/reviews (creating/updating a course review) + */ +describe("Course Reviews API (#630)", () => { + 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", + }); + + describe("GET /api/v1/courses/:id/reviews", () => { + it("rejects invalid course UUID parameter", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/not-a-valid-uuid/reviews", + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("handles review listing with valid course ID", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/reviews", + }); + + expect([200, 404, 500]).toContain(response.statusCode); + if (response.statusCode === 200) { + const body = JSON.parse(response.payload); + expect(body).toHaveProperty("success"); + } + }); + + it("rejects invalid query parameters (e.g. invalid page)", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/reviews?page=0", + }); + + expect(response.statusCode).toBe(400); + }); + }); + + describe("POST /api/v1/courses/:id/reviews", () => { + it("rejects unauthenticated review submission", async () => { + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/reviews", + payload: { + rating: 5, + comment: "Great introductory course!", + }, + }); + + expect(response.statusCode).toBe(401); + }); + + it("validates rating range and required fields", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/reviews", + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + rating: 10, // Invalid: exceeds maximum rating + comment: "Invalid rating value", + }, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("handles valid review submission attempt", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/reviews", + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + rating: 5, + comment: "Great introductory course on blockchain!", + }, + }); + + expect([200, 201, 403, 404, 500]).toContain(response.statusCode); + }); + }); +}); diff --git a/tests/e2e/course-share.test.ts b/tests/e2e/course-share.test.ts new file mode 100644 index 0000000..a7aa1af --- /dev/null +++ b/tests/e2e/course-share.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for course share endpoints (#631). + * + * Covers: + * - POST /api/v1/courses/:id/share (share link creation, referral code, QR code generation) + * - GET /api/v1/courses/shared/:code (share link resolution) + */ +describe("Course Share API (#631)", () => { + 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", + }); + + describe("POST /api/v1/courses/:id/share", () => { + it("rejects unauthenticated share link creation", async () => { + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/share", + }); + + expect(response.statusCode).toBe(401); + }); + + it("rejects invalid course UUID parameter", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/invalid-id/share", + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("handles share generation attempt with valid UUID", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/share", + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect([200, 201, 404, 500]).toContain(response.statusCode); + if (response.statusCode === 200 || response.statusCode === 201) { + const body = JSON.parse(response.payload); + expect(body).toHaveProperty("success"); + } + }); + }); + + describe("GET /api/v1/courses/shared/:code", () => { + it("handles share code resolution attempt", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/shared/SAMPLECODE123", + }); + + expect([200, 404, 500]).toContain(response.statusCode); + }); + }); +}); diff --git a/tests/e2e/sessions.test.ts b/tests/e2e/sessions.test.ts new file mode 100644 index 0000000..43aa237 --- /dev/null +++ b/tests/e2e/sessions.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for session management endpoints (#633). + * + * Covers: + * - GET /api/v1/auth/sessions (listing active sessions) + * - DELETE /api/v1/auth/sessions/:sessionId (revoking individual session) + */ +describe("Sessions API (#633)", () => { + 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", + }); + + describe("GET /api/v1/auth/sessions", () => { + it("rejects unauthenticated requests", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/auth/sessions", + }); + + expect(response.statusCode).toBe(401); + const body = JSON.parse(response.payload); + expect(body.error).toBe("UNAUTHORIZED"); + }); + + it("rejects malformed authorization header", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/auth/sessions", + headers: { + authorization: "Bearer invalid.jwt.token", + }, + }); + + expect(response.statusCode).toBe(401); + }); + + it("handles authenticated call", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "GET", + url: "/api/v1/auth/sessions", + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect([200, 404, 500]).toContain(response.statusCode); + if (response.statusCode === 200) { + const body = JSON.parse(response.payload); + expect(body).toHaveProperty("success"); + } + }); + }); + + describe("DELETE /api/v1/auth/sessions/:sessionId", () => { + it("rejects unauthenticated requests", async () => { + const response = await app.inject({ + method: "DELETE", + url: "/api/v1/auth/sessions/c2222222-2222-4222-8222-222222222222", + }); + + expect(response.statusCode).toBe(401); + }); + + it("validates uuid format for sessionId param", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "DELETE", + url: "/api/v1/auth/sessions/not-a-valid-uuid", + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("handles valid uuid revocation request", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "DELETE", + url: "/api/v1/auth/sessions/c2222222-2222-4222-8222-222222222222", + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect([200, 404, 500]).toContain(response.statusCode); + }); + }); +});